-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Nested Backtesting Objects #896
-
Hi All,
I am trying to do a Multi-Year Backtest (MYB) - using minutely resolution time series data. I have a set of bespoke indicators whose behaviour triggers trading. These indicators are arranged as a 3d grid of shape (a x b x len(prices)
stored as a dictionary: The indicators are based solely on autoregressive style maths - they only take into account the time-series data of the instrument.
{ ( a1 , b1 ) : vals@len(prices) , ( a1 , b2 ) : vals@len(prices) , ... }
.
I preload these indicators as usual and then feed the (a,b)
hyperparameter grid into the backtest optimiser as:
bt.optimize(
a = range(1,10),
b = range(1,10),
maximize=maximiser,
method="grid"
)
Within the strategy object I access the values but passing the (a,b)
parameters as keys to the dictionary and use self.I
to create the indicator(s). There is an eventuality where I will look at indicators crossing so in the example below I have included 2 for clarity. All of this is standard stuff.
HOWEVER - I need to add a layer of complexity to this which I can't figure out. In real life, I will be re-optimising my strategy parameters based on the current equity performance of the model. So midway through the MYB, I will need to make use of different (a,b)
parameters, and not just find out which (a,b)
parameters worked over that full-term period. This will happen at undetermined intervals based on the nature of the times-series data and the performance of the model. I need to nest backtests.
Again - in real-life - the MYB occurs over multiple years, but the strategy parameter selection done at outset (and at any point mid-way through) will occur over only a few days, maybe less - that's user defined. As far as I can see - at the outset I could bt.optimize
on prices.iloc[:5000]
and then bt.run
on prices.iloc[5000:]
using the results from the optimisation. But this does not allow for any mid-MYB re-optimisation.
MY THINKING SO FAR:
I can access the current equity of the model during 'run-time' via the protected broker._equity
object (I patiently await your scorn), I can use that to assess whether or not to perform a re-optimisation, so the condition for mid-MYB re-optimisation is easy. However, I am stumped at how I go about actually re-optimising mid-way through. And then equally, after doing this how I pass the new (a,b)
results from the mid-MYB bt.optimize
back into the higher-leverl MYB strategy object.
class MyStrategy(Strategy): # precompute full indicator grid and initialise hyper-params inds: dict = CustomIndicator().indicators a1: int = 1 b1: int = 10 a2: int = 10 b2: int = 1 def init(self,): self.ind1 = self.I(lambda x: x, self.inds[(self.a1,self.b1)]) self.ind2 = self.I(lambda x: x, self.inds[(self.a2,self.b2)]) def next(self): if broker._equity < broker._cash_initial * (some_max_allowable_loss): inner_cash = self._cash # do I have to recreate a Strategy Class here? innerbt = Backtest( self._data[-5000:], ["WHAT GOES HERE AND HOW?"], # Should be a Strategy Object I think? cash=inner_cash, ) new_result = bt.optimize( a = range(1,10), b = range(1,10), maximize=maximiser, method="grid" ) # Can I update the currently working indicators of the MYB? self.ind1 = self.I(lambda x: x, self.inds[(new_result._strategy.a1,new_result._strategy.b1)] self.ind2 = self.I(lambda x: x, self.inds[(new_result._strategy.a2,new_result._strategy.b2)] if crossover(self.ind1 ,self.ind2): if not self.position: self.buy() elif self.position.is_short: self.position.close() self.buy() elif crossover(self.ind2 ,self.ind1): if not self.position: self.sell() elif self.position.is_long: self.position.close() self.sell() else: pass
MY QUESTION:
Is any of this possible? Am I looking at this in the right way? How would you go about this? How do I define an inner-strategy which draws some of its information from an outer-strategy? Do I need to define a new strategy class within the conditional?
Any pointer here would be very helpful!
Thanks in advance,
K
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment
-
I can access the current equity of the model during 'run-time' via the protected
broker._equity
object (I patiently await your scorn)
Underscored properties, other than keys on stats
, are private and implementation-specific and may disappear/change from one framework version to the next without notice.
That out of the way, I hope you find helpful the following example. I certainly think you need two separate strategy classes.
initial_cash = 1_000_000 class ABCrossStrat(Strategy): a: int = 1 b: int = 10 def next(self): if crossover(...): ... ... class MYBStrat(Strategy): def next(self): if self.equity < initial_cash * some_max_allowable_loss: inner_bt = Backtest(self.data[-5000:], ABCrossStrat) stats = inner_bt.optimize(...) # Do further investing according to best ABCrossStrat self.a, self.b = stats._strategy.a, stats._strategy.b # Calling by reference here to avoid code dup ABCrossStrat.next(self) stats = Backtest(df, MYBStrat, cash=initial_cash)
Beta Was this translation helpful? Give feedback.