Closed wesleywilian closed 4 years ago
self.timeseries["sma1"] = self.I(SMA, self.data.Close, self.n1)
I'm working on a dynamic strategy feature, that's why i need use the dicts, how could i fix this?
That's the root of the issue: Backtest
scours strategy instance for attributes that are indicators (values returned by Strategy.I()
):
https://github.com/kernc/backtesting.py/blob/0436e56ab42186a167c5ee187ff33e40eb39334b/backtesting/backtesting.py#L692-L697
One way you could probably achieve this is:
def init(self):
# Set attrs on strategy directly, e.g. self.sma1
# This will ensure they are picked up and run-through by Backtest
setattr(self, "sma1", self.I(SMA, self.data.Close, self.n1))
...
# Additionally, put them in a commonly accessible dict for later use elsewhere
self.timeseries["sma1"] = getattr(self, "sma1")
Thanks @kernc
I tried this, but no trades were made as well.
from backtesting.test import GOOG
from backtesting.test import SMA
from backtesting import Strategy
from backtesting.lib import crossover
from backtesting import Backtest
class SmaCross(Strategy):
# Define the two MA lags as *class variables*
# for later optimization
n1 = 10
n2 = 20
timeseries = {}
def init(self):
# Precompute two moving averages
setattr(self, "sma1", self.I(SMA, self.data.Close, self.n1))
setattr(self, "sma2", self.I(SMA, self.data.Close, self.n2))
self.timeseries["sma1"] = getattr(self, "sma1")
self.timeseries["sma2"] = getattr(self, "sma2")
def next(self):
# If sma1 crosses above sma2, buy the asset
if crossover(self.timeseries["sma1"], self.timeseries["sma2"]):
self.buy()
# Else, if sma1 crosses below sma2, sell it
elif crossover(self.timeseries["sma2"], self.timeseries["sma1"]):
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002)
print(bt.run())
So, with your explanation, the better approach will be use the getattr and setattr like this (discarding the dicts approach)
from backtesting.test import GOOG
from backtesting.test import SMA
from backtesting import Strategy
from backtesting.lib import crossover
from backtesting import Backtest
class SmaCross(Strategy):
# Define the two MA lags as *class variables*
# for later optimization
n1 = 10
n2 = 20
def init(self):
# Precompute two moving averages
setattr(self, "small sma", self.I(SMA, self.data.Close, self.n1))
setattr(self, "large sma", self.I(SMA, self.data.Close, self.n2))
def next(self):
# If sma1 crosses above sma2, buy the asset
if crossover(getattr(self, "small sma"), getattr(self, "large sma")):
self.buy()
# Else, if sma1 crosses below sma2, sell it
elif crossover(getattr(self, "large sma"), getattr(self, "small sma")):
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002)
print(bt.run())
Output
Start 2004-08-19 00:00:00
End 2013-03-01 00:00:00
Duration 3116 days 00:00:00
Exposure [%] 94.2875
Equity Final [$] 69665.1
Equity Peak [$] 69722.1
Return [%] 596.651
Buy & Hold Return [%] 703.458
Max. Drawdown [%] -33.6059
Avg. Drawdown [%] -5.62833
Max. Drawdown Duration 689 days 00:00:00
Avg. Drawdown Duration 41 days 00:00:00
# Trades 93
Win Rate [%] 53.7634
Best Trade [%] 56.9786
Worst Trade [%] -17.0259
Avg. Trade [%] 2.44454
Max. Trade Duration 121 days 00:00:00
Avg. Trade Duration 32 days 00:00:00
Expectancy [%] 6.91837
SQN 1.77227
Sharpe Ratio 0.220629
Sortino Ratio 0.541607
Calmar Ratio 0.0727415
_strategy SmaCross
dtype: object
Thanks @kernc
Ah, right, indicators are copied when sliced before each next()
call:
https://github.com/kernc/backtesting.py/blob/0436e56ab42186a167c5ee187ff33e40eb39334b/backtesting/backtesting.py#L711-L713
This wouldn't update self.timeseries
entries in-place. I'll think about it.
@kernc I am working a similar issue where I need to use the abstract module of talib
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import GOOG
from talib import abstract
class SmaCross(Strategy):
def init(self):
Ind = abstract.Function('sma')
inputs = {
'open': self.data.Open,
'high': self.data.High,
'low': self.data.Low,
'close': self.data.Close,
'volume': self.data.Volume
}
self.ma1 = self.I(Ind, inputs, 10)
self.ma2 = self.I(Ind, inputs, 20)
def next(self):
if crossover(self.ma1, self.ma2):
self.buy()
elif crossover(self.ma2, self.ma1):
self.sell()
bt = Backtest(GOOG, SmaCross,
cash=10000, commission=.002)
print(bt.run())
bt.plot()
I need this feature as I am dynamically getting Indicators using just function name
Running this ends up in this error
Traceback (most recent call last):
File "/home/skywalker/PycharmProjects/backtester/backtest.py", line 30, in <module>
print(bt.run())
File "/home/skywalker/PycharmProjects/backtester/venv/lib/python3.8/site-packages/backtesting/backtesting.py", line 692, in run
strategy.init()
File "/home/skywalker/PycharmProjects/backtester/backtest.py", line 18, in init
self.ma1 = self.I(Ind, inputs, 10)
File "/home/skywalker/PycharmProjects/backtester/venv/lib/python3.8/site-packages/backtesting/backtesting.py", line 118, in I
func_name = _as_str(func)
File "/home/skywalker/PycharmProjects/backtester/venv/lib/python3.8/site-packages/backtesting/_util.py", line 24, in _as_str
name = value.__name__.replace('<lambda>', 'λ')
AttributeError: 'Function' object has no attribute '__name__'
The issue seems to be way the function is constructed later in the backtesting.py
if name is None:
params = ','.join(filter(None, map(_as_str, chain(args, kwargs.values()))))
func_name = _as_str(func)
name = ('{}({})' if params else '{}').format(func_name, params)
Any tips on how to get around it?
More precisely, the issue is the assumption that all callables define __name__
attribute:
https://github.com/kernc/backtesting.py/blob/0436e56ab42186a167c5ee187ff33e40eb39334b/backtesting/_util.py#L23-L24
@arunavo4 Can you open a separate issue/PR for it?
@kernc sure, https://github.com/kernc/backtesting.py/issues/93
Hi @kernc
So, with your explanation, the better approach will be use the getattr and setattr like this (discarding the dicts approach)
Unfortunately the native optimization will not work with this approach.
setattr(bt, "SMA_close_10", range(5, 10, 1))
setattr(bt, "SMA_close_30", range(5, 30, 5))
setattr(bt, "maximize", 'Equity Final [$]')
stats = bt.optimize()
Gives
ValueError: Need some strategy parameters to optimize
Nobody claims you can setattr()
upon a Backtest
instance ... :confused:
The parameters to optimize need to be passed to Backtest.optimize()
method as kwargs.
Indeed,
I was just wondering, because with kwargs the strategy cannot be dynamic :(
The attributes cannot be dynamic for optimizations
The var names cannot be dynamic
stats = bt.optimize(SMA_close_10=range(5, 30, 5),
SMA_close_30=range(10, 70, 5),
maximize='Equity Final [$]')
The existence as well...
stats = bt.optimize(SMA_close_10=range(5, 30, 5),
maximize='Equity Final [$]')
Then, I will try to adapt the backtesting.py using something like this
stats = bt.optimize(optimization={"SMA_close_10": range(5, 30, 5),
"maximize": 'Equity Final [$]'})
Then replacing kwargs inside of the optimize() function from "optimization" dict
would you have another idea?
Thanks
Something like this might work:
stats = bt.optimize(optimization_dict=[
{"SMA_close_10": 5},
{"SMA_close_10": 10},
{"SMA_close_10": 15},
{"SMA_close_10": 20},
{"SMA_close_10": 25},
], maximize='Equity Final [$]')
Where optimization_dict
is timeseries
in terms of https://github.com/kernc/backtesting.py/issues/90#issuecomment-645930051.
@wesleywilian Did that work?
Hi @kernc sorry for late response.
The code below
from backtesting.test import GOOG
from backtesting.test import SMA
from backtesting import Strategy
from backtesting.lib import crossover
from backtesting import Backtest
class SmaCross(Strategy):
# Define the two MA lags as *class variables*
# for later optimization
def init(self):
# Precompute two moving averages
setattr(self, "small sma", self.I(SMA, self.data.Close, 10))
setattr(self, "large sma", self.I(SMA, self.data.Close, 20))
def next(self):
# If sma1 crosses above sma2, buy the asset
if crossover(getattr(self, "small sma"), getattr(self, "large sma")):
self.buy()
# Else, if sma1 crosses below sma2, sell it
elif crossover(getattr(self, "large sma"), getattr(self, "small sma")):
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002)
#print(bt.run())
stats = bt.optimize(optimization_dict=[
{"SMA_close_10": 5},
{"SMA_close_10": 10},
{"SMA_close_10": 15},
{"SMA_close_10": 20},
{"SMA_close_10": 25},
], maximize='Equity Final [$]')
Result in
Traceback (most recent call last):
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/arrays/categorical.py", line 384, in __init__
codes, categories = factorize(values, sort=True)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/util/_decorators.py", line 208, in wrapper
return func(*args, **kwargs)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/algorithms.py", line 672, in factorize
values, na_sentinel=na_sentinel, size_hint=size_hint, na_value=na_value
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/algorithms.py", line 508, in _factorize_array
values, na_sentinel=na_sentinel, na_value=na_value
File "pandas/_libs/hashtable_class_helper.pxi", line 1798, in pandas._libs.hashtable.PyObjectHashTable.factorize
File "pandas/_libs/hashtable_class_helper.pxi", line 1718, in pandas._libs.hashtable.PyObjectHashTable._unique
TypeError: unhashable type: 'dict'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/estrutura/opt/triggerbot/test/lib_backtesting_test1.py", line 36, in <module>
], maximize='Equity Final [$]')
File "/home/wesley/.local/lib/python3.6/site-packages/backtesting/backtesting.py", line 823, in optimize
names=next(iter(param_combos)).keys()))
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/indexes/multi.py", line 489, in from_tuples
return MultiIndex.from_arrays(arrays, sortorder=sortorder, names=names)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/indexes/multi.py", line 420, in from_arrays
codes, levels = _factorize_from_iterables(arrays)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/arrays/categorical.py", line 2816, in _factorize_from_iterables
return map(list, zip(*(_factorize_from_iterable(it) for it in iterables)))
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/arrays/categorical.py", line 2816, in <genexpr>
return map(list, zip(*(_factorize_from_iterable(it) for it in iterables)))
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/arrays/categorical.py", line 2788, in _factorize_from_iterable
cat = Categorical(values, ordered=False)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/arrays/categorical.py", line 386, in __init__
codes, categories = factorize(values, sort=False)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/util/_decorators.py", line 208, in wrapper
return func(*args, **kwargs)
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/algorithms.py", line 672, in factorize
values, na_sentinel=na_sentinel, size_hint=size_hint, na_value=na_value
File "/home/wesley/.local/lib/python3.6/site-packages/pandas/core/algorithms.py", line 508, in _factorize_array
values, na_sentinel=na_sentinel, na_value=na_value
File "pandas/_libs/hashtable_class_helper.pxi", line 1798, in pandas._libs.hashtable.PyObjectHashTable.factorize
File "pandas/_libs/hashtable_class_helper.pxi", line 1718, in pandas._libs.hashtable.PyObjectHashTable._unique
TypeError: unhashable type: 'dict'
Then I tried
from backtesting.test import GOOG
from backtesting.test import SMA
from backtesting import Strategy
from backtesting.lib import crossover
from backtesting import Backtest
class SmaCross(Strategy):
# Define the two MA lags as *class variables*
# for later optimization
def init(self):
# Precompute two moving averages
setattr(self, "SMA_close_10", self.I(SMA, self.data.Close, 30))
setattr(self, "SMA_close_30", self.I(SMA, self.data.Close, 50))
def next(self):
# If SMA_close_10 crosses above SMA_close_30, buy the asset
if crossover(getattr(self, "SMA_close_10"), getattr(self, "SMA_close_30")):
self.buy()
# Else, if SMA_close_10 crosses below SMA_close_30, sell it
elif crossover(getattr(self, "SMA_close_30"), getattr(self, "SMA_close_10")):
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002)
#print(bt.run())
# dynamic optimizing...
stats = bt.optimize(optimization_dict={"SMA_close_10": range(5, 30, 5),
"SMA_close_30": range(10, 70, 5)},
maximize='Equity Final [$]')
print(stats)
Changing backtesting.py, adding the content below in optimize() (line ~769)
if not kwargs:
raise ValueError('Need some strategy parameters to optimize')
# added this
dynamic = kwargs.get("optimization_dict")
if dynamic:
kwargs = dynamic
Result
AttributeError: Strategy 'SmaCross' is missing parameter 'SMA_close_10'. Strategy class should define parameters as class variables before they can be optimized or run with.
Then I tried again, change backtesting.py (line ~769)
if not kwargs:
raise ValueError('Need some strategy parameters to optimize')
dynamic = kwargs.get("optimization_dict")
if dynamic:
kwargs = dynamic
for key in kwargs.keys():
setattr(self._strategy, key, kwargs[key])
Result
Start 2004-08-19 00:00:00
End 2013-03-01 00:00:00
Duration 3116 days 00:00:00
Exposure [%] 93.5815
Equity Final [$] 5880.21
Equity Peak [$] 14758.1
Return [%] -41.1979
Buy & Hold Return [%] 703.458
Max. Drawdown [%] -67.5084
Avg. Drawdown [%] -10.5319
Max. Drawdown Duration 2606 days 00:00:00
Avg. Drawdown Duration 197 days 00:00:00
# Trades 41
Win Rate [%] 34.1463
Best Trade [%] 43.4218
Worst Trade [%] -19.5923
Avg. Trade [%] -0.84408
Max. Trade Duration 289 days 00:00:00
Avg. Trade Duration 72 days 00:00:00
Expectancy [%] 10.1078
SQN -0.770182
Sharpe Ratio -0.0614293
Sortino Ratio -0.17078
Calmar Ratio -0.0125033
_strategy SmaCross(SMA_close_10=5,SMA_close_30=10)
dtype: object
But the optimization doesn't work yet. The result is always the same
_strategy SmaCross(SMA_close_10=5,SMA_close_30=10)
No matter the range specified in the dict. The result of the optimization is always based on the parameters specified in the init. 30 and 50.
def init(self):
# Precompute two moving averages
setattr(self, "SMA_close_10", self.I(SMA, self.data.Close, 30))
setattr(self, "SMA_close_30", self.I(SMA, self.data.Close, 50))
The Return [%]
will change only if I change the values on init.
what could I be doing wrong?
Thanks
This works:
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import GOOG, SMA
class SmaCross(Strategy):
n1 = 15 # <-- this is required
n2 = 30
def init(self):
setattr(self, "sma1", self.I(SMA, self.data.Close, self.n1))
setattr(self, "sma2", self.I(SMA, self.data.Close, self.n2))
def next(self):
if crossover(getattr(self, "sma1"), getattr(self, "sma2")):
self.buy()
elif crossover(getattr(self, "sma2"), getattr(self, "sma1")):
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002, exclusive_orders=True)
bt.optimize(n1=range(5, 30, 5),
n2=range(10, 70, 5),
constraint=lambda p: p.n1 < p.n2)
You need the initially defined optimization class variables (n1
and n2
).
If those are also dynamic and based on some algorithm, something like this should work:
class SmaCross(Strategy):
# No variables declared here at this time
def init(self):
...
def prepare_optimization(cls: type) -> dict:
# Set the class variables upon the class
setattr(cls, 'n1', 1)
setattr(cls, 'n2', 2)
# Construct and return optimization kwargs
return {'n1': range(5, 30, 5),
'n2': range(10, 70, 5),
'constraint': lambda p: p['n1'] < p['n2']}
opt_kwargs = prepare_optimization(SmaCross)
assert getattr(SmaCross, 'n1') == 1
...
bt.optimize(**opt_kwargs)
The code for future references:
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import GOOG, SMA
class SmaCross(Strategy):
def init(self):
setattr(self, "sma1", self.I(SMA, self.data.Close, getattr(self, "n1")))
setattr(self, "sma2", self.I(SMA, self.data.Close, getattr(self, "n2")))
def next(self):
if crossover(getattr(self, "sma1"), getattr(self, "sma2")):
self.buy()
elif crossover(getattr(self, "sma2"), getattr(self, "sma1")):
self.sell()
def prepare_optimization(cls: type) -> dict:
# Set the class variables upon the class
setattr(cls, 'n1', 1)
setattr(cls, 'n2', 2)
# Construct and return optimization kwargs
return {'n1': range(5, 30, 5),
'n2': range(10, 70, 5),
'constraint': lambda p: p['n1'] < p['n2'],
'maximize': 'Equity Final [$]'}
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002, exclusive_orders=True)
opt_kwargs = prepare_optimization(SmaCross)
x = bt.optimize(**opt_kwargs)
print(x.to_string())
The result:
Start 2004-08-19 00:00:00
End 2013-03-01 00:00:00
Duration 3116 days 00:00:00
Exposure Time [%] 98.1378
Equity Final [$] 105040
Equity Peak [$] 108328
Return [%] 950.401
Buy & Hold Return [%] 703.458
Max. Drawdown [%] -43.9954
Avg. Drawdown [%] -6.13885
Max. Drawdown Duration 690 days 00:00:00
Avg. Drawdown Duration 43 days 00:00:00
# Trades 152
Win Rate [%] 51.3158
Best Trade [%] 61.5629
Worst Trade [%] -19.7783
Avg. Trade [%] 1.52108
Max. Trade Duration 83 days 00:00:00
Avg. Trade Duration 21 days 00:00:00
Profit Factor 1.96479
Expectancy [%] 6.00108
SQN 1.50885
Sharpe Ratio 0.150263
Sortino Ratio 0.392356
Calmar Ratio 0.0345735
_strategy SmaCross(n1=10,n2=15)
_equity_curve Equity DrawdownPct Drawdown...
_trades Size EntryBar ExitBar ... EntryTime ...
Now native optimization is really working. :smiley:
With that we can construct dynamic strategies from a json. Example below (example for dynamic timeseries only. Not for strategy logic (yet)):
Some json like this
{"timeseries": [
{
"name": "SMA_close_10",
"function": "sma",
"parameters": {
"source": "close",
"timeperiod": 10
}
},
{
"name": "SMA_close_30",
"function": "sma",
"parameters": {
"source": "close",
"timeperiod": 30
}
}
]}
@kernc Your help was great, I appreciate it. Thank you :grin: The project is great, keep it up!
Expected Behavior
Calculate the strategy and display the result:
Actual Behavior
No trades were made.
Steps to Reproduce
class SmaCross(Strategy):
Define the two MA lags as class variables
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002) print(bt.run())
Additional info
Thanks