Skip to content

Commit d9c3f41

Browse files
committed
Added docstrings and improved documentation. Related to GeomScale#6
1 parent 53db9cd commit d9c3f41

12 files changed

+1988
-473
lines changed

src/backtest.py

+162-67
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@
88
Licensed under GNU LGPL.3, see LICENCE file
99
'''
1010

11-
1211
############################################################################
1312
### CLASSES BacktestData, BacktestService, Backtest
1413
############################################################################
1514

16-
17-
18-
1915
import os
2016
from typing import Optional
2117
import pickle
@@ -30,16 +26,38 @@
3026
from builders import SelectionItemBuilder, OptimizationItemBuilder
3127

3228

29+
class BacktestData:
30+
"""
31+
Represents the data required for backtesting.
3332
33+
This class acts as a container for any data-related requirements for backtesting.
3434
35-
36-
class BacktestData():
35+
Attributes
36+
----------
37+
None
38+
"""
3739

3840
def __init__(self):
3941
pass
4042

4143

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+
"""
4361

4462
def __init__(self,
4563
data: BacktestData,
@@ -48,16 +66,34 @@ def __init__(self,
4866
optimization: Optional[Optimization] = EmptyOptimization(),
4967
settings: Optional[dict] = None,
5068
**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+
"""
5187
self.data = data
5288
self.optimization = optimization
5389
self.selection_item_builders = selection_item_builders
5490
self.optimization_item_builders = optimization_item_builders
5591
self.settings = settings if settings is not None else {}
5692
self.settings.update(kwargs)
57-
# Initialize the selection and optimization data
5893
self.selection = Selection()
5994
self.optimization_data = OptimizationData([])
6095

96+
6197
@property
6298
def data(self):
6399
return self._data
@@ -126,33 +162,66 @@ def settings(self, value):
126162
raise TypeError("Expected a dictionary for 'settings'")
127163
self._settings = value
128164

165+
166+
129167
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+
"""
131176
for key, item_builder in self.selection_item_builders.items():
132177
item_builder.arguments['item_name'] = key
133178
item_builder(self, rebdate)
134179
return None
135180

136181
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)
142191
for item_builder in self.optimization_item_builders.values():
143192
item_builder(self, rebdate)
144193
return None
145194

146195
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)
149206
return None
150207

151208

152-
153209
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+
"""
154220

155221
def __init__(self) -> None:
222+
"""
223+
Initializes the Backtest class.
224+
"""
156225
self._strategy = Strategy([])
157226
self._output = {}
158227

@@ -163,81 +232,93 @@ def strategy(self):
163232
@property
164233
def output(self):
165234
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+
"""
171250
if value is None:
172251
return True
173-
174252
if date_key in self.output.keys():
175253
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.")
178255
self.output[date_key][output_key] = value
179256
else:
180257
self.output[date_key] = {}
181258
self.output[date_key].update({output_key: value})
182-
183259
return True
184260

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)
193273
try:
194-
bs.optimization.set_objective(optimization_data = bs.optimization_data)
274+
bs.optimization.set_objective(optimization_data=bs.optimization_data)
195275
bs.optimization.solve()
196276
except Exception as error:
197277
raise RuntimeError(error)
198-
199278
return None
200279

201280
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+
"""
203289
for rebalancing_date in bs.settings['rebdates']:
204-
205290
if not bs.settings.get('quiet'):
206291
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)
212293
weights = bs.optimization.results['weights']
213-
portfolio = Portfolio(rebalancing_date = rebalancing_date, weights = weights)
294+
portfolio = Portfolio(rebalancing_date=rebalancing_date, weights=weights)
214295
self.strategy.portfolios.append(portfolio)
215-
216-
# Append stuff to output if a custom append function is provided
217296
append_fun = bs.settings.get('append_fun')
218297
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'))
224299
return None
225300

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+
"""
229312
try:
230313
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)
232315
with open(filename, "wb") as f:
233316
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
234317
except Exception as ex:
235318
print("Error during pickling object:", ex)
236-
237319
return None
238320

239321

240-
241322
# --------------------------------------------------------------------------
242323
# Helper functions
243324
# --------------------------------------------------------------------------
@@ -246,25 +327,39 @@ def append_custom(backtest: Backtest,
246327
bs: BacktestService,
247328
rebalancing_date: Optional[str] = None,
248329
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+
"""
250344
if what is None:
251345
what = ['w_dict', 'objective']
252-
253346
for key in what:
254347
if key == 'w_dict':
255348
w_dict = bs.optimization.results['w_dict']
256349
for key in w_dict.keys():
257-
weights = w_dict[key]
350+
weights = w_dict[key]
258351
if hasattr(weights, 'to_dict'):
259352
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))
264357
else:
265358
if not key in bs.optimization.results.keys():
266359
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])
270363
return None
364+
365+

0 commit comments

Comments
 (0)