Merge remote-tracking branch 'origin/master' into stock

This commit is contained in:
Markus
2020-12-27 20:25:02 +01:00
34 changed files with 804 additions and 376 deletions

View File

@@ -1,7 +1,6 @@
language: python
dist: bionic
python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"

View File

@@ -8,6 +8,7 @@
[![Website](https://img.shields.io/badge/Website-Start%20here!-9cf)](https://jesse.trade)
[![Docs](https://img.shields.io/badge/Docs-Learn%20how!-red)](https://docs.jesse.trade)
[![Docs](https://img.shields.io/discord/771690508413829141)](https://jesse.trade/discord)
[![Forum](https://img.shields.io/badge/Forum-Join%20us!-brightgreen)](https://forum.jesse.trade)
[![Blog](https://img.shields.io/badge/Blog-Get%20the%20news!-blueviolet)](https://jesse.trade/blog)
---

View File

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

View File

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

View 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
View 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]

View 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])

View 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
View 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])

View File

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

View File

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

View File

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

View File

@@ -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):

View File

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

View File

@@ -30,3 +30,5 @@ __pycache__
.vscode
/.ipynb_checkpoints
/plugins
config.py
routes.py

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -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):

View File

@@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View 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

View File

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

View File

@@ -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 = [

View File

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

View File

@@ -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 = {}