added exception checks to LocalTrade.leverage and LocalTrade.borrowed

This commit is contained in:
Sam Germain 2021-06-27 00:19:58 -06:00
parent 34073135b7
commit f5d7deedf4
4 changed files with 48 additions and 34 deletions

10
docs/leverage.md Normal file
View File

@ -0,0 +1,10 @@
An instance of a `Trade`/`LocalTrade` object is given either a value for `leverage` or a value for `borrowed`, but not both, on instantiation/update with a short/long.
- If given a value for `leverage`, then the `amount` value of the `Trade`/`Local` object is multiplied by the `leverage` value to obtain the new value for `amount`. The borrowed value is also calculated from the `amount` and `leverage` value
- If given a value for `borrowed`, then the `leverage` value is calculated from `borrowed` and `amount`
For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades).
For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased
The interest fee is paid following the closing trade, or simultaneously depending on the exchange

View File

@ -268,9 +268,9 @@ class LocalTrade():
collateral_currency: str = None collateral_currency: str = None
interest_rate: float = 0.0 interest_rate: float = 0.0
liquidation_price: float = None liquidation_price: float = None
is_short: bool = False
__leverage: float = 1.0 # * You probably want to use self.leverage instead | __leverage: float = 1.0 # * You probably want to use self.leverage instead |
__borrowed: float = 0.0 # * You probably want to use self.borrowed instead | __borrowed: float = 0.0 # * You probably want to use self.borrowed instead V
__is_short: bool = False # * You probably want to use self.is_short instead V
@property @property
def leverage(self) -> float: def leverage(self) -> float:
@ -280,24 +280,22 @@ class LocalTrade():
def borrowed(self) -> float: def borrowed(self) -> float:
return self.__borrowed or 0.0 return self.__borrowed or 0.0
@property
def is_short(self) -> bool:
return self.__is_short or False
@is_short.setter
def is_short(self, val: bool):
self.__is_short = val
@leverage.setter @leverage.setter
def leverage(self, lev: float): def leverage(self, lev: float):
if self.is_short is None or self.amount is None:
raise OperationalException(
'LocalTrade.amount and LocalTrade.is_short must be assigned before LocalTrade.leverage')
self.__leverage = lev self.__leverage = lev
self.__borrowed = self.amount * (lev-1) self.__borrowed = self.amount * (lev-1)
self.amount = self.amount * lev self.amount = self.amount * lev
@borrowed.setter @borrowed.setter
def borrowed(self, bor: float): def borrowed(self, bor: float):
self.__leverage = self.amount / (self.amount - self.borrowed) if not self.amount:
raise OperationalException(
'LocalTrade.amount must be assigned before LocalTrade.borrowed')
self.__borrowed = bor self.__borrowed = bor
self.__leverage = self.amount / (self.amount - self.borrowed)
# End of margin trading properties # End of margin trading properties
@property @property
@ -314,6 +312,8 @@ class LocalTrade():
raise OperationalException('Cannot pass both borrowed and leverage to Trade') raise OperationalException('Cannot pass both borrowed and leverage to Trade')
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
if not self.is_short:
self.is_short = False
self.recalc_open_trade_value() self.recalc_open_trade_value()
def __repr__(self): def __repr__(self):
@ -464,16 +464,14 @@ class LocalTrade():
Determines if the trade is an opening (long buy or short sell) trade Determines if the trade is an opening (long buy or short sell) trade
:param side (string): the side (buy/sell) that order happens on :param side (string): the side (buy/sell) that order happens on
""" """
is_short = self.is_short or False return (side == 'buy' and not self.is_short) or (side == 'sell' and self.is_short)
return (side == 'buy' and not is_short) or (side == 'sell' and is_short)
def is_closing_trade(self, side) -> bool: def is_closing_trade(self, side) -> bool:
""" """
Determines if the trade is an closing (long sell or short buy) trade Determines if the trade is an closing (long sell or short buy) trade
:param side (string): the side (buy/sell) that order happens on :param side (string): the side (buy/sell) that order happens on
""" """
is_short = self.is_short or False return (side == 'sell' and not self.is_short) or (side == 'buy' and self.is_short)
return (side == 'sell' and not is_short) or (side == 'buy' and is_short)
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
@ -483,11 +481,13 @@ class LocalTrade():
""" """
order_type = order['type'] order_type = order['type']
# if ('leverage' in order and 'borrowed' in order): if ('leverage' in order and 'borrowed' in order):
# raise OperationalException('Cannot update a trade with both borrowed and leverage') raise OperationalException(
'Pass only one of Leverage or Borrowed to the order in update trade')
# TODO: I don't like this, but it might be the only way
if 'is_short' in order and order['side'] == 'sell': 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'] self.is_short = order['is_short']
# Ignore open and cancelled orders # Ignore open and cancelled orders
@ -499,9 +499,6 @@ class LocalTrade():
if order_type in ('market', 'limit') and self.is_opening_trade(order['side']): if order_type in ('market', 'limit') and self.is_opening_trade(order['side']):
# Update open rate and actual amount # Update open rate and actual amount
# self.is_short = safe_value_fallback(order, 'is_short', default_value=False)
# self.borrowed = safe_value_fallback(order, 'is_short', default_value=False)
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'))
if 'borrowed' in order: if 'borrowed' in order:
@ -654,7 +651,8 @@ class LocalTrade():
if self.is_short: if self.is_short:
amount = Decimal(self.amount) + interest amount = Decimal(self.amount) + interest
else: else:
amount = Decimal(self.amount) - interest # The interest does not need to be purchased on longs because the user already owns that currency in your wallet
amount = Decimal(self.amount)
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close) fees = close_trade * Decimal(fee or self.fee_close)
@ -662,7 +660,7 @@ class LocalTrade():
if (self.is_short): if (self.is_short):
return float(close_trade + fees) return float(close_trade + fees)
else: else:
return float(close_trade - fees) return float(close_trade - fees - interest)
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:

View File

@ -2132,6 +2132,7 @@ def limit_exit_short_order(limit_exit_short_order_open):
order['status'] = 'closed' order['status'] = 'closed'
return order return order
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def market_short_order(): def market_short_order():
return { return {
@ -2162,11 +2163,12 @@ def market_exit_short_order():
'amount': 91.99181073, 'amount': 91.99181073,
'filled': 91.99181073, 'filled': 91.99181073,
'remaining': 0.0, 'remaining': 0.0,
'status': 'closed' 'status': 'closed',
'leverage': 3,
'interest_rate': 0.0005
} }
@pytest.fixture @pytest.fixture
def interest_rate(): def interest_rate():
return MagicMock(return_value=0.0005) return MagicMock(return_value=0.0005)

View File

@ -101,8 +101,14 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int
# caplog # caplog
# ): # ):
# """Test Kraken and leverage arguments as well as update market order # """Test Kraken and leverage arguments as well as update market order
# fee: 0.25%
# interest_rate: 0.05% per 4 hrs
# open_rate: 0.00004173
# close_rate: 0.00004099
# amount: 91.99181073 * leverage(3) = 275.97543219
# borrowed: 183.98362146
# time: 10 minutes(rounds to min of 4hrs)
# interest
# """ # """
# trade = Trade( # trade = Trade(
# id=1, # id=1,
@ -114,27 +120,25 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, int
# fee_open=fee.return_value, # fee_open=fee.return_value,
# fee_close=fee.return_value, # fee_close=fee.return_value,
# open_date=ten_minutes_ago, # open_date=ten_minutes_ago,
# exchange='kraken', # exchange='kraken'
# interest_rate=interest_rate.return_value
# ) # )
# trade.open_order_id = 'something' # trade.open_order_id = 'something'
# trade.update(market_buy_order) # trade.update(market_buy_order)
# assert trade.leverage is 3 # assert trade.leverage is 3
# assert trade.is_short is true # assert trade.is_short is True
# assert trade.leverage is 3
# assert trade.open_order_id is None # assert trade.open_order_id is None
# assert trade.open_rate == 0.00004099 # assert trade.open_rate == 0.00004173
# assert trade.close_profit is None # assert trade.close_profit is None
# assert trade.close_date is None # assert trade.close_date is None
# assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " # assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, "
# r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", # r"pair=ETH/BTC, amount=275.97543219, open_rate=0.00004173, open_since=.*\).",
# caplog) # caplog)
# caplog.clear() # caplog.clear()
# trade.is_open = True # trade.is_open = True
# trade.open_order_id = 'something' # trade.open_order_id = 'something'
# trade.update(market_sell_order) # trade.update(market_sell_order)
# assert trade.open_order_id is None # assert trade.open_order_id is None
# assert trade.close_rate == 0.00004173 # assert trade.close_rate == 0.00004099
# assert trade.close_profit == 0.01297561 # assert trade.close_profit == 0.01297561
# assert trade.close_date is not None # assert trade.close_date is not None
# assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " # assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, "