Skip to content

Commit 22827fa

Browse files
authored
73 create builder (#74)
* builder * notebooks corrected * remove build_portfolio * return all timestamps * notebooks * return all timestamps
1 parent e274cb2 commit 22827fa

File tree

9 files changed

+1602
-1446
lines changed

9 files changed

+1602
-1446
lines changed

Diff for: README.md

+20-13
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ This tool shall help to simplify the accounting. It keeps track of the available
1515
The simulator shall be completely agnostic as to the trading policy/strategy.
1616
Our approach follows a rather common pattern:
1717

18-
* [Create the portfolio object](#create-the-portfolio-object)
18+
* [Create the builder object](#create-the-builder-object)
1919
* [Loop through time](#loop-through-time)
2020
* [Analyse results](#analyse-results)
2121

2222
We demonstrate those steps with somewhat silly policies. They are never good strategies, but are always valid ones.
2323

24-
### Create the portfolio object
24+
### Create the builder object
2525

26-
The user defines a portfolio object by loading a frame of prices and initialize the initial amount of cash used in our experiment:
26+
The user defines a builder object by loading a frame of prices
27+
and initialize the initial amount of cash used in our experiment:
2728

2829
```python
2930
from pathlib import Path
@@ -32,7 +33,7 @@ import pandas as pd
3233
from cvx.simulator.portfolio import build_portfolio
3334

3435
prices = pd.read_csv(Path("resources") / "price.csv", index_col=0, parse_dates=True, header=0).ffill()
35-
portfolio = build_portfolio(prices=prices, initial_cash=1e6)
36+
b = builder(prices=prices, initial_cash=1e6)
3637
```
3738

3839
It is also possible to specify a model for trading costs.
@@ -44,14 +45,14 @@ Let's start with a first strategy. Each day we choose two names from the univers
4445
Buy one (say 0.1 of your portfolio wealth) and short one the same amount.
4546

4647
```python
47-
for before, now, state in portfolio:
48+
for t, state in b:
4849
# pick two assets at random
49-
pair = np.random.choice(portfolio.assets, 2, replace=False)
50+
pair = np.random.choice(b.assets, 2, replace=False)
5051
# compute the pair
51-
stocks = pd.Series(index=portfolio.assets, data=0.0)
52+
stocks = pd.Series(index=b.assets, data=0.0)
5253
stocks[pair] = [state.nav, -state.nav] / state.prices[pair].values
5354
# update the position
54-
portfolio[now] = 0.1 * stocks
55+
b[t[-1]] = 0.1 * stocks
5556
```
5657

5758
A lot of magic is hidden in the state variable.
@@ -60,18 +61,24 @@ The state gives access to the currently available cash, the current prices and t
6061
Here's a slightly more realistic loop. Given a set of $4$ assets we want to implmenent the popular $1/n$ strategy.
6162

6263
```python
63-
for _, now, state in portfolio:
64+
for t, state in b:
6465
# each day we invest a quarter of the capital in the assets
65-
portfolio[now] = 0.25 * state.nav / state.prices
66+
b[t[-1]] = 0.25 * state.nav / state.prices
6667
```
6768

6869
Note that we update the position at time `now` using a series of actual stocks rather than weights or cashpositions.
69-
The portfolio class also exposes setters for such conventions.
70+
The builder class also exposes setters for such conventions.
7071

7172
```python
72-
for _, now, state in portfolio:
73+
for t, state in b:
7374
# each day we invest a quarter of the capital in the assets
74-
portfolio.set_weights(now, pd.Series(index=portfolio.assets, data = 0.25))
75+
b.set_weights(t[-1], pd.Series(index=b.assets, data = 0.25))
76+
```
77+
78+
Once finished it is possible to build the portfolio object
79+
80+
```python
81+
portfolio = b.build()
7582
```
7683

7784
### Analyse results

Diff for: book/docs/notebooks/demo.ipynb

+614-594
Large diffs are not rendered by default.

Diff for: book/docs/notebooks/monkey.ipynb

+374-347
Large diffs are not rendered by default.

Diff for: book/docs/notebooks/pairs.ipynb

+374-333
Large diffs are not rendered by default.

Diff for: book/docs/notebooks/quantstats.ipynb

+19-9
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@
3030
"\n",
3131
"import quantstats as qs\n",
3232
"\n",
33-
"from cvx.simulator.portfolio import build_portfolio\n",
34-
"from cvx.simulator.metrics import Metrics\n",
35-
"\n",
36-
"import matplotlib.font_manager"
33+
"from cvx.simulator.builder import builder\n",
34+
"from cvx.simulator.metrics import Metrics"
3735
]
3836
},
3937
{
@@ -435,7 +433,7 @@
435433
}
436434
],
437435
"source": [
438-
"prices=pd.read_csv(\"data/stock_prices.csv\", header=0, index_col=0, parse_dates=True) \n",
436+
"prices = pd.read_csv(\"data/stock_prices.csv\", header=0, index_col=0, parse_dates=True) \n",
439437
"prices"
440438
]
441439
},
@@ -460,7 +458,7 @@
460458
},
461459
"outputs": [],
462460
"source": [
463-
"portfolio = build_portfolio(prices=prices, initial_cash=capital)"
461+
"b = builder(prices=prices, initial_cash=capital)"
464462
]
465463
},
466464
{
@@ -472,15 +470,27 @@
472470
},
473471
"outputs": [],
474472
"source": [
475-
"for before, now, state in portfolio:\n",
473+
"for t, state in b:\n",
476474
" # each day we invest a quarter of the capital in the assets\n",
477-
" portfolio[now] = 0.05 * state.nav / state.prices"
475+
" b[t[-1]] = (1.0 / len(b.assets)) * state.nav / state.prices"
478476
]
479477
},
480478
{
481479
"cell_type": "code",
482480
"execution_count": 6,
483-
"id": "a0ff35b2-2c52-49b9-bf79-0a538bbd9df6",
481+
"id": "52a1e0bd-cdd5-40a2-a76f-e61306320288",
482+
"metadata": {
483+
"tags": []
484+
},
485+
"outputs": [],
486+
"source": [
487+
"portfolio= b.build()"
488+
]
489+
},
490+
{
491+
"cell_type": "code",
492+
"execution_count": 7,
493+
"id": "7f1438fe-59da-4ec5-92d5-34c21bce5705",
484494
"metadata": {
485495
"tags": []
486496
},

Diff for: cvx/simulator/builder.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from dataclasses import dataclass, field
2+
import pandas as pd
3+
4+
from cvx.simulator.portfolio import EquityPortfolio
5+
from cvx.simulator.trading_costs import TradingCostModel
6+
7+
8+
@dataclass
9+
class _State:
10+
prices: pd.Series = None
11+
position: pd.Series = None
12+
cash: float = 1e6
13+
14+
@property
15+
def value(self):
16+
return (self.prices * self.position).sum()
17+
18+
@property
19+
def nav(self):
20+
return self.value + self.cash
21+
22+
@property
23+
def weights(self):
24+
return (self.prices * self.position)/self.nav
25+
26+
@property
27+
def leverage(self):
28+
return self.weights.abs().sum()
29+
30+
def update(self, position, model=None, **kwargs):
31+
trades = position - self.position
32+
self.position = position
33+
self.cash -= (trades * self.prices).sum()
34+
35+
if model is not None:
36+
self.cash -= model.eval(self.prices, trades=trades, **kwargs).sum()
37+
38+
return self
39+
40+
41+
def builder(prices, initial_cash=1e6, trading_cost_model=None):
42+
assert isinstance(prices, pd.DataFrame)
43+
assert prices.index.is_monotonic_increasing
44+
assert prices.index.is_unique
45+
46+
stocks = pd.DataFrame(index=prices.index, columns=prices.columns, data=0.0, dtype=float)
47+
48+
trading_cost_model = trading_cost_model
49+
return _Builder(stocks=stocks, prices=prices.ffill(), initial_cash=float(initial_cash),
50+
trading_cost_model=trading_cost_model)
51+
52+
53+
@dataclass(frozen=True)
54+
class _Builder:
55+
prices: pd.DataFrame
56+
stocks: pd.DataFrame
57+
trading_cost_model: TradingCostModel
58+
initial_cash: float = 1e6
59+
_state: _State = field(default_factory=_State)
60+
61+
def __post_init__(self):
62+
self._state.position = self.stocks.loc[self.index[0]]
63+
self._state.prices = self.prices.loc[self.index[0]]
64+
self._state.cash = self.initial_cash - self._state.value
65+
66+
@property
67+
def index(self):
68+
return self.prices.index
69+
70+
@property
71+
def assets(self):
72+
return self.prices.columns
73+
74+
def set_weights(self, time, weights):
75+
"""
76+
Set the position via weights (e.g. fractions of the nav)
77+
78+
:param time: time
79+
:param weights: series of weights
80+
"""
81+
self[time] = (self._state.nav * weights) / self._state.prices
82+
83+
def set_cashposition(self, time, cashposition):
84+
"""
85+
Set the position via cash positions (e.g. USD invested per asset)
86+
87+
:param time: time
88+
:param cashposition: series of cash positions
89+
"""
90+
self[time] = cashposition / self._state.prices
91+
92+
def set_position(self, time, position):
93+
"""
94+
Set the position via number of assets (e.g. number of stocks)
95+
96+
:param time: time
97+
:param position: series of number of stocks
98+
"""
99+
self[time] = position
100+
101+
def __iter__(self):
102+
for t in self.index[1:]:
103+
# valuation of the current position
104+
self._state.prices = self.prices.loc[t]
105+
106+
# this is probably very slow...
107+
# portfolio = EquityPortfolio(prices=self.prices.truncate(after=now), stocks=self.stocks.truncate(after=now), initial_cash=self.initial_cash, trading_cost_model=self.trading_cost_model)
108+
109+
yield self.index[self.index <= t], self._state
110+
111+
def __setitem__(self, key, position):
112+
assert isinstance(position, pd.Series)
113+
assert set(position.index).issubset(set(self.assets))
114+
115+
self.stocks.loc[key, position.index] = position
116+
self._state.update(position, model=self.trading_cost_model)
117+
118+
def __getitem__(self, item):
119+
assert item in self.index
120+
return self.stocks.loc[item]
121+
122+
def build(self):
123+
return EquityPortfolio(prices=self.prices, stocks=self.stocks, initial_cash=self.initial_cash, trading_cost_model=self.trading_cost_model)
124+

0 commit comments

Comments
 (0)