kernc / backtesting.py

:mag_right: :chart_with_upwards_trend: :snake: :moneybag: Backtest trading strategies in Python.
https://kernc.github.io/backtesting.py/
GNU Affero General Public License v3.0
5.44k stars 1.06k forks source link

Using a dict to store dynamic indicators #90

Closed wesleywilian closed 4 years ago

wesleywilian commented 4 years ago

Expected Behavior

Calculate the strategy and display the result:

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

Actual Behavior

No trades were made.

Start                     2004-08-19 00:00:00
End                       2013-03-01 00:00:00
Duration                   3116 days 00:00:00
Exposure [%]                                0
Equity Final [$]                        10000
Equity Peak [$]                         10000
Return [%]                                  0
Buy & Hold Return [%]                 703.458
Max. Drawdown [%]                          -0
Avg. Drawdown [%]                         NaN
Max. Drawdown Duration                    NaN
Avg. Drawdown Duration                    NaN
# Trades                                    0
Win Rate [%]                              NaN
Best Trade [%]                            NaN
Worst Trade [%]                           NaN
Avg. Trade [%]                            NaN
Max. Trade Duration                       NaT
Avg. Trade Duration                       NaT
Expectancy [%]                            NaN
SQN                                       NaN
Sharpe Ratio                              NaN
Sortino Ratio                             NaN
Calmar Ratio                              NaN
_strategy                            SmaCross
dtype: object

Steps to Reproduce

  1. Using the code
    
    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
    self.sma1 = self.I(SMA, self.data.Close, self.n1)
    self.sma2 = self.I(SMA, self.data.Close, self.n2)

def next(self):
    # If sma1 crosses above sma2, buy the asset
    # Values ok here!
    print(self.data.index[-1],self.sma1[-1])
    if crossover(self.sma1, self.sma2):
        self.buy()

    # Else, if sma1 crosses below sma2, sell it
    elif crossover(self.sma2, self.sma1):
        self.sell()

bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002) print(bt.run())


2. Change the code to
```py
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
        self.timeseries["sma1"] = self.I(SMA, self.data.Close, self.n1)
        self.timeseries["sma2"] = self.I(SMA, self.data.Close, self.n2)

    def next(self):
        # If sma1 crosses above sma2, buy the asset
        # Is showing the same value!
        print(self.data.index[-1], self.timeseries["sma1"][-1])
        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())

Additional info

Thanks

kernc commented 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")
wesleywilian commented 4 years ago

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

kernc commented 4 years ago

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.

arunavo4 commented 4 years ago

@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?

kernc commented 4 years ago

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?

arunavo4 commented 4 years ago

@kernc sure, https://github.com/kernc/backtesting.py/issues/93

wesleywilian commented 4 years ago

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

kernc commented 4 years ago

Nobody claims you can setattr() upon a Backtest instance ... :confused: The parameters to optimize need to be passed to Backtest.optimize() method as kwargs.

wesleywilian commented 4 years ago

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

kernc commented 4 years ago

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.

kernc commented 4 years ago

@wesleywilian Did that work?

wesleywilian commented 4 years ago

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

kernc commented 4 years ago

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)
wesleywilian commented 4 years ago

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!