From 613eecf16a0345b188c5974e3cae8707d0268c2a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 21 Jun 2021 21:26:31 -0600 Subject: [PATCH] Adding templates for leverage/short tests All previous pytests pass --- freqtrade/persistence/migrations.py | 13 +-- freqtrade/persistence/models.py | 53 +++++++----- tests/conftest.py | 121 +++++++++++++++++++++++++--- tests/conftest_trades.py | 4 +- tests/rpc/test_rpc.py | 17 ++++ tests/test_persistence.py | 28 ++++++- 6 files changed, 197 insertions(+), 39 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 298b18775..c4e6368c5 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -47,7 +47,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col min_rate = get_column_def(cols, 'min_rate', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') - + leverage = get_column_def(cols, 'leverage', '0.0') borrowed = get_column_def(cols, 'borrowed', '0.0') borrowed_currency = get_column_def(cols, 'borrowed_currency', 'null') @@ -66,7 +66,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col close_profit_abs = get_column_def( cols, 'close_profit_abs', f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") - close_order_status = get_column_def(cols, 'close_order_status', 'null') + # TODO-mg: update to exit order status + sell_order_status = get_column_def(cols, 'sell_order_status', 'null') amount_requested = get_column_def(cols, 'amount_requested', 'amount') # Schema migration necessary @@ -88,7 +89,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, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, close_order_status, strategy, + max_rate, min_rate, sell_reason, sell_order_status, strategy, timeframe, open_trade_value, close_profit_abs, leverage, borrowed, borrowed_currency, collateral_currency, interest_rate, liquidation_price, is_short ) @@ -111,7 +112,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {close_order_status} close_order_status, + {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {leverage} leverage, {borrowed} borrowed, {borrowed_currency} borrowed_currency, @@ -120,7 +121,9 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col from {table_back_name} """)) -#TODO: Does leverage go in here? +# TODO: Does leverage go in here? + + def migrate_open_orders_to_trades(engine): with engine.begin() as connection: connection.execute(text(""" diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7af71ec89..1dd9fefa3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,7 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) - leverage = Column(Float, nullable=True, default=0.0) + leverage = Column(Float, nullable=True, default=1.0) def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -258,12 +258,12 @@ class LocalTrade(): # Lowest price reached min_rate: float = 0.0 sell_reason: str = '' - close_order_status: str = '' + sell_order_status: str = '' strategy: str = '' timeframe: Optional[int] = None # Margin trading properties - leverage: Optional[float] = 0.0 + leverage: Optional[float] = 1.0 borrowed: float = 0.0 borrowed_currency: str = None collateral_currency: str = None @@ -287,6 +287,8 @@ class LocalTrade(): 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): @@ -348,7 +350,7 @@ class LocalTrade(): 'profit_abs': self.close_profit_abs, 'sell_reason': self.sell_reason, - 'close_order_status': self.close_order_status, + 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, '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, @@ -371,7 +373,7 @@ class LocalTrade(): 'collateral_currency': self.collateral_currency, 'interest_rate': self.interest_rate, 'liquidation_price': self.liquidation_price, - 'leverage': self.leverage, + 'is_short': self.is_short, 'open_order_id': self.open_order_id, } @@ -474,12 +476,12 @@ class LocalTrade(): self.recalc_open_trade_value() if self.is_open: payment = "SELL" if self.is_short else "BUY" - logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') + logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') self.open_order_id = None 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" - logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.') + 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'): self.stoploss_order_id = None @@ -502,7 +504,7 @@ class LocalTrade(): self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() self.is_open = False - self.close_order_status = 'closed' + self.sell_order_status = 'closed' self.open_order_id = None if show_msg: logger.info( @@ -576,8 +578,18 @@ class LocalTrade(): close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = close_trade * Decimal(fee or self.fee_close) - #TODO: Interest rate could be hourly instead of daily - interest = ((Decimal(self.interest_rate) * Decimal(self.borrowed)) * Decimal((datetime.utcnow() - self.open_date).days)) or 0 # Interest/day * num of days + + # TODO: Need to set other conditions because sometimes self.open_date is not defined, but why would it ever not be set + try: + open = self.open_date.replace(tzinfo=None) + now = datetime.now() + + # breakpoint() + interest = ((Decimal(self.interest_rate or 0) * Decimal(self.borrowed or 0)) * + Decimal((now - open).total_seconds())/86400) or 0 # Interest/day * (seconds in trade)/(seconds per day) + except: + interest = 0 + if (self.is_short): return float(close_trade + fees + interest) else: @@ -617,12 +629,17 @@ class LocalTrade(): rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - if self.open_trade_value == 0.0: - return 0.0 if self.is_short: - profit_ratio = (close_trade_value / self.open_trade_value) - 1 + if close_trade_value == 0.0: + return 0.0 + else: + profit_ratio = (self.open_trade_value / close_trade_value) - 1 + else: - profit_ratio = (self.open_trade_value / close_trade_value) - 1 + if self.open_trade_value == 0.0: + return 0.0 + 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]: @@ -672,7 +689,7 @@ class LocalTrade(): sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - return sel_trades + return sel_trades @staticmethod def close_bt_trade(trade): @@ -768,13 +785,13 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String(100), nullable=True) #TODO: Change to close_reason - close_order_status = Column(String(100), nullable=True) + sell_reason = Column(String(100), nullable=True) # TODO: Change to close_reason + sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) # Margin trading properties - leverage = Column(Float, nullable=True, default=0.0) + leverage = Column(Float, nullable=True, default=1.0) borrowed = Column(Float, nullable=False, default=0.0) borrowed_currency = Column(Float, nullable=True) collateral_currency = Column(String(25), nullable=True) diff --git a/tests/conftest.py b/tests/conftest.py index fdd78094c..6fb6b6c0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -221,6 +221,8 @@ def create_mock_trades(fee, use_db: bool = True): trade = mock_trade_6(fee) add_trade(trade) + # TODO-mg: Add margin trades + if use_db: Trade.query.session.flush() @@ -250,6 +252,7 @@ def patch_coingekko(mocker) -> None: @pytest.fixture(scope='function') def init_persistence(default_conf): init_db(default_conf['db_url'], default_conf['dry_run']) + # TODO-mg: margin with leverage and/or borrowed? @pytest.fixture(scope="function") @@ -812,7 +815,7 @@ def shitcoinmarkets(markets): "future": False, "active": True }, - }) + }) return shitmarkets @@ -914,18 +917,17 @@ def limit_sell_order_old(): @pytest.fixture def limit_buy_order_old_partial(): - return { - 'id': 'mocked_limit_buy_old_partial', - 'type': 'limit', - 'side': 'buy', - 'symbol': 'ETH/BTC', - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), - 'price': 0.00001099, - 'amount': 90.99181073, - 'filled': 23.0, - 'remaining': 67.99181073, - 'status': 'open' - } + return {'id': 'mocked_limit_buy_old_partial', + 'type': 'limit', + 'side': 'buy', + 'symbol': 'ETH/BTC', + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'price': 0.00001099, + 'amount': 90.99181073, + 'filled': 23.0, + 'remaining': 67.99181073, + 'status': 'open' + } @pytest.fixture @@ -1769,6 +1771,7 @@ def rpc_balance(): 'used': 0.0 }, } + # TODO-mg: Add shorts and leverage? @pytest.fixture @@ -2084,3 +2087,95 @@ def saved_hyperopt_results(): ].total_seconds() return hyperopt_res + + +# * Margin Tests + +@pytest.fixture +def leveraged_fee(): + return + + +@pytest.fixture +def short_fee(): + return + + +@pytest.fixture +def ticker_short(): + return + + +@pytest.fixture +def ticker_exit_short_up(): + return + + +@pytest.fixture +def ticker_exit_short_down(): + return + + +@pytest.fixture +def leveraged_markets(): + return + + +@pytest.fixture(scope='function') +def limit_short_order_open(): + return + + +@pytest.fixture(scope='function') +def limit_short_order(limit_short_order_open): + return + + +@pytest.fixture(scope='function') +def market_short_order(): + return + + +@pytest.fixture +def market_short_exit_order(): + return + + +@pytest.fixture +def limit_short_order_old(): + return + + +@pytest.fixture +def limit_exit_short_order_old(): + return + + +@pytest.fixture +def limit_short_order_old_partial(): + return + + +@pytest.fixture +def limit_short_order_old_partial_canceled(limit_short_order_old_partial): + return + + +@pytest.fixture(scope='function') +def limit_short_order_canceled_empty(request): + return + + +@pytest.fixture +def limit_exit_short_order_open(): + return + + +@pytest.fixture +def limit_exit_short_order(limit_sell_order_open): + return + + +@pytest.fixture +def short_order_fee(): + return diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index b92b51144..de856a98d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta, timezone from freqtrade.persistence.models import Order, Trade -MOCK_TRADE_COUNT = 6 +MOCK_TRADE_COUNT = 6 # TODO-mg: Increase for short and leverage def mock_order_1(): @@ -303,3 +303,5 @@ def mock_trade_6(fee): o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') trade.orders.append(o) return trade + +# TODO-mg: Mock orders for leveraged and short trades diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index fad24f9e2..50c1a0b31 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -107,6 +107,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + + 'leverage': 1.0, + 'borrowed': 0.0, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': 0.0, + 'liquidation_price': None, + 'is_short': False, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -173,6 +181,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + + 'leverage': 1.0, + 'borrowed': 0.0, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': 0.0, + 'liquidation_price': None, + 'is_short': False, + } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 89d07ca74..e9441136b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -129,6 +129,9 @@ def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", caplog) + # TODO-mg: create a short order + # TODO-mg: create a leveraged long order + @pytest.mark.usefixtures("init_persistence") def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): @@ -167,6 +170,9 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", caplog) + # TODO-mg: market short + # TODO-mg: market leveraged long + @pytest.mark.usefixtures("init_persistence") def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): @@ -659,11 +665,13 @@ def test_migrate_new(mocker, default_conf, fee, caplog): order_date DATETIME, order_filled_date DATETIME, order_update_date DATETIME, + leverage FLOAT, PRIMARY KEY (id), CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), FOREIGN KEY(ft_trade_id) REFERENCES trades (id) ) """)) + # TODO-mg: Had to add field leverage to this table, check that this is correct connection.execute(text(""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, @@ -912,6 +920,14 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', + + 'leverage': None, + 'borrowed': None, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, } # Simulate dry_run entries @@ -977,6 +993,14 @@ def test_to_json(default_conf, fee): 'strategy': None, 'timeframe': None, 'exchange': 'binance', + + 'leverage': None, + 'borrowed': None, + 'borrowed_currency': None, + 'collateral_currency': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, } @@ -1315,11 +1339,11 @@ def test_Trade_object_idem(): 'get_overall_performance', 'get_total_closed_profit', 'total_open_trades_stakes', - 'get_sold_trades_without_assigned_fees', + 'get_closed_trades_without_assigned_fees', 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', - ) + ) # Parent (LocalTrade) should have the same attributes for item in trade: