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