From c98b9ab7689ef3e786a1d557c83f1a0d07ebb2ce Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 27 Jun 2021 23:46:22 -0600 Subject: [PATCH] margin test_trade_close --- freqtrade/persistence/models.py | 56 +++++++++++--------- tests/conftest.py | 4 +- tests/test_persistence.py | 1 + tests/test_persistence_margin.py | 88 ++++++++++++++++++++++---------- 4 files changed, 95 insertions(+), 54 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index fabd8871d..0253b1c74 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -610,33 +610,39 @@ class LocalTrade(): def calculate_interest(self) -> float: # 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.interest_rate or not (self.borrowed): - return 0.0 + return zero open_date = self.open_date.replace(tzinfo=None) now = datetime.utcnow() - secPerDay = 86400 - days = ((now - open_date).total_seconds())/secPerDay or 0.0 - hours = days * 24 + # sec_per_day = Decimal(86400) + 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 = self.interest_rate - borrowed = self.borrowed + rate = Decimal(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, 1)/24 # TODO-mg: Is hours rounded? + return borrowed * rate * max(hours, one)/twenty_four # TODO-mg: Is hours rounded? 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-4)/4) + 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/24) * max(hours, 1) + 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/24) * max(hours, 1) + 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") @@ -731,7 +737,7 @@ class LocalTrade(): else: return None - @ staticmethod + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: @@ -765,27 +771,27 @@ class LocalTrade(): return sel_trades - @ staticmethod + @staticmethod def close_bt_trade(trade): LocalTrade.trades_open.remove(trade) LocalTrade.trades.append(trade) LocalTrade.total_profit += trade.close_profit_abs - @ staticmethod + @staticmethod def add_bt_trade(trade): if trade.is_open: LocalTrade.trades_open.append(trade) else: LocalTrade.trades.append(trade) - @ staticmethod + @staticmethod def get_open_trades() -> List[Any]: """ Query trades from persistence layer """ return Trade.get_trades_proxy(is_open=True) - @ staticmethod + @staticmethod def stoploss_reinitialization(desired_stoploss): """ Adjust initial Stoploss to desired stoploss for all open trades. @@ -887,11 +893,11 @@ class Trade(_DECL_BASE, LocalTrade): Trade.query.session.delete(self) Trade.commit() - @ staticmethod + @staticmethod def commit(): Trade.query.session.commit() - @ staticmethod + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, ) -> List['LocalTrade']: @@ -921,7 +927,7 @@ class Trade(_DECL_BASE, LocalTrade): close_date=close_date ) - @ staticmethod + @staticmethod def get_trades(trade_filter=None) -> Query: """ Helper function to query Trades using filters. @@ -941,7 +947,7 @@ class Trade(_DECL_BASE, LocalTrade): else: return Trade.query - @ staticmethod + @staticmethod def get_open_order_trades(): """ Returns all open trades @@ -949,7 +955,7 @@ class Trade(_DECL_BASE, LocalTrade): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() - @ staticmethod + @staticmethod def get_open_trades_without_assigned_fees(): """ Returns all open trades which don't have open fees set correctly @@ -960,7 +966,7 @@ class Trade(_DECL_BASE, LocalTrade): Trade.is_open.is_(True), ]).all() - @ staticmethod + @staticmethod def get_closed_trades_without_assigned_fees(): """ Returns all closed trades which don't have fees set correctly @@ -971,7 +977,7 @@ class Trade(_DECL_BASE, LocalTrade): Trade.is_open.is_(False), ]).all() - @ staticmethod + @staticmethod def total_open_trades_stakes() -> float: """ Calculates total invested amount in open trades @@ -985,7 +991,7 @@ class Trade(_DECL_BASE, LocalTrade): t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) return total_open_stake_amount or 0 - @ staticmethod + @staticmethod def get_overall_performance() -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count @@ -1010,7 +1016,7 @@ class Trade(_DECL_BASE, LocalTrade): for pair, profit, profit_abs, count in pair_rates ] - @ staticmethod + @staticmethod def get_best_pair(): """ Get best pair with closed trade. @@ -1048,7 +1054,7 @@ class PairLock(_DECL_BASE): return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' f'lock_end_time={lock_end_time})') - @ staticmethod + @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ Get all currently active locks for this pair diff --git a/tests/conftest.py b/tests/conftest.py index 266790c5e..963291e0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,9 +57,9 @@ def log_has_re(line, logs): def get_args(args): return Arguments(args).get_parsed_arg() + + # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines - - def get_mock_coro(return_value): async def mock_coro(*args, **kwargs): return return_value diff --git a/tests/test_persistence.py b/tests/test_persistence.py index f1829c020..6d48d680f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -196,6 +196,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") def test_trade_close(limit_buy_order, limit_sell_order, fee): + #TODO: limit_buy_order and limit_sell_order aren't used, remove them probably trade = Trade( pair='ETH/BTC', stake_amount=0.001, diff --git a/tests/test_persistence_margin.py b/tests/test_persistence_margin.py index b99d7536e..4d4115a91 100644 --- a/tests/test_persistence_margin.py +++ b/tests/test_persistence_margin.py @@ -211,33 +211,67 @@ def test_calc_open_close_trade_price(limit_short_order, limit_exit_short_order, assert isclose(trade.calc_profit_ratio(), 0.06189996) -# @pytest.mark.usefixtures("init_persistence") -# def test_trade_close(limit_buy_order, limit_sell_order, fee): -# trade = Trade( -# pair='ETH/BTC', -# stake_amount=0.001, -# open_rate=0.01, -# amount=5, -# is_open=True, -# fee_open=fee.return_value, -# fee_close=fee.return_value, -# open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, -# exchange='binance', -# ) -# assert trade.close_profit is None -# assert trade.close_date is None -# assert trade.is_open is True -# trade.close(0.02) -# assert trade.is_open is False -# assert trade.close_profit == 0.99002494 -# assert trade.close_date is not None -# new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, -# assert trade.close_date != new_date -# # Close should NOT update close_date if the trade has been closed already -# assert trade.is_open is False -# trade.close_date = new_date -# trade.close(0.02) -# assert trade.close_date == new_date +@pytest.mark.usefixtures("init_persistence") +def test_trade_close(fee, five_hours_ago): + """ + This trade lasts for five hours, but the one above lasted for 10 minutes + Short trade + Exchange: Kraken + fee: 0.25% base + interest_rate: 0.05% per 4 hours + open_rate: 0.02 base + close_rate: 0.01 base + leverage: 3.0 + amount: 5 * 3 = 15 crypto + borrowed: 15 crypto + time-periods: 5 hours = 5/4 + + interest: borrowed * interest_rate * time-periods + = 15 * 0.0005 * 5/4 = 0.009375 crypto + open_value: (amount * open_rate) - (amount * open_rate * fee) + = (15 * 0.02) - (15 * 0.02 * 0.0025) + = 0.29925 + amount_closed: amount + interest = 15 + 0.009375 = 15.009375 + close_value: (amount_closed * close_rate) + (amount_closed * close_rate * fee) + = (15.009375 * 0.01) + (15.009375 * 0.01 * 0.0025) + = 0.150468984375 + total_profit = open_value - close_value + = 0.29925 - 0.150468984375 + = 0.148781015625 + total_profit_percentage = (open_value/close_value) - 1 + = (0.29925/0.150468984375)-1 + = 0.9887819489377738 + """ + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.02, + amount=5, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=five_hours_ago, + exchange='kraken', + is_short=True, + leverage=3.0, + interest_rate=0.0005 + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.01) + assert trade.is_open is False + assert trade.close_profit == 0.98878195 + assert trade.close_date is not None + + # TODO-mg: Remove these comments probably + #new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + # assert trade.close_date != new_date + # # Close should NOT update close_date if the trade has been closed already + # assert trade.is_open is False + # trade.close_date = new_date + # trade.close(0.02) + # assert trade.close_date == new_date # @pytest.mark.usefixtures("init_persistence")