|
|
|
@@ -132,7 +132,11 @@ class Order(_DECL_BASE):
|
|
|
|
|
order_filled_date = Column(DateTime, nullable=True)
|
|
|
|
|
order_update_date = Column(DateTime, nullable=True)
|
|
|
|
|
|
|
|
|
|
leverage = Column(Float, nullable=True, default=None)
|
|
|
|
|
is_short = Column(Boolean, nullable=True, default=False)
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
|
|
|
|
|
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})')
|
|
|
|
|
|
|
|
|
@@ -226,7 +230,6 @@ class LocalTrade():
|
|
|
|
|
fee_close_currency: str = ''
|
|
|
|
|
open_rate: float = 0.0
|
|
|
|
|
open_rate_requested: Optional[float] = None
|
|
|
|
|
|
|
|
|
|
# open_trade_value - calculated via _calc_open_trade_value
|
|
|
|
|
open_trade_value: float = 0.0
|
|
|
|
|
close_rate: Optional[float] = None
|
|
|
|
@@ -261,61 +264,23 @@ class LocalTrade():
|
|
|
|
|
timeframe: Optional[int] = None
|
|
|
|
|
|
|
|
|
|
# Margin trading properties
|
|
|
|
|
borrowed_currency: str = None
|
|
|
|
|
collateral_currency: str = None
|
|
|
|
|
interest_rate: float = 0.0
|
|
|
|
|
liquidation_price: float = None
|
|
|
|
|
is_short: bool = False
|
|
|
|
|
borrowed: float = 0.0
|
|
|
|
|
leverage: float = None
|
|
|
|
|
|
|
|
|
|
# @property
|
|
|
|
|
# def base_currency(self) -> str:
|
|
|
|
|
# if not self.pair:
|
|
|
|
|
# raise OperationalException('LocalTrade.pair must be assigned')
|
|
|
|
|
# return self.pair.split("/")[1]
|
|
|
|
|
@property
|
|
|
|
|
def has_no_leverage(self) -> bool:
|
|
|
|
|
return (self.leverage == 1.0 and not self.is_short) or self.leverage is None
|
|
|
|
|
|
|
|
|
|
# TODO: @samgermain: Amount should be persisted "as is".
|
|
|
|
|
# I've partially reverted this (this killed most of your tests)
|
|
|
|
|
# but leave this here as i'm not sure where you intended to use this.
|
|
|
|
|
# @property
|
|
|
|
|
# def amount(self) -> float:
|
|
|
|
|
# if self._leverage is not None:
|
|
|
|
|
# return self._amount * self.leverage
|
|
|
|
|
# else:
|
|
|
|
|
# return self._amount
|
|
|
|
|
|
|
|
|
|
# @amount.setter
|
|
|
|
|
# def amount(self, value):
|
|
|
|
|
# self._amount = value
|
|
|
|
|
|
|
|
|
|
# @property
|
|
|
|
|
# def borrowed(self) -> float:
|
|
|
|
|
# if self._leverage is not None:
|
|
|
|
|
# if self.is_short:
|
|
|
|
|
# # If shorting the full amount must be borrowed
|
|
|
|
|
# return self._amount * self._leverage
|
|
|
|
|
# else:
|
|
|
|
|
# # If not shorting, then the trader already owns a bit
|
|
|
|
|
# return self._amount * (self._leverage-1)
|
|
|
|
|
# else:
|
|
|
|
|
# return self._borrowed
|
|
|
|
|
|
|
|
|
|
# @borrowed.setter
|
|
|
|
|
# def borrowed(self, value):
|
|
|
|
|
# self._borrowed = value
|
|
|
|
|
# self._leverage = None
|
|
|
|
|
|
|
|
|
|
# @property
|
|
|
|
|
# def leverage(self) -> float:
|
|
|
|
|
# return self._leverage
|
|
|
|
|
|
|
|
|
|
# @leverage.setter
|
|
|
|
|
# def leverage(self, value):
|
|
|
|
|
# self._leverage = value
|
|
|
|
|
# self._borrowed = None
|
|
|
|
|
|
|
|
|
|
# End of margin trading properties
|
|
|
|
|
@property
|
|
|
|
|
def borrowed(self) -> float:
|
|
|
|
|
if self.has_no_leverage:
|
|
|
|
|
return 0.0
|
|
|
|
|
elif not self.is_short:
|
|
|
|
|
return self.stake_amount * (self.leverage-1)
|
|
|
|
|
else:
|
|
|
|
|
return self.amount
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def open_date_utc(self):
|
|
|
|
@@ -326,13 +291,8 @@ class LocalTrade():
|
|
|
|
|
return self.close_date.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
if kwargs.get('leverage') and kwargs.get('borrowed'):
|
|
|
|
|
# TODO-mg: should I raise an error?
|
|
|
|
|
raise OperationalException('Cannot pass both borrowed and leverage to Trade')
|
|
|
|
|
for key in kwargs:
|
|
|
|
|
setattr(self, key, kwargs[key])
|
|
|
|
|
if not self.is_short:
|
|
|
|
|
self.is_short = False
|
|
|
|
|
self.recalc_open_trade_value()
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
@@ -404,9 +364,6 @@ class LocalTrade():
|
|
|
|
|
'max_rate': self.max_rate,
|
|
|
|
|
|
|
|
|
|
'leverage': self.leverage,
|
|
|
|
|
'borrowed': self.borrowed,
|
|
|
|
|
'borrowed_currency': self.borrowed_currency,
|
|
|
|
|
'collateral_currency': self.collateral_currency,
|
|
|
|
|
'interest_rate': self.interest_rate,
|
|
|
|
|
'liquidation_price': self.liquidation_price,
|
|
|
|
|
'is_short': self.is_short,
|
|
|
|
@@ -473,7 +430,7 @@ class LocalTrade():
|
|
|
|
|
|
|
|
|
|
# evaluate if the stop loss needs to be updated
|
|
|
|
|
else:
|
|
|
|
|
# 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
|
|
|
|
|
# stop losses only walk up, never down!, #But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss
|
|
|
|
|
if (new_loss > self.stop_loss and not self.is_short) or (new_loss < self.stop_loss and self.is_short):
|
|
|
|
|
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
|
|
|
|
self._set_new_stoploss(new_loss, stoploss)
|
|
|
|
@@ -510,13 +467,8 @@ class LocalTrade():
|
|
|
|
|
"""
|
|
|
|
|
order_type = order['type']
|
|
|
|
|
|
|
|
|
|
if ('leverage' in order and 'borrowed' in order):
|
|
|
|
|
raise OperationalException(
|
|
|
|
|
'Pass only one of Leverage or Borrowed to the order in update trade')
|
|
|
|
|
|
|
|
|
|
if 'is_short' in order and order['side'] == 'sell':
|
|
|
|
|
# Only set's is_short on opening trades, ignores non-shorts
|
|
|
|
|
# TODO-mg: I don't like this, but it might be the only way
|
|
|
|
|
self.is_short = order['is_short']
|
|
|
|
|
|
|
|
|
|
# Ignore open and cancelled orders
|
|
|
|
@@ -527,15 +479,10 @@ class LocalTrade():
|
|
|
|
|
|
|
|
|
|
if order_type in ('market', 'limit') and self.is_opening_trade(order['side']):
|
|
|
|
|
# Update open rate and actual amount
|
|
|
|
|
|
|
|
|
|
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
|
|
|
|
|
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
|
|
|
|
|
|
|
|
|
|
if 'borrowed' in order:
|
|
|
|
|
self.borrowed = order['borrowed']
|
|
|
|
|
elif 'leverage' in order:
|
|
|
|
|
if 'leverage' in order:
|
|
|
|
|
self.leverage = order['leverage']
|
|
|
|
|
|
|
|
|
|
self.recalc_open_trade_value()
|
|
|
|
|
if self.is_open:
|
|
|
|
|
payment = "SELL" if self.is_short else "BUY"
|
|
|
|
@@ -544,7 +491,8 @@ class LocalTrade():
|
|
|
|
|
elif order_type in ('market', 'limit') and self.is_closing_trade(order['side']):
|
|
|
|
|
if self.is_open:
|
|
|
|
|
payment = "BUY" if self.is_short else "SELL"
|
|
|
|
|
# TODO: On Shorts technically your buying a little bit more than the amount because it's the ammount plus the interest
|
|
|
|
|
# TODO-mg: On Shorts technically your buying a little bit more than the amount because it's the ammount plus the interest
|
|
|
|
|
# But this wll only print the original
|
|
|
|
|
logger.info(f'{order_type.upper()}_{payment} 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'):
|
|
|
|
@@ -632,17 +580,16 @@ class LocalTrade():
|
|
|
|
|
: param interest_rate: interest_charge for borrowing this coin(optional).
|
|
|
|
|
If interest_rate is not set self.interest_rate will be used
|
|
|
|
|
"""
|
|
|
|
|
# TODO-mg: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set
|
|
|
|
|
|
|
|
|
|
zero = Decimal(0.0)
|
|
|
|
|
if not (self.borrowed):
|
|
|
|
|
# If nothing was borrowed
|
|
|
|
|
if (self.leverage == 1.0 and not self.is_short) or not self.leverage:
|
|
|
|
|
return zero
|
|
|
|
|
|
|
|
|
|
open_date = self.open_date.replace(tzinfo=None)
|
|
|
|
|
now = datetime.utcnow()
|
|
|
|
|
# sec_per_day = Decimal(86400)
|
|
|
|
|
now = (self.close_date or datetime.utcnow()).replace(tzinfo=None)
|
|
|
|
|
sec_per_hour = Decimal(3600)
|
|
|
|
|
total_seconds = Decimal((now - open_date).total_seconds())
|
|
|
|
|
# days = total_seconds/sec_per_day or zero
|
|
|
|
|
hours = total_seconds/sec_per_hour or zero
|
|
|
|
|
|
|
|
|
|
rate = Decimal(interest_rate or self.interest_rate)
|
|
|
|
@@ -654,7 +601,7 @@ class LocalTrade():
|
|
|
|
|
if self.exchange == 'binance':
|
|
|
|
|
# Rate is per day but accrued hourly or something
|
|
|
|
|
# binance: https://www.binance.com/en-AU/support/faq/360030157812
|
|
|
|
|
return borrowed * rate * max(hours, one)/twenty_four # TODO-mg: Is hours rounded?
|
|
|
|
|
return borrowed * rate * max(hours, one)/twenty_four
|
|
|
|
|
elif self.exchange == 'kraken':
|
|
|
|
|
# https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-
|
|
|
|
|
opening_fee = borrowed * rate
|
|
|
|
@@ -746,16 +693,15 @@ class LocalTrade():
|
|
|
|
|
if (self.is_short and close_trade_value == 0.0) or (not self.is_short and self.open_trade_value == 0.0):
|
|
|
|
|
return 0.0
|
|
|
|
|
else:
|
|
|
|
|
if self.borrowed: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else
|
|
|
|
|
if self.has_no_leverage:
|
|
|
|
|
# TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else
|
|
|
|
|
profit_ratio = (close_trade_value/self.open_trade_value) - 1
|
|
|
|
|
else:
|
|
|
|
|
if self.is_short:
|
|
|
|
|
profit_ratio = ((self.open_trade_value - close_trade_value) / self.stake_amount)
|
|
|
|
|
else:
|
|
|
|
|
profit_ratio = ((close_trade_value - self.open_trade_value) / self.stake_amount)
|
|
|
|
|
else: # TODO: This is only needed so that previous tests that included dummy stake_amounts don't fail. Undate those tests and get rid of this else
|
|
|
|
|
if self.is_short:
|
|
|
|
|
profit_ratio = 1 - (close_trade_value/self.open_trade_value)
|
|
|
|
|
else:
|
|
|
|
|
profit_ratio = (close_trade_value/self.open_trade_value) - 1
|
|
|
|
|
|
|
|
|
|
return float(f"{profit_ratio:.8f}")
|
|
|
|
|
|
|
|
|
|
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
|
|
|
@@ -907,14 +853,10 @@ class Trade(_DECL_BASE, LocalTrade):
|
|
|
|
|
timeframe = Column(Integer, nullable=True)
|
|
|
|
|
|
|
|
|
|
# Margin trading properties
|
|
|
|
|
leverage = Column(Float, nullable=True) # TODO: can this be nullable, or should it default to 1? (must also be changed in migrations eventually)
|
|
|
|
|
borrowed = Column(Float, nullable=False, default=0.0)
|
|
|
|
|
leverage = Column(Float, nullable=True)
|
|
|
|
|
interest_rate = Column(Float, nullable=False, default=0.0)
|
|
|
|
|
liquidation_price = Column(Float, nullable=True)
|
|
|
|
|
is_short = Column(Boolean, nullable=False, default=False)
|
|
|
|
|
# TODO: Bottom 2 might not be needed
|
|
|
|
|
borrowed_currency = Column(Float, nullable=True)
|
|
|
|
|
collateral_currency = Column(String(25), nullable=True)
|
|
|
|
|
# End of margin trading properties
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|