From 1b202ca22e2133c288f86541079fbbe9a733676b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 5 Jul 2021 21:48:56 -0600 Subject: [PATCH] Moved interest calculation to an enum --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/interestmode.py | 30 +++++++++++++++++++++++ freqtrade/persistence/models.py | 30 ++++++----------------- tests/test_persistence_leverage.py | 38 ++++++++++++++++++++---------- tests/test_persistence_short.py | 38 +++++++++++++++++++++--------- 5 files changed, 90 insertions(+), 47 deletions(-) create mode 100644 freqtrade/enums/interestmode.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index ac5f804c9..179d2d5e9 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 from freqtrade.enums.backteststate import BacktestState +from freqtrade.enums.interestmode import InterestMode from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType diff --git a/freqtrade/enums/interestmode.py b/freqtrade/enums/interestmode.py new file mode 100644 index 000000000..c95f4731f --- /dev/null +++ b/freqtrade/enums/interestmode.py @@ -0,0 +1,30 @@ +from enum import Enum, auto +from decimal import Decimal + +one = Decimal(1.0) +four = Decimal(4.0) +twenty_four = Decimal(24.0) + + +class FunctionProxy: + """Allow to mask a function as an Object.""" + + def __init__(self, function): + self.function = function + + def __call__(self, *args, **kwargs): + return self.function(*args, **kwargs) + + +class InterestMode(Enum): + """Equations to calculate interest""" + + # Interest_rate is per day, minimum time of 1 hour + HOURSPERDAY = FunctionProxy( + lambda borrowed, rate, hours: borrowed * rate * max(hours, one)/twenty_four + ) + + # Interest_rate is per 4 hours, minimum time of 4 hours + HOURSPER4 = FunctionProxy( + lambda borrowed, rate, hours: borrowed * rate * (1 + max(0, (hours-four)/four)) + ) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a22ff6238..54a5676d9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.enums import SellType +from freqtrade.enums import InterestMode, SellType from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -265,9 +265,10 @@ class LocalTrade(): # Margin trading properties interest_rate: float = 0.0 - liquidation_price: float = None + liquidation_price: Optional[float] = None is_short: bool = False leverage: float = 1.0 + interest_mode: Optional[InterestMode] = None @property def has_no_leverage(self) -> bool: @@ -585,6 +586,8 @@ class LocalTrade(): # If nothing was borrowed if self.has_no_leverage: return zero + elif not self.interest_mode: + raise OperationalException(f"Leverage not available on {self.exchange} using freqtrade") open_date = self.open_date.replace(tzinfo=None) now = (self.close_date or datetime.utcnow()).replace(tzinfo=None) @@ -594,28 +597,8 @@ class LocalTrade(): rate = Decimal(interest_rate or self.interest_rate) borrowed = Decimal(self.borrowed) - one = Decimal(1.0) - twenty_four = Decimal(24.0) - four = Decimal(4.0) - 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 - elif self.exchange == 'kraken': - # https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading- - opening_fee = borrowed * rate - roll_over_fee = borrowed * rate * max(0, (hours-four)/four) - return opening_fee + roll_over_fee - elif self.exchange == 'binance_usdm_futures': - # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/twenty_four) * max(hours, one) - elif self.exchange == 'binance_coinm_futures': - # ! TODO-mg: This is incorrect, I didn't look it up - return borrowed * (rate/twenty_four) * max(hours, one) - else: - # TODO-mg: make sure this breaks and can't be squelched - raise OperationalException("Leverage not available on this exchange") + return self.interest_mode.value(borrowed, rate, hours) def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None, @@ -857,6 +840,7 @@ class Trade(_DECL_BASE, LocalTrade): interest_rate = Column(Float, nullable=False, default=0.0) liquidation_price = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) + interest_mode = Column(String(100), nullable=True) # End of margin trading properties def __init__(self, **kwargs): diff --git a/tests/test_persistence_leverage.py b/tests/test_persistence_leverage.py index 98b6735e0..7850a134f 100644 --- a/tests/test_persistence_leverage.py +++ b/tests/test_persistence_leverage.py @@ -8,6 +8,7 @@ import pytest from math import isclose from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import InterestMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @@ -49,7 +50,8 @@ def test_interest_kraken(market_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='kraken', leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) # The trades that last 10 minutes do not need to be rounded because they round up to 4 hours on kraken so we can predict the correct value @@ -69,7 +71,8 @@ def test_interest_kraken(market_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='kraken', leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert float(round(trade.calculate_interest(), 11) @@ -113,9 +116,9 @@ def test_interest_binance(market_leveraged_buy_order, fee): fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) # The trades that last 10 minutes do not always need to be rounded because they round up to 4 hours on kraken so we can predict the correct value assert round(float(trade.calculate_interest()), 22) == round(4.166666666344583e-08, 22) @@ -134,7 +137,8 @@ def test_interest_binance(market_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='binance', leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) assert float(round(trade.calculate_interest(), 14)) == round(4.1666666663445834e-07, 14) @@ -155,6 +159,7 @@ def test_update_open_order(limit_leveraged_buy_order): interest_rate=0.0005, leverage=3.0, exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) assert trade.open_order_id is None assert trade.close_profit is None @@ -195,7 +200,8 @@ def test_calc_open_trade_value(market_leveraged_buy_order, fee): fee_close=fee.return_value, interest_rate=0.0005, exchange='kraken', - leverage=3 + leverage=3, + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'open_trade' trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 @@ -243,7 +249,8 @@ def test_calc_open_close_trade_price(limit_leveraged_buy_order, limit_leveraged_ fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_leveraged_buy_order) @@ -296,7 +303,8 @@ def test_trade_close(fee): open_date=datetime.utcnow() - timedelta(hours=5, minutes=0), exchange='kraken', leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert trade.close_profit is None assert trade.close_date is None @@ -349,9 +357,9 @@ def test_calc_close_trade_price(market_leveraged_buy_order, market_leveraged_sel fee_close=fee.return_value, open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), interest_rate=0.0005, - leverage=3.0, exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'close_trade' trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 @@ -406,7 +414,8 @@ def test_update_limit_order(limit_leveraged_buy_order, limit_leveraged_sell_orde fee_close=fee.return_value, leverage=3.0, interest_rate=0.0005, - exchange='binance' + exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) # assert trade.open_order_id is None assert trade.close_profit is None @@ -474,7 +483,8 @@ def test_update_market_order(market_leveraged_buy_order, market_leveraged_sell_o fee_close=fee.return_value, open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), interest_rate=0.0005, - exchange='kraken' + exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_leveraged_buy_order) @@ -516,7 +526,8 @@ def test_calc_close_trade_price_exception(limit_leveraged_buy_order, fee): fee_close=fee.return_value, exchange='binance', interest_rate=0.0005, - leverage=3.0 + leverage=3.0, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_leveraged_buy_order) @@ -572,7 +583,8 @@ def test_calc_profit(market_leveraged_buy_order, market_leveraged_sell_order, fe fee_close=fee.return_value, exchange='kraken', leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_leveraged_buy_order) # Buy @ 0.00001099 diff --git a/tests/test_persistence_short.py b/tests/test_persistence_short.py index 6c8d9e4f0..1f39f7439 100644 --- a/tests/test_persistence_short.py +++ b/tests/test_persistence_short.py @@ -8,6 +8,7 @@ import pytest from math import isclose from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import InterestMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades_with_leverage, log_has, log_has_re @@ -48,7 +49,8 @@ def test_interest_kraken(market_short_order, fee): exchange='kraken', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert float(round(trade.calculate_interest(), 8)) == round(0.137987716095, 8) @@ -67,7 +69,8 @@ def test_interest_kraken(market_short_order, fee): exchange='kraken', is_short=True, leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert float(round(trade.calculate_interest(), 8)) == round(0.28747440853125, 8) @@ -111,7 +114,8 @@ def test_interest_binance(market_short_order, fee): exchange='binance', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) assert float(round(trade.calculate_interest(), 8)) == 0.00574949 @@ -129,7 +133,8 @@ def test_interest_binance(market_short_order, fee): exchange='binance', is_short=True, leverage=5.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) assert float(round(trade.calculate_interest(), 8)) == 0.04791240 @@ -151,6 +156,7 @@ def test_calc_open_trade_value(market_short_order, fee): is_short=True, leverage=3.0, exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'open_trade' trade.update(market_short_order) # Buy @ 0.00001099 @@ -174,6 +180,7 @@ def test_update_open_order(limit_short_order): interest_rate=0.0005, is_short=True, exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) assert trade.open_order_id is None assert trade.close_profit is None @@ -197,7 +204,8 @@ def test_calc_close_trade_price_exception(limit_short_order, fee): exchange='binance', interest_rate=0.0005, leverage=3.0, - is_short=True + is_short=True, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_short_order) @@ -235,6 +243,7 @@ def test_calc_close_trade_price(market_short_order, market_exit_short_order, fee is_short=True, leverage=3.0, exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'close_trade' trade.update(market_short_order) # Buy @ 0.00001099 @@ -285,7 +294,8 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPERDAY ) trade.open_order_id = 'something' trade.update(limit_short_order) @@ -343,7 +353,8 @@ def test_trade_close(fee): exchange='kraken', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) assert trade.close_profit is None assert trade.close_date is None @@ -406,7 +417,8 @@ def test_update_with_binance(limit_short_order, limit_exit_short_order, fee, cap fee_close=fee.return_value, # borrowed=90.99181073, interest_rate=0.0005, - exchange='binance' + exchange='binance', + interest_mode=InterestMode.HOURSPERDAY ) # assert trade.open_order_id is None assert trade.close_profit is None @@ -482,7 +494,8 @@ def test_update_market_order( open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), leverage=3.0, interest_rate=0.0005, - exchange='kraken' + exchange='kraken', + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_short_order) @@ -569,7 +582,8 @@ def test_calc_profit(market_short_order, market_exit_short_order, fee): exchange='kraken', is_short=True, leverage=3.0, - interest_rate=0.0005 + interest_rate=0.0005, + interest_mode=InterestMode.HOURSPER4 ) trade.open_order_id = 'something' trade.update(market_short_order) # Buy @ 0.00001099 @@ -620,7 +634,8 @@ def test_adjust_stop_loss(fee): exchange='binance', open_rate=1, max_rate=1, - is_short=True + is_short=True, + interest_mode=InterestMode.HOURSPERDAY ) trade.adjust_stop_loss(trade.open_rate, 0.05, True) assert trade.stop_loss == 1.05 @@ -685,6 +700,7 @@ def test_stoploss_reinitialization(default_conf, fee): max_rate=1, is_short=True, leverage=3.0, + interest_mode=InterestMode.HOURSPERDAY ) trade.adjust_stop_loss(trade.open_rate, -0.05, True) assert trade.stop_loss == 1.05