Trading strategy

This notebook show how grammar-guided genetic programming (G3P) can be used to evolve a trading strategy based on a backtesting library.

References

[1]:
import alogos as al
import unified_map as um
from backtesting import Backtest, Strategy
from backtesting.lib import crossover

from backtesting.test import SMA, GOOG
/home/r/programs/dev/miniconda/envs/alogos/lib/python3.10/site-packages/backtesting/_plotting.py:50: UserWarning: Jupyter Notebook detected. Setting Bokeh output to notebook. This may not work in Jupyter clients without JavaScript support (e.g. PyCharm, Spyder IDE). Reset with `backtesting.set_bokeh_output(notebook=False)`.
  warnings.warn('Jupyter Notebook detected. '
Loading BokehJS ...

Definition of search space and goal

1) Grammar

This grammar defines the search space: a Python program that implements a trading strategy which can be backtested

[2]:
import alogos as al

ebnf_text = """
PROGRAM = L00 NL L01 NL L02 NL L03 NL L04 NL L05 NL L06 NL L07 NL L08 NL L09 NL L10 NL L11 NL L12 NL L13 NL L14 NL L15 NL L16 NL L17 NL L18 NL L19 NL L20 NL L21
L00 = "class EvoStrat(Strategy):"
L01 = "    n1 = " NUMBER
L02 = "    n2 = " NUMBER
L03 = "    n3 = " NUMBER
L04 = "    n4 = " NUMBER
L05 = ""
L06 = "    def init(self):"
L07 = "        close = self.data.Close"
L08 = "        self.sma1 = self.I(SMA, close, self.n1)"
L09 = "        self.sma2 = self.I(SMA, close, self.n2)"
L10 = "        self.sma3 = self.I(SMA, close, self.n3)"
L11 = "        self.sma4 = self.I(SMA, close, self.n4)"
L12 = ""
L13 = "    def next(self):"
L14 = "        if " CONDITION ":"
L15 = "            " ACTION
L16 = "        if " CONDITION ":"
L17 = "            " ACTION
L18 = "        if " CONDITION ":"
L19 = "            " ACTION
L20 = "        if " CONDITION ":"
L21 = "            " ACTION

NL = "\n"

CONDITION = "crossover(" VAR ", " VAR ")"
VAR = "self.sma1" | "self.sma2" | "self.sma3" | "self.sma4"

ACTION = "self.buy()" | "self.sell()"

NUMBER = NONZERO_DIGIT DIGIT | DIGIT
NONZERO_DIGIT = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
DIGIT = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
"""

grammar = al.Grammar(ebnf_text=ebnf_text)

2) Objective function

The objective function gets a candidate solution (=a string of the grammar’s language) and returns a fitness value for it. This is done by 1) executing the string as Python program, which creates a class that implements a trading strategy, and 2) backtest that strategy on chosen data.

[3]:
def string_to_strategy(string):
    var = dict()
    exec(string, None, var)
    return var['EvoStrat']


def strategy_to_performance(strat):
    bt = Backtest(GOOG, strat, cash=10000, commission=.002, exclusive_orders=True)
    output = bt.run()
    # return output['Return [%]']
    # return output['SQN']
    # return output['Calmar Ratio']
    # return output['Sharpe Ratio']
    return output['Sortino Ratio']


def objective_function(string):
    strat = string_to_strategy(string)
    ret = strategy_to_performance(strat)
    return ret

objective_function(grammar.generate_string())
[3]:
0.0

Generation of a random solution

Check if grammar and objective function work as intended.

[4]:
random_string = grammar.generate_string()
print(random_string)
class EvoStrat(Strategy):
    n1 = 8
    n2 = 7
    n3 = 2
    n4 = 6

    def init(self):
        close = self.data.Close
        self.sma1 = self.I(SMA, close, self.n1)
        self.sma2 = self.I(SMA, close, self.n2)
        self.sma3 = self.I(SMA, close, self.n3)
        self.sma4 = self.I(SMA, close, self.n4)

    def next(self):
        if crossover(self.sma2, self.sma2):
            self.sell()
        if crossover(self.sma2, self.sma1):
            self.sell()
        if crossover(self.sma1, self.sma2):
            self.buy()
        if crossover(self.sma4, self.sma3):
            self.buy()
[5]:
objective_function(random_string)
[5]:
0.0

Search for an optimal solution

Evolutionary optimization with random variation and non-random selection is used to find increasingly better candidate solutions.

1) Parameterization

[6]:
ea = al.EvolutionaryAlgorithm(
    grammar, objective_function, 'max',
    population_size=100, offspring_size=100, max_generations=100,
    evaluator=um.univariate.parallel.futures, verbose=True)

2) Run

[7]:
best_ind = ea.run()
Progress         Generations      Evaluations      Runtime (sec)    Best fitness
..... .....      10               652              14.7             1.9178000290853139
..... .....      20               1143             27.1             2.1298806522009377
..... .....      30               1382             34.0             2.1298806522009377
..... .....      40               1606             40.5             2.1298806522009377
..... .....      50               1845             47.7             2.145917834382448
..... .....      60               2253             59.4             2.4053639480645486
..... .....      70               2615             72.8             2.5086855411961304
..... .....      80               2897             82.1             2.5086855411961304
..... .....      90               3149             91.4             2.5086855411961304
..... .....      100              3401             100.2            2.5086855411961304


Finished         100              3401             101.1            2.5086855411961304

3) Result

[8]:
string = best_ind.phenotype
print(string)
class EvoStrat(Strategy):
    n1 = 38
    n2 = 9
    n3 = 71
    n4 = 33

    def init(self):
        close = self.data.Close
        self.sma1 = self.I(SMA, close, self.n1)
        self.sma2 = self.I(SMA, close, self.n2)
        self.sma3 = self.I(SMA, close, self.n3)
        self.sma4 = self.I(SMA, close, self.n4)

    def next(self):
        if crossover(self.sma3, self.sma2):
            self.sell()
        if crossover(self.sma3, self.sma1):
            self.sell()
        if crossover(self.sma1, self.sma4):
            self.buy()
        if crossover(self.sma2, self.sma1):
            self.buy()
[9]:
objective_function(string)
[9]:
2.5086855411961304
[10]:
strat = string_to_strategy(string)
bt = Backtest(GOOG, strat, cash=10000, commission=.002, exclusive_orders=True)
output = bt.run()
bt.plot()
[10]:
Row(
id = '1492', …)
[11]:
output
[11]:
Start                     2004-08-19 00:00:00
End                       2013-03-01 00:00:00
Duration                   3116 days 00:00:00
Exposure Time [%]                    96.13594
Equity Final [$]                 263417.46512
Equity Peak [$]                  271275.72758
Return [%]                        2534.174651
Buy & Hold Return [%]              703.458242
Return (Ann.) [%]                   46.780309
Volatility (Ann.) [%]               44.805723
Sharpe Ratio                          1.04407
Sortino Ratio                        2.508686
Calmar Ratio                         1.487458
Max. Drawdown [%]                  -31.449836
Avg. Drawdown [%]                   -4.391446
Max. Drawdown Duration      377 days 00:00:00
Avg. Drawdown Duration       26 days 00:00:00
# Trades                                   93
Win Rate [%]                        62.365591
Best Trade [%]                      47.242932
Worst Trade [%]                      -8.37119
Avg. Trade [%]                       3.600005
Max. Trade Duration         188 days 00:00:00
Avg. Trade Duration          33 days 00:00:00
Profit Factor                        3.857222
Expectancy [%]                       4.021266
SQN                                  3.069101
_strategy                            EvoStrat
_equity_curve                             ...
_trades                       Size  EntryB...
dtype: object
[12]:
output['Return [%]'] / output['Buy & Hold Return [%]']
[12]:
3.602452142826494