214 lines
6.4 KiB
Python
214 lines
6.4 KiB
Python
from playhouse.postgres_ext import *
|
|
|
|
import jesse.helpers as jh
|
|
import jesse.services.logger as logger
|
|
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.services.db import database
|
|
|
|
|
|
if database.is_closed():
|
|
database.open_connection()
|
|
|
|
|
|
class Order(Model):
|
|
# id generated by Jesse for database usage
|
|
id = UUIDField(primary_key=True)
|
|
trade_id = UUIDField(index=True, null=True)
|
|
session_id = UUIDField(index=True)
|
|
|
|
# id generated by market, used in live-trade mode
|
|
exchange_id = CharField(null=True)
|
|
# some exchanges might require even further info
|
|
vars = JSONField(default={})
|
|
symbol = CharField()
|
|
exchange = CharField()
|
|
side = CharField()
|
|
type = CharField()
|
|
flag = CharField(null=True)
|
|
qty = FloatField()
|
|
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:
|
|
from jesse.services.db import database
|
|
|
|
database = database.db
|
|
indexes = ((('exchange', 'symbol'), False),)
|
|
|
|
def __init__(self, attributes: dict = None, **kwargs) -> None:
|
|
Model.__init__(self, attributes=attributes, **kwargs)
|
|
|
|
if attributes is None:
|
|
attributes = {}
|
|
|
|
for a, value in attributes.items():
|
|
setattr(self, a, value)
|
|
|
|
if self.created_at is None:
|
|
self.created_at = jh.now_to_timestamp()
|
|
|
|
if jh.is_live():
|
|
from jesse.store import store
|
|
self.session_id = store.app.session_id
|
|
self.save(force_insert=True)
|
|
|
|
if jh.is_live():
|
|
self.notify_submission()
|
|
if jh.is_debuggable('order_submission'):
|
|
txt = f'{"QUEUED" if self.is_queued else "SUBMITTED"} order: {self.symbol}, {self.type}, {self.side}, {self.qty}'
|
|
if self.price:
|
|
txt += f', ${round(self.price, 2)}'
|
|
logger.info(txt)
|
|
|
|
# handle exchange balance for ordered asset
|
|
e = selectors.get_exchange(self.exchange)
|
|
e.on_order_submission(self)
|
|
|
|
def broadcast(self) -> None:
|
|
sync_publish('order', self.to_dict)
|
|
|
|
def notify_submission(self) -> None:
|
|
self.broadcast()
|
|
|
|
if config['env']['notifications']['events']['submitted_orders']:
|
|
txt = f'{"QUEUED" if self.is_queued else "SUBMITTED"} order: {self.symbol}, {self.type}, {self.side}, {self.qty}'
|
|
if self.price:
|
|
txt += f', ${round(self.price, 2)}'
|
|
notify(txt)
|
|
|
|
@property
|
|
def is_canceled(self) -> bool:
|
|
return self.status == order_statuses.CANCELED
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
return self.status == order_statuses.ACTIVE
|
|
|
|
@property
|
|
def is_queued(self) -> bool:
|
|
"""
|
|
Used in live mode only: it means the strategy has considered the order as submitted,
|
|
but the exchange does not accept it because of the distance between the current
|
|
price and price of the order. Hence it's been queued for later submission.
|
|
|
|
:return: bool
|
|
"""
|
|
return self.status == order_statuses.QUEUED
|
|
|
|
@property
|
|
def is_new(self) -> bool:
|
|
return self.is_active
|
|
|
|
@property
|
|
def is_executed(self) -> bool:
|
|
return self.status == order_statuses.EXECUTED
|
|
|
|
@property
|
|
def is_filled(self) -> bool:
|
|
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
|
|
|
|
@property
|
|
def is_stop_loss(self):
|
|
return self.submitted_via == 'stop-loss'
|
|
|
|
@property
|
|
def is_take_profit(self):
|
|
return self.submitted_via == 'take-profit'
|
|
|
|
@property
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'session_id': self.session_id,
|
|
'exchange_id': self.exchange_id,
|
|
'symbol': self.symbol,
|
|
'side': self.side,
|
|
'type': self.type,
|
|
'qty': self.qty,
|
|
'price': self.price,
|
|
'flag': self.flag,
|
|
'status': self.status,
|
|
'created_at': self.created_at,
|
|
'canceled_at': self.canceled_at,
|
|
'executed_at': self.executed_at,
|
|
}
|
|
|
|
def cancel(self, silent=False) -> None:
|
|
if self.is_canceled or self.is_executed:
|
|
return
|
|
|
|
self.canceled_at = jh.now_to_timestamp()
|
|
self.status = order_statuses.CANCELED
|
|
|
|
if jh.is_live():
|
|
self.save()
|
|
|
|
if not silent:
|
|
txt = f'CANCELED order: {self.symbol}, {self.type}, {self.side}, {self.qty}'
|
|
if self.price:
|
|
txt += f', ${round(self.price, 2)}'
|
|
if jh.is_debuggable('order_cancellation'):
|
|
logger.info(txt)
|
|
if jh.is_live():
|
|
self.broadcast()
|
|
if config['env']['notifications']['events']['cancelled_orders']:
|
|
notify(txt)
|
|
|
|
# handle exchange balance
|
|
e = selectors.get_exchange(self.exchange)
|
|
e.on_order_cancellation(self)
|
|
|
|
def execute(self, silent=False) -> None:
|
|
if self.is_canceled or self.is_executed:
|
|
return
|
|
|
|
self.executed_at = jh.now_to_timestamp()
|
|
self.status = order_statuses.EXECUTED
|
|
|
|
if jh.is_live():
|
|
self.save()
|
|
|
|
if not silent:
|
|
txt = f'EXECUTED order: {self.symbol}, {self.type}, {self.side}, {self.qty}'
|
|
if self.price:
|
|
txt += f', ${round(self.price, 2)}'
|
|
# log
|
|
if jh.is_debuggable('order_execution'):
|
|
logger.info(txt)
|
|
# notify
|
|
if jh.is_live():
|
|
self.broadcast()
|
|
if config['env']['notifications']['events']['executed_orders']:
|
|
notify(txt)
|
|
|
|
p = selectors.get_position(self.exchange, self.symbol)
|
|
|
|
if p:
|
|
p._on_executed_order(self)
|
|
|
|
# handle exchange balance for ordered asset
|
|
e = selectors.get_exchange(self.exchange)
|
|
e.on_order_execution(self)
|
|
|
|
|
|
# if database is open, create the table
|
|
if database.is_open():
|
|
Order.create_table()
|