mirror of
https://github.com/webclinic017/drift.git
synced 2026-08-22 07:18:08 +00:00
feat(Reporting): added backtesting with vectorbt (#148)
* feat(Reporting): added vectorbt-based backtest * fix(Reporting): added transaction costs * feat(Reporting): added ability to rebalance only every n days * feat(Dependencies): added pytorch * fix(Dependencies): added pytorch-lightning * feat(CI): added portfolio reporting step * feat(Reporting): save weights as well * fix(Reporting): start with less cash
This commit is contained in:
@@ -23,6 +23,10 @@ jobs:
|
|||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
run: |
|
run: |
|
||||||
python run_pipeline.py
|
python run_pipeline.py
|
||||||
|
- name: Run portfolio reporting
|
||||||
|
shell: bash -l {0}
|
||||||
|
run: |
|
||||||
|
python run_portfolio_reporting.py
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
name: artifacts
|
||||||
|
|||||||
+22
-22
@@ -4,33 +4,33 @@ channels:
|
|||||||
- conda-forge
|
- conda-forge
|
||||||
- defaults
|
- defaults
|
||||||
- ml4t
|
- ml4t
|
||||||
|
- ranaroussi
|
||||||
dependencies:
|
dependencies:
|
||||||
- python=3.9
|
- python=3.9
|
||||||
- seaborn
|
|
||||||
- scikit-learn-intelex=2021.4.0
|
- scikit-learn-intelex=2021.4.0
|
||||||
- ipython
|
|
||||||
- ipykernel
|
|
||||||
- scipy
|
|
||||||
- scikit-learn
|
|
||||||
- numba
|
|
||||||
- pytorch
|
|
||||||
- matplotlib
|
|
||||||
- numpy
|
|
||||||
- quantstats
|
|
||||||
- pytorch-lightning
|
|
||||||
- pytest
|
|
||||||
- wandb
|
|
||||||
- python-dotenv
|
|
||||||
- tscv
|
|
||||||
- tqdm
|
|
||||||
- pip
|
|
||||||
- pandas-ta
|
|
||||||
- xgboost
|
|
||||||
- lightgbm
|
|
||||||
- alphalens-reloaded
|
|
||||||
- pyfolio-reloaded
|
|
||||||
- pip:
|
- pip:
|
||||||
|
- torch
|
||||||
|
- pytorch-lightning
|
||||||
- fracdiff
|
- fracdiff
|
||||||
- ray
|
- ray
|
||||||
- diskcache
|
- diskcache
|
||||||
|
- seaborn
|
||||||
|
- ipython
|
||||||
|
- ipykernel
|
||||||
|
- scipy
|
||||||
|
- scikit-learn
|
||||||
|
- numba
|
||||||
|
- matplotlib
|
||||||
|
- numpy
|
||||||
|
- quantstats
|
||||||
|
- pytest
|
||||||
|
- wandb
|
||||||
|
- python-dotenv
|
||||||
|
- tscv
|
||||||
|
- tqdm
|
||||||
|
- pandas-ta
|
||||||
|
- xgboost
|
||||||
|
- lightgbm
|
||||||
|
- alphalens-reloaded
|
||||||
|
- vectorbt
|
||||||
prefix: /usr/local/anaconda3/envs/quant
|
prefix: /usr/local/anaconda3/envs/quant
|
||||||
|
|||||||
+77
-53
@@ -5,15 +5,10 @@ from data_loader.load_data import load_only_returns
|
|||||||
from data_loader.collections import data_collections
|
from data_loader.collections import data_collections
|
||||||
|
|
||||||
from utils.helpers import get_first_valid_return_index
|
from utils.helpers import get_first_valid_return_index
|
||||||
from alphalens.tears import (create_returns_tear_sheet,
|
|
||||||
create_information_tear_sheet,
|
|
||||||
create_turnover_tear_sheet,
|
|
||||||
create_summary_tear_sheet,
|
|
||||||
create_full_tear_sheet,
|
|
||||||
create_event_returns_tear_sheet,
|
|
||||||
create_event_study_tear_sheet)
|
|
||||||
import alphalens
|
import alphalens
|
||||||
import pyfolio
|
import vectorbt as vbt
|
||||||
|
from vectorbt.portfolio.enums import SizeType, CallSeqType, Direction
|
||||||
|
import quantstats as qs
|
||||||
|
|
||||||
from alphalens.utils import get_clean_factor_and_forward_returns
|
from alphalens.utils import get_clean_factor_and_forward_returns
|
||||||
|
|
||||||
@@ -73,7 +68,6 @@ def equal_weight(row: pd.Series, availability: pd.Series) -> pd.Series:
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_naive_portfolio_weights(predictions: pd.DataFrame, availability: pd.DataFrame, allow_short: bool) -> pd.DataFrame:
|
def create_naive_portfolio_weights(predictions: pd.DataFrame, availability: pd.DataFrame, allow_short: bool) -> pd.DataFrame:
|
||||||
weights = predictions.copy()
|
weights = predictions.copy()
|
||||||
assert weights.shape[1] == availability.shape[1]
|
assert weights.shape[1] == availability.shape[1]
|
||||||
@@ -82,10 +76,31 @@ def create_naive_portfolio_weights(predictions: pd.DataFrame, availability: pd.D
|
|||||||
# row = row / row.sum()
|
# row = row / row.sum()
|
||||||
# row = only_top_bottom_2(row)
|
# row = only_top_bottom_2(row)
|
||||||
# row = equal_weight(row, availability.iloc[index])
|
# row = equal_weight(row, availability.iloc[index])
|
||||||
# row = limit_weight(row)
|
row = limit_weight(row)
|
||||||
weights.iloc[index] = row
|
weights.iloc[index] = row
|
||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
def create_quantile_weights(predictions: pd.DataFrame, availability: pd.DataFrame, allow_short: bool) -> pd.DataFrame:
|
||||||
|
weights = predictions.copy()
|
||||||
|
assert weights.shape[1] == availability.shape[1]
|
||||||
|
quantiles = weights.copy()
|
||||||
|
for column in weights.columns:
|
||||||
|
quantiles[column] = pd.qcut(weights[column], q=4, labels=False, duplicates='drop')
|
||||||
|
|
||||||
|
for index, row in quantiles.iterrows():
|
||||||
|
def only_select_bottom_top(x):
|
||||||
|
if x == 0:
|
||||||
|
return -1
|
||||||
|
elif x == 3:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
row = row.apply(only_select_bottom_top)
|
||||||
|
no_of_nonzero_predictions = row[row != 0].count()
|
||||||
|
units = min(1 / no_of_nonzero_predictions, 0.25)
|
||||||
|
weights.iloc[index] = row * units
|
||||||
|
return weights
|
||||||
|
|
||||||
|
|
||||||
predictions = pd.read_csv('output/predictions.csv', index_col=0)
|
predictions = pd.read_csv('output/predictions.csv', index_col=0)
|
||||||
predictions.columns = ['_'.join(col.replace("model_", "").split("_")[:2]) for col in predictions.columns]
|
predictions.columns = ['_'.join(col.replace("model_", "").split("_")[:2]) for col in predictions.columns]
|
||||||
@@ -100,61 +115,68 @@ close = close[predictions.columns]
|
|||||||
|
|
||||||
availability = close.applymap(lambda x: 0 if x == 0.0 or x == 0 or np.isnan(x) else 1)
|
availability = close.applymap(lambda x: 0 if x == 0.0 or x == 0 or np.isnan(x) else 1)
|
||||||
|
|
||||||
weights = create_naive_portfolio_weights(predictions, availability, allow_short=True)
|
def report_alphalens():
|
||||||
weights.index = close.index
|
alpha_factors = predictions.copy()
|
||||||
|
alpha_factors.index = close.index
|
||||||
|
|
||||||
weights_long = pd.melt(weights.reset_index(), id_vars=['time'], value_vars=weights.columns).set_index(['time', 'variable'])
|
alpha_factors_long = pd.melt(alpha_factors.reset_index(), id_vars=['time'], value_vars=alpha_factors.columns).set_index(['time', 'variable'])
|
||||||
close_long = pd.melt(close.reset_index(), id_vars=['time'], value_vars=close.columns).set_index(['time', 'variable'])
|
factor_data = get_clean_factor_and_forward_returns(
|
||||||
#%%
|
alpha_factors_long,
|
||||||
factor_data = get_clean_factor_and_forward_returns(
|
close,
|
||||||
weights_long,
|
quantiles=4,
|
||||||
close,
|
periods=(1, 2, 3, 4, 5, 6, 10),
|
||||||
# groupby=weights.columns.to_list(),
|
filter_zscore=None)
|
||||||
quantiles=4,
|
# create_full_tear_sheet(factor_data, long_short=True)
|
||||||
periods=(1, 2, 3, 4, 5, 6, 10),
|
|
||||||
filter_zscore=None)
|
|
||||||
|
|
||||||
#%%
|
from matplotlib.backends.backend_pdf import PdfPages
|
||||||
factor_data.head(10)
|
|
||||||
|
|
||||||
#%%
|
mean_return_by_q_daily, std_err = alphalens.performance.mean_return_by_quantile(factor_data, by_date=True)
|
||||||
create_full_tear_sheet(factor_data, long_short=True)
|
mean_return_by_q, std_err_by_q = alphalens.performance.mean_return_by_quantile(factor_data, by_group=False)
|
||||||
|
plot1 = alphalens.plotting.plot_quantile_returns_bar(mean_return_by_q)
|
||||||
|
plot2 = alphalens.plotting.plot_quantile_returns_violin(mean_return_by_q_daily)
|
||||||
|
plot3 = alphalens.plotting.plot_cumulative_returns_by_quantile(mean_return_by_q_daily, period='D')
|
||||||
|
|
||||||
from matplotlib.backends.backend_pdf import PdfPages
|
|
||||||
|
|
||||||
mean_return_by_q_daily, std_err = alphalens.performance.mean_return_by_quantile(factor_data, by_date=True)
|
with PdfPages('output/factors.pdf') as pdf:
|
||||||
mean_return_by_q, std_err_by_q = alphalens.performance.mean_return_by_quantile(factor_data, by_group=False)
|
pdf.savefig(plot1.figure)
|
||||||
plot1 = alphalens.plotting.plot_quantile_returns_bar(mean_return_by_q)
|
pdf.savefig(plot2.figure)
|
||||||
plot2 = alphalens.plotting.plot_quantile_returns_violin(mean_return_by_q_daily)
|
pdf.savefig(plot3.figure)
|
||||||
plot3 = alphalens.plotting.plot_cumulative_returns_by_quantile(mean_return_by_q_daily, period='D')
|
|
||||||
full_tear = create_full_tear_sheet(factor_data, long_short=True)
|
|
||||||
avg_returns = create_event_returns_tear_sheet(factor_data, close, avgretplot=(1, 3, 5), long_short=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
with PdfPages('output/factors.pdf') as pdf:
|
def report_backtest() -> vbt.Portfolio:
|
||||||
pdf.savefig(plot1.figure)
|
weights = create_quantile_weights(predictions, availability, allow_short=True)
|
||||||
pdf.savefig(plot2.figure)
|
|
||||||
pdf.savefig(plot3.figure)
|
|
||||||
|
|
||||||
# create_event_returns_tear_sheet(factor_data, close, avgretplot=(1, 3, 5), long_short=True)
|
weights.index = close.index
|
||||||
|
# rebalance every n days
|
||||||
|
# weights.iloc[np.arange(len(weights)) % 2 != 0] = np.nan
|
||||||
|
|
||||||
#%%
|
weights.to_csv('output/weights.csv')
|
||||||
|
|
||||||
pf_returns, pf_positions, pf_benchmark = alphalens.performance.create_pyfolio_input(factor_data,
|
portfolio = vbt.Portfolio.from_orders(
|
||||||
period='1D',
|
close=close,
|
||||||
capital=100000,
|
size=weights,
|
||||||
long_short=True,
|
size_type=SizeType.TargetPercent,
|
||||||
equal_weight=True,
|
direction=Direction.Both,
|
||||||
quantiles=[1,4],
|
cash_sharing=True,
|
||||||
groups=None,
|
call_seq=CallSeqType.Auto,
|
||||||
benchmark_period='1D')
|
group_by=True,
|
||||||
|
freq='1D',
|
||||||
|
raise_reject=True,
|
||||||
|
fees=0.001, # assuming 0.1% fees (1.5x of FTX)
|
||||||
|
slippage= 0.002, # assuming 0.2% slippage (5x of avg. spread on FTX)
|
||||||
|
seed=1,
|
||||||
|
init_cash=1e5,
|
||||||
|
log=True
|
||||||
|
)
|
||||||
|
|
||||||
pyfolio.tears.create_full_tear_sheet(pf_returns,
|
qs.reports.full(portfolio.returns(), portfolio.benchmark_returns())
|
||||||
positions=pf_positions,
|
|
||||||
benchmark_rets=pf_benchmark)
|
qs.reports.html(portfolio.returns(), portfolio.benchmark_returns(), output='output/report.html')
|
||||||
# rebalance every n days
|
print(portfolio.stats())
|
||||||
# weights.iloc[np.arange(len(weights)) % 7 != 0] = np.nan
|
return portfolio
|
||||||
|
|
||||||
|
portfolio = report_backtest()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -170,3 +192,5 @@ pyfolio.tears.create_full_tear_sheet(pf_returns,
|
|||||||
# cleaned_weights = ef.clean_weights()
|
# cleaned_weights = ef.clean_weights()
|
||||||
# print(ef.portfolio_performance(verbose=True))
|
# print(ef.portfolio_performance(verbose=True))
|
||||||
|
|
||||||
|
|
||||||
|
# %%
|
||||||
|
|||||||
Reference in New Issue
Block a user