Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92269df6b8 | ||
|
|
25f1076629 | ||
|
|
2f45ddfca5 | ||
|
|
bafb17684d | ||
|
|
5b42f88531 |
@@ -12,7 +12,7 @@ class order_statuses:
|
||||
ACTIVE = 'ACTIVE'
|
||||
CANCELED = 'CANCELED'
|
||||
EXECUTED = 'EXECUTED'
|
||||
PARTIALLY_EXECUTED = 'PARTIALLY EXECUTED'
|
||||
PARTIALLY_FILLED = 'PARTIALLY FILLED'
|
||||
QUEUED = 'QUEUED'
|
||||
LIQUIDATED = 'LIQUIDATED'
|
||||
|
||||
@@ -42,21 +42,6 @@ class colors:
|
||||
BLACK = 'black'
|
||||
|
||||
|
||||
class order_roles:
|
||||
OPEN_POSITION = 'OPEN POSITION'
|
||||
CLOSE_POSITION = 'CLOSE POSITION'
|
||||
INCREASE_POSITION = 'INCREASE POSITION'
|
||||
REDUCE_POSITION = 'REDUCE POSITION'
|
||||
|
||||
|
||||
class order_flags:
|
||||
OCO = 'OCO'
|
||||
POST_ONLY = 'PostOnly'
|
||||
CLOSE = 'Close'
|
||||
HIDDEN = 'Hidden'
|
||||
REDUCE_ONLY = 'ReduceOnly'
|
||||
|
||||
|
||||
class order_types:
|
||||
MARKET = 'MARKET'
|
||||
LIMIT = 'LIMIT'
|
||||
|
||||
@@ -8,15 +8,15 @@ class Exchange(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def market_order(self, symbol: str, qty: float, current_price: float, side: str, role: str, flags: list) -> Order:
|
||||
def market_order(self, symbol: str, qty: float, current_price: float, side: str, reduce_only: bool) -> Order:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def limit_order(self, symbol: str, qty: float, price: float, side: str, role: str, flags: list) -> Order:
|
||||
def limit_order(self, symbol: str, qty: float, price: float, side: str, reduce_only: bool) -> Order:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_order(self, symbol: str, qty: float, price: float, side: str, role: str, flags: list) -> Order:
|
||||
def stop_order(self, symbol: str, qty: float, price: float, side: str, reduce_only: bool) -> Order:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -27,10 +27,6 @@ class Exchange(ABC):
|
||||
def cancel_order(self, symbol: str, order_id: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_exec_inst(self, flags: list) -> Union[str, None]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _fetch_precisions(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -5,22 +5,22 @@ from jesse.models import Order
|
||||
from jesse.store import store
|
||||
from typing import Union
|
||||
|
||||
|
||||
class Sandbox(Exchange):
|
||||
def __init__(self, name='Sandbox'):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
||||
def market_order(self, symbol: str, qty: float, current_price: float, side: str, role: str, flags: list) -> Order:
|
||||
def market_order(self, symbol: str, qty: float, current_price: float, side: str, reduce_only: bool) -> Order:
|
||||
order = Order({
|
||||
'id': jh.generate_unique_id(),
|
||||
'symbol': symbol,
|
||||
'exchange': self.name,
|
||||
'side': side,
|
||||
'type': order_types.MARKET,
|
||||
'flag': self.get_exec_inst(flags),
|
||||
'reduce_only': reduce_only,
|
||||
'qty': jh.prepare_qty(qty, side),
|
||||
'price': current_price,
|
||||
'role': role
|
||||
})
|
||||
|
||||
store.orders.add_order(order)
|
||||
@@ -29,34 +29,32 @@ class Sandbox(Exchange):
|
||||
|
||||
return order
|
||||
|
||||
def limit_order(self, symbol: str, qty: float, price: float, side: str, role: str, flags: list) -> Order:
|
||||
def limit_order(self, symbol: str, qty: float, price: float, side: str, reduce_only: bool) -> Order:
|
||||
order = Order({
|
||||
'id': jh.generate_unique_id(),
|
||||
'symbol': symbol,
|
||||
'exchange': self.name,
|
||||
'side': side,
|
||||
'type': order_types.LIMIT,
|
||||
'flag': self.get_exec_inst(flags),
|
||||
'reduce_only': reduce_only,
|
||||
'qty': jh.prepare_qty(qty, side),
|
||||
'price': price,
|
||||
'role': role
|
||||
})
|
||||
|
||||
store.orders.add_order(order)
|
||||
|
||||
return order
|
||||
|
||||
def stop_order(self, symbol: str, qty: float, price: float, side: str, role: str, flags: list) -> Order:
|
||||
def stop_order(self, symbol: str, qty: float, price: float, side: str, reduce_only: bool) -> Order:
|
||||
order = Order({
|
||||
'id': jh.generate_unique_id(),
|
||||
'symbol': symbol,
|
||||
'exchange': self.name,
|
||||
'side': side,
|
||||
'type': order_types.STOP,
|
||||
'flag': self.get_exec_inst(flags),
|
||||
'reduce_only': reduce_only,
|
||||
'qty': jh.prepare_qty(qty, side),
|
||||
'price': price,
|
||||
'role': role
|
||||
})
|
||||
|
||||
store.orders.add_order(order)
|
||||
@@ -76,10 +74,5 @@ class Sandbox(Exchange):
|
||||
def cancel_order(self, symbol: str, order_id: str) -> None:
|
||||
store.orders.get_order_by_id(self.name, symbol, order_id).cancel()
|
||||
|
||||
def get_exec_inst(self, flags: list) -> Union[str, None]:
|
||||
if flags:
|
||||
return flags[0]
|
||||
return None
|
||||
|
||||
def _fetch_precisions(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -4,6 +4,8 @@ import peewee
|
||||
import jesse.helpers as jh
|
||||
from jesse.config import config
|
||||
from jesse.services.db import database
|
||||
from jesse.libs.dynamic_numpy_array import DynamicNumpyArray
|
||||
from jesse.enums import trade_types
|
||||
|
||||
|
||||
if database.is_closed():
|
||||
@@ -19,19 +21,10 @@ class CompletedTrade(peewee.Model):
|
||||
exchange = peewee.CharField()
|
||||
type = peewee.CharField()
|
||||
timeframe = peewee.CharField()
|
||||
entry_price = peewee.FloatField(default=np.nan)
|
||||
exit_price = peewee.FloatField(default=np.nan)
|
||||
take_profit_at = peewee.FloatField(default=np.nan)
|
||||
stop_loss_at = peewee.FloatField(default=np.nan)
|
||||
qty = peewee.FloatField(default=np.nan)
|
||||
opened_at = peewee.BigIntegerField()
|
||||
closed_at = peewee.BigIntegerField()
|
||||
entry_candle_timestamp = peewee.BigIntegerField()
|
||||
exit_candle_timestamp = peewee.BigIntegerField()
|
||||
leverage = peewee.IntegerField()
|
||||
|
||||
orders = []
|
||||
|
||||
class Meta:
|
||||
from jesse.services.db import database
|
||||
|
||||
@@ -47,8 +40,13 @@ class CompletedTrade(peewee.Model):
|
||||
for a, value in attributes.items():
|
||||
setattr(self, a, value)
|
||||
|
||||
# used for fast calculation of the total qty, entry_price, exit_price, etc.
|
||||
self.buy_orders = DynamicNumpyArray((10, 2))
|
||||
self.sell_orders = DynamicNumpyArray((10, 2))
|
||||
# to store the actual order objects
|
||||
self.orders = []
|
||||
|
||||
def toJSON(self) -> dict:
|
||||
orders = [o.__dict__ for o in self.orders]
|
||||
return {
|
||||
"id": self.id,
|
||||
"strategy_name": self.strategy_name,
|
||||
@@ -65,9 +63,6 @@ class CompletedTrade(peewee.Model):
|
||||
"holding_period": self.holding_period,
|
||||
"opened_at": self.opened_at,
|
||||
"closed_at": self.closed_at,
|
||||
"entry_candle_timestamp": self.entry_candle_timestamp,
|
||||
"exit_candle_timestamp": self.exit_candle_timestamp,
|
||||
"orders": orders,
|
||||
}
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -82,8 +77,6 @@ class CompletedTrade(peewee.Model):
|
||||
'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,
|
||||
"size": self.size,
|
||||
"PNL": self.pnl,
|
||||
@@ -136,6 +129,45 @@ class CompletedTrade(peewee.Model):
|
||||
"""How many SECONDS has it taken for the trade to be done."""
|
||||
return (self.closed_at - self.opened_at) / 1000
|
||||
|
||||
@property
|
||||
def is_long(self) -> bool:
|
||||
return self.type == trade_types.LONG
|
||||
|
||||
@property
|
||||
def is_short(self) -> bool:
|
||||
return self.type == trade_types.SHORT
|
||||
|
||||
@property
|
||||
def qty(self) -> float:
|
||||
if self.is_long:
|
||||
return self.buy_orders[:][:, 0].sum()
|
||||
elif self.is_short:
|
||||
return self.sell_orders[:][:, 0].sum()
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def entry_price(self) -> float:
|
||||
if self.is_long:
|
||||
orders = self.buy_orders[:]
|
||||
elif self.is_short:
|
||||
orders = self.sell_orders[:]
|
||||
else:
|
||||
return np.nan
|
||||
|
||||
return (orders[:, 0] * orders[:, 1]).sum() / orders[:, 0].sum()
|
||||
|
||||
@property
|
||||
def exit_price(self) -> float:
|
||||
if self.is_long:
|
||||
orders = self.sell_orders[:]
|
||||
elif self.is_short:
|
||||
orders = self.buy_orders[:]
|
||||
else:
|
||||
return np.nan
|
||||
|
||||
return (orders[:, 0] * orders[:, 1]).sum() / orders[:, 0].sum()
|
||||
|
||||
|
||||
# if database is open, create the table
|
||||
if database.is_open():
|
||||
|
||||
@@ -11,10 +11,20 @@ from .Exchange import Exchange
|
||||
|
||||
|
||||
class FuturesExchange(Exchange):
|
||||
# # used for live-trading only:
|
||||
# in futures trading, margin is only with one asset, so:
|
||||
_available_margin = 0
|
||||
# in futures trading, wallet is only with one asset, so:
|
||||
_wallet_balance = 0
|
||||
# so is started_balance
|
||||
_started_balance = 0
|
||||
|
||||
# current holding assets
|
||||
assets = {}
|
||||
# current available assets (dynamically changes based on active orders)
|
||||
available_assets = {}
|
||||
# used to estimating metrics
|
||||
starting_assets = {}
|
||||
|
||||
buy_orders = {}
|
||||
sell_orders = {}
|
||||
@@ -58,10 +68,22 @@ class FuturesExchange(Exchange):
|
||||
|
||||
self.settlement_currency = settlement_currency.upper()
|
||||
|
||||
def started_balance(self) -> float:
|
||||
if jh.is_livetrading():
|
||||
return self._started_balance
|
||||
|
||||
return self.starting_assets[jh.app_currency()]
|
||||
|
||||
def wallet_balance(self, symbol: str = '') -> float:
|
||||
if jh.is_livetrading():
|
||||
return self._wallet_balance
|
||||
|
||||
return self.assets[self.settlement_currency]
|
||||
|
||||
def available_margin(self, symbol: str = '') -> float:
|
||||
if jh.is_livetrading():
|
||||
return self._available_margin
|
||||
|
||||
# a temp which gets added to per each asset (remember that all future assets use the same currency for settlement)
|
||||
temp_credits = self.assets[self.settlement_currency]
|
||||
|
||||
@@ -97,6 +119,9 @@ class FuturesExchange(Exchange):
|
||||
return temp_credits * self.futures_leverage
|
||||
|
||||
def charge_fee(self, amount: float) -> None:
|
||||
if jh.is_livetrading():
|
||||
return
|
||||
|
||||
fee_amount = abs(amount) * self.fee_rate
|
||||
new_balance = self.assets[self.settlement_currency] - fee_amount
|
||||
if fee_amount != 0:
|
||||
@@ -106,16 +131,22 @@ class FuturesExchange(Exchange):
|
||||
self.assets[self.settlement_currency] = new_balance
|
||||
|
||||
def add_realized_pnl(self, realized_pnl: float) -> None:
|
||||
if jh.is_livetrading():
|
||||
return
|
||||
|
||||
new_balance = self.assets[self.settlement_currency] + realized_pnl
|
||||
logger.info(
|
||||
f'Added realized PNL of {round(realized_pnl, 2)}. Balance for {self.settlement_currency} on {self.name} changed from {round(self.assets[self.settlement_currency], 2)} to {round(new_balance, 2)}')
|
||||
self.assets[self.settlement_currency] = new_balance
|
||||
|
||||
def on_order_submission(self, order: Order, skip_market_order: bool = True) -> None:
|
||||
if jh.is_livetrading():
|
||||
return
|
||||
|
||||
base_asset = jh.base_asset(order.symbol)
|
||||
|
||||
# make sure we don't spend more than we're allowed considering current allowed leverage
|
||||
if (order.type != order_types.MARKET or skip_market_order) and not order.is_reduce_only:
|
||||
if (order.type != order_types.MARKET or skip_market_order) and not order.reduce_only:
|
||||
order_size = abs(order.qty * order.price)
|
||||
remaining_margin = self.available_margin()
|
||||
if order_size > remaining_margin:
|
||||
@@ -130,19 +161,22 @@ class FuturesExchange(Exchange):
|
||||
|
||||
self.available_assets[base_asset] += order.qty
|
||||
|
||||
if not order.is_reduce_only:
|
||||
if not order.reduce_only:
|
||||
if order.side == sides.BUY:
|
||||
self.buy_orders[base_asset].append(np.array([order.qty, order.price]))
|
||||
else:
|
||||
self.sell_orders[base_asset].append(np.array([order.qty, order.price]))
|
||||
|
||||
def on_order_execution(self, order: Order) -> None:
|
||||
if jh.is_livetrading():
|
||||
return
|
||||
|
||||
base_asset = jh.base_asset(order.symbol)
|
||||
|
||||
if order.type == order_types.MARKET:
|
||||
self.on_order_submission(order, skip_market_order=False)
|
||||
|
||||
if not order.is_reduce_only:
|
||||
if not order.reduce_only:
|
||||
if order.side == sides.BUY:
|
||||
# find and set order to [0, 0] (same as removing it)
|
||||
for index, item in enumerate(self.buy_orders[base_asset]):
|
||||
@@ -157,11 +191,14 @@ class FuturesExchange(Exchange):
|
||||
break
|
||||
|
||||
def on_order_cancellation(self, order: Order) -> None:
|
||||
if jh.is_livetrading():
|
||||
return
|
||||
|
||||
base_asset = jh.base_asset(order.symbol)
|
||||
|
||||
self.available_assets[base_asset] -= order.qty
|
||||
# self.available_assets[quote_asset] += order.qty * order.price
|
||||
if not order.is_reduce_only:
|
||||
if not order.reduce_only:
|
||||
if order.side == sides.BUY:
|
||||
# find and set order to [0, 0] (same as removing it)
|
||||
for index, item in enumerate(self.buy_orders[base_asset]):
|
||||
@@ -174,3 +211,15 @@ class FuturesExchange(Exchange):
|
||||
if item[0] == order.qty and item[1] == order.price:
|
||||
self.sell_orders[base_asset][index] = np.array([0, 0])
|
||||
break
|
||||
|
||||
def update_from_stream(self, data: dict) -> None:
|
||||
"""
|
||||
Used for updating the exchange from the WS stream (only for live trading)
|
||||
"""
|
||||
if not jh.is_livetrading():
|
||||
raise Exception('This method is only for live trading')
|
||||
|
||||
self._available_margin = data['available_margin']
|
||||
self._wallet_balance = data['wallet_balance']
|
||||
if self._started_balance == 0:
|
||||
self._started_balance = self._wallet_balance
|
||||
|
||||
@@ -6,7 +6,7 @@ import jesse.services.selectors as selectors
|
||||
from jesse import sync_publish
|
||||
from jesse.config import config
|
||||
from jesse.services.notifier import notify
|
||||
from jesse.enums import order_statuses, order_flags
|
||||
from jesse.enums import order_statuses
|
||||
from jesse.services.db import database
|
||||
|
||||
|
||||
@@ -28,14 +28,14 @@ class Order(Model):
|
||||
exchange = CharField()
|
||||
side = CharField()
|
||||
type = CharField()
|
||||
flag = CharField(null=True)
|
||||
reduce_only = BooleanField()
|
||||
qty = FloatField()
|
||||
filled_qty = FloatField(default=0)
|
||||
price = FloatField(null=True)
|
||||
status = CharField(default=order_statuses.ACTIVE)
|
||||
created_at = BigIntegerField()
|
||||
executed_at = BigIntegerField(null=True)
|
||||
canceled_at = BigIntegerField(null=True)
|
||||
role = CharField(null=True)
|
||||
submitted_via = None
|
||||
|
||||
class Meta:
|
||||
@@ -117,12 +117,8 @@ class Order(Model):
|
||||
return self.is_executed
|
||||
|
||||
@property
|
||||
def is_reduce_only(self) -> bool:
|
||||
return self.flag == order_flags.REDUCE_ONLY
|
||||
|
||||
@property
|
||||
def is_close(self) -> bool:
|
||||
return self.flag == order_flags.CLOSE
|
||||
def is_partially_filled(self) -> bool:
|
||||
return self.status == order_statuses.PARTIALLY_FILLED
|
||||
|
||||
@property
|
||||
def is_stop_loss(self):
|
||||
@@ -142,6 +138,7 @@ class Order(Model):
|
||||
'side': self.side,
|
||||
'type': self.type,
|
||||
'qty': self.qty,
|
||||
'filled_qty': self.filled_qty,
|
||||
'price': self.price,
|
||||
'flag': self.flag,
|
||||
'status': self.status,
|
||||
@@ -198,6 +195,10 @@ class Order(Model):
|
||||
if config['env']['notifications']['events']['executed_orders']:
|
||||
notify(txt)
|
||||
|
||||
# log the order of the trade for metrics
|
||||
from jesse.store import store
|
||||
store.completed_trades.add_executed_order(self)
|
||||
|
||||
p = selectors.get_position(self.exchange, self.symbol)
|
||||
|
||||
if p:
|
||||
|
||||
@@ -225,7 +225,7 @@ class Position:
|
||||
'mode': self.mode,
|
||||
}
|
||||
|
||||
def _close(self, close_price: float) -> None:
|
||||
def _mutating_close(self, close_price: float) -> None:
|
||||
if self.is_open is False:
|
||||
raise EmptyPosition('The position is already closed.')
|
||||
|
||||
@@ -243,18 +243,24 @@ class Position:
|
||||
if self.exchange:
|
||||
self.exchange.add_realized_pnl(estimated_profit)
|
||||
self.exchange.temp_reduced_amount[jh.base_asset(self.symbol)] += abs(close_qty * close_price)
|
||||
self.qty = 0
|
||||
self.entry_price = None
|
||||
self.closed_at = jh.now_to_timestamp()
|
||||
|
||||
if not jh.is_unit_testing():
|
||||
info_text = f'CLOSED {trade_type} position: {self.exchange_name}, {self.symbol}, {self.strategy.name}. PNL: ${round(estimated_profit, 2)}, Balance: ${jh.format_currency(round(self.exchange.wallet_balance(self.symbol), 2))}, entry: {entry}, exit: {close_price}'
|
||||
self.qty = 0
|
||||
self.entry_price = None
|
||||
|
||||
if jh.is_debuggable('position_closed'):
|
||||
logger.info(info_text)
|
||||
self._close()
|
||||
|
||||
if jh.is_live() and config['env']['notifications']['events']['updated_position']:
|
||||
notifier.notify(info_text)
|
||||
def _close(self):
|
||||
from jesse.store import store
|
||||
store.completed_trades.close_trade(self)
|
||||
# if not jh.is_unit_testing():
|
||||
# info_text = f'CLOSED {trade_type} position: {self.exchange_name}, {self.symbol}, {self.strategy.name}. PNL: ${round(estimated_profit, 2)}, Balance: ${jh.format_currency(round(self.exchange.wallet_balance(self.symbol), 2))}, entry: {entry}, exit: {close_price}'
|
||||
#
|
||||
# if jh.is_debuggable('position_closed'):
|
||||
# logger.info(info_text)
|
||||
#
|
||||
# if jh.is_live() and config['env']['notifications']['events']['updated_position']:
|
||||
# notifier.notify(info_text)
|
||||
|
||||
def _reduce(self, qty: float, price: float) -> None:
|
||||
if self.is_open is False:
|
||||
@@ -288,10 +294,6 @@ class Position:
|
||||
raise OpenPositionError('position must be already open in order to increase its size')
|
||||
|
||||
qty = abs(qty)
|
||||
# size = qty * price
|
||||
|
||||
# if self.exchange:
|
||||
# self.exchange.decrease_futures_balance(size)
|
||||
|
||||
self.entry_price = jh.estimate_average_price(qty, price, self.qty,
|
||||
self.entry_price)
|
||||
@@ -309,7 +311,7 @@ class Position:
|
||||
if jh.is_live() and config['env']['notifications']['events']['updated_position']:
|
||||
notifier.notify(info_text)
|
||||
|
||||
def _open(self, qty: float, price: float, change_balance: bool = True) -> None:
|
||||
def _mutating_open(self, qty: float, price: float, change_balance: bool = True) -> None:
|
||||
if self.is_open:
|
||||
raise OpenPositionError('an already open position cannot be opened')
|
||||
|
||||
@@ -318,6 +320,12 @@ class Position:
|
||||
self.qty = qty
|
||||
self.opened_at = jh.now_to_timestamp()
|
||||
|
||||
self._open()
|
||||
|
||||
def _open(self):
|
||||
from jesse.store import store
|
||||
store.completed_trades.open_trade(self)
|
||||
|
||||
info_text = f'OPENED {self.type} position: {self.exchange_name}, {self.symbol}, {self.qty}, ${round(self.entry_price, 2)}'
|
||||
|
||||
if jh.is_debuggable('position_opened'):
|
||||
@@ -327,43 +335,64 @@ class Position:
|
||||
notifier.notify(info_text)
|
||||
|
||||
def _on_executed_order(self, order: Order) -> None:
|
||||
qty = order.qty
|
||||
price = order.price
|
||||
if not jh.is_livetrading():
|
||||
qty = order.qty
|
||||
price = order.price
|
||||
|
||||
# TODO: detect reduce_only order, and if so, see if you need to adjust qty and price (above variables)
|
||||
# TODO: detect reduce_only order, and if so, see if you need to adjust qty and price (above variables)
|
||||
|
||||
self.exchange.charge_fee(qty * price)
|
||||
self.exchange.charge_fee(qty * price)
|
||||
|
||||
# order opens position
|
||||
if self.qty == 0:
|
||||
change_balance = order.type == order_types.MARKET
|
||||
self._open(qty, price, change_balance)
|
||||
# order closes position
|
||||
elif (sum_floats(self.qty, qty)) == 0:
|
||||
self._close(price)
|
||||
# order increases the size of the position
|
||||
elif self.qty * qty > 0:
|
||||
if order.is_reduce_only:
|
||||
logger.info('Did not increase position because order is a reduce_only order')
|
||||
else:
|
||||
self._increase(qty, price)
|
||||
# order reduces the size of the position
|
||||
elif self.qty * qty < 0:
|
||||
# if size of the order is big enough to both close the
|
||||
# position AND open it on the opposite side
|
||||
if abs(qty) > abs(self.qty):
|
||||
if order.is_reduce_only:
|
||||
logger.info(
|
||||
f'Executed order is bigger than the current position size but it is a reduce_only order so it just closes it. Order QTY: {qty}, Position QTY: {self.qty}')
|
||||
self._close(price)
|
||||
# order opens position
|
||||
if self.qty == 0:
|
||||
change_balance = order.type == order_types.MARKET
|
||||
self._mutating_open(qty, price, change_balance)
|
||||
# order closes position
|
||||
elif (sum_floats(self.qty, qty)) == 0:
|
||||
self._mutating_close(price)
|
||||
# order increases the size of the position
|
||||
elif self.qty * qty > 0:
|
||||
if order.reduce_only:
|
||||
logger.info('Did not increase position because order is a reduce_only order')
|
||||
else:
|
||||
logger.info(
|
||||
f'Executed order is big enough to not close, but flip the position type. Order QTY: {qty}, Position QTY: {self.qty}')
|
||||
diff_qty = sum_floats(self.qty, qty)
|
||||
self._close(price)
|
||||
self._open(diff_qty, price)
|
||||
else:
|
||||
self._reduce(qty, price)
|
||||
self._increase(qty, price)
|
||||
# order reduces the size of the position
|
||||
elif self.qty * qty < 0:
|
||||
# if size of the order is big enough to both close the
|
||||
# position AND open it on the opposite side
|
||||
if abs(qty) > abs(self.qty):
|
||||
if order.reduce_only:
|
||||
logger.info(
|
||||
f'Executed order is bigger than the current position size but it is a reduce_only order so it just closes it. Order QTY: {qty}, Position QTY: {self.qty}')
|
||||
self._mutating_close(price)
|
||||
else:
|
||||
logger.info(
|
||||
f'Executed order is big enough to not close, but flip the position type. Order QTY: {qty}, Position QTY: {self.qty}')
|
||||
diff_qty = sum_floats(self.qty, qty)
|
||||
self._mutating_close(price)
|
||||
self._mutating_open(diff_qty, price)
|
||||
else:
|
||||
self._reduce(qty, price)
|
||||
|
||||
if self.strategy:
|
||||
self.strategy._on_updated_position(order)
|
||||
|
||||
def update_from_stream(self, data: dict) -> None:
|
||||
"""
|
||||
Used for updating the position from the WS stream (only for live trading)
|
||||
"""
|
||||
before_qty = abs(self.qty)
|
||||
after_qty = abs(data['qty'])
|
||||
|
||||
self.entry_price = data['entry_price']
|
||||
self._liquidation_price = data['liquidation_price']
|
||||
self.qty = data['qty']
|
||||
|
||||
# if opening position
|
||||
if before_qty == 0 and after_qty != 0:
|
||||
self.opened_at = jh.now_to_timestamp()
|
||||
self._open()
|
||||
# if closing position
|
||||
elif after_qty != 0 and before_qty == 0:
|
||||
self.closed_at = jh.now_to_timestamp()
|
||||
self._close()
|
||||
|
||||
@@ -96,8 +96,6 @@ def store_completed_trade_into_db(completed_trade) -> None:
|
||||
'qty': completed_trade.qty,
|
||||
'opened_at': completed_trade.opened_at,
|
||||
'closed_at': completed_trade.closed_at,
|
||||
'entry_candle_timestamp': completed_trade.entry_candle_timestamp,
|
||||
'exit_candle_timestamp': completed_trade.exit_candle_timestamp,
|
||||
'leverage': completed_trade.leverage,
|
||||
}
|
||||
|
||||
@@ -124,14 +122,14 @@ def store_order_into_db(order) -> None:
|
||||
'exchange': order.exchange,
|
||||
'side': order.side,
|
||||
'type': order.type,
|
||||
'flag': order.flag,
|
||||
'reduce_only': order.reduce_only,
|
||||
'qty': order.qty,
|
||||
'filled_qty': order.filled_qty,
|
||||
'price': order.price,
|
||||
'status': order.status,
|
||||
'created_at': order.created_at,
|
||||
'executed_at': order.executed_at,
|
||||
'canceled_at': order.canceled_at,
|
||||
'role': order.role,
|
||||
}
|
||||
|
||||
def async_save() -> None:
|
||||
|
||||
@@ -12,7 +12,7 @@ import jesse.services.required_candles as required_candles
|
||||
import jesse.services.selectors as selectors
|
||||
from jesse import exceptions
|
||||
from jesse.config import config
|
||||
from jesse.enums import timeframes, order_types, order_roles, order_flags
|
||||
from jesse.enums import timeframes, order_types
|
||||
from jesse.models import Candle, Order, Position
|
||||
from jesse.modes.utils import save_daily_portfolio_balance
|
||||
from jesse.routes import router
|
||||
@@ -450,10 +450,9 @@ def _check_for_liquidations(candle: np.ndarray, exchange: str, symbol: str) -> N
|
||||
'exchange': exchange,
|
||||
'side': closing_order_side,
|
||||
'type': order_types.MARKET,
|
||||
'flag': order_flags.REDUCE_ONLY,
|
||||
'reduce_only': True,
|
||||
'qty': jh.prepare_qty(p.qty, closing_order_side),
|
||||
'price': p.bankruptcy_price,
|
||||
'role': order_roles.CLOSE_POSITION
|
||||
'price': p.bankruptcy_price
|
||||
})
|
||||
|
||||
store.orders.add_order(order)
|
||||
|
||||
@@ -49,13 +49,12 @@ class API:
|
||||
qty: float,
|
||||
current_price: float,
|
||||
side: str,
|
||||
role: str,
|
||||
flags: list
|
||||
reduce_only: bool
|
||||
) -> Union[Order, None]:
|
||||
if exchange not in self.drivers:
|
||||
logger.info(f'Exchange "{exchange}" driver not initiated yet. Trying again in the next candle')
|
||||
return None
|
||||
return self.drivers[exchange].market_order(symbol, qty, current_price, side, role, flags)
|
||||
return self.drivers[exchange].market_order(symbol, qty, current_price, side, reduce_only)
|
||||
|
||||
def limit_order(
|
||||
self,
|
||||
@@ -64,13 +63,12 @@ class API:
|
||||
qty: float,
|
||||
price: float,
|
||||
side: str,
|
||||
role: str,
|
||||
flags: list
|
||||
reduce_only: bool
|
||||
) -> Union[Order, None]:
|
||||
if exchange not in self.drivers:
|
||||
logger.info(f'Exchange "{exchange}" driver not initiated yet. Trying again in the next candle')
|
||||
return None
|
||||
return self.drivers[exchange].limit_order(symbol, qty, price, side, role, flags)
|
||||
return self.drivers[exchange].limit_order(symbol, qty, price, side, reduce_only)
|
||||
|
||||
def stop_order(
|
||||
self, exchange: str,
|
||||
@@ -78,13 +76,12 @@ class API:
|
||||
qty: float,
|
||||
price: float,
|
||||
side: str,
|
||||
role: str,
|
||||
flags: list
|
||||
reduce_only: bool
|
||||
) -> Union[Order, None]:
|
||||
if exchange not in self.drivers:
|
||||
logger.info(f'Exchange "{exchange}" driver not initiated yet. Trying again in the next candle')
|
||||
return None
|
||||
return self.drivers[exchange].stop_order(symbol, qty, price, side, role, flags)
|
||||
return self.drivers[exchange].stop_order(symbol, qty, price, side, reduce_only)
|
||||
|
||||
def cancel_all_orders(self, exchange: str, symbol: str) -> bool:
|
||||
if exchange not in self.drivers:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Union
|
||||
|
||||
import jesse.helpers as jh
|
||||
from jesse.enums import sides, order_flags
|
||||
from jesse.enums import sides
|
||||
from jesse.exceptions import OrderNotAllowed, InvalidStrategy
|
||||
from jesse.models import Order
|
||||
from jesse.models import Position
|
||||
@@ -21,7 +21,7 @@ class Broker:
|
||||
if qty == 0:
|
||||
raise InvalidStrategy('qty cannot be 0')
|
||||
|
||||
def sell_at_market(self, qty: float, role: str = None) -> Union[Order, None]:
|
||||
def sell_at_market(self, qty: float) -> Union[Order, None]:
|
||||
self._validate_qty(qty)
|
||||
|
||||
return self.api.market_order(
|
||||
@@ -30,10 +30,10 @@ class Broker:
|
||||
abs(qty),
|
||||
self.position.current_price,
|
||||
sides.SELL,
|
||||
role, []
|
||||
reduce_only=False
|
||||
)
|
||||
|
||||
def sell_at(self, qty: float, price: float, role: str = None) -> Union[Order, None]:
|
||||
def sell_at(self, qty: float, price: float) -> Union[Order, None]:
|
||||
self._validate_qty(qty)
|
||||
|
||||
if price < 0:
|
||||
@@ -45,11 +45,10 @@ class Broker:
|
||||
abs(qty),
|
||||
price,
|
||||
sides.SELL,
|
||||
role,
|
||||
[]
|
||||
reduce_only=False
|
||||
)
|
||||
|
||||
def buy_at_market(self, qty: float, role: str = None) -> Union[Order, None]:
|
||||
def buy_at_market(self, qty: float) -> Union[Order, None]:
|
||||
self._validate_qty(qty)
|
||||
|
||||
return self.api.market_order(
|
||||
@@ -58,11 +57,10 @@ class Broker:
|
||||
abs(qty),
|
||||
self.position.current_price,
|
||||
sides.BUY,
|
||||
role,
|
||||
[]
|
||||
reduce_only=False
|
||||
)
|
||||
|
||||
def buy_at(self, qty: float, price: float, role: str = None) -> Union[Order, None]:
|
||||
def buy_at(self, qty: float, price: float) -> Union[Order, None]:
|
||||
self._validate_qty(qty)
|
||||
|
||||
if price < 0:
|
||||
@@ -74,11 +72,10 @@ class Broker:
|
||||
abs(qty),
|
||||
price,
|
||||
sides.BUY,
|
||||
role,
|
||||
[]
|
||||
reduce_only=False
|
||||
)
|
||||
|
||||
def reduce_position_at(self, qty: float, price: float, role: str = None) -> Union[Order, None]:
|
||||
def reduce_position_at(self, qty: float, price: float) -> Union[Order, None]:
|
||||
self._validate_qty(qty)
|
||||
|
||||
qty = abs(qty)
|
||||
@@ -102,8 +99,7 @@ class Broker:
|
||||
qty,
|
||||
price,
|
||||
side,
|
||||
role,
|
||||
[order_flags.REDUCE_ONLY]
|
||||
reduce_only=True
|
||||
)
|
||||
|
||||
elif (side == 'sell' and self.position.type == 'long' and price > self.position.current_price) or (
|
||||
@@ -114,8 +110,7 @@ class Broker:
|
||||
qty,
|
||||
price,
|
||||
side,
|
||||
role,
|
||||
[order_flags.REDUCE_ONLY]
|
||||
reduce_only=True
|
||||
)
|
||||
elif (side == 'sell' and self.position.type == 'long' and price < self.position.current_price) or (
|
||||
side == 'buy' and self.position.type == 'short' and price > self.position.current_price):
|
||||
@@ -125,13 +120,12 @@ class Broker:
|
||||
abs(qty),
|
||||
price,
|
||||
side,
|
||||
role,
|
||||
[order_flags.REDUCE_ONLY]
|
||||
reduce_only=True
|
||||
)
|
||||
else:
|
||||
raise OrderNotAllowed("This order doesn't seem to be for reducing the position.")
|
||||
|
||||
def start_profit_at(self, side: str, qty: float, price: float, role: str = None) -> Union[Order, None]:
|
||||
def start_profit_at(self, side: str, qty: float, price: float) -> Union[Order, None]:
|
||||
self._validate_qty(qty)
|
||||
|
||||
if price < 0:
|
||||
@@ -152,8 +146,7 @@ class Broker:
|
||||
abs(qty),
|
||||
price,
|
||||
side,
|
||||
role,
|
||||
[]
|
||||
reduce_only=False
|
||||
)
|
||||
|
||||
def cancel_all_orders(self) -> bool:
|
||||
|
||||
@@ -36,7 +36,7 @@ def positions() -> list:
|
||||
'value': round(p.value, 2),
|
||||
'entry': p.entry_price,
|
||||
'current_price': p.current_price,
|
||||
'liq_price': p.liquidation_price,
|
||||
'liquidation_price': p.liquidation_price,
|
||||
'pnl': p.pnl,
|
||||
'pnl_perc': p.pnl_percentage
|
||||
})
|
||||
@@ -88,15 +88,13 @@ def candles() -> dict:
|
||||
|
||||
|
||||
def livetrade():
|
||||
# TODO: for now, we assume that we trade on one exchange only. Later, we need to support for more than one exchange at a time
|
||||
# sum up balance of all trading exchanges
|
||||
starting_balance = 0
|
||||
current_balance = 0
|
||||
for e in store.exchanges.storage:
|
||||
starting_balance += store.exchanges.storage[e].starting_assets[jh.app_currency()]
|
||||
current_balance += store.exchanges.storage[e].assets[jh.app_currency()]
|
||||
starting_balance = round(starting_balance, 2)
|
||||
current_balance = round(current_balance, 2)
|
||||
starting_balance = round(store.exchanges.storage[e].started_balance(), 2)
|
||||
current_balance = round(store.exchanges.storage[e].wallet_balance(), 2)
|
||||
# there's only one exchange, so we can break
|
||||
break
|
||||
|
||||
# short trades summary
|
||||
if len(store.completed_trades.trades):
|
||||
|
||||
@@ -1,9 +1,83 @@
|
||||
import numpy as np
|
||||
|
||||
from jesse.models import Position, CompletedTrade, Order
|
||||
import jesse.helpers as jh
|
||||
from jesse.models.utils import store_completed_trade_into_db
|
||||
from jesse.enums import sides
|
||||
|
||||
|
||||
class CompletedTrades:
|
||||
def __init__(self) -> None:
|
||||
self.trades = []
|
||||
self.tempt_trades = {}
|
||||
|
||||
def add_trade(self, trade) -> None:
|
||||
self.trades.append(trade)
|
||||
def _get_current_trade(self, exchange: str, symbol: str) -> CompletedTrade:
|
||||
key = jh.key(exchange, symbol)
|
||||
# if already exists, return it
|
||||
if key in self.tempt_trades:
|
||||
t: CompletedTrade = self.tempt_trades[key]
|
||||
# set the trade.id if not generated already
|
||||
if not t.id:
|
||||
t.id = jh.generate_unique_id()
|
||||
return t
|
||||
# else, create a new trade, store it, and return it
|
||||
t = CompletedTrade()
|
||||
t.id = jh.generate_unique_id()
|
||||
self.tempt_trades[key] = t
|
||||
return t
|
||||
|
||||
def _reset_current_trade(self, exchange: str, symbol: str) -> None:
|
||||
key = jh.key(exchange, symbol)
|
||||
self.tempt_trades[key] = CompletedTrade()
|
||||
|
||||
def add_executed_order(self, executed_order: Order) -> None:
|
||||
t = self._get_current_trade(executed_order.exchange, executed_order.symbol)
|
||||
executed_order.trade_id = t.id
|
||||
t.orders.append(executed_order)
|
||||
if executed_order.side == sides.BUY:
|
||||
t.buy_orders.append(np.array([abs(executed_order.qty), executed_order.price]))
|
||||
elif executed_order.side == sides.SELL:
|
||||
t.sell_orders.append(np.array([abs(executed_order.qty), executed_order.price]))
|
||||
else:
|
||||
raise Exception("Invalid order side")
|
||||
|
||||
def open_trade(self, position: Position) -> None:
|
||||
t = self._get_current_trade(position.exchange_name, position.symbol)
|
||||
t.opened_at = position.opened_at
|
||||
t.leverage = position.leverage
|
||||
try:
|
||||
t.timeframe = position.strategy.timeframe
|
||||
t.strategy_name = position.strategy.name
|
||||
except AttributeError:
|
||||
if not jh.is_unit_testing():
|
||||
raise
|
||||
t.timeframe = None
|
||||
t.strategy_name = None
|
||||
t.exchange = position.exchange_name
|
||||
t.symbol = position.symbol
|
||||
t.type = position.type
|
||||
|
||||
def close_trade(self, position: Position) -> None:
|
||||
t = self._get_current_trade(position.exchange_name, position.symbol)
|
||||
t.closed_at = position.closed_at
|
||||
try:
|
||||
position.strategy.trades_count += 1
|
||||
except AttributeError:
|
||||
if not jh.is_unit_testing():
|
||||
raise
|
||||
|
||||
if jh.is_livetrading():
|
||||
store_completed_trade_into_db(t)
|
||||
# store the trade into the list
|
||||
self.trades.append(t)
|
||||
# at the end, reset the trade variable
|
||||
self._reset_current_trade(position.exchange_name, position.symbol)
|
||||
|
||||
# TODO: to detect initially received orders from the exchange in live mode:
|
||||
# position qty increase from 0: OPEN
|
||||
# position qty becoming 0: CLOSE
|
||||
# position qty increasing in size: INCREASE
|
||||
# position qty decreasing in size: REDUCE
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
|
||||
14
jesse/strategies/CanAddCompletedTradeToStore/__init__.py
Normal file
14
jesse/strategies/CanAddCompletedTradeToStore/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from jesse.strategies import Strategy
|
||||
import jesse.helpers as jh
|
||||
|
||||
|
||||
class CanAddCompletedTradeToStore(Strategy):
|
||||
def should_long(self):
|
||||
return self.price == 10
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def go_long(self):
|
||||
self.buy = 1, self.price
|
||||
self.take_profit = 1, 15
|
||||
@@ -3,15 +3,13 @@ from time import sleep
|
||||
from typing import List, Dict
|
||||
|
||||
import numpy as np
|
||||
import pydash
|
||||
|
||||
import jesse.helpers as jh
|
||||
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.enums import sides
|
||||
from jesse.models import CompletedTrade, Order, Route, FuturesExchange, SpotExchange, Position
|
||||
from jesse.models.utils import store_completed_trade_into_db, store_order_into_db
|
||||
from jesse.services import metrics
|
||||
from jesse.services.broker import Broker
|
||||
from jesse.store import store
|
||||
@@ -119,40 +117,34 @@ class Strategy(ABC):
|
||||
|
||||
def _on_updated_position(self, order: Order) -> None:
|
||||
"""
|
||||
Handles the after-effect of the executed order
|
||||
|
||||
Note that it assumes that the position has already been affected
|
||||
by the executed order.
|
||||
|
||||
Arguments:
|
||||
order {Order} -- the executed order object
|
||||
Handles the after-effect of the executed order to execute strategy
|
||||
events. Note that it assumes that the position has already
|
||||
been affected by the executed order.
|
||||
"""
|
||||
|
||||
# in live-mode, sometimes order-update effects and new execution has overlaps, so:
|
||||
self._is_handling_updated_order = True
|
||||
|
||||
role = order.role
|
||||
# this is the last executed order, and had its effect on
|
||||
# the position. We need to know what its effect was:
|
||||
before_qty = self.position.qty - order.qty
|
||||
after_qty = self.position.qty
|
||||
|
||||
# if the order's role is CLOSE_POSITION but the position qty is not the same as this order's qty,
|
||||
# then it's increase_position order (because the position was already open before this)
|
||||
if self.trade and 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
|
||||
|
||||
self._log_position_update(order, role)
|
||||
|
||||
if role == order_roles.OPEN_POSITION:
|
||||
# call the relevant strategy event handler:
|
||||
# if opening position
|
||||
if before_qty == 0 and after_qty != 0:
|
||||
self._on_open_position(order)
|
||||
elif role == order_roles.CLOSE_POSITION:
|
||||
# if closing position
|
||||
elif before_qty != 0 and after_qty == 0:
|
||||
self._on_close_position(order)
|
||||
elif role == order_roles.INCREASE_POSITION:
|
||||
# if increasing position size
|
||||
elif abs(after_qty) > abs(before_qty):
|
||||
self._on_increased_position(order)
|
||||
elif role == order_roles.REDUCE_POSITION:
|
||||
# if reducing position size
|
||||
elif abs(after_qty) < abs(before_qty):
|
||||
self._on_reduced_position(order)
|
||||
else:
|
||||
pass
|
||||
|
||||
self._is_handling_updated_order = False
|
||||
|
||||
@@ -196,13 +188,13 @@ class Strategy(ABC):
|
||||
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)
|
||||
submitted_order = self.broker.buy_at_market(o[0])
|
||||
# STOP order
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.BUY, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.start_profit_at(sides.BUY, o[0], o[1])
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.buy_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.buy_at(o[0], o[1])
|
||||
else:
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
@@ -236,13 +228,13 @@ class Strategy(ABC):
|
||||
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)
|
||||
submitted_order = self.broker.sell_at_market(o[0])
|
||||
# STOP order
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.SELL, o[0], o[1], order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.start_profit_at(sides.SELL, o[0], o[1])
|
||||
# LIMIT order
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.sell_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.sell_at(o[0], o[1])
|
||||
else:
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
@@ -484,14 +476,13 @@ class Strategy(ABC):
|
||||
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)
|
||||
submitted_order = self.broker.buy_at_market(o[0])
|
||||
# STOP order
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.BUY, o[0], o[1],
|
||||
order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.start_profit_at(sides.BUY, o[0], o[1])
|
||||
# LIMIT order
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.buy_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.buy_at(o[0], o[1])
|
||||
else:
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
@@ -514,14 +505,13 @@ class Strategy(ABC):
|
||||
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)
|
||||
submitted_order = self.broker.sell_at_market(o[0])
|
||||
# STOP order
|
||||
elif o[1] < self.price:
|
||||
submitted_order = self.broker.start_profit_at(sides.SELL, o[0], o[1],
|
||||
order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.start_profit_at(sides.SELL, o[0], o[1])
|
||||
# LIMIT order
|
||||
elif o[1] > self.price:
|
||||
submitted_order = self.broker.sell_at(o[0], o[1], order_roles.OPEN_POSITION)
|
||||
submitted_order = self.broker.sell_at(o[0], o[1])
|
||||
else:
|
||||
raise ValueError(f'Invalid order price: o[1]:{o[1]}, self.price:{self.price}')
|
||||
|
||||
@@ -543,11 +533,7 @@ class Strategy(ABC):
|
||||
# remove canceled orders to optimize the loop
|
||||
self._exit_orders = [o for o in self._exit_orders if not o.is_canceled]
|
||||
for o in self._take_profit:
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
submitted_order: Order = self.broker.reduce_position_at(o[0], o[1])
|
||||
if submitted_order:
|
||||
submitted_order.submitted_via = 'take-profit'
|
||||
self._exit_orders.append(submitted_order)
|
||||
@@ -568,11 +554,7 @@ class Strategy(ABC):
|
||||
# remove canceled orders to optimize the loop
|
||||
self._exit_orders = [o for o in self._exit_orders if not o.is_canceled]
|
||||
for o in self._stop_loss:
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
submitted_order: Order = self.broker.reduce_position_at(o[0], o[1])
|
||||
if submitted_order:
|
||||
submitted_order.submitted_via = 'stop-loss'
|
||||
self._exit_orders.append(submitted_order)
|
||||
@@ -670,19 +652,15 @@ class Strategy(ABC):
|
||||
for o in self._take_profit:
|
||||
# validation: make sure take-profit will exit with profit, if not, close the position
|
||||
if self.is_long and o[1] <= self.position.entry_price:
|
||||
submitted_order: Order = self.broker.sell_at_market(o[0], order_roles.CLOSE_POSITION)
|
||||
submitted_order: Order = self.broker.sell_at_market(o[0])
|
||||
logger.info(
|
||||
'The take-profit is below entry-price for long position, so it will be replaced with a market order instead')
|
||||
elif self.is_short and o[1] >= self.position.entry_price:
|
||||
submitted_order: Order = self.broker.buy_at_market(o[0], order_roles.CLOSE_POSITION)
|
||||
submitted_order: Order = self.broker.buy_at_market(o[0])
|
||||
logger.info(
|
||||
'The take-profit is above entry-price for a short position, so it will be replaced with a market order instead')
|
||||
else:
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
submitted_order: Order = self.broker.reduce_position_at(o[0], o[1])
|
||||
|
||||
if submitted_order:
|
||||
submitted_order.submitted_via = 'take-profit'
|
||||
@@ -692,17 +670,13 @@ class Strategy(ABC):
|
||||
for o in self._stop_loss:
|
||||
# validation: make sure stop-loss will exit with profit, if not, close the position
|
||||
if self.is_long and o[1] >= self.position.entry_price:
|
||||
submitted_order: Order = self.broker.sell_at_market(o[0], order_roles.CLOSE_POSITION)
|
||||
submitted_order: Order = self.broker.sell_at_market(o[0])
|
||||
logger.info('The stop-loss is above entry-price for long position, so it will be replaced with a market order instead')
|
||||
elif self.is_short and o[1] <= self.position.entry_price:
|
||||
submitted_order: Order = self.broker.buy_at_market(o[0], order_roles.CLOSE_POSITION)
|
||||
submitted_order: Order = self.broker.buy_at_market(o[0])
|
||||
logger.info('The stop-loss is below entry-price for a short position, so it will be replaced with a market order instead')
|
||||
else:
|
||||
submitted_order: Order = self.broker.reduce_position_at(
|
||||
o[0],
|
||||
o[1],
|
||||
order_roles.CLOSE_POSITION
|
||||
)
|
||||
submitted_order: Order = self.broker.reduce_position_at(o[0], o[1])
|
||||
|
||||
if submitted_order:
|
||||
submitted_order.submitted_via = 'stop-loss'
|
||||
@@ -853,9 +827,7 @@ class Strategy(ABC):
|
||||
f"Closed open {self.exchange}-{self.symbol} position at {self.position.current_price} with PNL: {round(self.position.pnl, 4)}({round(self.position.pnl_percentage, 2)}%) because we reached the end of the backtest session."
|
||||
)
|
||||
# fake a closing (market) order so that the calculations would be correct
|
||||
self.broker.reduce_position_at(
|
||||
self.position.qty, self.position.current_price, order_roles.CLOSE_POSITION
|
||||
)
|
||||
self.broker.reduce_position_at(self.position.qty, self.position.current_price)
|
||||
return
|
||||
|
||||
if len(self._entry_orders):
|
||||
@@ -1015,83 +987,6 @@ class Strategy(ABC):
|
||||
def fee_rate(self) -> float:
|
||||
return selectors.get_exchange(self.exchange).fee_rate
|
||||
|
||||
def _log_position_update(self, order: Order, role: str) -> None:
|
||||
"""
|
||||
A log can be either about opening, adding, reducing, or closing the position.
|
||||
|
||||
Arguments:
|
||||
order {order} -- the order object
|
||||
"""
|
||||
# set the trade_id for the order if we're in the middle of a trade. Otherwise, it
|
||||
# is done at order_roles.OPEN_POSITION
|
||||
if self.trade:
|
||||
order.trade_id = self.trade.id
|
||||
|
||||
if role == order_roles.OPEN_POSITION:
|
||||
self.trade = CompletedTrade()
|
||||
self.trade.leverage = self.leverage
|
||||
self.trade.orders = [order]
|
||||
self.trade.timeframe = self.timeframe
|
||||
self.trade.id = jh.generate_unique_id()
|
||||
order.trade_id = self.trade.id
|
||||
self.trade.strategy_name = self.name
|
||||
self.trade.exchange = order.exchange
|
||||
self.trade.symbol = order.symbol
|
||||
self.trade.type = trade_types.LONG if order.side == sides.BUY else trade_types.SHORT
|
||||
self.trade.qty = order.qty
|
||||
self.trade.opened_at = jh.now_to_timestamp()
|
||||
self.trade.entry_candle_timestamp = self.current_candle[0]
|
||||
elif role in [order_roles.INCREASE_POSITION, order_roles.REDUCE_POSITION]:
|
||||
self.trade.orders.append(order)
|
||||
self.trade.qty += order.qty
|
||||
elif role == order_roles.CLOSE_POSITION:
|
||||
self.trade.exit_candle_timestamp = self.current_candle[0]
|
||||
self.trade.orders.append(order)
|
||||
|
||||
# calculate average entry_price price
|
||||
sum_price = 0
|
||||
sum_qty = 0
|
||||
for trade_order in self.trade.orders:
|
||||
if not trade_order.is_executed:
|
||||
continue
|
||||
|
||||
if jh.side_to_type(trade_order.side) != self.trade.type:
|
||||
continue
|
||||
|
||||
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 trade_order in self.trade.orders:
|
||||
if not trade_order.is_executed:
|
||||
continue
|
||||
|
||||
if jh.side_to_type(trade_order.side) == self.trade.type:
|
||||
continue
|
||||
|
||||
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()
|
||||
self.trade.qty = pydash.sum_by(
|
||||
filter(lambda o: o.side == jh.type_to_side(self.trade.type), self.trade.orders),
|
||||
lambda o: abs(o.qty)
|
||||
)
|
||||
|
||||
store.completed_trades.add_trade(self.trade)
|
||||
if jh.is_livetrading():
|
||||
store_completed_trade_into_db(self.trade)
|
||||
self.trade = None
|
||||
self.trades_count += 1
|
||||
if jh.is_livetrading():
|
||||
store_order_into_db(order)
|
||||
|
||||
|
||||
@property
|
||||
def is_long(self) -> bool:
|
||||
return self.position.type == 'long'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from jesse.strategies import Strategy
|
||||
import jesse.helpers as jh
|
||||
|
||||
|
||||
# test_is_smart_enough_to_open_positions_via_market_orders
|
||||
@@ -26,6 +27,3 @@ class Test05(Strategy):
|
||||
|
||||
def should_cancel(self):
|
||||
return False
|
||||
|
||||
def filters(self):
|
||||
return []
|
||||
|
||||
@@ -7,7 +7,7 @@ class TestStopLossPriceIsReplacedWithMarketOrderForBetterPriceLongPosition(Strat
|
||||
if self.price == 15:
|
||||
last_trade = self.trades[-1]
|
||||
# it should have closed on the market price at the time being 10 instead of 12
|
||||
last_trade.exit_price = 10
|
||||
assert last_trade.exit_price == 10
|
||||
|
||||
# the order type should be market
|
||||
assert self.orders[0].type == order_types.MARKET
|
||||
|
||||
@@ -7,7 +7,7 @@ class TestStopLossPriceIsReplacedWithMarketOrderForBetterPriceShortPosition(Stra
|
||||
if self.price == 15:
|
||||
last_trade = self.trades[-1]
|
||||
# it should have closed on the market price at the time being 10 instead of 8
|
||||
last_trade.exit_price = 10
|
||||
assert last_trade.exit_price == 10
|
||||
|
||||
# the order type should be market
|
||||
assert self.orders[0].type == order_types.MARKET
|
||||
|
||||
@@ -7,7 +7,7 @@ class TestTakeProfitPriceIsReplacedWithMarketOrderWhenMoreConvenientLongPosition
|
||||
if self.price == 15:
|
||||
last_trade = self.trades[-1]
|
||||
# it should have closed on the market price at the time being 10 instead of 8
|
||||
last_trade.exit_price = 10
|
||||
assert last_trade.exit_price == 10
|
||||
|
||||
# the order type should be market
|
||||
assert self.orders[0].type == order_types.MARKET
|
||||
|
||||
@@ -7,7 +7,7 @@ class TestTakeProfitPriceIsReplacedWithMarketOrderWhenMoreConvenientShortPositio
|
||||
if self.price == 15:
|
||||
last_trade = self.trades[-1]
|
||||
# it should have closed on the market price at the time being 10 instead of 12
|
||||
last_trade.exit_price = 10
|
||||
assert last_trade.exit_price == 10
|
||||
|
||||
# the order type should be market
|
||||
assert self.orders[0].type == order_types.MARKET
|
||||
|
||||
0
tests/storage/logs/backtest-mode/.txt
Normal file
0
tests/storage/logs/backtest-mode/.txt
Normal file
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
import jesse.services.selectors as selectors
|
||||
from jesse.config import config, reset_config
|
||||
from jesse.enums import exchanges, timeframes, order_types, order_flags, order_roles
|
||||
from jesse.enums import exchanges, timeframes, order_types
|
||||
from jesse.exceptions import InvalidStrategy, NegativeBalance, OrderNotAllowed
|
||||
from jesse.models import Position, Exchange
|
||||
from jesse.routes import router
|
||||
@@ -169,7 +169,7 @@ def test_opening_and_closing_position_with_stop():
|
||||
assert exchange.available_margin() == 1000
|
||||
assert exchange.wallet_balance() == 1000
|
||||
# open position
|
||||
open_position_order = broker.start_profit_at('buy', 1, 60, order_roles.OPEN_POSITION)
|
||||
open_position_order = broker.start_profit_at('buy', 1, 60)
|
||||
open_position_order.execute()
|
||||
position.current_price = 60
|
||||
assert position.is_open is True
|
||||
@@ -180,14 +180,14 @@ def test_opening_and_closing_position_with_stop():
|
||||
assert exchange.available_margin() == 940
|
||||
|
||||
# submit stop-loss order
|
||||
stop_loss_order = broker.reduce_position_at(1, 40, order_roles.CLOSE_POSITION)
|
||||
assert stop_loss_order.flag == order_flags.REDUCE_ONLY
|
||||
stop_loss_order = broker.reduce_position_at(1, 40)
|
||||
assert stop_loss_order.reduce_only is True
|
||||
# balance should NOT have changed
|
||||
assert exchange.assets['USDT'] == 1000
|
||||
assert exchange.wallet_balance() == 1000
|
||||
# submit take-profit order also
|
||||
take_profit_order = broker.reduce_position_at(1, 80, order_roles.CLOSE_POSITION)
|
||||
assert take_profit_order.flag == order_flags.REDUCE_ONLY
|
||||
take_profit_order = broker.reduce_position_at(1, 80)
|
||||
assert take_profit_order.reduce_only is True
|
||||
assert exchange.assets['USDT'] == 1000
|
||||
|
||||
# execute stop order
|
||||
@@ -259,7 +259,7 @@ def test_stop_loss():
|
||||
assert order.price == 40
|
||||
assert order.qty == -1
|
||||
assert order.side == 'sell'
|
||||
assert order.flag == order_flags.REDUCE_ONLY
|
||||
assert order.reduce_only is True
|
||||
# balance should NOT have changed
|
||||
assert exchange.available_margin() == 950
|
||||
assert exchange.wallet_balance() == 1000
|
||||
|
||||
@@ -1,186 +1,71 @@
|
||||
import jesse.helpers as jh
|
||||
from jesse.config import config
|
||||
from jesse.models import CompletedTrade
|
||||
from jesse.store import store
|
||||
from .utils import set_up, single_route_backtest
|
||||
from .utils import single_route_backtest
|
||||
import numpy as np
|
||||
|
||||
|
||||
def test_can_add_trade_to_store():
|
||||
set_up()
|
||||
|
||||
def test_completed_trade_in_a_simple_strategy():
|
||||
assert store.completed_trades.trades == []
|
||||
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 20,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': 1552309186171,
|
||||
'closed_at': 1552309186171 + 60000
|
||||
})
|
||||
single_route_backtest('CanAddCompletedTradeToStore')
|
||||
|
||||
store.completed_trades.add_trade(trade)
|
||||
assert store.completed_trades.trades == [trade]
|
||||
store.reset()
|
||||
assert store.completed_trades.trades == []
|
||||
assert len(store.completed_trades.trades) == 1
|
||||
assert store.completed_trades.count == 1
|
||||
|
||||
t: CompletedTrade = store.completed_trades.trades[0]
|
||||
|
||||
assert t.entry_price == 10
|
||||
assert t.exit_price == 15
|
||||
assert t.exchange == 'Sandbox'
|
||||
assert t.symbol == 'BTC-USDT'
|
||||
assert t.type == 'long'
|
||||
assert t.strategy_name == 'CanAddCompletedTradeToStore'
|
||||
assert t.qty == 1
|
||||
assert t.size == 1*10
|
||||
assert t.fee == 0
|
||||
assert t.pnl == 5
|
||||
assert t.pnl_percentage == 50
|
||||
assert t.holding_period == 60*5
|
||||
|
||||
|
||||
def test_holding_period():
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 20,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': 1552309186171,
|
||||
'closed_at': 1552309186171 + 60000
|
||||
})
|
||||
|
||||
# 1 minute == 60 seconds
|
||||
assert trade.holding_period == 60
|
||||
|
||||
|
||||
def test_pnl_percentage():
|
||||
set_up(zero_fee=True)
|
||||
|
||||
# 1x leverage
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 12,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp(),
|
||||
'leverage': 1,
|
||||
})
|
||||
assert trade.pnl_percentage == 20
|
||||
|
||||
# 2x leverage
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 12,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp(),
|
||||
'leverage': 2,
|
||||
})
|
||||
assert trade.pnl_percentage == 40
|
||||
|
||||
|
||||
def test_pnl_with_fee():
|
||||
# set fee (0.20%)
|
||||
config['env']['exchanges']['Sandbox']['fee'] = 0.002
|
||||
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 20,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
|
||||
assert trade.fee == 0.06
|
||||
assert trade.pnl == 9.94
|
||||
|
||||
|
||||
def test_pnl_without_fee():
|
||||
set_up(zero_fee=True)
|
||||
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 20,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
assert trade.pnl == 10
|
||||
|
||||
|
||||
def test_r():
|
||||
set_up(zero_fee=True)
|
||||
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 12,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
|
||||
|
||||
def test_risk_percentage():
|
||||
set_up(zero_fee=True)
|
||||
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 12,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
|
||||
|
||||
def test_trade_size():
|
||||
trade = CompletedTrade({
|
||||
'type': 'long',
|
||||
'exchange': 'Sandbox',
|
||||
'entry_price': 10,
|
||||
'exit_price': 20,
|
||||
'take_profit_at': 20,
|
||||
'stop_loss_at': 5,
|
||||
'qty': 1,
|
||||
'orders': [],
|
||||
'symbol': 'BTC-USD',
|
||||
'opened_at': jh.now_to_timestamp(),
|
||||
'closed_at': jh.now_to_timestamp()
|
||||
})
|
||||
|
||||
assert trade.size == 10
|
||||
def test_completed_trade_in_a_strategy_with_two_trades():
|
||||
pass
|
||||
|
||||
|
||||
def test_completed_trade_after_exiting_the_trade():
|
||||
single_route_backtest('TestCompletedTradeAfterExitingTrade', leverage=2)
|
||||
|
||||
|
||||
def test_trade_qty_entry_price_exit_price_size_properties():
|
||||
# long trade
|
||||
t1 = CompletedTrade({
|
||||
'type': 'long',
|
||||
})
|
||||
# add buy orders
|
||||
t1.buy_orders.append(np.array([10, 100]))
|
||||
t1.buy_orders.append(np.array([10, 200]))
|
||||
# add sell orders
|
||||
t1.sell_orders.append(np.array([10, 300]))
|
||||
t1.sell_orders.append(np.array([10, 400]))
|
||||
# assert qty, entry price and exit price
|
||||
assert t1.qty == 20
|
||||
assert t1.entry_price == 150
|
||||
assert t1.exit_price == 350
|
||||
assert t1.size == 20*150
|
||||
|
||||
# short trade
|
||||
t2 = CompletedTrade({
|
||||
'type': 'short',
|
||||
})
|
||||
# add sell orders
|
||||
t2.sell_orders.append(np.array([10, 300]))
|
||||
t2.sell_orders.append(np.array([10, 400]))
|
||||
# add buy orders
|
||||
t2.buy_orders.append(np.array([10, 100]))
|
||||
t2.buy_orders.append(np.array([10, 200]))
|
||||
# assert qty, entry price and exit price
|
||||
assert t2.qty == 20
|
||||
assert t2.exit_price == 150
|
||||
assert t2.entry_price == 350
|
||||
assert t2.size == 20 * 350
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import jesse.helpers as jh
|
||||
from jesse.config import reset_config
|
||||
from jesse.enums import exchanges, order_roles
|
||||
from jesse.enums import exchanges
|
||||
from jesse.factories import candles_from_close_prices
|
||||
from jesse.models import CompletedTrade
|
||||
from jesse.routes import router
|
||||
@@ -42,13 +42,6 @@ def test_can_handle_multiple_entry_orders_too_close_to_each_other():
|
||||
assert t.exit_price == 3
|
||||
# 4 entry + 1 exit
|
||||
assert len(t.orders) == 5
|
||||
# last order is closing order
|
||||
assert t.orders[-1].role == order_roles.CLOSE_POSITION
|
||||
# first order must be opening order
|
||||
assert t.orders[0].role == order_roles.OPEN_POSITION
|
||||
# second order must be increasing order
|
||||
assert t.orders[1].role == order_roles.INCREASE_POSITION
|
||||
assert t.orders[2].role == order_roles.INCREASE_POSITION
|
||||
|
||||
|
||||
def test_conflicting_orders():
|
||||
|
||||
@@ -7,7 +7,7 @@ import jesse.helpers as jh
|
||||
import jesse.services.selectors as selectors
|
||||
from jesse import exceptions
|
||||
from jesse.config import reset_config
|
||||
from jesse.enums import exchanges, timeframes, order_roles, order_types
|
||||
from jesse.enums import exchanges, timeframes, order_types
|
||||
from jesse.factories import range_candles, candles_from_close_prices
|
||||
from jesse.models import CompletedTrade
|
||||
from jesse.models import Order
|
||||
@@ -91,11 +91,8 @@ def test_can_perform_backtest_with_multiple_routes():
|
||||
assert o.price == s.candles[0][2]
|
||||
assert o.created_at == short_candles[4][0] + 60_000
|
||||
assert o.is_executed is True
|
||||
assert s.orders[0].role == order_roles.OPEN_POSITION
|
||||
assert s.orders[0].type == order_types.MARKET
|
||||
assert s.orders[2].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[2].type == order_types.STOP
|
||||
assert s.orders[1].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[1].type == order_types.LIMIT
|
||||
assert s.trade is None
|
||||
assert len(store.completed_trades.trades) == 2
|
||||
@@ -190,8 +187,6 @@ def test_is_smart_enough_to_open_positions_via_market_orders():
|
||||
assert t1.fee == 0
|
||||
assert t1.opened_at == 1547201100000 + 60000
|
||||
assert t1.closed_at == 1547202840000 + 60000
|
||||
assert t1.entry_candle_timestamp == 1547201100000
|
||||
assert t1.exit_candle_timestamp == 1547202840000
|
||||
assert t1.orders[0].type == order_types.MARKET
|
||||
|
||||
t2: CompletedTrade = store.completed_trades.trades[1]
|
||||
@@ -202,8 +197,6 @@ def test_is_smart_enough_to_open_positions_via_market_orders():
|
||||
assert t2.fee == 0
|
||||
assert t2.opened_at == 1547203560000 + 60000
|
||||
assert t2.closed_at == 1547203740000 + 60000
|
||||
assert t2.entry_candle_timestamp == 1547203560000
|
||||
assert t2.exit_candle_timestamp == 1547203740000
|
||||
assert t2.orders[0].type == order_types.MARKET
|
||||
|
||||
|
||||
@@ -234,8 +227,6 @@ def test_is_smart_enough_to_open_positions_via_stop_orders():
|
||||
assert t1.fee == 0
|
||||
assert t1.opened_at == 1547201100000 + 60000
|
||||
assert t1.closed_at == 1547202840000 + 60000
|
||||
assert t1.entry_candle_timestamp == 1547201100000
|
||||
assert t1.exit_candle_timestamp == 1547202660000
|
||||
assert t1.orders[0].type == order_types.STOP
|
||||
|
||||
t2: CompletedTrade = store.completed_trades.trades[1]
|
||||
@@ -246,8 +237,6 @@ def test_is_smart_enough_to_open_positions_via_stop_orders():
|
||||
assert t2.fee == 0
|
||||
assert t2.opened_at == 1547203560000 + 60000
|
||||
assert t2.closed_at == 1547203740000 + 60000
|
||||
assert t2.entry_candle_timestamp == 1547203560000
|
||||
assert t2.exit_candle_timestamp == 1547203560000
|
||||
assert t2.orders[0].type == order_types.STOP
|
||||
|
||||
|
||||
@@ -355,16 +344,12 @@ def test_multiple_routes_can_communicate_with_each_other():
|
||||
assert len(s.orders) == 1
|
||||
# assert that the order got canceled
|
||||
assert o.is_canceled is True
|
||||
assert s.orders[0].role == order_roles.OPEN_POSITION
|
||||
assert s.orders[0].type == order_types.LIMIT
|
||||
elif r.strategy.trades_count == 1:
|
||||
assert len(s.orders) == 3
|
||||
assert o.is_executed is True
|
||||
assert s.orders[0].role == order_roles.OPEN_POSITION
|
||||
assert s.orders[0].type == order_types.LIMIT
|
||||
assert s.orders[2].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[2].type == order_types.STOP
|
||||
assert s.orders[1].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[1].type == order_types.LIMIT
|
||||
|
||||
|
||||
@@ -559,9 +544,6 @@ def test_should_buy_and_execute_buy():
|
||||
assert o.price == s.candles[0][2]
|
||||
assert o.created_at == short_candles[4][0] + 60_000
|
||||
assert o.is_executed is True
|
||||
assert s.orders[1].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[2].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[0].role == order_roles.OPEN_POSITION
|
||||
assert s.trade is None
|
||||
trade: CompletedTrade = store.completed_trades.trades[0]
|
||||
assert trade.type == 'long'
|
||||
@@ -605,9 +587,6 @@ def test_should_sell_and_execute_sell():
|
||||
assert o.price == s.candles[0][2]
|
||||
assert o.created_at == short_candles[4][0] + 60_000
|
||||
assert o.is_executed is True
|
||||
assert s.orders[1].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[2].role == order_roles.CLOSE_POSITION
|
||||
assert s.orders[0].role == order_roles.OPEN_POSITION
|
||||
assert s.trade is None
|
||||
assert len(store.completed_trades.trades) == 1
|
||||
assert store.completed_trades.trades[0].type == 'short'
|
||||
@@ -710,8 +689,6 @@ def test_updating_stop_loss_and_take_profit_after_opening_the_position():
|
||||
assert t1.fee == 0
|
||||
assert t1.opened_at == 1547201100000 + 60000
|
||||
assert t1.closed_at == 1547201700000 + 60000
|
||||
assert t1.entry_candle_timestamp == 1547201100000
|
||||
assert t1.exit_candle_timestamp == 1547201700000
|
||||
assert t1.orders[0].type == order_types.MARKET
|
||||
|
||||
t2: CompletedTrade = store.completed_trades.trades[1]
|
||||
@@ -722,8 +699,6 @@ def test_updating_stop_loss_and_take_profit_after_opening_the_position():
|
||||
assert t2.fee == 0
|
||||
assert t2.opened_at == 1547203560000 + 60000
|
||||
assert t2.closed_at == 1547203680000 + 60000
|
||||
assert t2.entry_candle_timestamp == 1547203560000
|
||||
assert t2.exit_candle_timestamp == 1547203680000
|
||||
assert t2.orders[0].type == order_types.MARKET
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ def test_close_position():
|
||||
})
|
||||
assert p.exit_price is None
|
||||
|
||||
p._close(50)
|
||||
p._mutating_close(50)
|
||||
|
||||
assert p.qty == 0
|
||||
assert p.entry_price is None
|
||||
@@ -90,7 +90,7 @@ def test_open_position():
|
||||
assert p.exit_price is None
|
||||
assert p.current_price is None
|
||||
|
||||
p._open(1, 50)
|
||||
p._mutating_open(1, 50)
|
||||
|
||||
assert p.qty == 1
|
||||
assert p.entry_price == 50
|
||||
@@ -178,7 +178,7 @@ def test_position_pnl_percentage():
|
||||
def test_position_roi():
|
||||
set_up()
|
||||
p = Position(exchanges.SANDBOX, 'BTC-USDT')
|
||||
p._open(3, 100)
|
||||
p._mutating_open(3, 100)
|
||||
p.current_price = 110
|
||||
|
||||
assert p.value == 330
|
||||
|
||||
Reference in New Issue
Block a user