Skip to content
This repository has been archived by the owner on Feb 15, 2022. It is now read-only.

Genetic algorithm to fine tune strategy parameters #598

Merged
merged 1 commit into from
Oct 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions scripts/genetic_algo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
FROM node:latest



# NOTE: THIS DOCKERFILE IS GENERATED VIA "update.sh"
#
# PLEASE DO NOT EDIT IT DIRECTLY.
#


# ensure local python is preferred over distribution python
ENV PATH /usr/local/bin:$PATH

# http://bugs.python.org/issue19846
# > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK.
ENV LANG C.UTF-8

# runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
tcl \
tk \
&& rm -rf /var/lib/apt/lists/*

ENV GPG_KEY 0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
ENV PYTHON_VERSION 3.6.0

# if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'"
ENV PYTHON_PIP_VERSION 9.0.1

RUN set -ex \
&& buildDeps=' \
tcl-dev \
tk-dev \
' \
&& apt-get update && apt-get install -y $buildDeps --no-install-recommends && rm -rf /var/lib/apt/lists/* \
\
&& wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz" \
&& wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc" \
&& export GNUPGHOME="$(mktemp -d)" \
&& gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$GPG_KEY" \
&& gpg --batch --verify python.tar.xz.asc python.tar.xz \
&& rm -r "$GNUPGHOME" python.tar.xz.asc \
&& mkdir -p /usr/src/python \
&& tar -xJC /usr/src/python --strip-components=1 -f python.tar.xz \
&& rm python.tar.xz \
\
&& cd /usr/src/python \
&& ./configure \
--enable-loadable-sqlite-extensions \
&& make -j$(nproc) \
&& make install \
&& ldconfig \
\
# explicit path to "pip3" to ensure distribution-provided "pip3" cannot interfere
&& if [ ! -e /usr/local/bin/pip3 ]; then : \
&& wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \
&& python3 /tmp/get-pip.py "pip==$PYTHON_PIP_VERSION" \
&& rm /tmp/get-pip.py \
; fi \
# we use "--force-reinstall" for the case where the version of pip we're trying to install is the same as the version bundled with Python
# ("Requirement already up-to-date: pip==8.1.2 in /usr/local/lib/python3.6/site-packages")
# https://github.com/docker-library/python/pull/143#issuecomment-241032683
&& pip3 install --no-cache-dir --upgrade --force-reinstall "pip==$PYTHON_PIP_VERSION" \
# then we use "pip list" to ensure we don't have more than one pip version installed
# https://github.com/docker-library/python/pull/100
&& [ "$(pip list |tac|tac| awk -F '[ ()]+' '$1 == "pip" { print $2; exit }')" = "$PYTHON_PIP_VERSION" ] \
\
&& find /usr/local -depth \
\( \
\( -type d -a -name test -o -name tests \) \
-o \
\( -type f -a -name '*.pyc' -o -name '*.pyo' \) \
\) -exec rm -rf '{}' + \
&& apt-get purge -y --auto-remove $buildDeps \
&& rm -rf /usr/src/python ~/.cache

# make some useful symlinks that are expected to exist
RUN cd /usr/local/bin \
&& { [ -e easy_install ] || ln -s easy_install-* easy_install; } \
&& ln -s idle3 idle \
&& ln -s pydoc3 pydoc \
&& ln -s python3 python \
&& ln -s python3-config python-config

RUN apt-get update
RUN apt install -y graphviz libgraphviz-dev pkg-config
RUN PKG_CONFIG_ALLOW_SYSTEM_LIBS=OHYESPLEASE pip install pygraphviz
ADD requirements.txt /
RUN pip3 install -r /requirements.txt

ADD ../../ /app
ADD fabfile.py /app
WORKDIR /app
RUN /usr/local/bin/npm install
RUN npm update
20 changes: 20 additions & 0 deletions scripts/genetic_algo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Genetic Algorithm by @arpheno

For context:
- #298 https://github.com/carlos8f/zenbot/issues/298
- the original PR: https://github.com/carlos8f/zenbot/pull/299
- merged PR https://github.com/carlos8f/zenbot/pull/598
- please note that the code has decayed and you may encounter weird things

On your host:
```
$ docker-compose up
$ docker-compose exec server bash
```

On docker host:
```
$ fab backfill_local:<days>
$ cd scripts/genetic_algo
$ python -m scoop main.py <product> <days> <individuals> <strategy>
```
18 changes: 18 additions & 0 deletions scripts/genetic_algo/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import random


selectors = {
'BTC-CUR': ['gdax.BTC-USD', 'gdax.BTC-EUR', 'gdax.BTC-GBP'],
'ETH-BTC': ['gdax.ETH-BTC'],
'ETH-EUR': ['gdax.ETH-EUR'],
'ETH-USD': ['gdax.ETH-USD'],
'ETH-CUR': ['gdax.ETH-USD', 'gdax.ETH-EUR'],
}
partitions = 2
selectivity = 0.3

runid = random.randint(1000, 9999)
sigma = 20
indpb = 0.3
mutpb = 0.3
cxpb = 0.3
5 changes: 5 additions & 0 deletions scripts/genetic_algo/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Product = str
""" ETH-BTC or BTC-CUR"""

Selector = str
""" <exchange>.<product> like gdax.ETH-BTC """
22 changes: 22 additions & 0 deletions scripts/genetic_algo/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
server:
build: .
volumes:
- ./conf.js:/app/conf.js
- ./extensions:/app/extensions
links:
- mongodb
command: [ "trade", "--paper" ]
restart: always
tty: true

mongodb:
image: mongo:latest
volumes_from:
- mongodb-data
command: mongod --smallfiles

mongodb-data:
image: mongo:latest
volumes:
- ./data/db:/data/db
command: "true"
133 changes: 133 additions & 0 deletions scripts/genetic_algo/evaluation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import datetime
import os
import random
import shlex
import subprocess
import sys
from typing import List

from termcolor import colored

from conf import partitions
from evolution.individual_base import Individual
from objective_function import soft_maximum_worst_case
from parsing import parse_trades, args_for_strategy


def pct(x):
return x / 100.0


def minutes(x):
return str(int(x)) + 'm'


def runzen(cmdline):
with open(os.devnull, 'w') as devnull:
a = subprocess.check_output(shlex.split(cmdline), stderr=devnull)
profit = a.split(b'}')[-1].splitlines()[3].split(b': ')[-1][:-1]
trades = parse_trades(a.split(b'}')[-1].splitlines()[4])
return float(profit), float(trades)


class Andividual(Individual):
BASE_COMMAND = '/app/zenbot.sh sim {instrument} --strategy {strategy} --avg_slippage_pct 0.33 --filename temp.html'
def __init__(self, *args,**kwargs):
super(Andividual, self).__init__(*args, **kwargs)
self.args = args_for_strategy(self.strategy)
for _ in self.args:
self.append(50 + (random.random() - 0.5) * 100)

def __repr__(self):
return colored(f"{self.cmdline} {super(Andividual, self).__repr__()}", 'grey')

@property
def instrument(self):
return random.choice(self.instruments)

@property
def objective(self):
return soft_maximum_worst_case(self)

def compress(self):
res = dict(zip(self.args, self))
period = res['period']
del res['period']
normalized = {param: self.normalize(value, period) if 'period' in param or param == 'trend_ema' else value for
param, value in
res.items()}
normalized['period'] = period
output = dict(self.convert(param, value) for param, value in normalized.items())
return output.items()

@property
def params(self) -> List[str]:
def format(key, value):
if isinstance(value, float):
return f'--{key} {value:.6f}'
else:
return f'--{key} {value}'

params = [format(key, value) for key, value in self.compress()]
return params

@property
def cmdline(self) -> str:
base = self.BASE_COMMAND.format(instrument=self.instrument, strategy=self.strategy)
result = ' '.join([base] + self.params)
return result

def normalize(self, value: float, period: int):
return (value / period)

def convert(self, param, value):
if param == 'period':
res = minutes(int(value/2))
elif param == 'min_periods':
res = int(value * 20)
elif param == 'trend_ema':
res = int(value*15 )
elif 'period' in param:
res = int(value *10)
elif 'pct' in param:
res = pct(value)
elif 'rate' in param:
res = pct(value)
elif 'rsi' in param:
res = float(value)
elif 'threshold' in param:
res = value/100000.0
elif 'sar_af' == param:
res = value / 1000.0
elif 'sar_max_af' == param:
res = pct(value)
else:
raise ValueError(colored(f"I don't understand {param} please add it to evaluation.py", 'red'))
return param, res




def evaluate_zen(cmdline:str, days: int):
periods = time_params(days, partitions)
try:
fitness = []
for period in periods:
cmd = ' '.join([cmdline, period])
f,t = runzen(cmd)
fitness.append(f)
if t==0:
raise subprocess.CalledProcessError(-1,'TooFewTrades')
sys.stdout.write('.')
except subprocess.CalledProcessError:
fitness = [-100 for _ in periods]
sys.stdout.write('x')
sys.stdout.flush()
return tuple(fitness)


def time_params(days: int, partitions: int) -> List[str]:
now = datetime.date.today()
delta = datetime.timedelta(days=days)
splits = [now - delta / partitions * i for i in range(partitions + 1)][::-1]
return [f' --start {start} --end {end}' for start, end in zip(splits, splits[1:])]
18 changes: 18 additions & 0 deletions scripts/genetic_algo/evolution/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from functools import partial

from deap.tools import History
from scoop import futures

from evolution.core import algorithm, breed, mutate
from halloffame import ObjectiveFunctionHallOfFame
from .selection import harsh_winter
from .utils import statsa


def evolve(evaluate, cls, popsize=10):
select = partial(harsh_winter, count=popsize)

history = History()
stats = statsa()
hof = ObjectiveFunctionHallOfFame(maxsize=15)
return algorithm(cls, popsize, futures.map, evaluate, select, breed, mutate, stats, history, hof)
59 changes: 59 additions & 0 deletions scripts/genetic_algo/evolution/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import random

from deap.tools import History, Statistics
from termcolor import colored

from conf import cxpb, mutpb
from .utils import log_stuff


def algorithm(individual,popsize,map,evaluate,select,breed,mutate,stats,history,hof):
# Create initial Population and evaluate it
population = set()
print(colored(f"Sampling an initial valid population, this may take a while...", 'blue'))
while len(population) < popsize:
print(colored(f"Currently {len(population)} valid individuals", 'blue'))
would_be = [individual() for _ in range(popsize)]
evaluate_group(would_be,map,evaluate)
population = population | set(would_be)
# Commence evolution
for g in range(0, 1000):
log_stuff(g, history, hof, population, stats)
print(colored(f"It's breeding season, we're expecting new members of the tribe...", 'blue'))
offspring = breed(population)
print(colored(f"Radiation and toxic waste are causing mutations in the population...", 'blue'))
mutants = mutate(population)
print(colored(f"Summer is here, evaluating our new arrivals...", 'blue'))
evaluate_group(offspring + mutants, map,evaluate)
survivors = select(set(offspring) | set(mutants) | population)
population = survivors

return hof


def evaluate_group(population, map, evaluate):
invalid_ind = [ind for ind in population if not ind.fitness.valid]
print(' ' * len(invalid_ind) + '|')
fitnesses = map(evaluate, [ind.cmdline for ind in invalid_ind])
for ind, fit in zip(invalid_ind, fitnesses):
ind.fitness.values = fit


def breed(population):
offspring = []
while len(offspring) < len(population) * cxpb:
parent1, parent2 = random.sample(population, 2)
child1, child2 = parent1 + parent2
offspring.append(child1)
offspring.append(child2)
print(colored(len(offspring), 'green') + colored(f" children have been born.", 'blue'))
return offspring


def mutate(population):
mutants = []
for individual in population:
if random.random() < mutpb:
mutants.append(~individual)
print(colored(len(mutants), 'green') + colored(f" individuals have mutated.", 'blue'))
return mutants
Loading