Skip to content

Commit

Permalink
73 create builder (#74)
Browse files Browse the repository at this point in the history
* builder

* notebooks corrected

* remove build_portfolio

* return all timestamps

* notebooks

* return all timestamps
  • Loading branch information
tschm authored May 9, 2023
1 parent e274cb2 commit 22827fa
Show file tree
Hide file tree
Showing 9 changed files with 1,602 additions and 1,446 deletions.
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ This tool shall help to simplify the accounting. It keeps track of the available
The simulator shall be completely agnostic as to the trading policy/strategy.
Our approach follows a rather common pattern:

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

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

### Create the portfolio object
### Create the builder object

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

```python
from pathlib import Path
Expand All @@ -32,7 +33,7 @@ import pandas as pd
from cvx.simulator.portfolio import build_portfolio

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

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

```python
for before, now, state in portfolio:
for t, state in b:
# pick two assets at random
pair = np.random.choice(portfolio.assets, 2, replace=False)
pair = np.random.choice(b.assets, 2, replace=False)
# compute the pair
stocks = pd.Series(index=portfolio.assets, data=0.0)
stocks = pd.Series(index=b.assets, data=0.0)
stocks[pair] = [state.nav, -state.nav] / state.prices[pair].values
# update the position
portfolio[now] = 0.1 * stocks
b[t[-1]] = 0.1 * stocks
```

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

```python
for _, now, state in portfolio:
for t, state in b:
# each day we invest a quarter of the capital in the assets
portfolio[now] = 0.25 * state.nav / state.prices
b[t[-1]] = 0.25 * state.nav / state.prices
```

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

```python
for _, now, state in portfolio:
for t, state in b:
# each day we invest a quarter of the capital in the assets
portfolio.set_weights(now, pd.Series(index=portfolio.assets, data = 0.25))
b.set_weights(t[-1], pd.Series(index=b.assets, data = 0.25))
```

Once finished it is possible to build the portfolio object

```python
portfolio = b.build()
```

### Analyse results
Expand Down
1,208 changes: 614 additions & 594 deletions book/docs/notebooks/demo.ipynb

Large diffs are not rendered by default.

721 changes: 374 additions & 347 deletions book/docs/notebooks/monkey.ipynb

Large diffs are not rendered by default.

707 changes: 374 additions & 333 deletions book/docs/notebooks/pairs.ipynb

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions book/docs/notebooks/quantstats.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@
"\n",
"import quantstats as qs\n",
"\n",
"from cvx.simulator.portfolio import build_portfolio\n",
"from cvx.simulator.metrics import Metrics\n",
"\n",
"import matplotlib.font_manager"
"from cvx.simulator.builder import builder\n",
"from cvx.simulator.metrics import Metrics"
]
},
{
Expand Down Expand Up @@ -435,7 +433,7 @@
}
],
"source": [
"prices=pd.read_csv(\"data/stock_prices.csv\", header=0, index_col=0, parse_dates=True) \n",
"prices = pd.read_csv(\"data/stock_prices.csv\", header=0, index_col=0, parse_dates=True) \n",
"prices"
]
},
Expand All @@ -460,7 +458,7 @@
},
"outputs": [],
"source": [
"portfolio = build_portfolio(prices=prices, initial_cash=capital)"
"b = builder(prices=prices, initial_cash=capital)"
]
},
{
Expand All @@ -472,15 +470,27 @@
},
"outputs": [],
"source": [
"for before, now, state in portfolio:\n",
"for t, state in b:\n",
" # each day we invest a quarter of the capital in the assets\n",
" portfolio[now] = 0.05 * state.nav / state.prices"
" b[t[-1]] = (1.0 / len(b.assets)) * state.nav / state.prices"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a0ff35b2-2c52-49b9-bf79-0a538bbd9df6",
"id": "52a1e0bd-cdd5-40a2-a76f-e61306320288",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"portfolio= b.build()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "7f1438fe-59da-4ec5-92d5-34c21bce5705",
"metadata": {
"tags": []
},
Expand Down
124 changes: 124 additions & 0 deletions cvx/simulator/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from dataclasses import dataclass, field
import pandas as pd

from cvx.simulator.portfolio import EquityPortfolio
from cvx.simulator.trading_costs import TradingCostModel


@dataclass
class _State:
prices: pd.Series = None
position: pd.Series = None
cash: float = 1e6

@property
def value(self):
return (self.prices * self.position).sum()

@property
def nav(self):
return self.value + self.cash

@property
def weights(self):
return (self.prices * self.position)/self.nav

@property
def leverage(self):
return self.weights.abs().sum()

def update(self, position, model=None, **kwargs):
trades = position - self.position
self.position = position
self.cash -= (trades * self.prices).sum()

if model is not None:
self.cash -= model.eval(self.prices, trades=trades, **kwargs).sum()

return self


def builder(prices, initial_cash=1e6, trading_cost_model=None):
assert isinstance(prices, pd.DataFrame)
assert prices.index.is_monotonic_increasing
assert prices.index.is_unique

stocks = pd.DataFrame(index=prices.index, columns=prices.columns, data=0.0, dtype=float)

trading_cost_model = trading_cost_model
return _Builder(stocks=stocks, prices=prices.ffill(), initial_cash=float(initial_cash),
trading_cost_model=trading_cost_model)


@dataclass(frozen=True)
class _Builder:
prices: pd.DataFrame
stocks: pd.DataFrame
trading_cost_model: TradingCostModel
initial_cash: float = 1e6
_state: _State = field(default_factory=_State)

def __post_init__(self):
self._state.position = self.stocks.loc[self.index[0]]
self._state.prices = self.prices.loc[self.index[0]]
self._state.cash = self.initial_cash - self._state.value

@property
def index(self):
return self.prices.index

@property
def assets(self):
return self.prices.columns

def set_weights(self, time, weights):
"""
Set the position via weights (e.g. fractions of the nav)
:param time: time
:param weights: series of weights
"""
self[time] = (self._state.nav * weights) / self._state.prices

def set_cashposition(self, time, cashposition):
"""
Set the position via cash positions (e.g. USD invested per asset)
:param time: time
:param cashposition: series of cash positions
"""
self[time] = cashposition / self._state.prices

def set_position(self, time, position):
"""
Set the position via number of assets (e.g. number of stocks)
:param time: time
:param position: series of number of stocks
"""
self[time] = position

def __iter__(self):
for t in self.index[1:]:
# valuation of the current position
self._state.prices = self.prices.loc[t]

# this is probably very slow...
# 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)

yield self.index[self.index <= t], self._state

def __setitem__(self, key, position):
assert isinstance(position, pd.Series)
assert set(position.index).issubset(set(self.assets))

self.stocks.loc[key, position.index] = position
self._state.update(position, model=self.trading_cost_model)

def __getitem__(self, item):
assert item in self.index
return self.stocks.loc[item]

def build(self):
return EquityPortfolio(prices=self.prices, stocks=self.stocks, initial_cash=self.initial_cash, trading_cost_model=self.trading_cost_model)

Loading

0 comments on commit 22827fa

Please sign in to comment.