refactor order execution and event firing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -146,7 +146,7 @@ class FuturesExchange(Exchange):
|
||||
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:
|
||||
@@ -161,7 +161,7 @@ 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:
|
||||
@@ -176,7 +176,7 @@ class FuturesExchange(Exchange):
|
||||
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]):
|
||||
@@ -198,7 +198,7 @@ class FuturesExchange(Exchange):
|
||||
|
||||
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]):
|
||||
|
||||
@@ -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:
|
||||
@@ -120,14 +120,6 @@ class Order(Model):
|
||||
def is_partially_filled(self) -> bool:
|
||||
return self.status == order_statuses.PARTIALLY_FILLED
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def is_stop_loss(self):
|
||||
return self.submitted_via == 'stop-loss'
|
||||
@@ -146,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,
|
||||
@@ -215,9 +208,6 @@ class Order(Model):
|
||||
e = selectors.get_exchange(self.exchange)
|
||||
e.on_order_execution(self)
|
||||
|
||||
def update_from_stream(self, data: dict) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# if database is open, create the table
|
||||
if database.is_open():
|
||||
|
||||
@@ -352,7 +352,7 @@ class Position:
|
||||
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)
|
||||
@@ -361,7 +361,7 @@ 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._mutating_close(price)
|
||||
@@ -381,14 +381,18 @@ class Position:
|
||||
"""
|
||||
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 self.is_close and data['qty'] != 0:
|
||||
if before_qty == 0 and after_qty != 0:
|
||||
self.opened_at = jh.now_to_timestamp()
|
||||
self._open()
|
||||
# if closing position
|
||||
if self.is_open and data['qty'] == 0:
|
||||
elif after_qty != 0 and before_qty == 0:
|
||||
self.closed_at = jh.now_to_timestamp()
|
||||
self._close()
|
||||
self.qty = data['qty']
|
||||
|
||||
@@ -122,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:
|
||||
|
||||
@@ -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,38 +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 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
|
||||
|
||||
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
|
||||
|
||||
@@ -194,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}')
|
||||
|
||||
@@ -234,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}')
|
||||
|
||||
@@ -482,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}')
|
||||
|
||||
@@ -512,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}')
|
||||
|
||||
@@ -541,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)
|
||||
@@ -566,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)
|
||||
@@ -668,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'
|
||||
@@ -690,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'
|
||||
@@ -851,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):
|
||||
|
||||
@@ -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 []
|
||||
|
||||
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,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
|
||||
@@ -347,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
|
||||
|
||||
|
||||
@@ -551,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'
|
||||
@@ -597,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'
|
||||
|
||||
Reference in New Issue
Block a user