Skip to content

Commit ecd0efe

Browse files
committed
Create comdty_spread_roll.py
1 parent cdd0f68 commit ecd0efe

File tree

1 file changed

+224
-0
lines changed

1 file changed

+224
-0
lines changed

backtest/comdty_spread_roll.py

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
'''
2+
Comdty roll according to roll schedule
3+
'''
4+
import os
5+
import numpy as np
6+
import pandas as pd
7+
import pytz
8+
from datetime import datetime, timezone
9+
import multiprocessing
10+
import talib
11+
import quanttrader as qt
12+
import matplotlib.pyplot as plt
13+
import empyrical as ep
14+
import pyfolio as pf
15+
import futures_tools
16+
import data_loader
17+
# set browser full width
18+
from IPython.core.display import display, HTML
19+
pd.set_option('display.max_columns', None)
20+
display(HTML("<style>.container { width:100% !important; }</style>"))
21+
22+
23+
class ComdtySpreadMonthlyRoll(qt.StrategyBase):
24+
def __init__(self,
25+
n_roll_ahead=0, # 0 is last day roll, 1 is penultimate day, and so on
26+
n_leg1=0, # 0 is front month, 1 is second month, and so on
27+
n_leg2=1,
28+
):
29+
super(ComdtySpreadMonthlyRoll, self).__init__()
30+
self.n_roll_ahead = n_roll_ahead
31+
self.n_leg1= n_leg1
32+
self.n_leg2 = n_leg2
33+
self.sym = 'CL'
34+
self.current_time = None
35+
self.df_meta = data_loader.load_futures_meta(self.sym)
36+
self.holding_contract = None
37+
38+
def on_tick(self, tick_event):
39+
"""
40+
front_contract decides when to roll
41+
if not roll ==> if no holding_contract, sell leg1, buy leg2; else do nothing
42+
if roll ==> if no holding_contract, sell leg1+1, buy leg2+1; else close out spread as well (buy leg1 and sell leg2)
43+
"""
44+
super().on_tick(tick_event)
45+
self.current_time = tick_event.timestamp
46+
#symbol = self.symbols[0]
47+
#df_hist = self._data_board.get_hist_price(symbol, tick_event.timestamp)
48+
df_time_idx = self._data_board.get_hist_time_index()
49+
50+
df_live_futures = futures_tools.get_futures_chain(meta_data = self.df_meta, asofdate = self.current_time.replace(tzinfo=None)) # remove tzinfo
51+
# front_contract = df_live_futures.index[0]
52+
leg1 = df_live_futures.index[self.n_leg1]
53+
leg1new = df_live_futures.index[self.n_leg1+1]
54+
leg2 = df_live_futures.index[self.n_leg2]
55+
leg2new = df_live_futures.index[self.n_leg2+1]
56+
exp_date = pytz.timezone('US/Eastern').localize(df_live_futures.Last_Trade_Date[0]) # front contract
57+
dte = df_time_idx.searchsorted(exp_date) - df_time_idx.searchsorted(self.current_time) # 0 is expiry date
58+
59+
if self.n_roll_ahead < dte: # not ready to roll
60+
if self.holding_contract is None: # empty
61+
print(f'{self.current_time}, dte {dte}, sell {leg1} buy {leg2}')
62+
self.adjust_position(leg1, size_from=0, size_to=-1, timestamp=self.current_time)
63+
self.adjust_position(leg2, size_from=0, size_to=1, timestamp=self.current_time)
64+
self.holding_contract = leg1
65+
else:
66+
if self.holding_contract is None: # empty
67+
print(f'{self.current_time}, dte {dte}, sell {leg1new} buy {leg2new}')
68+
self.adjust_position(leg1new, size_from=0, size_to=-1, timestamp=self.current_time)
69+
self.adjust_position(leg2new, size_from=0, size_to=1, timestamp=self.current_time)
70+
self.holding_contract = leg1new
71+
else:
72+
if self.holding_contract == leg1new: # already rolled this month
73+
pass
74+
else:
75+
print(f'{self.current_time}, dte {dte}, roll short spread {leg1}-{leg2} to {leg1new}-{leg2new}')
76+
self.adjust_position(leg1, size_from=-1, size_to=0, timestamp=self.current_time)
77+
self.adjust_position(leg2, size_from=1, size_to=0, timestamp=self.current_time)
78+
self.adjust_position(leg1new, size_from=0, size_to=-1, timestamp=self.current_time)
79+
self.adjust_position(leg2new, size_from=0, size_to=1, timestamp=self.current_time)
80+
self.holding_contract = leg1new
81+
82+
83+
def parameter_search(symbol, init_capital, sd, ed, df_data, params, target_name, return_dict):
84+
"""
85+
This function should be the same for all strategies.
86+
The only reason not included in quanttrader is because of its dependency on pyfolio (to get perf_stats)
87+
"""
88+
strategy = ComdtySpreadMonthlyRoll()
89+
strategy.set_capital(init_capital)
90+
strategy.set_symbols([symbol])
91+
engine = qt.BacktestEngine(sd, ed)
92+
engine.set_capital(init_capital) # capital or portfolio >= capital for one strategy
93+
engine.add_data(symbol, df_data)
94+
strategy.set_params({'n_roll_ahead': params['n_roll_ahead'], 'n_leg1': params['n_leg1'], 'n_leg2': params['n_leg2']})
95+
engine.set_strategy(strategy)
96+
ds_equity, _, _ = engine.run()
97+
try:
98+
strat_ret = ds_equity.pct_change().dropna()
99+
perf_stats_strat = pf.timeseries.perf_stats(strat_ret)
100+
target_value = perf_stats_strat.loc[target_name] # first table in tuple
101+
except KeyError:
102+
target_value = 0
103+
return_dict[(params['n_roll_ahead'], params['n_leg1'], params['n_leg2'])] = target_value
104+
105+
106+
if __name__ == '__main__':
107+
do_optimize = True
108+
run_in_jupyter = False
109+
symbol = 'CL'
110+
benchmark = None
111+
init_capital = 100_000.0
112+
df_future = data_loader.load_futures_hist_prices(symbol)
113+
df_future.index = df_future.index.tz_localize('US/Eastern')
114+
test_start_date = datetime(2019, 1, 1, 0, 0, 0, 0, pytz.timezone('US/Eastern'))
115+
test_end_date = datetime(2021, 12, 30, 0, 0, 0, 0, pytz.timezone('US/Eastern'))
116+
117+
init_capital = 50.0
118+
if do_optimize: # parallel parameter search
119+
params_list = []
120+
for n_roll_ahead in range(20):
121+
for n_leg1 in range(12):
122+
for n_leg2 in range(12):
123+
if n_leg1 >= n_leg2:
124+
continue
125+
params_list.append({'n_roll_ahead': n_roll_ahead, 'n_leg1': n_leg1, 'n_leg2': n_leg2})
126+
target_name = 'Sharpe ratio'
127+
manager = multiprocessing.Manager()
128+
return_dict = manager.dict()
129+
jobs = []
130+
for params in params_list:
131+
p = multiprocessing.Process(target=parameter_search, args=(symbol, init_capital, test_start_date, test_end_date, df_future, params, target_name, return_dict))
132+
jobs.append(p)
133+
p.start()
134+
135+
for proc in jobs:
136+
proc.join()
137+
for k,v in return_dict.items():
138+
print(k, v)
139+
else:
140+
strategy = ComdtySpreadMonthlyRoll()
141+
strategy.set_capital(init_capital)
142+
strategy.set_symbols([symbol])
143+
strategy.set_params({'n_roll_ahead': 0, 'n_leg1': 0, 'n_leg2': 2})
144+
145+
# Create a Data Feed
146+
backtest_engine = qt.BacktestEngine(test_start_date, test_end_date)
147+
backtest_engine.set_capital(init_capital) # capital or portfolio >= capital for one strategy
148+
backtest_engine.add_data(symbol, df_future)
149+
backtest_engine.set_strategy(strategy)
150+
ds_equity, df_positions, df_trades = backtest_engine.run()
151+
# save to excel
152+
qt.util.save_one_run_results('./output', ds_equity, df_positions, df_trades)
153+
154+
# ------------------------- Evaluation and Plotting -------------------------------------- #
155+
strat_ret = ds_equity.pct_change().dropna()
156+
strat_ret.name = 'strat'
157+
# bm = qt.util.read_ohlcv_csv(os.path.join('../data/', f'{benchmark}.csv'))
158+
# bm_ret = bm['Close'].pct_change().dropna()
159+
# bm_ret.index = pd.to_datetime(bm_ret.index)
160+
# bm_ret = bm_ret[strat_ret.index]
161+
# bm_ret.name = 'benchmark'
162+
bm_ret = strat_ret.copy()
163+
bm_ret.name = 'benchmark'
164+
165+
perf_stats_strat = pf.timeseries.perf_stats(strat_ret)
166+
perf_stats_all = perf_stats_strat
167+
perf_stats_bm = pf.timeseries.perf_stats(bm_ret)
168+
perf_stats_all = pd.concat([perf_stats_strat, perf_stats_bm], axis=1)
169+
perf_stats_all.columns = ['Strategy', 'Benchmark']
170+
171+
drawdown_table = pf.timeseries.gen_drawdown_table(strat_ret, 5)
172+
monthly_ret_table = ep.aggregate_returns(strat_ret, 'monthly')
173+
monthly_ret_table = monthly_ret_table.unstack().round(3)
174+
ann_ret_df = pd.DataFrame(ep.aggregate_returns(strat_ret, 'yearly'))
175+
ann_ret_df = ann_ret_df.unstack().round(3)
176+
177+
print('-------------- PERFORMANCE ----------------')
178+
print(perf_stats_all)
179+
print('-------------- DRAWDOWN ----------------')
180+
print(drawdown_table)
181+
print('-------------- MONTHLY RETURN ----------------')
182+
print(monthly_ret_table)
183+
print('-------------- ANNUAL RETURN ----------------')
184+
print(ann_ret_df)
185+
186+
if run_in_jupyter:
187+
pf.create_full_tear_sheet(
188+
strat_ret,
189+
benchmark_rets=bm_ret,
190+
positions=df_positions,
191+
transactions=df_trades,
192+
round_trips=False)
193+
plt.show()
194+
else:
195+
f1 = plt.figure(1)
196+
pf.plot_rolling_returns(strat_ret, factor_returns=bm_ret)
197+
f1.show()
198+
f2 = plt.figure(2)
199+
pf.plot_rolling_volatility(strat_ret, factor_returns=bm_ret)
200+
f2.show()
201+
f3 = plt.figure(3)
202+
pf.plot_rolling_sharpe(strat_ret)
203+
f3.show()
204+
f4 = plt.figure(4)
205+
pf.plot_drawdown_periods(strat_ret)
206+
f4.show()
207+
f5 = plt.figure(5)
208+
pf.plot_monthly_returns_heatmap(strat_ret)
209+
f5.show()
210+
f6 = plt.figure(6)
211+
pf.plot_annual_returns(strat_ret)
212+
f6.show()
213+
f7 = plt.figure(7)
214+
pf.plot_monthly_returns_dist(strat_ret)
215+
plt.show()
216+
f8 = plt.figure(8)
217+
pf.create_position_tear_sheet(strat_ret, df_positions)
218+
plt.show()
219+
f9 = plt.figure(9)
220+
pf.create_txn_tear_sheet(strat_ret, df_positions, df_trades)
221+
plt.show()
222+
f10 = plt.figure(10)
223+
pf.create_round_trip_tear_sheet(strat_ret, df_positions, df_trades)
224+
plt.show()

0 commit comments

Comments
 (0)