Merge remote-tracking branch 'origin/master' into stock
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
language: python
|
||||
dist: bionic
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
[](https://jesse.trade)
|
||||
[](https://docs.jesse.trade)
|
||||
[](https://jesse.trade/discord)
|
||||
[](https://forum.jesse.trade)
|
||||
[](https://jesse.trade/blog)
|
||||
---
|
||||
|
||||
@@ -395,6 +395,10 @@ def now_to_timestamp():
|
||||
return arrow.utcnow().int_timestamp * 1000
|
||||
|
||||
|
||||
def now():
|
||||
return now_to_timestamp()
|
||||
|
||||
|
||||
def np_shift(arr: np.ndarray, num: int, fill_value=0):
|
||||
result = np.empty_like(arr)
|
||||
|
||||
@@ -661,6 +665,10 @@ def timestamp_to_arrow(timestamp):
|
||||
return arrow.get(timestamp / 1000)
|
||||
|
||||
|
||||
def get_arrow(timestamp):
|
||||
return timestamp_to_arrow(timestamp)
|
||||
|
||||
|
||||
def timestamp_to_date(timestamp: int) -> str:
|
||||
return str(arrow.get(timestamp / 1000))[:10]
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from .cc import cc
|
||||
from .cci import cci
|
||||
from .chande import chande
|
||||
from .cmo import cmo
|
||||
from .correlation_cycle import correlation_cycle
|
||||
from .correl import correl
|
||||
from .cvi import cvi
|
||||
from .damiani_volatmeter import damiani_volatmeter
|
||||
@@ -29,6 +30,7 @@ from .dm import dm
|
||||
from .dx import dx
|
||||
from .donchian import donchian
|
||||
from .dpo import dpo
|
||||
from .dti import dti
|
||||
from .efi import efi
|
||||
from .ema import ema
|
||||
from .emd import emd
|
||||
@@ -47,6 +49,7 @@ from .ht_sine import ht_sine
|
||||
from .ht_trendline import ht_trendline
|
||||
from .ht_trendmode import ht_trendmode
|
||||
from .ichimoku_cloud import ichimoku_cloud
|
||||
from .ichimoku_cloud_seq import ichimoku_cloud_seq
|
||||
from .itrend import itrend
|
||||
from .kama import kama
|
||||
from .keltner import keltner
|
||||
@@ -61,6 +64,7 @@ from .macd import macd
|
||||
from .macdext import macdext
|
||||
from .mama import mama
|
||||
from .mass import mass
|
||||
from .mcginley_dynamic import mcginley_dynamic
|
||||
from .marketfi import marketfi
|
||||
from .medprice import medprice
|
||||
from .mfi import mfi
|
||||
@@ -107,6 +111,7 @@ from .ultosc import ultosc
|
||||
from .var import var
|
||||
from .vi import vi
|
||||
from .vidya import vidya
|
||||
from .vpci import vpci
|
||||
from .vpt import vpt
|
||||
from .vwma import vwma
|
||||
from .vwmacd import vwmacd
|
||||
|
||||
90
jesse/indicators/correlation_cycle.py
Normal file
90
jesse/indicators/correlation_cycle.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from jesse.helpers import get_candle_source, np_shift
|
||||
|
||||
CC = namedtuple('CC', ['real', 'imag', 'angle', 'state'])
|
||||
|
||||
|
||||
def correlation_cycle(candles: np.ndarray, period=20, threshold=9, source_type="close", sequential=False) -> CC:
|
||||
"""
|
||||
"Correlation Cycle, Correlation Angle, Market State - John Ehlers
|
||||
|
||||
:param candles: np.ndarray
|
||||
:param period: int - default: 20
|
||||
:param threshold: int - default: 9
|
||||
:param source_type: str - default: "close"
|
||||
:param sequential: bool - default=False
|
||||
|
||||
:return: CC(real, imag)
|
||||
"""
|
||||
if not sequential and len(candles) > 240:
|
||||
candles = candles[-240:]
|
||||
|
||||
source = get_candle_source(candles, source_type=source_type)
|
||||
|
||||
# Correlation Cycle Function
|
||||
PIx2 = 4.0 * math.asin(1.0)
|
||||
period = max(2, period)
|
||||
|
||||
realPart = np.full_like(source, np.nan)
|
||||
imagPart = np.full_like(source, np.nan)
|
||||
|
||||
for i in range(period, source.shape[0]):
|
||||
Rx = 0.0
|
||||
Rxx = 0.0
|
||||
Rxy = 0.0
|
||||
Ryy = 0.0
|
||||
Ry = 0.0
|
||||
Ix = 0.0
|
||||
Ixx = 0.0
|
||||
Ixy = 0.0
|
||||
Iyy = 0.0
|
||||
Iy = 0.0
|
||||
|
||||
for j in range(period):
|
||||
jMinusOne = j + 1
|
||||
if np.isnan(source[i - jMinusOne]):
|
||||
X = 0
|
||||
else:
|
||||
X = source[i - jMinusOne]
|
||||
temp = PIx2 * jMinusOne / period
|
||||
Yc = np.cos(temp)
|
||||
Ys = -np.sin(temp)
|
||||
Rx = Rx + X
|
||||
Ix = Ix + X
|
||||
Rxx = Rxx + X * X
|
||||
Ixx = Ixx + X * X
|
||||
Rxy = Rxy + X * Yc
|
||||
Ixy = Ixy + X * Ys
|
||||
Ryy = Ryy + Yc * Yc
|
||||
Iyy = Iyy + Ys * Ys
|
||||
Ry = Ry + Yc
|
||||
Iy = Iy + Ys
|
||||
|
||||
temp_1 = period * Rxx - Rx * Rx
|
||||
temp_2 = period * Ryy - Ry * Ry
|
||||
if (temp_1 > 0.0 and temp_2 > 0.0):
|
||||
realPart[i] = (period * Rxy - Rx * Ry) / np.sqrt(temp_1 * temp_2)
|
||||
|
||||
temp_1 = period * Ixx - Ix * Ix
|
||||
temp_2 = period * Iyy - Iy * Iy
|
||||
if (temp_1 > 0.0 and temp_2 > 0.0):
|
||||
imagPart[i] = (period * Ixy - Ix * Iy) / np.sqrt(temp_1 * temp_2)
|
||||
|
||||
# Correlation Angle Phasor
|
||||
HALF_OF_PI = math.asin(1.0)
|
||||
angle = np.where(imagPart == 0, 0.0, np.degrees(np.arctan(realPart / imagPart) + HALF_OF_PI))
|
||||
angle = np.where(imagPart > 0.0, angle - 180.0, angle)
|
||||
priorAngle = np_shift(angle, 1, fill_value=np.nan)
|
||||
angle = np.where(np.logical_and(priorAngle > angle, priorAngle - angle < 270.0), priorAngle, angle)
|
||||
|
||||
# Market State Function
|
||||
state = np.where(np.abs(angle - priorAngle) < threshold, np.where(angle >= 0.0, 1, np.where(angle < 0.0, -1, 0)), 0)
|
||||
|
||||
if sequential:
|
||||
return CC(realPart, imagPart, angle, state)
|
||||
else:
|
||||
return CC(realPart[-1], imagPart[-1], angle[-1], state[-1])
|
||||
46
jesse/indicators/dti.py
Normal file
46
jesse/indicators/dti.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import talib
|
||||
|
||||
import jesse.helpers as jh
|
||||
|
||||
|
||||
def dti(candles: np.ndarray, r=14, s=10, u=5, sequential=False) -> Union[float, np.ndarray]:
|
||||
"""
|
||||
DTI by William Blau
|
||||
|
||||
:param candles: np.ndarray
|
||||
:param r: int - default=14
|
||||
:param s: int - default=10
|
||||
:param u: int - default=5
|
||||
:param sequential: bool - default=False
|
||||
|
||||
:return: float
|
||||
"""
|
||||
if not sequential and len(candles) > 240:
|
||||
candles = candles[-240:]
|
||||
|
||||
high = candles[:, 3]
|
||||
low = candles[:, 4]
|
||||
|
||||
high_1 = jh.np_shift(high, 1, np.nan)
|
||||
low_1 = jh.np_shift(low, 1, np.nan)
|
||||
|
||||
xHMU = np.where(high - high_1 > 0, high - high_1, 0)
|
||||
xLMD = np.where(low - low_1 < 0, -(low - low_1), 0)
|
||||
|
||||
xPrice = xHMU - xLMD
|
||||
xPriceAbs = np.absolute(xPrice)
|
||||
|
||||
xuXA = talib.EMA(talib.EMA(talib.EMA(xPrice, r), s), u)
|
||||
xuXAAbs = talib.EMA(talib.EMA(talib.EMA(xPriceAbs, r), s), u)
|
||||
|
||||
Val1 = 100 * xuXA
|
||||
Val2 = xuXAAbs
|
||||
dti_val = np.where(Val2 != 0, Val1 / Val2, 0)
|
||||
|
||||
if sequential:
|
||||
return dti_val
|
||||
else:
|
||||
return None if np.isnan(dti_val[-1]) else dti_val[-1]
|
||||
55
jesse/indicators/ichimoku_cloud_seq.py
Normal file
55
jesse/indicators/ichimoku_cloud_seq.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from collections import namedtuple
|
||||
|
||||
import numpy as np
|
||||
import talib
|
||||
|
||||
from jesse.helpers import np_shift
|
||||
|
||||
IchimokuCloud = namedtuple('IchimokuCloud',
|
||||
['conversion_line', 'base_line', 'span_a', 'span_b', 'lagging_line', 'future_span_a',
|
||||
'future_span_b'])
|
||||
|
||||
|
||||
def ichimoku_cloud_seq(candles: np.ndarray, conversion_line_period=9, base_line_period=26, lagging_line_period=52,
|
||||
displacement=26, sequential=False) -> IchimokuCloud:
|
||||
"""
|
||||
Ichimoku Cloud
|
||||
|
||||
:param candles: np.ndarray
|
||||
:param conversion_line_period: int - default=9
|
||||
:param base_line_period: int - default=26
|
||||
:param lagging_line_period: int - default=52
|
||||
:param displacement: - default=26
|
||||
:param sequential: bool - default=False
|
||||
|
||||
:return: IchimokuCloud
|
||||
"""
|
||||
|
||||
if len(candles) < lagging_line_period + displacement:
|
||||
raise ValueError("Too few candles available for lagging_line_period + displacement.")
|
||||
|
||||
if not sequential and len(candles) > 240:
|
||||
candles = candles[-240:]
|
||||
|
||||
small_ph = talib.MAX(candles[:, 3], conversion_line_period)
|
||||
small_pl = talib.MIN(candles[:, 4], conversion_line_period)
|
||||
conversion_line = (small_ph + small_pl) / 2
|
||||
|
||||
mid_ph = talib.MAX(candles[:, 3], base_line_period)
|
||||
mid_pl = talib.MIN(candles[:, 4], base_line_period)
|
||||
base_line = (mid_ph + mid_pl) / 2
|
||||
|
||||
long_ph = talib.MAX(candles[:, 3], lagging_line_period)
|
||||
long_pl = talib.MIN(candles[:, 4], lagging_line_period)
|
||||
span_b_pre = (long_ph + long_pl) / 2
|
||||
span_b = np_shift(span_b_pre, displacement, fill_value=np.nan)
|
||||
span_a_pre = (conversion_line + base_line) / 2
|
||||
span_a = np_shift(span_a_pre, displacement, fill_value=np.nan)
|
||||
|
||||
lagging_line = np_shift(candles[:, 2], displacement - 1, fill_value=np.nan)
|
||||
|
||||
if sequential:
|
||||
return IchimokuCloud(conversion_line, base_line, span_a, span_b, lagging_line, span_a_pre, span_b_pre)
|
||||
else:
|
||||
return IchimokuCloud(conversion_line[-1], base_line[-1], span_a[-1], span_b[-1], lagging_line[-1],
|
||||
span_a_pre[-1], span_b_pre[-1])
|
||||
35
jesse/indicators/mcginley_dynamic.py
Normal file
35
jesse/indicators/mcginley_dynamic.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from jesse.helpers import get_candle_source
|
||||
|
||||
|
||||
def mcginley_dynamic(candles: np.ndarray, period=10, k=0.6, source_type="close", sequential=False) -> Union[
|
||||
float, np.ndarray]:
|
||||
"""
|
||||
McGinley Dynamic
|
||||
|
||||
:param candles: np.ndarray
|
||||
:param period: int - default: 10
|
||||
:param k: float - default: 0.6
|
||||
:param sequential: bool - default=False
|
||||
|
||||
:return: float | np.ndarray
|
||||
"""
|
||||
if not sequential and len(candles) > 240:
|
||||
candles = candles[-240:]
|
||||
|
||||
source = get_candle_source(candles, source_type=source_type)
|
||||
|
||||
mg = np.full_like(source, np.nan)
|
||||
for i in range(len(source)):
|
||||
if i == 0:
|
||||
mg[i] = source[i]
|
||||
else:
|
||||
mg[i] = mg[i - 1] + ((source[i] - mg[i - 1]) / np.max([(k * period * ((source[i] / mg[i - 1]) ** 4)), 1]))
|
||||
|
||||
if sequential:
|
||||
return mg
|
||||
else:
|
||||
return None if np.isnan(mg[-1]) else mg[-1]
|
||||
37
jesse/indicators/vpci.py
Normal file
37
jesse/indicators/vpci.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
VPCI = namedtuple('VPCI', ['vpci', 'vpcis'])
|
||||
|
||||
|
||||
def vpci(candles: np.ndarray, short_range=5, long_range=25, sequential=False) -> VPCI:
|
||||
"""
|
||||
VPCI - Volume Price Confirmation Indicator
|
||||
|
||||
:param candles: np.ndarray
|
||||
:param short_range: int - default: 5
|
||||
:param long_range: int - default: 25
|
||||
:param sequential: bool - default=False
|
||||
|
||||
:return: float | np.ndarray
|
||||
"""
|
||||
if not sequential and len(candles) > 240:
|
||||
candles = candles[-240:]
|
||||
|
||||
vwma_long = talib.SMA( candles[:, 2] * candles[:, 5], long_range) / talib.SMA(candles[:, 5], long_range)
|
||||
VPC = vwma_long - talib.SMA(candles[:, 2], long_range)
|
||||
|
||||
vwma_short = talib.SMA( candles[:, 2] * candles[:, 5], short_range) / talib.SMA(candles[:, 5], short_range)
|
||||
VPR = vwma_short / talib.SMA(candles[:, 2], short_range)
|
||||
|
||||
VM = talib.SMA(candles[:, 5], short_range) / talib.SMA(candles[:, 5], long_range)
|
||||
VPCI_val = VPC * VPR * VM
|
||||
|
||||
VPCIS = talib.SMA( VPCI_val * candles[:, 5], short_range) / talib.SMA(candles[:, 5], short_range)
|
||||
|
||||
if sequential:
|
||||
return VPCI(VPCI_val, VPCIS)
|
||||
else:
|
||||
return VPCI(VPCI_val[-1], VPCIS[-1])
|
||||
@@ -43,7 +43,7 @@ def run(start_date: str, finish_date: str, candles=None, chart=False, tradingvie
|
||||
|
||||
if not jh.should_execute_silently():
|
||||
# print candles table
|
||||
key = '{}-{}'.format(config['app']['trading_exchanges'][0], config['app']['trading_symbols'][0])
|
||||
key = '{}-{}'.format(config['app']['considering_candles'][0][0], config['app']['considering_candles'][0][1])
|
||||
table.key_value(stats.candles(candles[key]['candles']), 'candles', alignments=('left', 'right'))
|
||||
print('\n')
|
||||
|
||||
@@ -97,52 +97,53 @@ def load_candles(start_date_str: str, finish_date_str: str):
|
||||
|
||||
# download candles for the duration of the backtest
|
||||
candles = {}
|
||||
for exchange in config['app']['considering_exchanges']:
|
||||
for symbol in config['app']['considering_symbols']:
|
||||
key = jh.key(exchange, symbol)
|
||||
for c in config['app']['considering_candles']:
|
||||
exchange, symbol = c[0], c[1]
|
||||
|
||||
cache_key = '{}-{}-'.format(start_date_str, finish_date_str) + key
|
||||
cached_value = cache.get_value(cache_key)
|
||||
# if cache exists
|
||||
if cached_value:
|
||||
candles_tuple = cached_value
|
||||
# not cached, get and cache for later calls in the next 5 minutes
|
||||
else:
|
||||
# fetch from database
|
||||
candles_tuple = Candle.select(
|
||||
Candle.timestamp, Candle.open, Candle.close, Candle.high, Candle.low,
|
||||
Candle.volume
|
||||
).where(
|
||||
Candle.timestamp.between(start_date, finish_date),
|
||||
Candle.exchange == exchange,
|
||||
Candle.symbol == symbol
|
||||
).order_by(Candle.timestamp.asc()).tuples()
|
||||
key = jh.key(exchange, symbol)
|
||||
|
||||
# validate that there are enough candles for selected period
|
||||
required_candles_count = (finish_date - start_date) / 60_000
|
||||
if len(candles_tuple) == 0 or candles_tuple[-1][0] != finish_date or candles_tuple[0][0] != start_date:
|
||||
raise exceptions.CandleNotFoundInDatabase(
|
||||
'Not enough candles for {}. Try running "jesse import-candles"'.format(symbol))
|
||||
elif len(candles_tuple) != required_candles_count + 1:
|
||||
raise exceptions.CandleNotFoundInDatabase('There are missing candles between {} => {}'.format(
|
||||
start_date_str, finish_date_str
|
||||
))
|
||||
cache_key = '{}-{}-'.format(start_date_str, finish_date_str) + key
|
||||
cached_value = cache.get_value(cache_key)
|
||||
# if cache exists
|
||||
if cached_value:
|
||||
candles_tuple = cached_value
|
||||
# not cached, get and cache for later calls in the next 5 minutes
|
||||
else:
|
||||
# fetch from database
|
||||
candles_tuple = Candle.select(
|
||||
Candle.timestamp, Candle.open, Candle.close, Candle.high, Candle.low,
|
||||
Candle.volume
|
||||
).where(
|
||||
Candle.timestamp.between(start_date, finish_date),
|
||||
Candle.exchange == exchange,
|
||||
Candle.symbol == symbol
|
||||
).order_by(Candle.timestamp.asc()).tuples()
|
||||
|
||||
# cache it for near future calls
|
||||
cache.set_value(cache_key, tuple(candles_tuple), expire_seconds=60 * 60 * 24 * 7)
|
||||
# validate that there are enough candles for selected period
|
||||
required_candles_count = (finish_date - start_date) / 60_000
|
||||
if len(candles_tuple) == 0 or candles_tuple[-1][0] != finish_date or candles_tuple[0][0] != start_date:
|
||||
raise exceptions.CandleNotFoundInDatabase(
|
||||
'Not enough candles for {}. Try running "jesse import-candles"'.format(symbol))
|
||||
elif len(candles_tuple) != required_candles_count + 1:
|
||||
raise exceptions.CandleNotFoundInDatabase('There are missing candles between {} => {}'.format(
|
||||
start_date_str, finish_date_str
|
||||
))
|
||||
|
||||
candles[key] = {
|
||||
'exchange': exchange,
|
||||
'symbol': symbol,
|
||||
'candles': np.array(candles_tuple)
|
||||
}
|
||||
# cache it for near future calls
|
||||
cache.set_value(cache_key, tuple(candles_tuple), expire_seconds=60 * 60 * 24 * 7)
|
||||
|
||||
candles[key] = {
|
||||
'exchange': exchange,
|
||||
'symbol': symbol,
|
||||
'candles': np.array(candles_tuple)
|
||||
}
|
||||
|
||||
return candles
|
||||
|
||||
|
||||
def simulator(candles, hyperparameters=None):
|
||||
begin_time_track = time.time()
|
||||
key = '{}-{}'.format(config['app']['trading_exchanges'][0], config['app']['trading_symbols'][0])
|
||||
key = '{}-{}'.format(config['app']['considering_candles'][0][0], config['app']['considering_candles'][0][1])
|
||||
first_candles_set = candles[key]['candles']
|
||||
length = len(first_candles_set)
|
||||
# to preset the array size for performance
|
||||
|
||||
@@ -6,9 +6,6 @@ from .interface import CandleExchange
|
||||
|
||||
|
||||
class Bitfinex(CandleExchange):
|
||||
"""
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__('Bitfinex', 1440, 1)
|
||||
self.endpoint = 'https://api-pub.bitfinex.com/v2/candles'
|
||||
@@ -18,11 +15,6 @@ class Bitfinex(CandleExchange):
|
||||
self.backup_exchange = Coinbase()
|
||||
|
||||
def get_starting_time(self, symbol: str):
|
||||
"""
|
||||
|
||||
:param symbol:
|
||||
:return:
|
||||
"""
|
||||
# hard-code few common symbols
|
||||
if symbol == 'BTCUSD':
|
||||
return jh.date_to_timestamp('2015-08-01')
|
||||
@@ -52,13 +44,7 @@ class Bitfinex(CandleExchange):
|
||||
|
||||
return second_timestamp
|
||||
|
||||
def fetch(self, symbol, start_timestamp):
|
||||
"""
|
||||
|
||||
:param symbol:
|
||||
:param start_timestamp:
|
||||
:return:
|
||||
"""
|
||||
def fetch(self, symbol: str, start_timestamp):
|
||||
# since Bitfinex API skips candles with "volume=0", we have to send end_timestamp
|
||||
# instead of limit. Therefore, we use limit number to calculate the end_timestamp
|
||||
end_timestamp = start_timestamp + (self.count - 1) * 60000
|
||||
|
||||
@@ -343,7 +343,7 @@ class Genetics(ABC):
|
||||
|
||||
# one person has to die and be replaced with the newborn baby
|
||||
for baby in people:
|
||||
random_index = randint(0, len(self.population) - 1)
|
||||
random_index = randint(1, len(self.population) - 1) # never kill our best perforemr
|
||||
try:
|
||||
self.population[random_index] = baby
|
||||
except IndexError:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from math import log10
|
||||
from multiprocessing import cpu_count
|
||||
|
||||
import arrow
|
||||
import click
|
||||
|
||||
@@ -15,6 +14,9 @@ from jesse.services.validators import validate_routes
|
||||
from jesse.store import store
|
||||
from .Genetics import Genetics
|
||||
|
||||
import os
|
||||
os.environ['NUMEXPR_MAX_THREADS'] = str(cpu_count())
|
||||
|
||||
|
||||
class Optimizer(Genetics):
|
||||
def __init__(self, training_candles, testing_candles, optimal_total, cpu_cores):
|
||||
|
||||
@@ -5,19 +5,19 @@ import sys
|
||||
|
||||
|
||||
class RouterClass:
|
||||
"""
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self.routes = []
|
||||
self.extra_candles = []
|
||||
self.market_data = []
|
||||
|
||||
def set_routes(self, routes):
|
||||
"""
|
||||
def _reset(self):
|
||||
self.routes = []
|
||||
self.extra_candles = []
|
||||
self.market_data = []
|
||||
|
||||
def set_routes(self, routes):
|
||||
self._reset()
|
||||
|
||||
:param routes:
|
||||
"""
|
||||
self.routes = []
|
||||
|
||||
for r in routes:
|
||||
@@ -43,19 +43,11 @@ class RouterClass:
|
||||
self.routes.append(Route(*r))
|
||||
|
||||
def set_market_data(self, routes):
|
||||
"""
|
||||
|
||||
:param routes:
|
||||
"""
|
||||
self.market_data = []
|
||||
for r in routes:
|
||||
self.market_data.append(Route(*r))
|
||||
|
||||
def set_extra_candles(self, extra_candles):
|
||||
"""
|
||||
|
||||
:param extra_candles:
|
||||
"""
|
||||
self.extra_candles = extra_candles
|
||||
|
||||
|
||||
|
||||
@@ -30,3 +30,5 @@ __pycache__
|
||||
.vscode
|
||||
/.ipynb_checkpoints
|
||||
/plugins
|
||||
config.py
|
||||
routes.py
|
||||
|
||||
@@ -15,10 +15,6 @@ from .state_trades import TradesState
|
||||
|
||||
|
||||
def install_routes():
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
considering_candles = set()
|
||||
|
||||
# when importing market data, considering_candles is all we need
|
||||
@@ -44,6 +40,13 @@ def install_routes():
|
||||
raise InvalidRoutes(
|
||||
'each exchange-symbol pair can be traded only once. \nMore info: https://docs.jesse.trade/docs/routes.html#trading-multiple-routes')
|
||||
|
||||
# check to make sure if trading more than one route, they all have the same quote
|
||||
# currency because otherwise we cannot calculate the correct performance metrics
|
||||
first_routes_quote = jh.quote_asset(router.routes[0].symbol)
|
||||
for r in router.routes:
|
||||
if jh.quote_asset(r.symbol) != first_routes_quote:
|
||||
raise InvalidRoutes('All trading routes must have the same quote asset.')
|
||||
|
||||
trading_exchanges = set()
|
||||
trading_timeframes = set()
|
||||
trading_symbols = set()
|
||||
@@ -77,9 +80,6 @@ def install_routes():
|
||||
|
||||
|
||||
class StoreClass:
|
||||
"""
|
||||
|
||||
"""
|
||||
app = AppState()
|
||||
orders = OrdersState()
|
||||
completed_trades = CompletedTrades()
|
||||
@@ -95,7 +95,8 @@ class StoreClass:
|
||||
self.vars = {}
|
||||
|
||||
def reset(self, force_install_routes=False):
|
||||
"""resets all the states within the store
|
||||
"""
|
||||
Resets all the states within the store
|
||||
|
||||
Keyword Arguments:
|
||||
force_install_routes {bool} -- used for unit_testing (default: {False})
|
||||
@@ -117,5 +118,6 @@ class StoreClass:
|
||||
|
||||
if not jh.is_unit_testing():
|
||||
install_routes()
|
||||
|
||||
store = StoreClass()
|
||||
store.reset()
|
||||
|
||||
@@ -28,17 +28,18 @@ class CandlesState:
|
||||
)
|
||||
|
||||
def init_storage(self, bucket_size=1000):
|
||||
for exchange in config['app']['considering_exchanges']:
|
||||
for symbol in config['app']['considering_symbols']:
|
||||
# initiate the '1m' timeframes
|
||||
key = jh.key(exchange, symbol, timeframes.MINUTE_1)
|
||||
self.storage[key] = DynamicNumpyArray((bucket_size, 6))
|
||||
for c in config['app']['considering_candles']:
|
||||
exchange, symbol = c[0], c[1]
|
||||
|
||||
for timeframe in config['app']['considering_timeframes']:
|
||||
key = jh.key(exchange, symbol, timeframe)
|
||||
# ex: 1440 / 60 + 1 (reserve one for forming candle)
|
||||
total_bigger_timeframe = int((bucket_size / jh.timeframe_to_one_minutes(timeframe)) + 1)
|
||||
self.storage[key] = DynamicNumpyArray((total_bigger_timeframe, 6))
|
||||
# initiate the '1m' timeframes
|
||||
key = jh.key(exchange, symbol, timeframes.MINUTE_1)
|
||||
self.storage[key] = DynamicNumpyArray((bucket_size, 6))
|
||||
|
||||
for timeframe in config['app']['considering_timeframes']:
|
||||
key = jh.key(exchange, symbol, timeframe)
|
||||
# ex: 1440 / 60 + 1 (reserve one for forming candle)
|
||||
total_bigger_timeframe = int((bucket_size / jh.timeframe_to_one_minutes(timeframe)) + 1)
|
||||
self.storage[key] = DynamicNumpyArray((total_bigger_timeframe, 6))
|
||||
|
||||
def add_candle(
|
||||
self,
|
||||
|
||||
@@ -10,7 +10,7 @@ import jesse.services.logger as logger
|
||||
import jesse.services.selectors as selectors
|
||||
from jesse import exceptions
|
||||
from jesse.enums import sides, trade_types, order_roles
|
||||
from jesse.models import CompletedTrade, Order
|
||||
from jesse.models import CompletedTrade, Order, Route
|
||||
from jesse.services.broker import Broker
|
||||
from jesse.store import store
|
||||
|
||||
@@ -29,6 +29,9 @@ class Strategy(ABC):
|
||||
self.index = 0
|
||||
self.vars = {}
|
||||
|
||||
self.increased_count = 0
|
||||
self.reduced_count = 0
|
||||
|
||||
self.buy = None
|
||||
self._buy = None
|
||||
self.sell = None
|
||||
@@ -47,7 +50,6 @@ class Strategy(ABC):
|
||||
self.trade = None
|
||||
self.trades_count = 0
|
||||
|
||||
self._initial_qty = None
|
||||
self._is_executing = False
|
||||
self._is_initiated = False
|
||||
|
||||
@@ -69,24 +71,6 @@ class Strategy(ABC):
|
||||
for dna in self.hyperparameters():
|
||||
self.hp[dna['name']] = dna['default']
|
||||
|
||||
@property
|
||||
def is_reduced(self):
|
||||
"""
|
||||
Has the size of position been reduced since it was opened
|
||||
:return: bool
|
||||
"""
|
||||
if self.position.is_close:
|
||||
return None
|
||||
|
||||
return self.position.qty < self._initial_qty
|
||||
|
||||
@property
|
||||
def is_increased(self):
|
||||
if self.position.is_close:
|
||||
return None
|
||||
|
||||
return self.position.qty > self._initial_qty
|
||||
|
||||
def _broadcast(self, msg: str):
|
||||
"""Broadcasts the event to all OTHER strategies
|
||||
|
||||
@@ -116,7 +100,9 @@ class Strategy(ABC):
|
||||
r.strategy._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def _on_updated_position(self, order: Order):
|
||||
"""handles executed order
|
||||
"""
|
||||
Handles the after-effect of the executed order
|
||||
|
||||
Note that it assumes that the position has already been affected
|
||||
by the executed order.
|
||||
|
||||
@@ -136,15 +122,15 @@ class Strategy(ABC):
|
||||
self._log_position_update(order, role)
|
||||
|
||||
if role == order_roles.OPEN_POSITION:
|
||||
self._on_open_position()
|
||||
self._on_open_position(order)
|
||||
elif role == order_roles.CLOSE_POSITION and order in self._take_profit_orders:
|
||||
self._on_take_profit()
|
||||
self._on_take_profit(order)
|
||||
elif role == order_roles.CLOSE_POSITION and order in self._stop_loss_orders:
|
||||
self._on_stop_loss()
|
||||
self._on_stop_loss(order)
|
||||
elif role == order_roles.INCREASE_POSITION:
|
||||
self._on_increased_position()
|
||||
self._on_increased_position(order)
|
||||
elif role == order_roles.REDUCE_POSITION:
|
||||
self._on_reduced_position()
|
||||
self._on_reduced_position(order)
|
||||
|
||||
def filters(self):
|
||||
return []
|
||||
@@ -198,6 +184,9 @@ class Strategy(ABC):
|
||||
)
|
||||
|
||||
def _prepare_buy(self, make_copies=True):
|
||||
if type(self.buy) is np.ndarray:
|
||||
return
|
||||
|
||||
# create a copy in the placeholders variables so we can detect future modifications
|
||||
# also, make it list of orders even if there's only one, to make it easier to loop
|
||||
if type(self.buy[0]) not in [list, tuple]:
|
||||
@@ -208,6 +197,9 @@ class Strategy(ABC):
|
||||
self._buy = self.buy.copy()
|
||||
|
||||
def _prepare_sell(self, make_copies=True):
|
||||
if type(self.sell) is np.ndarray:
|
||||
return
|
||||
|
||||
# create a copy in the placeholders variables so we can detect future modifications
|
||||
# also, make it list of orders even if there's only one, to make it easier to loop
|
||||
if type(self.sell[0]) not in [list, tuple]:
|
||||
@@ -395,7 +387,8 @@ class Strategy(ABC):
|
||||
self._stop_loss_orders = []
|
||||
self._take_profit_orders = []
|
||||
|
||||
self._initial_qty = None
|
||||
self.increased_count = 0
|
||||
self.reduced_count = 0
|
||||
|
||||
def on_cancel(self):
|
||||
"""
|
||||
@@ -417,8 +410,16 @@ class Strategy(ABC):
|
||||
def should_cancel(self) -> bool:
|
||||
pass
|
||||
|
||||
def prepare(self):
|
||||
"""What should get updated after each strategy execution?"""
|
||||
def before(self):
|
||||
"""
|
||||
Get's executed BEFORE executing the strategy's logic
|
||||
"""
|
||||
pass
|
||||
|
||||
def after(self):
|
||||
"""
|
||||
Get's executed AFTER executing the strategy's logic
|
||||
"""
|
||||
pass
|
||||
|
||||
def _update_position(self):
|
||||
@@ -430,167 +431,171 @@ class Strategy(ABC):
|
||||
if self.position.is_close:
|
||||
return
|
||||
|
||||
if self.is_long:
|
||||
# prepare format
|
||||
if type(self.buy[0]) not in [list, tuple, np.ndarray]:
|
||||
self.buy = [self.buy]
|
||||
self.buy = np.array(self.buy, dtype=float)
|
||||
try:
|
||||
if self.is_long:
|
||||
# prepare format
|
||||
self._prepare_buy(make_copies=False)
|
||||
|
||||
# if entry has been modified
|
||||
if not np.array_equal(self.buy, self._buy):
|
||||
self._buy = self.buy.copy()
|
||||
# if entry has been modified
|
||||
if not np.array_equal(self.buy, self._buy):
|
||||
self._buy = self.buy.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._open_position_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
# cancel orders
|
||||
for o in self._open_position_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._open_position_orders = [o for o in self._open_position_orders if o.is_executed]
|
||||
for o in self._buy:
|
||||
# STOP order
|
||||
if o[1] > self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.start_profit_at(sides.BUY, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.buy_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# MARKET order
|
||||
elif o[1] == self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.buy_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
)
|
||||
|
||||
elif self.is_short:
|
||||
# prepare format
|
||||
if type(self.sell[0]) not in [list, tuple, np.ndarray]:
|
||||
self.sell = [self.sell]
|
||||
self.sell = np.array(self.sell, dtype=float)
|
||||
|
||||
# if entry has been modified
|
||||
if not np.array_equal(self.sell, self._sell):
|
||||
self._sell = self.sell.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._open_position_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._open_position_orders = [o for o in self._open_position_orders if o.is_executed]
|
||||
|
||||
for o in self._sell:
|
||||
# STOP order
|
||||
if o[1] > self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.start_profit_at(sides.BUY, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.sell_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# MARKET order
|
||||
elif o[1] == self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.sell_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
)
|
||||
|
||||
if self.position.is_open and self.take_profit is not None:
|
||||
self._validate_take_profit()
|
||||
self._prepare_take_profit(False)
|
||||
|
||||
# if _take_profit has been modified
|
||||
if not np.array_equal(self.take_profit, self._take_profit):
|
||||
self._take_profit = self.take_profit.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._take_profit_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._take_profit_orders = [o for o in self._take_profit_orders if o.is_executed]
|
||||
self._log_take_profit = []
|
||||
for s in self._take_profit_orders:
|
||||
self._log_take_profit.append(
|
||||
(abs(s.qty), s.price)
|
||||
)
|
||||
for o in self._take_profit:
|
||||
self._log_take_profit.append(o)
|
||||
|
||||
if o[1] == self.price:
|
||||
if self.is_long:
|
||||
self._take_profit_orders.append(
|
||||
self.broker.sell_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
# clean orders array but leave executed ones
|
||||
self._open_position_orders = [o for o in self._open_position_orders if o.is_executed]
|
||||
for o in self._buy:
|
||||
# STOP order
|
||||
if o[1] > self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.start_profit_at(sides.BUY, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
elif self.is_short:
|
||||
self._take_profit_orders.append(
|
||||
self.broker.buy_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.buy_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# MARKET order
|
||||
elif o[1] == self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.buy_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
)
|
||||
else:
|
||||
if (self.is_long and o[1] > self.price) or (self.is_short and o[1] < self.price):
|
||||
|
||||
self._take_profit_orders.append(
|
||||
self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
elif self.is_short:
|
||||
# prepare format
|
||||
self._prepare_sell(make_copies=False)
|
||||
|
||||
# if entry has been modified
|
||||
if not np.array_equal(self.sell, self._sell):
|
||||
self._sell = self.sell.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._open_position_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._open_position_orders = [o for o in self._open_position_orders if o.is_executed]
|
||||
|
||||
for o in self._sell:
|
||||
# STOP order
|
||||
if o[1] < self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.start_profit_at(sides.SELL, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# LIMIT order
|
||||
elif o[1] > self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.sell_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
)
|
||||
# MARKET order
|
||||
elif o[1] == self.price:
|
||||
self._open_position_orders.append(
|
||||
self.broker.sell_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
)
|
||||
|
||||
if self.position.is_open and self.take_profit is not None:
|
||||
self._validate_take_profit()
|
||||
self._prepare_take_profit(False)
|
||||
|
||||
# if _take_profit has been modified
|
||||
if not np.array_equal(self.take_profit, self._take_profit):
|
||||
self._take_profit = self.take_profit.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._take_profit_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._take_profit_orders = [o for o in self._take_profit_orders if o.is_executed]
|
||||
self._log_take_profit = []
|
||||
for s in self._take_profit_orders:
|
||||
self._log_take_profit.append(
|
||||
(abs(s.qty), s.price)
|
||||
)
|
||||
for o in self._take_profit:
|
||||
self._log_take_profit.append(o)
|
||||
|
||||
if o[1] == self.price:
|
||||
if self.is_long:
|
||||
self._take_profit_orders.append(
|
||||
self.broker.sell_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
)
|
||||
)
|
||||
elif (self.is_long and o[1] < self.price) or (self.is_short and o[1] > self.price):
|
||||
self._take_profit_orders.append(
|
||||
elif self.is_short:
|
||||
self._take_profit_orders.append(
|
||||
self.broker.buy_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
)
|
||||
else:
|
||||
if (self.is_long and o[1] > self.price) or (self.is_short and o[1] < self.price):
|
||||
|
||||
self._take_profit_orders.append(
|
||||
self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
)
|
||||
elif (self.is_long and o[1] < self.price) or (self.is_short and o[1] > self.price):
|
||||
self._take_profit_orders.append(
|
||||
self.broker.stop_loss_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
)
|
||||
|
||||
if self.position.is_open and self.stop_loss is not None:
|
||||
self._validate_stop_loss()
|
||||
self._prepare_stop_loss(False)
|
||||
|
||||
# if stop_loss has been modified
|
||||
if not np.array_equal(self.stop_loss, self._stop_loss):
|
||||
# prepare format
|
||||
self._stop_loss = self.stop_loss.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._stop_loss_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._stop_loss_orders = [o for o in self._stop_loss_orders if o.is_executed]
|
||||
self._log_stop_loss = []
|
||||
for s in self._stop_loss_orders:
|
||||
self._log_stop_loss.append(
|
||||
(abs(s.qty), s.price)
|
||||
)
|
||||
for o in self._stop_loss:
|
||||
self._log_stop_loss.append(o)
|
||||
|
||||
if o[1] == self.price:
|
||||
if self.is_long:
|
||||
self._stop_loss_orders.append(
|
||||
self.broker.sell_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
)
|
||||
elif self.is_short:
|
||||
self._stop_loss_orders.append(
|
||||
self.broker.buy_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
)
|
||||
else:
|
||||
self._stop_loss_orders.append(
|
||||
self.broker.stop_loss_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
)
|
||||
|
||||
if self.position.is_open and self.stop_loss is not None:
|
||||
self._validate_stop_loss()
|
||||
self._prepare_stop_loss(False)
|
||||
|
||||
# if stop_loss has been modified
|
||||
if not np.array_equal(self.stop_loss, self._stop_loss):
|
||||
# prepare format
|
||||
self._stop_loss = self.stop_loss.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._stop_loss_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
self.broker.cancel_order(o.id)
|
||||
|
||||
# clean orders array but leave executed ones
|
||||
self._stop_loss_orders = [o for o in self._stop_loss_orders if o.is_executed]
|
||||
self._log_stop_loss = []
|
||||
for s in self._stop_loss_orders:
|
||||
self._log_stop_loss.append(
|
||||
(abs(s.qty), s.price)
|
||||
)
|
||||
for o in self._stop_loss:
|
||||
self._log_stop_loss.append(o)
|
||||
|
||||
if o[1] == self.price:
|
||||
if self.is_long:
|
||||
self._stop_loss_orders.append(
|
||||
self.broker.sell_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
)
|
||||
elif self.is_short:
|
||||
self._stop_loss_orders.append(
|
||||
self.broker.buy_at_market(o[0], role=order_roles.CLOSE_POSITION)
|
||||
)
|
||||
else:
|
||||
self._stop_loss_orders.append(
|
||||
self.broker.stop_loss_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
)
|
||||
except TypeError:
|
||||
raise exceptions.InvalidStrategy(
|
||||
'Something odd is going on with your strategy. '
|
||||
'Try running it with "--debug" to see what was going on near the end, and fix it.'
|
||||
)
|
||||
except:
|
||||
raise
|
||||
|
||||
# validations: stop-loss and take-profit should not be the same
|
||||
if self.position.is_open:
|
||||
@@ -655,7 +660,9 @@ class Strategy(ABC):
|
||||
elif should_short:
|
||||
self._execute_short()
|
||||
|
||||
def _on_open_position(self):
|
||||
def _on_open_position(self, order: Order):
|
||||
self.increased_count = 1
|
||||
|
||||
self._broadcast('route-open-position')
|
||||
|
||||
if self.take_profit is not None:
|
||||
@@ -717,58 +724,59 @@ class Strategy(ABC):
|
||||
)
|
||||
|
||||
self._open_position_orders = []
|
||||
self._initial_qty = self.position.qty
|
||||
self.on_open_position()
|
||||
self.on_open_position(order)
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_open_position(self):
|
||||
def on_open_position(self, order: Order):
|
||||
"""
|
||||
What should happen after the open position order has been executed
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_stop_loss(self):
|
||||
def _on_stop_loss(self, order: Order):
|
||||
if not jh.should_execute_silently() or jh.is_debugging():
|
||||
logger.info('Stop-loss has been executed.')
|
||||
|
||||
self._broadcast('route-stop-loss')
|
||||
self._execute_cancel()
|
||||
self.on_stop_loss()
|
||||
self.on_stop_loss(order)
|
||||
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_stop_loss(self):
|
||||
def on_stop_loss(self, order: Order):
|
||||
"""
|
||||
What should happen after the stop-loss order has been executed
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_take_profit(self):
|
||||
def _on_take_profit(self, order: Order):
|
||||
if not jh.should_execute_silently() or jh.is_debugging():
|
||||
logger.info("Take-profit order has been executed.")
|
||||
|
||||
self._broadcast('route-take-profit')
|
||||
self._execute_cancel()
|
||||
self.on_take_profit()
|
||||
self.on_take_profit(order)
|
||||
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_take_profit(self):
|
||||
def on_take_profit(self, order: Order):
|
||||
"""
|
||||
What should happen after the take-profit order is executed.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_increased_position(self):
|
||||
def _on_increased_position(self, order: Order):
|
||||
self.increased_count += 1
|
||||
|
||||
self._open_position_orders = []
|
||||
|
||||
self._broadcast('route-increased-position')
|
||||
|
||||
self.on_increased_position()
|
||||
self.on_increased_position(order)
|
||||
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_increased_position(self):
|
||||
def on_increased_position(self, order: Order):
|
||||
"""
|
||||
What should happen after the order (if any) increasing the
|
||||
size of the position is executed. Overwrite it if needed.
|
||||
@@ -776,19 +784,21 @@ class Strategy(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_reduced_position(self):
|
||||
def _on_reduced_position(self, order: Order):
|
||||
"""
|
||||
prepares for on_reduced_position() is implemented by user
|
||||
"""
|
||||
self.reduced_count += 1
|
||||
|
||||
self._open_position_orders = []
|
||||
|
||||
self._broadcast('route-reduced-position')
|
||||
|
||||
self.on_reduced_position()
|
||||
self.on_reduced_position(order)
|
||||
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_reduced_position(self):
|
||||
def on_reduced_position(self, order: Order):
|
||||
"""
|
||||
What should happen after the order (if any) reducing the size of the position is executed.
|
||||
"""
|
||||
@@ -849,8 +859,9 @@ class Strategy(ABC):
|
||||
|
||||
self._is_executing = True
|
||||
|
||||
self.prepare()
|
||||
self.before()
|
||||
self._check()
|
||||
self.after()
|
||||
|
||||
self._is_executing = False
|
||||
self.index += 1
|
||||
@@ -1005,11 +1016,6 @@ class Strategy(ABC):
|
||||
"""returns the current time"""
|
||||
return store.app.time
|
||||
|
||||
@property
|
||||
def BTCUSD(self):
|
||||
"""shortcut for BTCUSD symbol string """
|
||||
return 'BTCUSD' if self.exchange == 'Bitfinex' else 'BTCUSDT'
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
"""alias for self.capital"""
|
||||
@@ -1177,3 +1183,12 @@ class Strategy(ABC):
|
||||
@property
|
||||
def shared_vars(self):
|
||||
return store.vars
|
||||
|
||||
@property
|
||||
def routes(self) -> List[Route]:
|
||||
from jesse.routes import router
|
||||
return router.routes
|
||||
|
||||
@property
|
||||
def has_active_entry_orders(self) -> bool:
|
||||
return len(self._open_position_orders) > 0
|
||||
|
||||
@@ -29,5 +29,5 @@ class Test13(Strategy):
|
||||
return []
|
||||
|
||||
def update_position(self):
|
||||
if self.is_reduced:
|
||||
if self.reduced_count > 0:
|
||||
self.take_profit = self.position.qty, 16
|
||||
|
||||
@@ -20,7 +20,7 @@ class Test14(Strategy):
|
||||
self.take_profit = qty, 13
|
||||
|
||||
def update_position(self):
|
||||
if self.is_reduced:
|
||||
if self.reduced_count > 0:
|
||||
self.stop_loss = self.position.qty, 4
|
||||
|
||||
def go_short(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ class Test18(Strategy):
|
||||
(1, 13)
|
||||
]
|
||||
|
||||
def on_reduced_position(self):
|
||||
def on_reduced_position(self, order):
|
||||
self.take_profit = abs(self.position.qty), self.price
|
||||
|
||||
def go_short(self):
|
||||
|
||||
@@ -45,10 +45,10 @@ class Test29(Strategy):
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def on_take_profit(self):
|
||||
def on_take_profit(self, order):
|
||||
self.vars['should_long'] = False
|
||||
self.vars['should_short'] = False
|
||||
|
||||
def on_stop_loss(self):
|
||||
def on_stop_loss(self, order):
|
||||
self.vars['should_long'] = False
|
||||
self.vars['should_short'] = False
|
||||
|
||||
@@ -3,15 +3,12 @@ from jesse.strategies import Strategy
|
||||
|
||||
# test_shared_vars [part 1]
|
||||
class Test32(Strategy):
|
||||
"""
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.shared_vars['buy-eth'] = False
|
||||
|
||||
def prepare(self):
|
||||
def before(self):
|
||||
if self.index == 10:
|
||||
self.shared_vars['buy-eth'] = True
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from jesse.strategies import Strategy
|
||||
|
||||
# test_filters
|
||||
class Test37(Strategy):
|
||||
def prepare(self):
|
||||
def before(self):
|
||||
"""used it to do assertions"""
|
||||
if self.index in [3, 11]:
|
||||
assert self.take_profit is None
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_is_increased
|
||||
class Test42(Strategy):
|
||||
def should_long(self) -> bool:
|
||||
return self.index == 0
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
self.buy = [
|
||||
(.5, 2),
|
||||
(.5, 4),
|
||||
]
|
||||
self.take_profit = [
|
||||
(.5, 6),
|
||||
(.5, 10),
|
||||
]
|
||||
|
||||
def update_position(self):
|
||||
if self.price < 4 and self.position.qty == .5:
|
||||
assert not self.is_increased
|
||||
assert not self.is_reduced
|
||||
|
||||
if self.position.qty == 1:
|
||||
assert self.is_increased
|
||||
assert not self.is_reduced
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
@@ -1,31 +0,0 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_is_reduced
|
||||
class Test43(Strategy):
|
||||
def should_long(self) -> bool:
|
||||
return self.index == 0
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
self.buy = 1, 2
|
||||
self.take_profit = [
|
||||
(.5, 6),
|
||||
(.5, 10),
|
||||
]
|
||||
|
||||
def update_position(self):
|
||||
if self.position.qty == 1:
|
||||
assert not self.is_increased
|
||||
assert not self.is_reduced
|
||||
elif self.position.qty == .5:
|
||||
assert not self.is_increased
|
||||
assert self.is_reduced
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
27
jesse/strategies/TestAfterMethod/__init__.py
Normal file
27
jesse/strategies/TestAfterMethod/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_after
|
||||
class TestAfterMethod(Strategy):
|
||||
def should_long(self) -> bool:
|
||||
return False
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
pass
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def before(self):
|
||||
if self.index == 1:
|
||||
assert self.vars['counter'] == 100
|
||||
|
||||
def after(self):
|
||||
if self.index == 0:
|
||||
self.vars['counter'] = 100
|
||||
32
jesse/strategies/TestBeforeMethod/__init__.py
Normal file
32
jesse/strategies/TestBeforeMethod/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_before
|
||||
class TestBeforeMethod(Strategy):
|
||||
def should_long(self) -> bool:
|
||||
if self.index == 0:
|
||||
assert self.vars['counter'] == 10
|
||||
|
||||
if self.index == 2:
|
||||
assert self.vars['counter'] == 100
|
||||
|
||||
return False
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
pass
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def before(self):
|
||||
if self.index == 0:
|
||||
self.vars['counter'] = 10
|
||||
|
||||
if self.index == 2:
|
||||
self.vars['counter'] = 100
|
||||
25
jesse/strategies/TestHasEntryOrders/__init__.py
Normal file
25
jesse/strategies/TestHasEntryOrders/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_has_entry_orders
|
||||
class TestHasEntryOrders(Strategy):
|
||||
def should_long(self) -> bool:
|
||||
return self.index == 0
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
qty = 1
|
||||
self.buy = qty, self.price + 2
|
||||
self.take_profit = qty, self.price + 4
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def before(self):
|
||||
if 0 < self.index < 2:
|
||||
assert self.has_active_entry_orders is True
|
||||
47
jesse/strategies/TestIncreasedAndReducedCount/__init__.py
Normal file
47
jesse/strategies/TestIncreasedAndReducedCount/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_increaed_and_reduced_count
|
||||
class TestIncreasedAndReducedCount(Strategy):
|
||||
def should_long(self) -> bool:
|
||||
return self.index == 0
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
qty = 1
|
||||
self.buy = qty, self.price
|
||||
|
||||
def update_position(self):
|
||||
if self.position.qty == 1 and self.index == 1:
|
||||
assert self.reduced_count == 0
|
||||
|
||||
assert self.increased_count == 1
|
||||
# now increase position
|
||||
self.buy = 1, self.price
|
||||
|
||||
elif self.position.qty == 2:
|
||||
assert self.increased_count == 2
|
||||
|
||||
# reduce it by one
|
||||
self.take_profit = 0.5, self.price
|
||||
elif self.position.qty == 1.5:
|
||||
assert self.reduced_count == 1
|
||||
self.take_profit = 0.5, self.price
|
||||
else:
|
||||
assert self.reduced_count == 2
|
||||
|
||||
# close trade
|
||||
self.liquidate()
|
||||
|
||||
def before(self):
|
||||
if self.trades_count == 1:
|
||||
assert self.increased_count == 0
|
||||
assert self.reduced_count == 0
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
@@ -5,13 +5,12 @@ crypto_empyrical~=1.0.4
|
||||
matplotlib~=3.3.3
|
||||
newtulipy~=0.4.2
|
||||
numpy~=1.19.4
|
||||
pandas~=1.1.4
|
||||
pandas==1.2.0
|
||||
peewee~=3.14.0
|
||||
polygon-api-client~=0.1.9
|
||||
psycopg2-binary~=2.8.6
|
||||
pydash~=4.9.0
|
||||
pytest-watch~=4.2.0
|
||||
pytest~=6.1.2
|
||||
pytest==6.2.1
|
||||
requests~=2.25.0
|
||||
scipy~=1.5.4
|
||||
TA-Lib~=0.4.19
|
||||
|
||||
2
setup.py
2
setup.py
@@ -1,6 +1,6 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
VERSION = '0.15.2'
|
||||
VERSION = '0.17.2'
|
||||
DESCRIPTION = "A trading framework for cryptocurrencies"
|
||||
|
||||
REQUIRED_PACKAGES = [
|
||||
|
||||
@@ -267,6 +267,27 @@ def test_correl():
|
||||
assert seq[-1] == single
|
||||
|
||||
|
||||
def test_correlation_cycle():
|
||||
candles = np.array(mama_candles)
|
||||
|
||||
single = ta.correlation_cycle(candles)
|
||||
assert type(single).__name__ == 'CC'
|
||||
assert round(single.real, 2) == 0.23
|
||||
assert round(single.imag, 2) == 0.38
|
||||
assert round(single.angle, 2) == -55.87
|
||||
assert round(single.state, 2) == -1
|
||||
|
||||
seq = ta.correlation_cycle(candles, sequential=True)
|
||||
assert seq.real[-1] == single.real
|
||||
assert seq.imag[-1] == single.imag
|
||||
assert seq.angle[-1] == single.angle
|
||||
assert seq.state[-1] == single.state
|
||||
assert len(seq.real) == len(candles)
|
||||
assert len(seq.imag) == len(candles)
|
||||
assert len(seq.angle) == len(candles)
|
||||
assert len(seq.state) == len(candles)
|
||||
|
||||
|
||||
def test_cvi():
|
||||
candles = np.array(mama_candles)
|
||||
|
||||
@@ -380,6 +401,17 @@ def test_dpo():
|
||||
assert seq[-1] == single
|
||||
|
||||
|
||||
def test_dti():
|
||||
candles = np.array(mama_candles)
|
||||
|
||||
single = ta.dti(candles)
|
||||
seq = ta.dti(candles, sequential=True)
|
||||
|
||||
assert round(single, 2) == -32.6
|
||||
assert len(seq) == len(candles)
|
||||
assert seq[-1] == single
|
||||
|
||||
|
||||
def test_dx():
|
||||
candles = np.array(dema_candles)
|
||||
|
||||
@@ -609,6 +641,22 @@ def test_ichimoku_cloud():
|
||||
assert (conversion_line, base_line, span_a, span_b, lagging_line, future_span_a, future_span_b) == (8861.59, 8861.59, 8465.25, 8217.45, 8627.13, 8861.59, 8579.49)
|
||||
|
||||
|
||||
def test_ichimoku_cloud_seq():
|
||||
candles = np.array(ichimoku_candles)
|
||||
|
||||
conversion_line, base_line, span_a, span_b, lagging_line, future_span_a, future_span_b = ta.ichimoku_cloud_seq(
|
||||
candles)
|
||||
seq = ta.ichimoku_cloud_seq(candles, sequential=True)
|
||||
|
||||
assert type(seq).__name__ == 'IchimokuCloud'
|
||||
assert (conversion_line, base_line, span_a, span_b, lagging_line, future_span_a, future_span_b) == (
|
||||
seq.conversion_line[-1], seq.base_line[-1], seq.span_a[-1], seq.span_b[-1], seq.lagging_line[-1],
|
||||
seq.future_span_a[-1], seq.future_span_b[-1])
|
||||
assert (conversion_line, base_line, span_a, span_b, lagging_line, future_span_a, future_span_b) == (
|
||||
8861.59, 8861.59, 8465.25, 8204.715, 8730.0, 8861.59, 8579.49)
|
||||
assert len(seq.conversion_line) == len(candles)
|
||||
|
||||
|
||||
def test_itrend():
|
||||
candles = np.array(mama_candles)
|
||||
single = ta.itrend(candles)
|
||||
@@ -817,6 +865,16 @@ def test_mass():
|
||||
assert seq[-1] == single
|
||||
|
||||
|
||||
def test_mcginley_dynamic():
|
||||
candles = np.array(mama_candles)
|
||||
|
||||
single = ta.mcginley_dynamic(candles)
|
||||
seq = ta.mcginley_dynamic(candles, sequential=True)
|
||||
assert round(single, 2) == 107.82
|
||||
assert len(seq) == len(candles)
|
||||
assert seq[-1] == single
|
||||
|
||||
|
||||
def test_medprice():
|
||||
# use the same candles as mama_candles
|
||||
candles = np.array(mama_candles)
|
||||
@@ -1498,6 +1556,17 @@ def test_voss():
|
||||
assert len(seq.filt) == len(candles)
|
||||
|
||||
|
||||
def test_vpci():
|
||||
candles = np.array(mama_candles)
|
||||
single = ta.vpci(candles)
|
||||
seq = ta.vpci(candles, sequential=True)
|
||||
|
||||
assert round(single.vpci, 2) == -29.46
|
||||
assert round(single.vpcis, 2) == -14.4
|
||||
assert len(seq.vpci) == len(candles)
|
||||
assert seq.vpci[-1] == single.vpci
|
||||
|
||||
|
||||
def test_vpt():
|
||||
candles = np.array(mama_candles)
|
||||
single = ta.vpt(candles)
|
||||
|
||||
@@ -219,14 +219,6 @@ def test_increasing_position_size_after_opening():
|
||||
assert t1.fee == 0
|
||||
|
||||
|
||||
def test_is_increased():
|
||||
single_route_backtest('Test42')
|
||||
|
||||
|
||||
def test_is_reduced():
|
||||
single_route_backtest('Test43')
|
||||
|
||||
|
||||
def test_is_smart_enough_to_open_positions_via_market_orders():
|
||||
set_up([
|
||||
(exchanges.SANDBOX, 'ETHUSDT', timeframes.MINUTE_1, 'Test05'),
|
||||
@@ -844,7 +836,7 @@ def test_terminate():
|
||||
"""
|
||||
test that user can use terminate() method. in this unit test use it
|
||||
to close the open position.
|
||||
"""
|
||||
`"""
|
||||
single_route_backtest('Test41')
|
||||
|
||||
# assert terminate() is actually executed by logging a
|
||||
@@ -922,6 +914,35 @@ def test_validation_for_equal_stop_loss_and_take_profit():
|
||||
|
||||
assert str(err.value).startswith('stop-loss and take-profit should not be exactly the same')
|
||||
|
||||
|
||||
def test_has_active_entry_orders():
|
||||
single_route_backtest('TestHasEntryOrders')
|
||||
|
||||
|
||||
def test_increased_and_reduced_count():
|
||||
single_route_backtest('TestIncreasedAndReducedCount')
|
||||
|
||||
|
||||
def test_before():
|
||||
single_route_backtest('TestBeforeMethod')
|
||||
|
||||
|
||||
def test_after():
|
||||
single_route_backtest('TestAfterMethod')
|
||||
|
||||
|
||||
# def test_route_capital_isolation():
|
||||
# set_up(
|
||||
# [
|
||||
# (exchanges.SANDBOX, 'BTCUSDT', timeframes.MINUTE_1, 'TestRouteCapitalIsolation1'),
|
||||
# (exchanges.SANDBOX, 'ETHUSDT', timeframes.MINUTE_1, 'TestRouteCapitalIsolation2'),
|
||||
# ],
|
||||
# )
|
||||
#
|
||||
# # run backtest (dates are fake just to pass)
|
||||
# backtest_mode.run('2019-04-01', '2019-04-02', get_btc_and_eth_candles())
|
||||
|
||||
|
||||
# def test_inputs_get_rounded_behind_the_scene():
|
||||
# set_up([(exchanges.SANDBOX, 'EOSUSDT', timeframes.MINUTE_1, 'Test44')])
|
||||
# candles = {}
|
||||
|
||||
Reference in New Issue
Block a user