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¶
Performance measures
Backtesting libraries in Python
[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. '
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]:
[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