Merge branch 'master' into dashboard
This commit is contained in:
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -200,7 +200,7 @@ config = {
|
||||
# Below configurations are related to the optimize mode
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
'optimization': {
|
||||
# sharpe, calmar, sortino, omega
|
||||
# sharpe, calmar, sortino, omega, serenity, smart sharpe, smart sortino
|
||||
'ratio': 'sharpe',
|
||||
},
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class order_statuses:
|
||||
ACTIVE = 'ACTIVE'
|
||||
CANCELED = 'CANCELED'
|
||||
EXECUTED = 'EXECUTED'
|
||||
PARTIALLY_EXECUTED = 'PARTIALLY EXECUTED'
|
||||
QUEUED = 'QUEUED'
|
||||
LIQUIDATED = 'LIQUIDATED'
|
||||
|
||||
|
||||
@@ -269,7 +269,15 @@ def get_strategy_class(strategy_name: str):
|
||||
from pydoc import locate
|
||||
|
||||
if is_unit_testing():
|
||||
return locate(f'jesse.strategies.{strategy_name}.{strategy_name}')
|
||||
path = sys.path[0]
|
||||
# live plugin
|
||||
if path.endswith('jesse-live'):
|
||||
strategy_dir = f'tests.strategies.{strategy_name}.{strategy_name}'
|
||||
# main framework
|
||||
else:
|
||||
strategy_dir = f'jesse.strategies.{strategy_name}.{strategy_name}'
|
||||
|
||||
return locate(strategy_dir)
|
||||
else:
|
||||
return locate(f'strategies.{strategy_name}.{strategy_name}')
|
||||
|
||||
@@ -417,6 +425,10 @@ def now_to_timestamp(force_fresh=False) -> int:
|
||||
return arrow.utcnow().int_timestamp * 1000
|
||||
|
||||
|
||||
def current_1m_candle_timestamp():
|
||||
return arrow.utcnow().floor('minute').int_timestamp * 1000
|
||||
|
||||
|
||||
def np_ffill(arr: np.ndarray, axis: int = 0) -> np.ndarray:
|
||||
idx_shape = tuple([slice(None)] + [np.newaxis] * (len(arr.shape) - axis - 1))
|
||||
idx = np.where(~np.isnan(arr), np.arange(arr.shape[axis])[idx_shape], 0)
|
||||
|
||||
@@ -51,15 +51,9 @@ class CompletedTrade(peewee.Model):
|
||||
"type": self.type,
|
||||
"entry_price": self.entry_price,
|
||||
"exit_price": self.exit_price,
|
||||
"take_profit_at": self.take_profit_at,
|
||||
"stop_loss_at": self.stop_loss_at,
|
||||
"qty": self.qty,
|
||||
"fee": self.fee,
|
||||
"reward": self.reward,
|
||||
"size": self.size,
|
||||
"risk": self.risk,
|
||||
"risk_percentage": self.risk_percentage,
|
||||
"R": self.r,
|
||||
"PNL": self.pnl,
|
||||
"PNL_percentage": self.pnl_percentage,
|
||||
"holding_period": self.holding_period,
|
||||
@@ -79,19 +73,13 @@ class CompletedTrade(peewee.Model):
|
||||
'type': self.type,
|
||||
'entry_price': self.entry_price,
|
||||
'exit_price': self.exit_price,
|
||||
'take_profit_at': self.take_profit_at,
|
||||
'stop_loss_at': self.stop_loss_at,
|
||||
'qty': self.qty,
|
||||
'opened_at': self.opened_at,
|
||||
'closed_at': self.closed_at,
|
||||
'entry_candle_timestamp': self.entry_candle_timestamp,
|
||||
'exit_candle_timestamp': self.exit_candle_timestamp,
|
||||
"fee": self.fee,
|
||||
"reward": self.reward,
|
||||
"size": self.size,
|
||||
"risk": self.risk,
|
||||
"risk_percentage": self.risk_percentage,
|
||||
"R": self.r,
|
||||
"PNL": self.pnl,
|
||||
"PNL_percentage": self.pnl_percentage,
|
||||
"holding_period": self.holding_period,
|
||||
@@ -102,31 +90,10 @@ class CompletedTrade(peewee.Model):
|
||||
trading_fee = jh.get_config(f'env.exchanges.{self.exchange}.fee')
|
||||
return trading_fee * self.qty * (self.entry_price + self.exit_price)
|
||||
|
||||
@property
|
||||
def reward(self) -> float:
|
||||
return abs(self.take_profit_at - self.entry_price) * self.qty
|
||||
|
||||
@property
|
||||
def size(self) -> float:
|
||||
return self.qty * self.entry_price
|
||||
|
||||
@property
|
||||
def risk(self) -> float:
|
||||
return abs(self.stop_loss_at - self.entry_price) * self.qty
|
||||
|
||||
@property
|
||||
def risk_percentage(self) -> float:
|
||||
return round((self.risk / self.size) * 100, 2)
|
||||
|
||||
@property
|
||||
def risk_reward_ratio(self) -> float:
|
||||
return self.reward / self.risk
|
||||
|
||||
@property
|
||||
def r(self) -> float:
|
||||
"""alias for risk_reward_ratio"""
|
||||
return self.risk_reward_ratio
|
||||
|
||||
@property
|
||||
def pnl(self) -> float:
|
||||
"""PNL"""
|
||||
|
||||
@@ -6,9 +6,9 @@ import jesse.services.logger as logger
|
||||
import jesse.services.selectors as selectors
|
||||
from jesse import sync_publish
|
||||
from jesse.config import config
|
||||
from jesse.enums import order_statuses, order_flags
|
||||
from jesse.services.db import db
|
||||
from jesse.services.notifier import notify
|
||||
from jesse.enums import order_statuses, order_flags
|
||||
|
||||
|
||||
class Order(Model):
|
||||
@@ -34,6 +34,7 @@ class Order(Model):
|
||||
executed_at = BigIntegerField(null=True)
|
||||
canceled_at = BigIntegerField(null=True)
|
||||
role = CharField(null=True)
|
||||
submitted_via = None
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
@@ -132,7 +133,7 @@ class Order(Model):
|
||||
'executed_at': self.executed_at,
|
||||
}
|
||||
|
||||
def cancel(self) -> None:
|
||||
def cancel(self, silent=False) -> None:
|
||||
if self.is_canceled or self.is_executed:
|
||||
return
|
||||
|
||||
@@ -142,23 +143,23 @@ class Order(Model):
|
||||
if jh.is_live():
|
||||
self.save()
|
||||
|
||||
if jh.is_debuggable('order_cancellation') or jh.is_live():
|
||||
logger.info(
|
||||
f'CANCELED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, ${round(self.price, 2)}'
|
||||
)
|
||||
if jh.is_live():
|
||||
sync_publish('order', self.to_dict)
|
||||
|
||||
if config['env']['notifications']['events']['cancelled_orders']:
|
||||
notify(
|
||||
f'CANCELED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, {round(self.price, 2)}'
|
||||
if not silent:
|
||||
if jh.is_debuggable('order_cancellation') or jh.is_live():
|
||||
logger.info(
|
||||
f'CANCELED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, ${round(self.price, 2)}'
|
||||
)
|
||||
if jh.is_live():
|
||||
sync_publish('order', self.to_dict)
|
||||
if config['env']['notifications']['events']['cancelled_orders']:
|
||||
notify(
|
||||
f'CANCELED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, {round(self.price, 2)}'
|
||||
)
|
||||
|
||||
# handle exchange balance
|
||||
e = selectors.get_exchange(self.exchange)
|
||||
e.on_order_cancellation(self)
|
||||
|
||||
def execute(self) -> None:
|
||||
def execute(self, silent=False) -> None:
|
||||
if self.is_canceled or self.is_executed:
|
||||
return
|
||||
|
||||
@@ -168,19 +169,19 @@ class Order(Model):
|
||||
if jh.is_live():
|
||||
self.save()
|
||||
|
||||
# log
|
||||
if jh.is_debuggable('order_execution') or jh.is_live():
|
||||
logger.info(
|
||||
f'EXECUTED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, ${round(self.price, 2)}'
|
||||
)
|
||||
# notify
|
||||
if jh.is_live():
|
||||
sync_publish('order', self.to_dict)
|
||||
|
||||
if config['env']['notifications']['events']['executed_orders']:
|
||||
notify(
|
||||
f'EXECUTED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, {round(self.price, 2)}'
|
||||
if not silent:
|
||||
# log
|
||||
if jh.is_debuggable('order_execution') or jh.is_live():
|
||||
logger.info(
|
||||
f'EXECUTED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, ${round(self.price, 2)}'
|
||||
)
|
||||
# notify
|
||||
if jh.is_live():
|
||||
sync_publish('order', self.to_dict)
|
||||
if config['env']['notifications']['events']['executed_orders']:
|
||||
notify(
|
||||
f'EXECUTED order: {self.symbol}, {self.type}, {self.side}, {self.qty}, {round(self.price, 2)}'
|
||||
)
|
||||
|
||||
p = selectors.get_position(self.exchange, self.symbol)
|
||||
|
||||
|
||||
@@ -96,8 +96,6 @@ def store_completed_trade_into_db(completed_trade: CompletedTrade) -> None:
|
||||
'timeframe': completed_trade.timeframe,
|
||||
'entry_price': completed_trade.entry_price,
|
||||
'exit_price': completed_trade.exit_price,
|
||||
'take_profit_at': completed_trade.take_profit_at,
|
||||
'stop_loss_at': completed_trade.stop_loss_at,
|
||||
'qty': completed_trade.qty,
|
||||
'opened_at': completed_trade.opened_at,
|
||||
'closed_at': completed_trade.closed_at,
|
||||
|
||||
@@ -124,9 +124,18 @@ class Optimizer(Genetics):
|
||||
elif ratio_config == 'omega':
|
||||
ratio = training_data['omega_ratio']
|
||||
ratio_normalized = jh.normalize(ratio, -.5, 5)
|
||||
elif ratio_config == 'serenity':
|
||||
ratio = training_data['serenity_index']
|
||||
ratio_normalized = jh.normalize(ratio, -.5, 15)
|
||||
elif ratio_config == 'smart sharpe':
|
||||
ratio = training_data['smart_sharpe']
|
||||
ratio_normalized = jh.normalize(ratio, -.5, 5)
|
||||
elif ratio_config == 'smart sortino':
|
||||
ratio = training_data['smart_sortino']
|
||||
ratio_normalized = jh.normalize(ratio, -.5, 15)
|
||||
else:
|
||||
raise ValueError(
|
||||
f'The entered ratio configuration `{ratio_config}` for the optimization is unknown. Choose between sharpe, calmar, sortino and omega.')
|
||||
f'The entered ratio configuration `{ratio_config}` for the optimization is unknown. Choose between sharpe, calmar, sortino, serenity, smart shapre, smart sortino and omega.')
|
||||
|
||||
if ratio < 0:
|
||||
score = 0.0001
|
||||
|
||||
@@ -35,7 +35,14 @@ class RouterClass:
|
||||
# validate strategy
|
||||
strategy_name = r["strategy"]
|
||||
if jh.is_unit_testing():
|
||||
exists = jh.file_exists(f"{sys.path[0]}/jesse/strategies/{strategy_name}/__init__.py")
|
||||
path = sys.path[0]
|
||||
# live plugin
|
||||
if path.endswith('jesse-live'):
|
||||
strategies_dir = f'{sys.path[0]}/tests/strategies'
|
||||
# main framework
|
||||
else:
|
||||
strategies_dir = f'{sys.path[0]}/jesse/strategies'
|
||||
exists = jh.file_exists(f"{strategies_dir}/{strategy_name}/__init__.py")
|
||||
else:
|
||||
exists = jh.file_exists(f'strategies/{strategy_name}/__init__.py')
|
||||
if not exists:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Any, Union
|
||||
|
||||
import crypto_empyrical
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from quantstats import stats
|
||||
|
||||
import jesse.helpers as jh
|
||||
from jesse.store import store
|
||||
@@ -42,7 +43,7 @@ def routes(routes_arr: list) -> dict:
|
||||
return array
|
||||
|
||||
|
||||
def trades(trades_list: list, daily_balance: list) -> dict:
|
||||
def trades(trades_list: list, daily_balance: list, final: bool = True) -> dict:
|
||||
starting_balance = 0
|
||||
current_balance = 0
|
||||
|
||||
@@ -77,9 +78,6 @@ def trades(trades_list: list, daily_balance: list) -> dict:
|
||||
largest_winning_trade = 0 if total_winning_trades == 0 else winning_trades['PNL'].max()
|
||||
|
||||
win_rate = len(winning_trades) / (len(losing_trades) + len(winning_trades))
|
||||
max_R = df['R'].max()
|
||||
min_R = df['R'].min()
|
||||
mean_R = df['R'].mean()
|
||||
longs_count = len(df.loc[df['type'] == 'long'])
|
||||
shorts_count = len(df.loc[df['type'] == 'short'])
|
||||
longs_percentage = longs_count / (longs_count + shorts_count) * 100
|
||||
@@ -101,16 +99,38 @@ def trades(trades_list: list, daily_balance: list) -> dict:
|
||||
gross_profit = winning_trades['PNL'].sum()
|
||||
gross_loss = losing_trades['PNL'].sum()
|
||||
|
||||
daily_returns = pd.Series(daily_balance).pct_change(1).values
|
||||
max_drawdown = crypto_empyrical.max_drawdown(daily_returns) * 100
|
||||
annual_return = crypto_empyrical.annual_return(daily_returns) * 100
|
||||
sharpe_ratio = crypto_empyrical.sharpe_ratio(daily_returns)
|
||||
calmar_ratio = crypto_empyrical.calmar_ratio(daily_returns)
|
||||
sortino_ratio = crypto_empyrical.sortino_ratio(daily_returns)
|
||||
omega_ratio = crypto_empyrical.omega_ratio(daily_returns)
|
||||
start_date = datetime.fromtimestamp(store.app.starting_time / 1000)
|
||||
date_list = [start_date + timedelta(days=x) for x in range(len(daily_balance))]
|
||||
|
||||
daily_return = pd.DataFrame(daily_balance, index=pd.to_datetime(list(date_list))).pct_change(1)
|
||||
|
||||
total_open_trades = store.app.total_open_trades
|
||||
open_pl = store.app.total_open_pl
|
||||
|
||||
|
||||
max_drawdown = np.nan
|
||||
annual_return = np.nan
|
||||
sharpe_ratio = np.nan
|
||||
calmar_ratio = np.nan
|
||||
sortino_ratio = np.nan
|
||||
omega_ratio = np.nan
|
||||
serenity_index = np.nan
|
||||
smart_sharpe = np.nan
|
||||
smart_sortino = np.nan
|
||||
|
||||
if len(daily_return) > 2:
|
||||
max_drawdown = stats.max_drawdown(daily_return).values[0] * 100
|
||||
annual_return = stats.cagr(daily_return).values[0] * 100
|
||||
sharpe_ratio = stats.sharpe(daily_return, periods=365).values[0]
|
||||
calmar_ratio = stats.calmar(daily_return).values[0]
|
||||
sortino_ratio = stats.sortino(daily_return, periods=365).values[0]
|
||||
omega_ratio = stats.omega(daily_return, periods=365)
|
||||
serenity_index = stats.serenity_index(daily_return).values[0]
|
||||
# As those calculations are slow they are only done for the final report and not at self.metrics in the strategy.
|
||||
if final:
|
||||
smart_sharpe = stats.smart_sharpe(daily_return, periods=365).values[0]
|
||||
smart_sortino = stats.smart_sortino(daily_return, periods=365).values[0]
|
||||
|
||||
return {
|
||||
'total': np.nan if np.isnan(total_completed) else total_completed,
|
||||
'total_winning_trades': np.nan if np.isnan(total_winning_trades) else total_winning_trades,
|
||||
@@ -118,9 +138,6 @@ def trades(trades_list: list, daily_balance: list) -> dict:
|
||||
'starting_balance': np.nan if np.isnan(starting_balance) else starting_balance,
|
||||
'finishing_balance': np.nan if np.isnan(current_balance) else current_balance,
|
||||
'win_rate': np.nan if np.isnan(win_rate) else win_rate,
|
||||
'max_R': np.nan if np.isnan(max_R) else max_R,
|
||||
'min_R': np.nan if np.isnan(min_R) else min_R,
|
||||
'mean_R': np.nan if np.isnan(mean_R) else mean_R,
|
||||
'ratio_avg_win_loss': np.nan if np.isnan(ratio_avg_win_loss) else ratio_avg_win_loss,
|
||||
'longs_count': np.nan if np.isnan(longs_count) else longs_count,
|
||||
'longs_percentage': np.nan if np.isnan(longs_percentage) else longs_percentage,
|
||||
@@ -140,12 +157,15 @@ def trades(trades_list: list, daily_balance: list) -> dict:
|
||||
'average_losing_holding_period': average_losing_holding_period,
|
||||
'gross_profit': gross_profit,
|
||||
'gross_loss': gross_loss,
|
||||
'max_drawdown': max_drawdown,
|
||||
'annual_return': annual_return,
|
||||
'sharpe_ratio': sharpe_ratio,
|
||||
'calmar_ratio': calmar_ratio,
|
||||
'sortino_ratio': sortino_ratio,
|
||||
'omega_ratio': omega_ratio,
|
||||
'max_drawdown': np.nan if np.isnan(max_drawdown) else max_drawdown,
|
||||
'annual_return': np.nan if np.isnan(annual_return) else annual_return,
|
||||
'sharpe_ratio': np.nan if np.isnan(sharpe_ratio) else sharpe_ratio,
|
||||
'calmar_ratio': np.nan if np.isnan(calmar_ratio) else calmar_ratio,
|
||||
'sortino_ratio': np.nan if np.isnan(sortino_ratio) else sortino_ratio,
|
||||
'omega_ratio': np.nan if np.isnan(omega_ratio) else omega_ratio,
|
||||
'serenity_index': np.nan if np.isnan(serenity_index) else serenity_index,
|
||||
'smart_sharpe': np.nan if np.isnan(smart_sharpe) else smart_sharpe,
|
||||
'smart_sortino': np.nan if np.isnan(smart_sortino) else smart_sortino,
|
||||
'total_open_trades': total_open_trades,
|
||||
'open_pl': open_pl,
|
||||
'winning_streak': winning_streak,
|
||||
|
||||
@@ -32,8 +32,8 @@ def quantstats_tearsheet(buy_and_hold_returns: pd.Series, study_name: str) -> No
|
||||
title = f"{modes[mode][1]} → {arrow.utcnow().strftime('%d %b, %Y %H:%M:%S')} → {study_name}"
|
||||
|
||||
try:
|
||||
qs.reports.html(returns=returns_time_series, trading_year_days=365, benchmark=buy_and_hold_returns, title=title, output=file_path)
|
||||
qs.reports.html(returns=returns_time_series, periods_per_year=365, benchmark=buy_and_hold_returns, title=title, output=file_path)
|
||||
except IndexError:
|
||||
qs.reports.html(returns=returns_time_series, trading_year_days=365, title=title, output=file_path)
|
||||
qs.reports.html(returns=returns_time_series, periods_per_year=365, title=title, output=file_path)
|
||||
except:
|
||||
raise
|
||||
|
||||
@@ -10,6 +10,7 @@ from jesse.models import store_candle_into_db
|
||||
from jesse.services.candle import generate_candle_from_one_minutes
|
||||
from timeloop import Timeloop
|
||||
from datetime import timedelta
|
||||
from jesse.services import logger
|
||||
|
||||
|
||||
class CandlesState:
|
||||
@@ -37,6 +38,15 @@ class CandlesState:
|
||||
for c in config['app']['considering_candles']:
|
||||
exchange, symbol = c[0], c[1]
|
||||
current_candle = self.get_current_candle(exchange, symbol, '1m')
|
||||
|
||||
# TODO: debugging
|
||||
if current_candle[0] <= 60_000:
|
||||
logger.error(
|
||||
'[REPORT TO Saleh for debugging please!] current_candle[0] is being zero but got fixed. more info:\n '
|
||||
f'current_candle: {current_candle[0]}, {current_candle[1]}, {current_candle[2]}, {current_candle[3]}, {current_candle[4]}, {current_candle[5]}'
|
||||
)
|
||||
continue
|
||||
|
||||
if jh.now() > current_candle[0] + 60_000:
|
||||
new_candle = self._generate_empty_candle_from_previous_candle(current_candle)
|
||||
self.add_candle(new_candle, exchange, symbol, '1m')
|
||||
@@ -47,6 +57,7 @@ class CandlesState:
|
||||
def _generate_empty_candle_from_previous_candle(previous_candle: np.ndarray) -> np.ndarray:
|
||||
new_candle = previous_candle.copy()
|
||||
new_candle[0] = previous_candle[0] + 60_000
|
||||
|
||||
# new candle's open, close, high, and low all equal to previous candle's close
|
||||
new_candle[1] = previous_candle[2]
|
||||
new_candle[2] = previous_candle[2]
|
||||
@@ -169,11 +180,6 @@ class CandlesState:
|
||||
if jh.now() > current_candle[0] + 60_000:
|
||||
new_candle = self._generate_empty_candle_from_previous_candle(current_candle)
|
||||
self.add_candle(new_candle, exchange, symbol, '1m')
|
||||
# if jh.now() > current_candle[0] + 60_000:
|
||||
# while jh.now() > current_candle[0] + 60_000:
|
||||
# new_candle = self._generate_empty_candle_from_previous_candle(current_candle)
|
||||
# self.add_candle(new_candle, exchange, symbol, '1m')
|
||||
# current_candle = self.get_current_candle(exchange, symbol, '1m')
|
||||
|
||||
# update position's current price
|
||||
self.update_position(exchange, symbol, trade['price'])
|
||||
@@ -219,8 +225,24 @@ class CandlesState:
|
||||
|
||||
last_candle = self.get_current_candle(exchange, symbol, timeframe)
|
||||
generate_from_count = int((candle[0] - last_candle[0]) / 60_000)
|
||||
number_of_candles = len(self.get_candles(exchange, symbol, '1m'))
|
||||
short_candles = self.get_candles(exchange, symbol, '1m')[-1 - generate_from_count:]
|
||||
|
||||
if generate_from_count < 0:
|
||||
current_1m = self.get_current_candle(exchange, symbol, '1m')
|
||||
raise ValueError(
|
||||
f'generate_from_count cannot be negative! '
|
||||
f'generate_from_count:{generate_from_count}, candle[0]:{candle[0]}, '
|
||||
f'last_candle[0]:{last_candle[0]}, current_1m:{current_1m[0]}, number_of_candles:{number_of_candles}')
|
||||
|
||||
if len(short_candles) == 0:
|
||||
raise ValueError(
|
||||
f'No candles were passed. More info:'
|
||||
f'\nexchange:{exchange}, symbol:{symbol}, timeframe:{timeframe}, generate_from_count:{generate_from_count}'
|
||||
f'\nlast_candle\'s timestamp: {last_candle[0]}'
|
||||
f'\ncurrent timestamp: {jh.now()}'
|
||||
)
|
||||
|
||||
# update latest candle
|
||||
generated_candle = generate_candle_from_one_minutes(
|
||||
timeframe,
|
||||
|
||||
@@ -17,8 +17,11 @@ from jesse.services.broker import Broker
|
||||
from jesse.store import store
|
||||
from jesse.services.cache import cached
|
||||
|
||||
|
||||
class Strategy(ABC):
|
||||
"""The parent strategy class which every strategy must extend"""
|
||||
"""
|
||||
The parent strategy class which every strategy must extend. It is the heart of the framework!
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.id = jh.generate_unique_id()
|
||||
@@ -42,12 +45,9 @@ class Strategy(ABC):
|
||||
self._stop_loss = None
|
||||
self.take_profit = None
|
||||
self._take_profit = None
|
||||
self._log_take_profit = None
|
||||
self._log_stop_loss = None
|
||||
|
||||
self._open_position_orders = []
|
||||
self._stop_loss_orders = []
|
||||
self._take_profit_orders = []
|
||||
self._close_position_orders = []
|
||||
|
||||
self.trade: CompletedTrade = None
|
||||
self.trades_count = 0
|
||||
@@ -105,10 +105,8 @@ class Strategy(ABC):
|
||||
|
||||
if msg == 'route-open-position':
|
||||
r.strategy.on_route_open_position(self)
|
||||
elif msg == 'route-stop-loss':
|
||||
r.strategy.on_route_stop_loss(self)
|
||||
elif msg == 'route-take-profit':
|
||||
r.strategy.on_route_take_profit(self)
|
||||
elif msg == 'route-close-position':
|
||||
r.strategy.on_route_close_position(self)
|
||||
elif msg == 'route-increased-position':
|
||||
r.strategy.on_route_increased_position(self)
|
||||
elif msg == 'route-reduced-position':
|
||||
@@ -133,10 +131,12 @@ class Strategy(ABC):
|
||||
|
||||
role = order.role
|
||||
|
||||
# if the order's role is CLOSE_POSITION but the position is still open, then it's increase_position order
|
||||
if role == order_roles.OPEN_POSITION and abs(self.position.qty) != abs(order.qty):
|
||||
order.role = order_roles.INCREASE_POSITION
|
||||
role = order_roles.INCREASE_POSITION
|
||||
|
||||
# if the order's role is CLOSE_POSITION but the position is still open, then it's reduce_position order
|
||||
if role == order_roles.CLOSE_POSITION and self.position.is_open:
|
||||
order.role = order_roles.REDUCE_POSITION
|
||||
role = order_roles.REDUCE_POSITION
|
||||
@@ -145,10 +145,8 @@ class Strategy(ABC):
|
||||
|
||||
if role == order_roles.OPEN_POSITION:
|
||||
self._on_open_position(order)
|
||||
elif role == order_roles.CLOSE_POSITION and order in self._take_profit_orders:
|
||||
self._on_take_profit(order)
|
||||
elif role == order_roles.CLOSE_POSITION and order in self._stop_loss_orders:
|
||||
self._on_stop_loss(order)
|
||||
elif role == order_roles.CLOSE_POSITION:
|
||||
self._on_close_position(order)
|
||||
elif role == order_roles.INCREASE_POSITION:
|
||||
self._on_increased_position(order)
|
||||
elif role == order_roles.REDUCE_POSITION:
|
||||
@@ -191,20 +189,20 @@ class Strategy(ABC):
|
||||
return
|
||||
|
||||
for o in self._buy:
|
||||
# MARKET order
|
||||
if abs(o[1] - self.price) < 0.0001:
|
||||
submitted_order = self.broker.buy_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
# STOP order
|
||||
if o[1] > self.price:
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.BUY, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.buy_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
# MARKET order
|
||||
else:
|
||||
submitted_order = self.broker.buy_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
if submitted_order:
|
||||
self._open_position_orders.append(
|
||||
submitted_order
|
||||
)
|
||||
self._open_position_orders.append(submitted_order)
|
||||
|
||||
def _prepare_buy(self, make_copies: bool = True) -> None:
|
||||
if type(self.buy) is np.ndarray:
|
||||
@@ -243,7 +241,6 @@ class Strategy(ABC):
|
||||
|
||||
if make_copies:
|
||||
self._stop_loss = self.stop_loss.copy()
|
||||
self._log_stop_loss = self._stop_loss.copy()
|
||||
|
||||
def _prepare_take_profit(self, make_copies: bool = True) -> None:
|
||||
# if it's numpy, then it has already been prepared
|
||||
@@ -256,7 +253,6 @@ class Strategy(ABC):
|
||||
|
||||
if make_copies:
|
||||
self._take_profit = self.take_profit.copy()
|
||||
self._log_take_profit = self._take_profit.copy()
|
||||
|
||||
def _convert_to_numpy_array(self, arr, name) -> np.ndarray:
|
||||
if type(arr) is np.ndarray:
|
||||
@@ -327,17 +323,19 @@ class Strategy(ABC):
|
||||
return
|
||||
|
||||
for o in self._sell:
|
||||
# MARKET order
|
||||
if abs(o[1] - self.price) < 0.0001:
|
||||
submitted_order = self.broker.sell_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
# STOP order
|
||||
if o[1] < self.price:
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.SELL, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
# LIMIT order
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.sell_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
# MARKET order
|
||||
else:
|
||||
submitted_order = self.broker.sell_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
if submitted_order:
|
||||
for o in self._open_position_orders:
|
||||
self._open_position_orders.append(submitted_order)
|
||||
|
||||
def _execute_filters(self) -> bool:
|
||||
@@ -404,12 +402,9 @@ class Strategy(ABC):
|
||||
self._stop_loss = None
|
||||
self.take_profit = None
|
||||
self._take_profit = None
|
||||
self._log_take_profit = None
|
||||
self._log_stop_loss = None
|
||||
|
||||
self._open_position_orders = []
|
||||
self._stop_loss_orders = []
|
||||
self._take_profit_orders = []
|
||||
self._close_positi = []
|
||||
|
||||
self.increased_count = 0
|
||||
self.reduced_count = 0
|
||||
@@ -468,20 +463,20 @@ class Strategy(ABC):
|
||||
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:
|
||||
# MARKET order
|
||||
if abs(o[1] - self.price) < 0.0001:
|
||||
submitted_order = self.broker.buy_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
# STOP order
|
||||
if o[1] > self.price:
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.BUY, o[0], o[1],
|
||||
order_roles.OPEN_POSITION)
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.buy_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
# MARKET order
|
||||
else:
|
||||
submitted_order = self.broker.buy_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
if submitted_order:
|
||||
self._open_position_orders.append(submitted_order)
|
||||
@@ -498,21 +493,20 @@ class Strategy(ABC):
|
||||
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:
|
||||
# MARKET order
|
||||
if abs(o[1] - self.price) < 0.0001:
|
||||
submitted_order = self.broker.sell_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
# STOP order
|
||||
if o[1] < self.price:
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.SELL, o[0], o[1],
|
||||
order_roles.OPEN_POSITION)
|
||||
# LIMIT order
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.sell_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
# MARKET order
|
||||
else:
|
||||
submitted_order = self.broker.sell_at_market(o[0], order_roles.OPEN_POSITION)
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
if submitted_order:
|
||||
self._open_position_orders.append(submitted_order)
|
||||
@@ -526,26 +520,20 @@ class Strategy(ABC):
|
||||
self._take_profit = self.take_profit.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._take_profit_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
for o in self._close_position_orders:
|
||||
if o.submitted_via == 'take-profit' and 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)
|
||||
)
|
||||
self._close_position_orders = [o for o in self._close_position_orders if o.is_executed]
|
||||
for o in self._take_profit:
|
||||
submitted_order = self.broker.reduce_position_at(
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
submitted_order.submitted_via = 'take-profit'
|
||||
if submitted_order:
|
||||
self._log_take_profit.append(o)
|
||||
self._take_profit_orders.append(submitted_order)
|
||||
self._close_position_orders.append(submitted_order)
|
||||
|
||||
if self.position.is_open and self.stop_loss is not None:
|
||||
self._validate_stop_loss()
|
||||
@@ -557,26 +545,20 @@ class Strategy(ABC):
|
||||
self._stop_loss = self.stop_loss.copy()
|
||||
|
||||
# cancel orders
|
||||
for o in self._stop_loss_orders:
|
||||
if o.is_active or o.is_queued:
|
||||
for o in self._close_position_orders:
|
||||
if o.submitted_via == 'stop-loss' and 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)
|
||||
)
|
||||
self._close_position_orders = [o for o in self._close_position_orders if o.is_executed]
|
||||
for o in self._stop_loss:
|
||||
submitted_order = self.broker.reduce_position_at(
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
submitted_order.submitted_via = 'stop-loss'
|
||||
if submitted_order:
|
||||
self._log_stop_loss.append(o)
|
||||
self._stop_loss_orders.append(submitted_order)
|
||||
self._close_position_orders.append(submitted_order)
|
||||
except TypeError:
|
||||
raise exceptions.InvalidStrategy(
|
||||
'Something odd is going on within your strategy causing a TypeError exception. '
|
||||
@@ -619,7 +601,7 @@ class Strategy(ABC):
|
||||
logger.info('Maximum allowed trades in test-drive mode is reached')
|
||||
return
|
||||
|
||||
if self._open_position_orders != [] and self.is_close and self.should_cancel():
|
||||
if len(self._open_position_orders) and self.is_close and self.should_cancel():
|
||||
self._execute_cancel()
|
||||
|
||||
# make sure order cancellation response is received via WS
|
||||
@@ -631,13 +613,13 @@ class Strategy(ABC):
|
||||
if store.orders.count_active_orders(self.exchange, self.symbol) == 0:
|
||||
break
|
||||
|
||||
logger.info('sleeping 0.2 more seconds...')
|
||||
logger.info('sleeping 0.2 more seconds until cancellation is over...')
|
||||
sleep(0.2)
|
||||
|
||||
# If it's still not cancelled, something is wrong. Handle cancellation failure
|
||||
if store.orders.count_active_orders(self.exchange, self.symbol) != 0:
|
||||
raise exceptions.ExchangeNotResponding(
|
||||
'The exchange did not respond as expected'
|
||||
'The exchange did not respond as expected for order cancellation'
|
||||
)
|
||||
|
||||
if self.position.is_open:
|
||||
@@ -679,13 +661,14 @@ class Strategy(ABC):
|
||||
)
|
||||
|
||||
# submit take-profit
|
||||
submitted_order = self.broker.reduce_position_at(
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
if submitted_order:
|
||||
self._take_profit_orders.append(submitted_order)
|
||||
submitted_order.submitted_via = 'take-profit'
|
||||
self._close_position_orders.append(submitted_order)
|
||||
|
||||
if self.stop_loss is not None:
|
||||
for o in self._stop_loss:
|
||||
@@ -702,13 +685,14 @@ class Strategy(ABC):
|
||||
)
|
||||
|
||||
# submit stop-loss
|
||||
submitted_order = self.broker.stop_loss_at(
|
||||
submitted_order: Order = self.broker.stop_loss_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
if submitted_order:
|
||||
self._stop_loss_orders.append(submitted_order)
|
||||
submitted_order.submitted_via = 'stop-loss'
|
||||
self._close_position_orders.append(submitted_order)
|
||||
|
||||
self._open_position_orders = []
|
||||
self.on_open_position(order)
|
||||
@@ -720,38 +704,22 @@ class Strategy(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_stop_loss(self, order: Order) -> None:
|
||||
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(order)
|
||||
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_stop_loss(self, order) -> None:
|
||||
def on_close_position(self, order) -> None:
|
||||
"""
|
||||
What should happen after the stop-loss order has been executed
|
||||
What should happen after the open position order has been executed
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_take_profit(self, order: Order) -> None:
|
||||
def _on_close_position(self, order: Order):
|
||||
if not jh.should_execute_silently() or jh.is_debugging():
|
||||
logger.info("Take-profit order has been executed.")
|
||||
logger.info("A closing order has been executed")
|
||||
|
||||
self._broadcast('route-take-profit')
|
||||
self._broadcast('route-close-position')
|
||||
self._execute_cancel()
|
||||
self.on_take_profit(order)
|
||||
self.on_close_position(order)
|
||||
|
||||
self._detect_and_handle_entry_and_exit_modifications()
|
||||
|
||||
def on_take_profit(self, order) -> None:
|
||||
"""
|
||||
What should happen after the take-profit order is executed.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_increased_position(self, order: Order) -> None:
|
||||
self.increased_count += 1
|
||||
|
||||
@@ -799,12 +767,7 @@ class Strategy(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_route_stop_loss(self, strategy) -> None:
|
||||
"""used when trading multiple routes that related
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_route_take_profit(self, strategy) -> None:
|
||||
def on_route_close_position(self, strategy) -> None:
|
||||
"""used when trading multiple routes that related
|
||||
|
||||
Arguments:
|
||||
@@ -886,7 +849,7 @@ class Strategy(ABC):
|
||||
)
|
||||
return
|
||||
|
||||
if self._open_position_orders:
|
||||
if len(self._open_position_orders):
|
||||
self._execute_cancel()
|
||||
logger.info('Canceled open-position orders because we reached the end of the backtest session.')
|
||||
|
||||
@@ -1015,7 +978,7 @@ class Strategy(ABC):
|
||||
"""
|
||||
if self.trades_count not in self._cached_metrics:
|
||||
self._cached_metrics[self.trades_count] = metrics.trades(
|
||||
store.completed_trades.trades, store.app.daily_balance
|
||||
store.completed_trades.trades, store.app.daily_balance, final=False
|
||||
)
|
||||
return self._cached_metrics[self.trades_count]
|
||||
|
||||
@@ -1079,54 +1042,33 @@ class Strategy(ABC):
|
||||
self.trade.exit_candle_timestamp = self.current_candle[0]
|
||||
self.trade.orders.append(order)
|
||||
|
||||
# calculate average stop-loss price
|
||||
sum_price = 0
|
||||
sum_qty = 0
|
||||
if self._log_stop_loss is not None:
|
||||
for l in self._log_stop_loss:
|
||||
sum_qty += abs(l[0])
|
||||
sum_price += abs(l[0]) * l[1]
|
||||
self.trade.stop_loss_at = sum_price / sum_qty
|
||||
else:
|
||||
self.trade.stop_loss_at = np.nan
|
||||
|
||||
# calculate average take-profit price
|
||||
sum_price = 0
|
||||
sum_qty = 0
|
||||
if self._log_take_profit is not None:
|
||||
for l in self._log_take_profit:
|
||||
sum_qty += abs(l[0])
|
||||
sum_price += abs(l[0]) * l[1]
|
||||
self.trade.take_profit_at = sum_price / sum_qty
|
||||
else:
|
||||
self.trade.take_profit_at = np.nan
|
||||
|
||||
# calculate average entry_price price
|
||||
sum_price = 0
|
||||
sum_qty = 0
|
||||
for l in self.trade.orders:
|
||||
if not l.is_executed:
|
||||
for trade_order in self.trade.orders:
|
||||
if not trade_order.is_executed:
|
||||
continue
|
||||
|
||||
if jh.side_to_type(l.side) != self.trade.type:
|
||||
if jh.side_to_type(trade_order.side) != self.trade.type:
|
||||
continue
|
||||
|
||||
sum_qty += abs(l.qty)
|
||||
sum_price += abs(l.qty) * l.price
|
||||
sum_qty += abs(trade_order.qty)
|
||||
sum_price += abs(trade_order.qty) * trade_order.price
|
||||
self.trade.entry_price = sum_price / sum_qty
|
||||
|
||||
# calculate average exit_price
|
||||
sum_price = 0
|
||||
sum_qty = 0
|
||||
for l in self.trade.orders:
|
||||
if not l.is_executed:
|
||||
for trade_order in self.trade.orders:
|
||||
if not trade_order.is_executed:
|
||||
continue
|
||||
|
||||
if jh.side_to_type(l.side) == self.trade.type:
|
||||
if jh.side_to_type(trade_order.side) == self.trade.type:
|
||||
continue
|
||||
|
||||
sum_qty += abs(l.qty)
|
||||
sum_price += abs(l.qty) * l.price
|
||||
sum_qty += abs(trade_order.qty)
|
||||
sum_price += abs(trade_order.qty) * trade_order.price
|
||||
|
||||
self.trade.exit_price = sum_price / sum_qty
|
||||
|
||||
self.trade.closed_at = jh.now_to_timestamp()
|
||||
@@ -1242,7 +1184,8 @@ class Strategy(ABC):
|
||||
def liquidation_price(self) -> float:
|
||||
return self.position.liquidation_price
|
||||
|
||||
def log(self, msg: str, log_type: str = 'info') -> None:
|
||||
@staticmethod
|
||||
def log(msg: str, log_type: str = 'info') -> None:
|
||||
msg = str(msg)
|
||||
|
||||
if log_type == 'info':
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_on_route_take_profit part 1 - BTC-USD
|
||||
# test_on_route_close_position part 1 - BTC-USD
|
||||
class Test23(Strategy):
|
||||
def should_long(self):
|
||||
# buy on market at first candle, close when on_route_take_profit event is fired
|
||||
@@ -20,9 +20,5 @@ class Test23(Strategy):
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def on_route_take_profit(self, strategy):
|
||||
"""
|
||||
|
||||
:param strategy:
|
||||
"""
|
||||
def on_route_close_position(self, strategy) -> None:
|
||||
self.take_profit = 1, self.price
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_on_route_take_profit part 2 - ETH-USD
|
||||
# test_on_route_close_position part 2 - ETH-USD
|
||||
class Test24(Strategy):
|
||||
def should_long(self):
|
||||
return self.price == 10
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_on_route_stop_loss part 1 - BTC-USD
|
||||
# test_on_route_close_position part 1 - BTC-USD
|
||||
class Test25(Strategy):
|
||||
def should_long(self):
|
||||
# buy on market at first candle, close when on_route_stop_loss event is fired
|
||||
@@ -20,9 +20,5 @@ class Test25(Strategy):
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def on_route_stop_loss(self, strategy):
|
||||
"""
|
||||
|
||||
:param strategy:
|
||||
"""
|
||||
def on_route_close_position(self, strategy):
|
||||
self.take_profit = 1, self.price
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
# test_on_route_stop_loss part 2 - ETH-USD
|
||||
# test_on_route_close_position part 2 - ETH-USD
|
||||
class Test26(Strategy):
|
||||
def should_long(self):
|
||||
return False
|
||||
|
||||
@@ -3,10 +3,6 @@ from jesse.strategies import Strategy
|
||||
|
||||
# test_on_route_increased_position_and_on_route_reduced_position_and_strategy_vars part 1 - BTC-USD
|
||||
class Test29(Strategy):
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
@@ -28,28 +24,16 @@ class Test29(Strategy):
|
||||
self.stop_loss = 1, self.price + 10
|
||||
|
||||
def on_route_increased_position(self, strategy):
|
||||
"""
|
||||
|
||||
:param strategy:
|
||||
"""
|
||||
# setting it to True means we'll open a position on NEXT candle
|
||||
self.vars['should_long'] = True
|
||||
|
||||
def on_route_reduced_position(self, strategy):
|
||||
"""
|
||||
|
||||
:param strategy:
|
||||
"""
|
||||
# setting it to True means we'll open a position on NEXT candle
|
||||
self.vars['should_short'] = True
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def on_take_profit(self, order):
|
||||
self.vars['should_long'] = False
|
||||
self.vars['should_short'] = False
|
||||
|
||||
def on_stop_loss(self, order):
|
||||
def on_close_position(self, order):
|
||||
self.vars['should_long'] = False
|
||||
self.vars['should_short'] = False
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import jesse.helpers as jh
|
||||
from jesse.models import Order
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
@@ -21,7 +20,7 @@ class TestCompletedTradeAfterExitingTrade(Strategy):
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def on_take_profit(self, order: Order):
|
||||
def on_close_position(self, order):
|
||||
assert self.trades_count == 1
|
||||
|
||||
trade = self.trades[0]
|
||||
@@ -34,8 +33,6 @@ class TestCompletedTradeAfterExitingTrade(Strategy):
|
||||
assert trade.timeframe == '1m'
|
||||
assert trade.entry_price == 10
|
||||
assert trade.exit_price == 12
|
||||
assert trade.take_profit_at == 12
|
||||
assert trade.stop_loss_at == 5
|
||||
assert trade.qty == 10
|
||||
assert trade.opened_at == 1552309906171.0
|
||||
assert trade.closed_at == 1552310026171.0
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from jesse.strategies import Strategy
|
||||
|
||||
|
||||
class TestMarketOrderForLowPriceDifference(Strategy):
|
||||
def on_open_position(self, order):
|
||||
assert order.type == 'MARKET'
|
||||
|
||||
def should_long(self) -> bool:
|
||||
return self.index == 0
|
||||
|
||||
def should_short(self) -> bool:
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
# current-price: 1
|
||||
self.buy = 1, 1.00001
|
||||
|
||||
def go_short(self):
|
||||
pass
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
@@ -21,7 +21,7 @@ class TestMetrics1(Strategy):
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def on_take_profit(self, order):
|
||||
def on_close_position(self, order):
|
||||
assert self.metrics['total'] == 1
|
||||
assert self.metrics['starting_balance'] == 10000
|
||||
assert self.metrics['finishing_balance'] == 10050
|
||||
|
||||
1
jesse/version.py
Normal file
1
jesse/version.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = '0.27.10'
|
||||
@@ -1,7 +1,6 @@
|
||||
arrow==1.1.1
|
||||
arrow==1.2.0
|
||||
blinker==1.4
|
||||
click==8.0.1
|
||||
crypto-empyrical==1.0.4
|
||||
click==8.0.2
|
||||
matplotlib==3.4.3
|
||||
mplfinance==0.12.7a17
|
||||
newtulipy==0.4.6
|
||||
@@ -10,12 +9,13 @@ numpy_groupies==0.9.14
|
||||
pandas==1.3.3
|
||||
peewee==3.14.4
|
||||
psycopg2-binary==2.9.1
|
||||
pydash==5.0.2
|
||||
pydash==5.1.0
|
||||
pytest==6.2.5
|
||||
PyWavelets==1.1.1
|
||||
quantstats==0.0.42
|
||||
requests==2.26.0
|
||||
scipy==1.7.1
|
||||
statsmodels==0.12.2
|
||||
statsmodels==0.13.0
|
||||
TA-Lib==0.4.21
|
||||
tabulate==0.8.9
|
||||
timeloop==1.0.2
|
||||
|
||||
4
setup.py
4
setup.py
@@ -1,13 +1,13 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
VERSION = '0.27.3'
|
||||
# also change in version.py
|
||||
VERSION = '0.27.10'
|
||||
DESCRIPTION = "A trading framework for cryptocurrencies"
|
||||
|
||||
REQUIRED_PACKAGES = [
|
||||
'arrow',
|
||||
'blinker',
|
||||
'Click',
|
||||
'crypto_empyrical',
|
||||
'matplotlib',
|
||||
'newtulipy',
|
||||
'numpy',
|
||||
|
||||
@@ -144,7 +144,6 @@ def test_r():
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
assert trade.risk_reward_ratio == 2
|
||||
|
||||
|
||||
def test_risk_percentage():
|
||||
@@ -163,7 +162,6 @@ def test_risk_percentage():
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
assert trade.risk_percentage == round((((10 - 5) / 1) * 10), 2)
|
||||
|
||||
|
||||
def test_trade_size():
|
||||
|
||||
@@ -39,10 +39,8 @@ def test_can_handle_multiple_entry_orders_too_close_to_each_other():
|
||||
t: CompletedTrade = store.completed_trades.trades[0]
|
||||
|
||||
assert t.type == 'long'
|
||||
assert t.stop_loss_at == 0.4
|
||||
assert t.entry_price == (1.1 + 1.2 + 1.3 + 1.4) / 4
|
||||
assert t.exit_price == 3
|
||||
assert t.take_profit_at == 3
|
||||
# 4 entry + 1 exit
|
||||
assert len(t.orders) == 5
|
||||
# last order is closing order
|
||||
@@ -62,10 +60,8 @@ def test_conflicting_orders():
|
||||
t: CompletedTrade = store.completed_trades.trades[0]
|
||||
|
||||
assert t.type == 'long'
|
||||
assert t.stop_loss_at == 1.0
|
||||
assert t.entry_price == (1.1 + 1.11) / 2
|
||||
assert t.exit_price == (1.2 + 1.3) / 2
|
||||
assert t.take_profit_at == (1.2 + 1.3) / 2
|
||||
|
||||
|
||||
def test_conflicting_orders_2():
|
||||
@@ -76,13 +72,9 @@ def test_conflicting_orders_2():
|
||||
t: CompletedTrade = store.completed_trades.trades[0]
|
||||
|
||||
assert t.entry_price == 2.5
|
||||
assert t.take_profit_at == 2.6
|
||||
assert t.stop_loss_at == 2.4
|
||||
assert t.exit_price == 2.6
|
||||
|
||||
|
||||
|
||||
#
|
||||
# def test_can_handle_not_correctly_sorted_multiple_orders():
|
||||
# set_up([
|
||||
# (exchanges.SANDBOX, 'BTC-USDT', timeframes.MINUTE_1, 'Test35'),
|
||||
@@ -95,7 +87,5 @@ def test_conflicting_orders_2():
|
||||
# t: CompletedTrade = store.completed_trades.trades[0]
|
||||
#
|
||||
# assert t.type == 'long'
|
||||
# assert t.stop_loss_at == 0.4
|
||||
# assert t.entry_price == (1.1 + 1.2 + 1.3 + 1.4) / 4
|
||||
# assert t.exit_price == 3
|
||||
# assert t.take_profit_at == 3
|
||||
|
||||
@@ -35,7 +35,6 @@ def test_average_take_profit_and_average_stop_loss():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 1
|
||||
assert t1.exit_price == 3.5
|
||||
assert t1.take_profit_at == 3.5
|
||||
assert t1.qty == 2
|
||||
|
||||
t2: CompletedTrade = store.completed_trades.trades[1]
|
||||
@@ -160,8 +159,6 @@ def test_increasing_position_size_after_opening():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == (7 + 10) / 2
|
||||
assert t1.exit_price == 15
|
||||
assert t1.take_profit_at == 15
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 2
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -190,8 +187,6 @@ def test_is_smart_enough_to_open_positions_via_market_orders():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 129.23
|
||||
assert t1.exit_price == 128.35
|
||||
assert t1.take_profit_at == 131.29
|
||||
assert t1.stop_loss_at == 128.35
|
||||
assert t1.qty == 10.204
|
||||
assert t1.fee == 0
|
||||
assert t1.opened_at == 1547201100000 + 60000
|
||||
@@ -204,8 +199,6 @@ def test_is_smart_enough_to_open_positions_via_market_orders():
|
||||
assert t2.type == 'short'
|
||||
assert t2.entry_price == 128.01
|
||||
assert t2.exit_price == 126.58
|
||||
assert t2.take_profit_at == 126.58
|
||||
assert t2.stop_loss_at == 129.52
|
||||
assert t2.qty == 10
|
||||
assert t2.fee == 0
|
||||
assert t2.opened_at == 1547203560000 + 60000
|
||||
@@ -238,8 +231,6 @@ def test_is_smart_enough_to_open_positions_via_stop_orders():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 129.33
|
||||
assert t1.exit_price == 128.35
|
||||
assert t1.take_profit_at == 131.29
|
||||
assert t1.stop_loss_at == 128.35
|
||||
assert t1.qty == 10.204
|
||||
assert t1.fee == 0
|
||||
assert t1.opened_at == 1547201100000 + 60000
|
||||
@@ -252,8 +243,6 @@ def test_is_smart_enough_to_open_positions_via_stop_orders():
|
||||
assert t2.type == 'short'
|
||||
assert t2.entry_price == 128.05
|
||||
assert t2.exit_price == 126.58
|
||||
assert t2.take_profit_at == 126.58
|
||||
assert t2.stop_loss_at == 129.52
|
||||
assert t2.qty == 10
|
||||
assert t2.fee == 0
|
||||
assert t2.opened_at == 1547203560000 + 60000
|
||||
@@ -273,14 +262,10 @@ def test_liquidate():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 1
|
||||
assert t1.exit_price == 11
|
||||
assert t1.take_profit_at == 11
|
||||
assert np.isnan(t1.stop_loss_at)
|
||||
|
||||
assert t2.type == 'short'
|
||||
assert t2.entry_price == 21
|
||||
assert t2.exit_price == 41
|
||||
assert t2.stop_loss_at == 41
|
||||
assert np.isnan(t2.take_profit_at)
|
||||
|
||||
|
||||
def test_modifying_stop_loss_after_part_of_position_is_already_reduced_with_stop_loss():
|
||||
@@ -309,8 +294,6 @@ def test_modifying_stop_loss_after_part_of_position_is_already_reduced_with_stop
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 7
|
||||
assert t1.exit_price == (4 * 2 + 6) / 3
|
||||
assert t1.take_profit_at == 13
|
||||
assert t1.stop_loss_at == (4 * 2 + 6) / 3
|
||||
assert t1.qty == 1.5
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -323,8 +306,6 @@ def test_modifying_take_profit_after_opening_position():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 7
|
||||
assert t1.exit_price == 16
|
||||
assert t1.take_profit_at == 16
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 1.5
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -337,8 +318,6 @@ def test_modifying_take_profit_after_part_of_position_is_already_reduced_with_pr
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 7
|
||||
assert t1.exit_price == (16 * 2 + 11) / 3
|
||||
assert t1.take_profit_at == (16 * 2 + 11) / 3
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 1.5
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -410,8 +389,6 @@ def test_on_reduced_position():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 7
|
||||
assert t1.exit_price == 13
|
||||
assert t1.take_profit_at == 13
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 2
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -425,9 +402,7 @@ def test_on_route_canceled():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 101
|
||||
assert t1.exit_price == 120
|
||||
assert t1.take_profit_at == 120
|
||||
assert t1.qty == 1
|
||||
assert np.isnan(t1.stop_loss_at)
|
||||
|
||||
|
||||
def test_on_route_increased_position_and_on_route_reduced_position_and_strategy_vars():
|
||||
@@ -444,17 +419,13 @@ def test_on_route_increased_position_and_on_route_reduced_position_and_strategy_
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 121
|
||||
assert t1.exit_price == 131
|
||||
assert t1.take_profit_at == 131
|
||||
assert t1.qty == 1
|
||||
assert np.isnan(t1.stop_loss_at)
|
||||
|
||||
assert t2.symbol == 'BTC-USDT'
|
||||
assert t2.type == 'short'
|
||||
assert t2.entry_price == 151
|
||||
assert t2.exit_price == 161
|
||||
assert t2.stop_loss_at == 161
|
||||
assert t2.qty == 1
|
||||
assert np.isnan(t2.take_profit_at)
|
||||
|
||||
assert t3.symbol == 'ETH-USDT'
|
||||
assert t3.type == 'long'
|
||||
@@ -462,9 +433,7 @@ def test_on_route_increased_position_and_on_route_reduced_position_and_strategy_
|
||||
assert t3.entry_price == 15
|
||||
# (50 + 70) / 2
|
||||
assert t3.exit_price == 60
|
||||
assert t3.take_profit_at == 60
|
||||
assert t3.qty == 2
|
||||
assert np.isnan(t3.stop_loss_at)
|
||||
|
||||
|
||||
def test_on_route_open_position():
|
||||
@@ -477,17 +446,13 @@ def test_on_route_open_position():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 101
|
||||
assert t1.exit_price == 110
|
||||
assert t1.take_profit_at == 110
|
||||
assert t1.qty == 1
|
||||
assert np.isnan(t1.stop_loss_at)
|
||||
|
||||
assert t2.symbol == 'ETH-USDT'
|
||||
assert t2.type == 'long'
|
||||
assert t2.entry_price == 10
|
||||
assert t2.exit_price == 20
|
||||
assert t2.take_profit_at == 20
|
||||
assert t2.qty == 1
|
||||
assert np.isnan(t2.stop_loss_at)
|
||||
|
||||
|
||||
def test_on_route_stop_loss():
|
||||
@@ -500,17 +465,13 @@ def test_on_route_stop_loss():
|
||||
assert t2.type == 'long'
|
||||
assert t2.entry_price == 101
|
||||
assert t2.exit_price == 120
|
||||
assert t2.take_profit_at == 120
|
||||
assert t2.qty == 1
|
||||
assert np.isnan(t2.stop_loss_at)
|
||||
|
||||
assert t1.symbol == 'ETH-USDT'
|
||||
assert t1.type == 'short'
|
||||
assert t1.entry_price == 10
|
||||
assert t1.exit_price == 20
|
||||
assert t1.stop_loss_at == 20
|
||||
assert t1.qty == 1
|
||||
assert np.isnan(t1.take_profit_at)
|
||||
|
||||
|
||||
def test_on_route_take_profit():
|
||||
@@ -523,17 +484,13 @@ def test_on_route_take_profit():
|
||||
assert t2.type == 'long'
|
||||
assert t2.entry_price == 101
|
||||
assert t2.exit_price == 120
|
||||
assert t2.take_profit_at == 120
|
||||
assert t2.qty == 1
|
||||
assert np.isnan(t2.stop_loss_at)
|
||||
|
||||
assert t1.symbol == 'ETH-USDT'
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 10
|
||||
assert t1.exit_price == 20
|
||||
assert t1.take_profit_at == 20
|
||||
assert t1.qty == 1
|
||||
assert np.isnan(t1.stop_loss_at)
|
||||
|
||||
|
||||
def test_opening_position_in_multiple_points():
|
||||
@@ -544,8 +501,6 @@ def test_opening_position_in_multiple_points():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == (7 + 9 + 11) / 3
|
||||
assert t1.exit_price == 15
|
||||
assert t1.take_profit_at == 15
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 1.5
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -558,8 +513,6 @@ def test_reducing_position_size_after_opening():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 7
|
||||
assert t1.exit_price == (15 + 10) / 2
|
||||
assert t1.take_profit_at == (15 + 10) / 2
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 2
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -573,9 +526,7 @@ def test_shared_vars():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 11
|
||||
assert t1.exit_price == 21
|
||||
assert t1.take_profit_at == 21
|
||||
assert t1.qty == 1
|
||||
assert np.isnan(t1.stop_loss_at)
|
||||
|
||||
|
||||
def test_should_buy_and_execute_buy():
|
||||
@@ -671,8 +622,6 @@ def test_stop_loss_at_multiple_points():
|
||||
assert t1.type == 'short'
|
||||
assert t1.entry_price == 3
|
||||
assert t1.exit_price == (6 + 5 + 4) / 3
|
||||
assert t1.take_profit_at == 1
|
||||
assert t1.stop_loss_at == (6 + 5 + 4) / 3
|
||||
assert t1.qty == 1.5
|
||||
assert t1.fee == 0
|
||||
|
||||
@@ -704,8 +653,6 @@ def test_taking_profit_at_multiple_points():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 7
|
||||
assert t1.exit_price == (15 + 13 + 11) / 3
|
||||
assert t1.take_profit_at == (15 + 13 + 11) / 3
|
||||
assert t1.stop_loss_at == 5
|
||||
assert t1.qty == 1.5
|
||||
assert t1.fee == 0
|
||||
assert t1.holding_period == 8 * 60
|
||||
@@ -767,8 +714,6 @@ def test_updating_stop_loss_and_take_profit_after_opening_the_position():
|
||||
assert t1.type == 'long'
|
||||
assert t1.entry_price == 129.23
|
||||
assert t1.exit_price == 128.98
|
||||
assert t1.take_profit_at == 131.29
|
||||
assert t1.stop_loss_at == 128.98
|
||||
assert t1.qty == 10.204
|
||||
assert t1.fee == 0
|
||||
assert t1.opened_at == 1547201100000 + 60000
|
||||
@@ -781,8 +726,6 @@ def test_updating_stop_loss_and_take_profit_after_opening_the_position():
|
||||
assert t2.type == 'short'
|
||||
assert t2.entry_price == 128.01
|
||||
assert t2.exit_price == 127.66
|
||||
assert t2.take_profit_at == 127.66
|
||||
assert t2.stop_loss_at == 129.52
|
||||
assert t2.qty == 10
|
||||
assert t2.fee == 0
|
||||
assert t2.opened_at == 1547203560000 + 60000
|
||||
@@ -853,6 +796,11 @@ def test_log_method():
|
||||
assert store.logs.info[1]['message'] == 'test info log'
|
||||
assert store.logs.errors[0]['message'] == 'test error log'
|
||||
|
||||
|
||||
def test_using_market_order_for_low_price_difference():
|
||||
single_route_backtest('TestMarketOrderForLowPriceDifference')
|
||||
|
||||
|
||||
# TODO: implement liquidation in backtest mode for cross mode
|
||||
# def test_liquidation_in_cross_mode_for_short_trades():
|
||||
# single_route_backtest(
|
||||
@@ -887,5 +835,3 @@ def test_log_method():
|
||||
# assert len(store.completed_trades.trades) == 1
|
||||
# assert t.qty == 1.5
|
||||
# assert t.entry_price == 5.123
|
||||
# assert t.take_profit_at == 10.12
|
||||
# assert t.stop_loss_at == 1.123
|
||||
|
||||
Reference in New Issue
Block a user