8
8
Licensed under GNU LGPL.3, see LICENCE file
9
9
'''
10
10
11
-
12
11
############################################################################
13
12
### CLASSES BacktestData, BacktestService, Backtest
14
13
############################################################################
15
14
16
-
17
-
18
-
19
15
import os
20
16
from typing import Optional
21
17
import pickle
30
26
from builders import SelectionItemBuilder , OptimizationItemBuilder
31
27
32
28
29
+ class BacktestData :
30
+ """
31
+ Represents the data required for backtesting.
33
32
33
+ This class acts as a container for any data-related requirements for backtesting.
34
34
35
-
36
- class BacktestData ():
35
+ Attributes
36
+ ----------
37
+ None
38
+ """
37
39
38
40
def __init__ (self ):
39
41
pass
40
42
41
43
42
- class BacktestService ():
44
+ class BacktestService :
45
+ """
46
+ Manages backtesting services, including selection, optimization, and settings.
47
+
48
+ Attributes
49
+ ----------
50
+ data : BacktestData
51
+ The backtest data.
52
+ selection_item_builders : dict[str, SelectionItemBuilder]
53
+ Builders for selection items.
54
+ optimization_item_builders : dict[str, OptimizationItemBuilder]
55
+ Builders for optimization items.
56
+ optimization : Optional[Optimization]
57
+ The optimization instance. Defaults to `EmptyOptimization`.
58
+ settings : Optional[dict]
59
+ Additional settings for the backtest.
60
+ """
43
61
44
62
def __init__ (self ,
45
63
data : BacktestData ,
@@ -48,16 +66,34 @@ def __init__(self,
48
66
optimization : Optional [Optimization ] = EmptyOptimization (),
49
67
settings : Optional [dict ] = None ,
50
68
** kwargs ) -> None :
69
+ """
70
+ Initializes the BacktestService class.
71
+
72
+ Parameters
73
+ ----------
74
+ data : BacktestData
75
+ The backtest data.
76
+ selection_item_builders : dict
77
+ Dictionary of selection item builders.
78
+ optimization_item_builders : dict
79
+ Dictionary of optimization item builders.
80
+ optimization : Optional[Optimization], optional
81
+ Optimization instance, by default EmptyOptimization().
82
+ settings : Optional[dict], optional
83
+ Additional settings, by default None.
84
+ **kwargs :
85
+ Additional settings.
86
+ """
51
87
self .data = data
52
88
self .optimization = optimization
53
89
self .selection_item_builders = selection_item_builders
54
90
self .optimization_item_builders = optimization_item_builders
55
91
self .settings = settings if settings is not None else {}
56
92
self .settings .update (kwargs )
57
- # Initialize the selection and optimization data
58
93
self .selection = Selection ()
59
94
self .optimization_data = OptimizationData ([])
60
95
96
+
61
97
@property
62
98
def data (self ):
63
99
return self ._data
@@ -126,33 +162,66 @@ def settings(self, value):
126
162
raise TypeError ("Expected a dictionary for 'settings'" )
127
163
self ._settings = value
128
164
165
+
166
+
129
167
def build_selection (self , rebdate : str ) -> None :
130
- # Loop over the selection_item_builders items
168
+ """
169
+ Builds the selection process for a given rebalancing date.
170
+
171
+ Parameters
172
+ ----------
173
+ rebdate : str
174
+ The rebalancing date.
175
+ """
131
176
for key , item_builder in self .selection_item_builders .items ():
132
177
item_builder .arguments ['item_name' ] = key
133
178
item_builder (self , rebdate )
134
179
return None
135
180
136
181
def build_optimization (self , rebdate : str ) -> None :
137
-
138
- # Initialize the optimization constraints
139
- self .optimization .constraints = Constraints (selection = self .selection .selected )
140
-
141
- # Loop over the optimization_item_builders items
182
+ """
183
+ Builds the optimization problem for a given rebalancing date.
184
+
185
+ Parameters
186
+ ----------
187
+ rebdate : str
188
+ The rebalancing date.
189
+ """
190
+ self .optimization .constraints = Constraints (selection = self .selection .selected )
142
191
for item_builder in self .optimization_item_builders .values ():
143
192
item_builder (self , rebdate )
144
193
return None
145
194
146
195
def prepare_rebalancing (self , rebalancing_date : str ) -> None :
147
- self .build_selection (rebdate = rebalancing_date )
148
- self .build_optimization (rebdate = rebalancing_date )
196
+ """
197
+ Prepares the selection and optimization for a rebalancing date.
198
+
199
+ Parameters
200
+ ----------
201
+ rebalancing_date : str
202
+ The rebalancing date.
203
+ """
204
+ self .build_selection (rebdate = rebalancing_date )
205
+ self .build_optimization (rebdate = rebalancing_date )
149
206
return None
150
207
151
208
152
-
153
209
class Backtest :
210
+ """
211
+ Performs portfolio backtesting, including strategy building and output storage.
212
+
213
+ Attributes
214
+ ----------
215
+ strategy : Strategy
216
+ The backtesting strategy.
217
+ output : dict
218
+ The backtesting output.
219
+ """
154
220
155
221
def __init__ (self ) -> None :
222
+ """
223
+ Initializes the Backtest class.
224
+ """
156
225
self ._strategy = Strategy ([])
157
226
self ._output = {}
158
227
@@ -163,81 +232,93 @@ def strategy(self):
163
232
@property
164
233
def output (self ):
165
234
return self ._output
166
-
167
- def append_output (self ,
168
- date_key = None ,
169
- output_key = None ,
170
- value = None ):
235
+
236
+
237
+ def append_output (self , date_key = None , output_key = None , value = None ):
238
+ """
239
+ Appends output data for a specific date and output key.
240
+
241
+ Parameters
242
+ ----------
243
+ date_key : str, optional
244
+ The date key for the output.
245
+ output_key : str, optional
246
+ The output key.
247
+ value : any, optional
248
+ The value to append.
249
+ """
171
250
if value is None :
172
251
return True
173
-
174
252
if date_key in self .output .keys ():
175
253
if output_key in self .output [date_key ].keys ():
176
- raise Warning (f"Output key '{ output_key } ' for date key '{ date_key } ' \
177
- already exists and will be overwritten." )
254
+ raise Warning (f"Output key '{ output_key } ' for date key '{ date_key } ' already exists and will be overwritten." )
178
255
self .output [date_key ][output_key ] = value
179
256
else :
180
257
self .output [date_key ] = {}
181
258
self .output [date_key ].update ({output_key : value })
182
-
183
259
return True
184
260
185
- def rebalance (self ,
186
- bs : BacktestService ,
187
- rebalancing_date : str ) -> None :
188
-
189
- # Prepare the rebalancing, i.e., the optimization problem
190
- bs .prepare_rebalancing (rebalancing_date = rebalancing_date )
191
-
192
- # Solve the optimization problem
261
+ def rebalance (self , bs : BacktestService , rebalancing_date : str ) -> None :
262
+ """
263
+ Performs portfolio rebalancing for a given date.
264
+
265
+ Parameters
266
+ ----------
267
+ bs : BacktestService
268
+ The backtesting service instance.
269
+ rebalancing_date : str
270
+ The rebalancing date.
271
+ """
272
+ bs .prepare_rebalancing (rebalancing_date = rebalancing_date )
193
273
try :
194
- bs .optimization .set_objective (optimization_data = bs .optimization_data )
274
+ bs .optimization .set_objective (optimization_data = bs .optimization_data )
195
275
bs .optimization .solve ()
196
276
except Exception as error :
197
277
raise RuntimeError (error )
198
-
199
278
return None
200
279
201
280
def run (self , bs : BacktestService ) -> None :
202
-
281
+ """
282
+ Executes the backtest for all rebalancing dates.
283
+
284
+ Parameters
285
+ ----------
286
+ bs : BacktestService
287
+ The backtesting service instance.
288
+ """
203
289
for rebalancing_date in bs .settings ['rebdates' ]:
204
-
205
290
if not bs .settings .get ('quiet' ):
206
291
print (f'Rebalancing date: { rebalancing_date } ' )
207
-
208
- self .rebalance (bs = bs ,
209
- rebalancing_date = rebalancing_date )
210
-
211
- # Append portfolio to strategy
292
+ self .rebalance (bs = bs , rebalancing_date = rebalancing_date )
212
293
weights = bs .optimization .results ['weights' ]
213
- portfolio = Portfolio (rebalancing_date = rebalancing_date , weights = weights )
294
+ portfolio = Portfolio (rebalancing_date = rebalancing_date , weights = weights )
214
295
self .strategy .portfolios .append (portfolio )
215
-
216
- # Append stuff to output if a custom append function is provided
217
296
append_fun = bs .settings .get ('append_fun' )
218
297
if append_fun is not None :
219
- append_fun (backtest = self ,
220
- bs = bs ,
221
- rebalancing_date = rebalancing_date ,
222
- what = bs .settings .get ('append_fun_args' ))
223
-
298
+ append_fun (backtest = self , bs = bs , rebalancing_date = rebalancing_date , what = bs .settings .get ('append_fun_args' ))
224
299
return None
225
300
226
- def save (self ,
227
- filename : str ,
228
- path : Optional [str ] = None ) -> None :
301
+ def save (self , filename : str , path : Optional [str ] = None ) -> None :
302
+ """
303
+ Saves the backtest object to a file.
304
+
305
+ Parameters
306
+ ----------
307
+ filename : str
308
+ The filename for the output file.
309
+ path : Optional[str], optional
310
+ The path where the file should be saved.
311
+ """
229
312
try :
230
313
if path is not None and filename is not None :
231
- filename = os .path .join (path , filename ) #// alternatively, use pathlib package
314
+ filename = os .path .join (path , filename )
232
315
with open (filename , "wb" ) as f :
233
316
pickle .dump (self , f , protocol = pickle .HIGHEST_PROTOCOL )
234
317
except Exception as ex :
235
318
print ("Error during pickling object:" , ex )
236
-
237
319
return None
238
320
239
321
240
-
241
322
# --------------------------------------------------------------------------
242
323
# Helper functions
243
324
# --------------------------------------------------------------------------
@@ -246,25 +327,39 @@ def append_custom(backtest: Backtest,
246
327
bs : BacktestService ,
247
328
rebalancing_date : Optional [str ] = None ,
248
329
what : Optional [list ] = None ) -> None :
249
-
330
+ """
331
+ Appends custom data to the backtest output.
332
+
333
+ Parameters
334
+ ----------
335
+ backtest : Backtest
336
+ The backtest instance.
337
+ bs : BacktestService
338
+ The backtesting service instance.
339
+ rebalancing_date : Optional[str], optional
340
+ The rebalancing date.
341
+ what : Optional[list], optional
342
+ List of output keys to append.
343
+ """
250
344
if what is None :
251
345
what = ['w_dict' , 'objective' ]
252
-
253
346
for key in what :
254
347
if key == 'w_dict' :
255
348
w_dict = bs .optimization .results ['w_dict' ]
256
349
for key in w_dict .keys ():
257
- weights = w_dict [key ]
350
+ weights = w_dict [key ]
258
351
if hasattr (weights , 'to_dict' ):
259
352
weights = weights .to_dict ()
260
- portfolio = Portfolio (rebalancing_date = rebalancing_date , weights = weights )
261
- backtest .append_output (date_key = rebalancing_date ,
262
- output_key = f'weights_{ key } ' ,
263
- value = pd .Series (portfolio .weights ))
353
+ portfolio = Portfolio (rebalancing_date = rebalancing_date , weights = weights )
354
+ backtest .append_output (date_key = rebalancing_date ,
355
+ output_key = f'weights_{ key } ' ,
356
+ value = pd .Series (portfolio .weights ))
264
357
else :
265
358
if not key in bs .optimization .results .keys ():
266
359
continue
267
- backtest .append_output (date_key = rebalancing_date ,
268
- output_key = key ,
269
- value = bs .optimization .results [key ])
360
+ backtest .append_output (date_key = rebalancing_date ,
361
+ output_key = key ,
362
+ value = bs .optimization .results [key ])
270
363
return None
364
+
365
+
0 commit comments