Updated LocalTrade and Order classes

This commit is contained in:
Sam Germain 2021-06-19 22:06:51 -06:00
parent 27fe6e0a1b
commit a27171b371
2 changed files with 101 additions and 43 deletions

View File

@ -45,7 +45,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0')
min_rate = get_column_def(cols, 'min_rate', 'null') min_rate = get_column_def(cols, 'min_rate', 'null')
sell_reason = get_column_def(cols, 'sell_reason', 'null') close_reason = get_column_def(cols, 'close_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
# If ticker-interval existed use that, else null. # If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'): if has_column(cols, 'ticker_interval'):
@ -58,7 +58,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
sell_order_status = get_column_def(cols, 'sell_order_status', 'null') close_order_status = get_column_def(cols, 'close_order_status', 'null')
amount_requested = get_column_def(cols, 'amount_requested', 'amount') amount_requested = get_column_def(cols, 'amount_requested', 'amount')
# Schema migration necessary # Schema migration necessary
@ -80,7 +80,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, max_rate, min_rate, close_reason, close_order_status, strategy,
timeframe, open_trade_value, close_profit_abs timeframe, open_trade_value, close_profit_abs
) )
select id, lower(exchange), select id, lower(exchange),
@ -101,8 +101,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason,
{sell_order_status} sell_order_status, {close_order_status} close_order_status,
{strategy} strategy, {timeframe} timeframe, {strategy} strategy, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
from {table_back_name} from {table_back_name}

View File

@ -29,13 +29,13 @@ _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database
def init_db(db_url: str, clean_open_orders: bool = False) -> None: def init_db(db_url: str, clean_open_orders: bool = False) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param db_url: Database to use :param db_url: Database to use
:param clean_open_orders: Remove open orders from the database. :param clean_open_orders: Remove open orders from the database.
Useful for dry-run or if all orders have been reset on the exchange. Useful for dry-run or if all orders have been reset on the exchange.
:return: None :return: None
""" """
kwargs = {} kwargs = {}
@ -131,9 +131,10 @@ class Order(_DECL_BASE):
order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True)
leverage = Column(Float, nullable=True)
def __repr__(self): def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
f'side={self.side}, order_type={self.order_type}, status={self.status})') f'side={self.side}, order_type={self.order_type}, status={self.status})')
@ -155,6 +156,7 @@ class Order(_DECL_BASE):
self.average = order.get('average', self.average) self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining) self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost) self.cost = order.get('cost', self.cost)
self.leverage = order.get('leverage', self.leverage)
if 'timestamp' in order and order['timestamp'] is not None: if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -226,6 +228,7 @@ class LocalTrade():
fee_close_currency: str = '' fee_close_currency: str = ''
open_rate: float = 0.0 open_rate: float = 0.0
open_rate_requested: Optional[float] = None open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value: float = 0.0 open_trade_value: float = 0.0
close_rate: Optional[float] = None close_rate: Optional[float] = None
@ -254,11 +257,20 @@ class LocalTrade():
max_rate: float = 0.0 max_rate: float = 0.0
# Lowest price reached # Lowest price reached
min_rate: float = 0.0 min_rate: float = 0.0
sell_reason: str = '' close_reason: str = ''
sell_order_status: str = '' close_order_status: str = ''
strategy: str = '' strategy: str = ''
timeframe: Optional[int] = None timeframe: Optional[int] = None
#Margin trading properties
leverage: float = 1.0
borrowed: float = 0
borrowed_currency: float = None
interest_rate: float = 0
min_stoploss: float = None
isShort: boolean = False
#End of margin trading properties
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
@ -322,8 +334,8 @@ class LocalTrade():
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs, 'profit_abs': self.close_profit_abs,
'sell_reason': self.sell_reason, 'close_reason': self.close_reason,
'sell_order_status': self.sell_order_status, 'close_order_status': self.close_order_status,
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
@ -340,6 +352,13 @@ class LocalTrade():
'min_rate': self.min_rate, 'min_rate': self.min_rate,
'max_rate': self.max_rate, 'max_rate': self.max_rate,
'leverage': self.leverage,
'borrowed': self.borrowed,
'borrowed_currency': self.borrowed_currency,
'interest_rate': self.interest_rate,
'min_stoploss': self.min_stoploss,
'leverage': self.leverage,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
} }
@ -379,6 +398,9 @@ class LocalTrade():
return return
new_loss = float(current_price * (1 - abs(stoploss))) new_loss = float(current_price * (1 - abs(stoploss)))
#TODO: Could maybe move this if into the new stoploss if branch
if (self.min_stoploss): #If trading on margin, don't set the stoploss below the liquidation price
new_loss = min(self.min_stoploss, new_loss)
# no stop loss assigned yet # no stop loss assigned yet
if not self.stop_loss: if not self.stop_loss:
@ -389,7 +411,7 @@ class LocalTrade():
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: else:
if new_loss > self.stop_loss: # stop losses only walk up, never down! if (new_loss > self.stop_loss and not self.isShort) or (new_loss < self.stop_loss and self.isShort): # stop losses only walk up, never down!, #TODO: But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss
logger.debug(f"{self.pair} - Adjusting stoploss...") logger.debug(f"{self.pair} - Adjusting stoploss...")
self._set_new_stoploss(new_loss, stoploss) self._set_new_stoploss(new_loss, stoploss)
else: else:
@ -403,6 +425,20 @@ class LocalTrade():
f"Trailing stoploss saved us: " f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
def is_opening_trade(self, side) -> bool:
"""
Determines if the trade is an opening (long buy or short sell) trade
:param side (string): the side (buy/sell) that order happens on
"""
return (side == 'buy' and not self.isShort) or (side == 'sell' and self.isShort)
def is_closing_trade(self, side) -> bool:
"""
Determines if the trade is an closing (long sell or short buy) trade
:param side (string): the side (buy/sell) that order happens on
"""
return (side == 'sell' and not self.isShort) or (side == 'buy' and self.isShort)
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
Updates this entity with amount and actual open/close rates. Updates this entity with amount and actual open/close rates.
@ -416,22 +452,24 @@ class LocalTrade():
logger.info('Updating trade (id=%s) ...', self.id) logger.info('Updating trade (id=%s) ...', self.id)
if order_type in ('market', 'limit') and order['side'] == 'buy': if order_type in ('market', 'limit') and self.isOpeningTrade(order['side']):
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') payment = "SELL" if self.isShort else "BUY"
logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.')
self.open_order_id = None self.open_order_id = None
elif order_type in ('market', 'limit') and order['side'] == 'sell': elif order_type in ('market', 'limit') and self.isClosingTrade(order['side']):
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') payment = "BUY" if self.isShort else "SELL"
self.close(safe_value_fallback(order, 'average', 'price')) logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) #TODO: Double check this
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.') logger.info(f'{order_type.upper()} is hit for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) self.close(safe_value_fallback(order, 'average', 'price'))
@ -445,11 +483,11 @@ class LocalTrade():
and marks trade as closed and marks trade as closed
""" """
self.close_rate = rate self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow()
self.is_open = False self.is_open = False
self.sell_order_status = 'closed' self.close_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
if show_msg: if show_msg:
logger.info( logger.info(
@ -462,14 +500,14 @@ class LocalTrade():
""" """
Update Fee parameters. Only acts once per side Update Fee parameters. Only acts once per side
""" """
if side == 'buy' and self.fee_open_currency is None: if self.is_opening_trade(side) and self.fee_open_currency is None:
self.fee_open_cost = fee_cost self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency self.fee_open_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
self.fee_open = fee_rate self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess # Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None: elif self.is_closing_trade(side) and self.fee_close_currency is None:
self.fee_close_cost = fee_cost self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency self.fee_close_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
@ -479,9 +517,9 @@ class LocalTrade():
""" """
Verify if this side (buy / sell) has already been updated Verify if this side (buy / sell) has already been updated
""" """
if side == 'buy': if self.is_opening_trade(side):
return self.fee_open_currency is not None return self.fee_open_currency is not None
elif side == 'sell': elif self.is_closing_trade(side):
return self.fee_close_currency is not None return self.fee_close_currency is not None
else: else:
return False return False
@ -494,9 +532,13 @@ class LocalTrade():
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees :return: Price in of the open trade incl. Fees
""" """
buy_trade = Decimal(self.amount) * Decimal(self.open_rate) open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = buy_trade * Decimal(self.fee_open) fees = open_trade * Decimal(self.fee_open)
return float(buy_trade + fees) if (self.isShort):
return float(open_trade - fees)
else:
return float(open_trade + fees)
def recalc_open_trade_value(self) -> None: def recalc_open_trade_value(self) -> None:
""" """
@ -513,34 +555,47 @@ class LocalTrade():
If rate is not set self.fee will be used If rate is not set self.fee will be used
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param borrowed: amount borrowed to make this trade
If borrowed is not set self.borrowed will be used
:return: Price in BTC of the open trade :return: Price in BTC of the open trade
""" """
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = sell_trade * Decimal(fee or self.fee_close) fees = close_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees) if (self.isShort):
interest = (self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days #Interest/day * num of days, 0 if not margin
return float(close_trade + fees + interest)
else:
return float(close_trade - fees)
def calc_profit(self, rate: Optional[float] = None, def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used If fee is not set self.fee will be used
:param rate: close rate to compare with (optional). :param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param borrowed: amount borrowed to make this trade
If borrowed is not set self.borrowed will be used
:return: profit in stake currency as float :return: profit in stake currency as float
""" """
close_trade_value = self.calc_close_trade_value( close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close)
) )
profit = close_trade_value - self.open_trade_value
if self.isShort:
profit = self.open_trade_value - close_trade_value
else:
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None, def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None,
borrowed: Optional[float] = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
@ -554,7 +609,10 @@ class LocalTrade():
) )
if self.open_trade_value == 0.0: if self.open_trade_value == 0.0:
return 0.0 return 0.0
profit_ratio = (close_trade_value / self.open_trade_value) - 1 if self.isShort:
profit_ratio = (close_trade_value / self.open_trade_value) - 1
else:
profit_ratio = (self.open_trade_value / close_trade_value) - 1
return float(f"{profit_ratio:.8f}") return float(f"{profit_ratio:.8f}")
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
@ -604,7 +662,7 @@ class LocalTrade():
sel_trades = [trade for trade in sel_trades if trade.close_date sel_trades = [trade for trade in sel_trades if trade.close_date
and trade.close_date > close_date] and trade.close_date > close_date]
return sel_trades return sel_trades #TODO: What is sel_trades does it mean sell_trades? If so, update this for margin
@staticmethod @staticmethod
def close_bt_trade(trade): def close_bt_trade(trade):
@ -700,8 +758,8 @@ class Trade(_DECL_BASE, LocalTrade):
max_rate = Column(Float, nullable=True, default=0.0) max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float, nullable=True)
sell_reason = Column(String(100), nullable=True) close_reason = Column(String(100), nullable=True)
sell_order_status = Column(String(100), nullable=True) close_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)