From 79b646104c661ad2f98fb8069448d970fa3f137a Mon Sep 17 00:00:00 2001 From: Rich <24254625+richklee@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:19:04 +0000 Subject: [PATCH] turnover --- alphavec/backtest.py | 37 +++++++++++++++---- example.ipynb | 84 +++++++++++++++++++++++------------------- tests/test_backtest.py | 9 +++++ 3 files changed, 84 insertions(+), 46 deletions(-) diff --git a/alphavec/backtest.py b/alphavec/backtest.py index cef9be3..4f3115c 100644 --- a/alphavec/backtest.py +++ b/alphavec/backtest.py @@ -153,19 +153,25 @@ def backtest( spread_costs = _spread(weights, prices, spread_pct) / prices costs = cmn_costs + borrow_costs + spread_costs - # Calc the number of valid trading periods for each asset - # to get correct number of trades - strat_valid_periods = weights.apply( - lambda col: col.loc[col.first_valid_index() :].count() - ) - strat_total_days = strat_valid_periods / freq_day - # Evaluate the cost-aware strategy returns and key performance metrics # Use the shift arg to prevent look-ahead bias # Truncate the returns to remove the empty intervals resulting from the shift strat_rets = weights * (prices.pct_change() - costs).shift(-shift_periods) strat_rets = strat_rets.iloc[:-shift_periods] if shift_periods > 0 else strat_rets strat_cum = (1 + strat_rets).cumprod() - 1 + + # Calc the number of valid trading periods for each asset + strat_valid_periods = weights.apply( + lambda col: col.loc[col.first_valid_index() :].count() + ) + strat_total_days = strat_valid_periods / freq_day + + # Calc the annual turnover for each asset + strat_ann_turnover = _turnover(weights, strat_rets) * ( + trading_days_year / strat_total_days + ) + + # Evaluate the strategy asset-wise performance strat_perf = pd.concat( [ strat_rets.apply( @@ -174,6 +180,7 @@ def backtest( strat_rets.apply(_ann_vol, periods=freq_year), strat_rets.apply(_cagr, periods=freq_year), strat_rets.apply(_max_drawdown), + strat_ann_turnover, _trade_count(weights) / strat_total_days, ], keys=[ @@ -181,6 +188,7 @@ def backtest( "annual_volatility", "cagr", "max_drawdown,", + "annual_turnover", "trades_per_day", ], axis=1, @@ -189,7 +197,9 @@ def backtest( # Evaluate the strategy portfolio performance port_rets = strat_rets.sum(axis=1) port_cum = strat_cum.sum(axis=1) - port_costs = costs.sum().sum() + + port_ann_turnover = 1 + port_perf = pd.DataFrame( { "annual_sharpe": _ann_sharpe( @@ -198,6 +208,7 @@ def backtest( "annual_volatility": _ann_vol(port_rets, periods=freq_year), "cagr": _cagr(port_rets, periods=freq_year), "max_drawdown": _max_drawdown(port_rets), + "annual_turnover": port_ann_turnover, }, index=["portfolio"], ) @@ -304,6 +315,16 @@ def _max_drawdown(rets: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series: return max_drawdown +def _turnover( + weights: pd.DataFrame | pd.Series, + rets: pd.DataFrame | pd.Series, +) -> pd.Series | float: + diff = weights.fillna(0).diff().abs() + port = (1 + rets).cumprod() - 1 + turnover = diff.sum() / ((1 + port.iloc[-1]) / 2) + return turnover + + def _trade_count(weights: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series: diff = weights.fillna(0).diff().abs() != 0 tx = diff.astype(int) diff --git a/example.ipynb b/example.ipynb index 2e37f35..4a965aa 100644 --- a/example.ipynb +++ b/example.ipynb @@ -609,28 +609,7 @@ "cell_type": "code", "execution_count": 7, "metadata": {}, - "outputs": [ - { - "ename": "AssertionError", - "evalue": "Cannot concat indices that do not have the same number of levels", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[7], line 7\u001b[0m\n\u001b[1;32m 4\u001b[0m trade_prices \u001b[38;5;241m=\u001b[39m trade_prices\u001b[38;5;241m.\u001b[39mmask(weights\u001b[38;5;241m.\u001b[39misna())\n\u001b[1;32m 5\u001b[0m trade_prices, weights \u001b[38;5;241m=\u001b[39m trade_prices\u001b[38;5;241m.\u001b[39malign(weights, join\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124minner\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m----> 7\u001b[0m perf, perf_cum, perf_sr, port_perf, port_returns, port_cum \u001b[38;5;241m=\u001b[39m \u001b[43mbacktest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[43m \u001b[49m\u001b[43mweights\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9\u001b[0m \u001b[43m \u001b[49m\u001b[43mtrade_prices\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 10\u001b[0m \u001b[43m \u001b[49m\u001b[43mfreq_day\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 11\u001b[0m \u001b[43m \u001b[49m\u001b[43mtrading_days_year\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m365\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 12\u001b[0m \u001b[43m \u001b[49m\u001b[43mshift_periods\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 13\u001b[0m \u001b[43m \u001b[49m\u001b[43mcommission_func\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpartial\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpct_commission\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfee\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0.001\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 14\u001b[0m \u001b[43m \u001b[49m\u001b[43mann_borrow_rate\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0.05\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 15\u001b[0m \u001b[43m \u001b[49m\u001b[43mspread_pct\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0.0005\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 16\u001b[0m \u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/alphavec/backtest.py:212\u001b[0m, in \u001b[0;36mbacktest\u001b[0;34m(weights, prices, freq_day, trading_days_year, shift_periods, commission_func, ann_borrow_rate, spread_pct, ann_risk_free_rate)\u001b[0m\n\u001b[1;32m 197\u001b[0m port_perf \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mDataFrame(\n\u001b[1;32m 198\u001b[0m {\n\u001b[1;32m 199\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mannual_sharpe\u001b[39m\u001b[38;5;124m\"\u001b[39m: _ann_sharpe(\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 207\u001b[0m index\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mportfolio\u001b[39m\u001b[38;5;124m\"\u001b[39m],\n\u001b[1;32m 208\u001b[0m )\n\u001b[1;32m 210\u001b[0m \u001b[38;5;66;03m# Combine the asset and strategy performance metrics\u001b[39;00m\n\u001b[1;32m 211\u001b[0m \u001b[38;5;66;03m# into a single dataframe for comparison\u001b[39;00m\n\u001b[0;32m--> 212\u001b[0m perf \u001b[38;5;241m=\u001b[39m \u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mconcat\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 213\u001b[0m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43masset_perf\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstrat_perf\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 214\u001b[0m \u001b[43m \u001b[49m\u001b[43mkeys\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43masset\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mstrategy\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 215\u001b[0m \u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 216\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 217\u001b[0m perf_cum \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mconcat(\n\u001b[1;32m 218\u001b[0m [asset_cum, strat_cum, port_cum],\n\u001b[1;32m 219\u001b[0m keys\u001b[38;5;241m=\u001b[39m[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124masset\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mstrategy\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mportfolio\u001b[39m\u001b[38;5;124m\"\u001b[39m],\n\u001b[1;32m 220\u001b[0m axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m,\n\u001b[1;32m 221\u001b[0m )\n\u001b[1;32m 222\u001b[0m perf_roll_sr \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mconcat(\n\u001b[1;32m 223\u001b[0m [\n\u001b[1;32m 224\u001b[0m _ann_roll_sharpe(\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 244\u001b[0m axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m,\n\u001b[1;32m 245\u001b[0m )\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/.venv/lib/python3.11/site-packages/pandas/core/reshape/concat.py:393\u001b[0m, in \u001b[0;36mconcat\u001b[0;34m(objs, axis, join, ignore_index, keys, levels, names, verify_integrity, sort, copy)\u001b[0m\n\u001b[1;32m 378\u001b[0m copy \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[1;32m 380\u001b[0m op \u001b[38;5;241m=\u001b[39m _Concatenator(\n\u001b[1;32m 381\u001b[0m objs,\n\u001b[1;32m 382\u001b[0m axis\u001b[38;5;241m=\u001b[39maxis,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 390\u001b[0m sort\u001b[38;5;241m=\u001b[39msort,\n\u001b[1;32m 391\u001b[0m )\n\u001b[0;32m--> 393\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mop\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_result\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/.venv/lib/python3.11/site-packages/pandas/core/reshape/concat.py:667\u001b[0m, in \u001b[0;36m_Concatenator.get_result\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 665\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m obj \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobjs:\n\u001b[1;32m 666\u001b[0m indexers \u001b[38;5;241m=\u001b[39m {}\n\u001b[0;32m--> 667\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m ax, new_labels \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnew_axes\u001b[49m):\n\u001b[1;32m 668\u001b[0m \u001b[38;5;66;03m# ::-1 to convert BlockManager ax to DataFrame ax\u001b[39;00m\n\u001b[1;32m 669\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m ax \u001b[38;5;241m==\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbm_axis:\n\u001b[1;32m 670\u001b[0m \u001b[38;5;66;03m# Suppress reindexing on concat axis\u001b[39;00m\n\u001b[1;32m 671\u001b[0m \u001b[38;5;28;01mcontinue\u001b[39;00m\n", - "File \u001b[0;32mproperties.pyx:36\u001b[0m, in \u001b[0;36mpandas._libs.properties.CachedProperty.__get__\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/.venv/lib/python3.11/site-packages/pandas/core/reshape/concat.py:698\u001b[0m, in \u001b[0;36m_Concatenator.new_axes\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 695\u001b[0m \u001b[38;5;129m@cache_readonly\u001b[39m\n\u001b[1;32m 696\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mnew_axes\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mlist\u001b[39m[Index]:\n\u001b[1;32m 697\u001b[0m ndim \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_result_dim()\n\u001b[0;32m--> 698\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m[\u001b[49m\n\u001b[1;32m 699\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_concat_axis\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m==\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbm_axis\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_comb_axis\u001b[49m\u001b[43m(\u001b[49m\u001b[43mi\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 700\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mrange\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mndim\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 701\u001b[0m \u001b[43m \u001b[49m\u001b[43m]\u001b[49m\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/.venv/lib/python3.11/site-packages/pandas/core/reshape/concat.py:699\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 695\u001b[0m \u001b[38;5;129m@cache_readonly\u001b[39m\n\u001b[1;32m 696\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mnew_axes\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mlist\u001b[39m[Index]:\n\u001b[1;32m 697\u001b[0m ndim \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_result_dim()\n\u001b[1;32m 698\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m [\n\u001b[0;32m--> 699\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_concat_axis\u001b[49m \u001b[38;5;28;01mif\u001b[39;00m i \u001b[38;5;241m==\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbm_axis \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_comb_axis(i)\n\u001b[1;32m 700\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(ndim)\n\u001b[1;32m 701\u001b[0m ]\n", - "File \u001b[0;32mproperties.pyx:36\u001b[0m, in \u001b[0;36mpandas._libs.properties.CachedProperty.__get__\u001b[0;34m()\u001b[0m\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/.venv/lib/python3.11/site-packages/pandas/core/reshape/concat.py:758\u001b[0m, in \u001b[0;36m_Concatenator._get_concat_axis\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 756\u001b[0m concat_axis \u001b[38;5;241m=\u001b[39m _concat_indexes(indexes)\n\u001b[1;32m 757\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 758\u001b[0m concat_axis \u001b[38;5;241m=\u001b[39m \u001b[43m_make_concat_multiindex\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 759\u001b[0m \u001b[43m \u001b[49m\u001b[43mindexes\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mkeys\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mlevels\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mnames\u001b[49m\n\u001b[1;32m 760\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 762\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_maybe_check_integrity(concat_axis)\n\u001b[1;32m 764\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m concat_axis\n", - "File \u001b[0;32m~/repos/github.com/thecolngroup/alphavec/.venv/lib/python3.11/site-packages/pandas/core/reshape/concat.py:841\u001b[0m, in \u001b[0;36m_make_concat_multiindex\u001b[0;34m(indexes, keys, levels, names)\u001b[0m\n\u001b[1;32m 838\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 839\u001b[0m \u001b[38;5;66;03m# make sure that all of the passed indices have the same nlevels\u001b[39;00m\n\u001b[1;32m 840\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mlen\u001b[39m({idx\u001b[38;5;241m.\u001b[39mnlevels \u001b[38;5;28;01mfor\u001b[39;00m idx \u001b[38;5;129;01min\u001b[39;00m indexes}) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[0;32m--> 841\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAssertionError\u001b[39;00m(\n\u001b[1;32m 842\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot concat indices that do not have the same number of levels\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 843\u001b[0m )\n\u001b[1;32m 845\u001b[0m \u001b[38;5;66;03m# also copies\u001b[39;00m\n\u001b[1;32m 846\u001b[0m names \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(names) \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mlist\u001b[39m(get_unanimous_names(\u001b[38;5;241m*\u001b[39mindexes))\n", - "\u001b[0;31mAssertionError\u001b[0m: Cannot concat indices that do not have the same number of levels" - ] - } - ], + "outputs": [], "source": [ "weights *= 2 # Apply fixed 2x leverage\n", "\n", @@ -661,7 +640,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -730,7 +709,7 @@ " 0.512587\n", " 1.616165\n", " 0.632456\n", - " 2.752784e+03\n", + " 0.644781\n", " 1.000637\n", " \n", " \n", @@ -743,7 +722,7 @@ " 1.058797\n", " 0.699507\n", " 0.659074\n", - " 3.604555e+03\n", + " 4.950039\n", " 1.000745\n", " \n", " \n", @@ -756,7 +735,7 @@ " 0.385551\n", " 0.233350\n", " 0.497637\n", - " 2.481621e+04\n", + " 15.555918\n", " 1.000637\n", " \n", " \n", @@ -769,7 +748,7 @@ " 0.615083\n", " 0.081175\n", " 0.585446\n", - " 9.819774e+04\n", + " 27.251474\n", " 1.000708\n", " \n", " \n", @@ -782,7 +761,7 @@ " 0.477024\n", " 0.174253\n", " 0.459495\n", - " 4.009968e+04\n", + " 21.644242\n", " 1.001065\n", " \n", " \n", @@ -795,7 +774,7 @@ " 0.408337\n", " -0.086424\n", " 0.659143\n", - " -2.010559e+06\n", + " 59.531211\n", " 1.000637\n", " \n", " \n", @@ -826,12 +805,12 @@ " \n", " annual_turnover trades_per_day \n", "symbol \n", - "BTCUSDT 2.752784e+03 1.000637 \n", - "DOGEUSDT 3.604555e+03 1.000745 \n", - "ETHUSDT 2.481621e+04 1.000637 \n", - "MATICUSDT 9.819774e+04 1.000708 \n", - "SOLUSDT 4.009968e+04 1.001065 \n", - "XRPUSDT -2.010559e+06 1.000637 " + "BTCUSDT 0.644781 1.000637 \n", + "DOGEUSDT 4.950039 1.000745 \n", + "ETHUSDT 15.555918 1.000637 \n", + "MATICUSDT 27.251474 1.000708 \n", + "SOLUSDT 21.644242 1.001065 \n", + "XRPUSDT 59.531211 1.000637 " ] }, "metadata": {}, @@ -876,7 +855,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -914,7 +893,7 @@ " 1.582458\n", " 2.094833\n", " 0.954154\n", - " 158060.566991\n", + " 1\n", " \n", " \n", "\n", @@ -925,7 +904,7 @@ "portfolio 1.425487 1.582458 2.094833 0.954154 \n", "\n", " annual_turnover \n", - "portfolio 158060.566991 " + "portfolio 1 " ] }, "metadata": {}, @@ -956,6 +935,35 @@ "display(port_perf)\n", "port_cum.plot()" ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "245475.6835328398\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "port_equity_curve = (1 + port_returns).cumprod() * 1000\n", + "port_equity_curve.plot()\n", + "print(port_equity_curve.iloc[-1])" + ] } ], "metadata": { diff --git a/tests/test_backtest.py b/tests/test_backtest.py index b6be21d..6ecd61d 100644 --- a/tests/test_backtest.py +++ b/tests/test_backtest.py @@ -88,6 +88,15 @@ def test_pct_commission(): assert act.iloc[4] == 3.5 # Case: fee for 1 to -2.5 +def test_turnover(): + weights = pd.Series([0, np.nan, 0, 1, -2.5]) + prices = pd.Series([10, 20, 40, 80, 40]) + returns = weights * prices.pct_change() + + act = bt._ann_turnover(returns, weights) + logging.info(act) + + def test_spread(): weights = pd.Series([np.nan, 0.5, -2.5]) prices = pd.Series([10, 10, 10])