Skip to content

Commit

Permalink
97 resample a grid (#98)
Browse files Browse the repository at this point in the history
* resampling

* testing the resampling

* Resampling in the notebook

* Resampling in the notebook

* Resampling in the notebook
  • Loading branch information
tschm authored May 19, 2023
1 parent 54807e7 commit f468a81
Show file tree
Hide file tree
Showing 9 changed files with 55,438 additions and 17,545 deletions.
72,720 changes: 55,235 additions & 17,485 deletions book/docs/notebooks/demo.ipynb

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions book/docs/notebooks/monkey.ipynb

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions book/docs/notebooks/pairs.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"cell_type": "markdown",
"id": "8ec59418-20cd-44a9-9b1d-3695324b5e34",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%% md\n"
}
},
"tags": []
},
"source": [
"# Almost pairs trading"
Expand All @@ -17,10 +17,10 @@
"cell_type": "markdown",
"id": "8ed6af86-8e44-4f05-b5e5-1a4a8a2aa8cf",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%% md\n"
}
},
"tags": []
},
"source": [
"This little exercise goes back to an idea by Stephen Boyd:\n",
Expand All @@ -38,10 +38,10 @@
"execution_count": 1,
"id": "d6c865f2-13ab-41c1-ba37-a60be923d722",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -62,10 +62,10 @@
"execution_count": 2,
"id": "13346284-0fca-46c7-a9b6-548cdf47a6e4",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [
{
Expand Down Expand Up @@ -109,10 +109,10 @@
"execution_count": 3,
"id": "32c28168-9dc9-40c8-9482-dbf50c30c945",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -124,10 +124,10 @@
"execution_count": 4,
"id": "e85efcd9-f0f5-458c-b417-825c99842c4c",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [
{
Expand Down Expand Up @@ -1756,7 +1756,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.9.7"
}
},
"nbformat": 4,
Expand Down
34 changes: 17 additions & 17 deletions book/docs/notebooks/quantstats.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
"execution_count": 1,
"id": "e82ec25c-fb0f-4b2d-91ff-963e581fab03",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -50,10 +50,10 @@
"execution_count": 2,
"id": "9a6949c3-6c2b-4f28-848d-aca31f6fe53a",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -65,10 +65,10 @@
"execution_count": 3,
"id": "9950a6b6-f0c6-4394-9b4c-d1e9dfe427fc",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -80,10 +80,10 @@
"execution_count": 4,
"id": "6e4831c4-150a-4b42-a650-7ae0693d58d9",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -95,10 +95,10 @@
"execution_count": 5,
"id": "354e42be-dcd1-46a6-a3a2-24914cc2de6d",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -112,10 +112,10 @@
"execution_count": 6,
"id": "52a1e0bd-cdd5-40a2-a76f-e61306320288",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [],
"source": [
Expand All @@ -127,10 +127,10 @@
"execution_count": 7,
"id": "89f696f5-8820-4201-93df-93fc59734e80",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [
{
Expand All @@ -153,10 +153,10 @@
"execution_count": 8,
"id": "036a33ea-0edd-4f7c-8653-270886d6781a",
"metadata": {
"tags": [],
"pycharm": {
"name": "#%%\n"
}
},
"tags": []
},
"outputs": [
{
Expand Down Expand Up @@ -205,7 +205,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.9.7"
}
},
"nbformat": 4,
Expand Down
30 changes: 30 additions & 0 deletions cvx/simulator/grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pandas as pd
import numpy as np


def resample_index(index, rule):
"""
The resample_index function resamples a pandas DatetimeIndex object
to a lower frequency using a specified rule.
Note that the function does not modify the input index object,
but rather returns a pandas DatetimeIndex
"""
series = pd.Series(index=index, data=index)
a = series.resample(rule=rule).first()
return pd.DatetimeIndex(a.values)


def project_frame_to_grid(frame, grid):
"""The project_frame_to_grid function projects a pandas
DataFrame object onto a given index grid.
The function returns a new DataFrame that is only updated for times in the grid,
otherwise the previous values carry over.
Note that the function does not modify the input frame object, but rather returns a new object.
"""
sample = pd.DataFrame(index=frame.index, columns=frame.columns, data=np.NaN)
sample.loc[grid] = frame.loc[grid]
return sample.ffill()
47 changes: 47 additions & 0 deletions cvx/simulator/portfolio.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from dataclasses import dataclass

import numpy as np
import pandas as pd

from cvx.simulator.grid import resample_index, project_frame_to_grid
from cvx.simulator.trading_costs import TradingCostModel


Expand Down Expand Up @@ -181,6 +184,10 @@ def trades_currency(self) -> pd.DataFrame:
The resulting dataframe will have the same dimensions as the stocks and prices dataframes. """
return self.trades_stocks * self.prices.ffill()

@property
def turnover(self) -> pd.DataFrame:
return self.trades_currency.abs()

@property
def cash(self) -> pd.Series:
""" A property that returns a pandas series representing the cash on hand in the portfolio.
Expand Down Expand Up @@ -336,3 +343,43 @@ def truncate(self, before=None, after=None):
stocks=self.stocks.truncate(before=before, after=after),
trading_cost_model=self.trading_cost_model,
initial_cash=self.nav.truncate(before=before, after=after).values[0])

@property
def start(self):
"""first index with a profit that is not zero"""
return self.profit.ne(0).idxmax()

def resample(self, rule, truncate=False):
"""The resample method resamples an EquityPortfolio object to a new frequency
specified by the rule argument.
The method returns a new EquityPortfolio object with the resampled stocks, but prices stay the same.
If the truncate parameter is set to True, the method first trims the original data
to start from the beginning of the EquityPortfolio object's timeline using the truncate method.
Otherwise, the original EquityPortfolio timescale is used. The start of trading may be missed
as the first point may not be in the resampled grid.
The function uses a utility function resample_index to create a new DatetimeIndex
with the specified resampled rule. The new index is used in the project_frame_to_grid
function to translate the stocks DataFrame onto the new grid.
Finally, a new EquityPortfolio object is created with the original prices
DataFrame and the resampled stocks DataFrame. The objects trading cost model and initial cash value
are also copied into the new object.
Note that the resample method does not modify the original EquityPortfolio object,
but rather returns a new object.
"""
if truncate:
portfolio = self.truncate(before=self.start)
else:
portfolio = self

grid = resample_index(portfolio.index, rule=rule)

stocks = project_frame_to_grid(portfolio.stocks, grid=grid)

return EquityPortfolio(prices=portfolio.prices,
stocks=stocks,
trading_cost_model=self.trading_cost_model,
initial_cash=self.initial_cash)
19 changes: 19 additions & 0 deletions tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def test_iteration_state(builder):
pd.testing.assert_series_equal(state.weights, pd.Series(index=builder.assets, data=0.0), check_names=False)
pd.testing.assert_series_equal(builder[t[-1]], pd.Series(index=builder.assets, data=0.0), check_names=False)


def test_build(builder_weights, prices):
# build the portfolio directly
portfolio = builder_weights.build()
Expand All @@ -72,3 +73,21 @@ def test_build(builder_weights, prices):

# verify both methods give the same result
pd.testing.assert_series_equal(portfolio.nav, portfolio2.nav)


def test_set_weights(prices):
b = _builder(prices=prices[["B", "C"]].head(5), initial_cash=50000)
for times, state in b:
b.set_weights(time=times[-1], weights=pd.Series(index=["B","C"], data=0.5))

portfolio = b.build()
assert portfolio.nav.values[-1] == pytest.approx(49773.093729)


def test_set_cashpositions(prices):
b = _builder(prices=prices[["B", "C"]].head(5), initial_cash=50000)
for times, state in b:
b.set_cashposition(time=times[-1], cashposition=pd.Series(index=["B", "C"], data=state.nav / 2))

portfolio = b.build()
assert portfolio.nav.values[-1] == pytest.approx(49773.093729)
51 changes: 51 additions & 0 deletions tests/test_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
import pandas as pd
import numpy as np

from cvx.simulator.builder import builder
from cvx.simulator.grid import resample_index, project_frame_to_grid


def test_resample_index(prices):
x = resample_index(prices.index, rule="M")
for t in x:
assert t in prices.index

assert len(x) == 28

# first day in the middle of a month
x = resample_index(prices.index[4:], rule="M")
assert len(x) == 28
assert x[0] == prices.index[4]


def test_project_frame_to_grid(prices):
grid = resample_index(prices.index[4:], rule="M")
frame = project_frame_to_grid(prices, grid=grid)
a = frame.diff().sum(axis=1)
assert a.tail(5).sum() == 0.0



def test_portfolio_resampling(prices):
# construct a portfolio with only one asset
b = builder(prices[["A"]])
assert set(b.assets) == {"A"}

# compute a grid and rebalance only on those particular days
grid = resample_index(prices.index, rule="M")

# change the position only at days that are in the grid
for times, _ in b:
if times[-1] in grid:
b[times[-1]] = pd.Series({"A": np.random.rand(1)})
else:
# new position is the old position
# This may look ineffective but we could use
# trading costs for just holding short positions etc.
b[times[-1]] = b[times[-2]]

portfolio = b.build()
print(portfolio.stocks)

assert portfolio.stocks["A"].tail(10).std() == pytest.approx(0.0, abs=1e-12)
Loading

0 comments on commit f468a81

Please sign in to comment.