5 Commits

Author SHA1 Message Date
Saleh Mir
92269df6b8 refactor order execution and event firing 2022-03-03 08:51:54 +01:00
Saleh Mir
25f1076629 refactor handling of CompletedTrade models and how orders affect positions 2022-02-28 10:56:49 +01:00
Saleh Mir
2f45ddfca5 handle exchange balances via WS 2022-02-24 11:34:01 +01:00
Saleh Mir
bafb17684d modify api text for liquidation_price of the dashboard 2022-02-23 19:01:46 +01:00
Saleh Mir
5b42f88531 handle positions stream in live-trade 2022-02-23 18:14:28 +01:00
26 changed files with 434 additions and 530 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,6 +335,7 @@ class Position:
notifier.notify(info_text)
def _on_executed_order(self, order: Order) -> None:
if not jh.is_livetrading():
qty = order.qty
price = order.price
@@ -337,13 +346,13 @@ class Position:
# order opens position
if self.qty == 0:
change_balance = order.type == order_types.MARKET
self._open(qty, price, change_balance)
self._mutating_open(qty, price, change_balance)
# order closes position
elif (sum_floats(self.qty, qty)) == 0:
self._close(price)
self._mutating_close(price)
# order increases the size of the position
elif self.qty * qty > 0:
if order.is_reduce_only:
if order.reduce_only:
logger.info('Did not increase position because order is a reduce_only order')
else:
self._increase(qty, price)
@@ -352,18 +361,38 @@ class Position:
# 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:
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._close(price)
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._close(price)
self._open(diff_qty, price)
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View 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

View File

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

View File

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

View File

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

View File

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