diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 91581e557..041b885db 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -478,9 +478,17 @@ class FreqtradeBot(LoggingMixin): if stake_amount is not None and stake_amount < 0.0: # We should decrease our position - # TODO: Selling part of the trade not implemented yet. - logger.error(f"Unable to decrease trade position / sell partially" - f" for pair {trade.pair}, feature not implemented.") + # TODO : debug + open_sell_order = trade.select_order('sell', True) + if open_sell_order: + msg = { + 'type': RPCMessageType.WARNING, + 'status': 'bug open_order_id is None' + } + self.rpc.send_msg(msg) + return + self.execute_trade_exit(trade, current_rate, sell_reason=SellCheckTuple( + sell_type=SellType.CUSTOM_SELL), sub_trade_amt=-stake_amount) def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ @@ -619,7 +627,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_enter(trade, order, order_type) + self._notify_enter(trade, order, order_type, sub_trade=pos_adjust) if pos_adjust: if order_status == 'closed': @@ -676,11 +684,12 @@ class FreqtradeBot(LoggingMixin): return enter_limit_requested, stake_amount def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, - fill: bool = False) -> None: + fill: bool = False, sub_trade: bool = False) -> None: """ Sends rpc notification when a buy occurred. """ open_rate = safe_value_fallback(order, 'average', 'price') + if open_rate is None: open_rate = trade.open_rate @@ -692,7 +701,7 @@ class FreqtradeBot(LoggingMixin): 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), + 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'limit': open_rate, # Deprecated (?) 'open_rate': open_rate, @@ -700,15 +709,17 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, + 'amount': order.get('filled') if fill else order.get('amount'), 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': current_rate, + 'sub_trade': sub_trade, } # Send the message self.rpc.send_msg(msg) - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str, + sub_trade: bool = False) -> None: """ Sends rpc notification when a buy cancel occurred. """ @@ -729,6 +740,7 @@ class FreqtradeBot(LoggingMixin): 'open_date': trade.open_date, 'current_rate': current_rate, 'reason': reason, + 'sub_trade': sub_trade, } # Send the message @@ -1155,6 +1167,7 @@ class FreqtradeBot(LoggingMixin): *, exit_tag: Optional[str] = None, ordertype: Optional[str] = None, + sub_trade_amt: float = None, ) -> bool: """ Executes a trade exit for the given trade and limit @@ -1192,7 +1205,7 @@ class FreqtradeBot(LoggingMixin): # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - amount = self._safe_exit_amount(trade.pair, trade.amount) + amount = sub_trade_amt or self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['sell'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( @@ -1227,7 +1240,7 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_exit(trade, order_type) + self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order) # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): self.update_trade_state(trade, trade.open_order_id, order) @@ -1235,16 +1248,33 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: + def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, + sub_trade: bool = False, order: Dict = None) -> None: """ Sends rpc notification when a sell occurred. """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( trade.pair, refresh=False, side="sell") if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) + if sub_trade: + assert order is not None + amount = safe_value_fallback(order, 'filled', 'amount') + profit_rate = safe_value_fallback(order, 'average', 'price') + + if not fill: + order_obj = trade.select_order_by_order_id(order['id']) + assert order_obj is not None + trade.process_sell_sub_trade(order_obj, is_closed=False) + + profit_ratio = trade.close_profit + profit = trade.close_profit_abs + open_rate = trade.get_open_rate(profit, profit_rate, amount) + else: + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit = trade.calc_profit(rate=profit_rate) + profit_ratio = trade.calc_profit_ratio(profit_rate) + amount = trade.amount + open_rate = trade.open_rate gain = "profit" if profit_ratio > 0 else "loss" msg = { @@ -1256,29 +1286,27 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': profit_rate, 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, + 'amount': amount, + 'open_rate': open_rate, + 'close_rate': profit_rate, 'current_rate': current_rate, - 'profit_amount': profit_trade, + 'profit_amount': profit, 'profit_ratio': profit_ratio, 'buy_tag': trade.buy_tag, 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), + 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), + 'sub_trade': sub_trade, } - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - # Send the message self.rpc.send_msg(msg) - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str, + sub_trade: bool = False) -> None: """ Sends rpc notification when a sell cancel occurred. """ @@ -1313,6 +1341,8 @@ class FreqtradeBot(LoggingMixin): 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, + 'sub_trade': sub_trade, + 'stake_amount': trade.stake_amount, } if 'fiat_display_currency' in self.config: @@ -1378,12 +1408,14 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if not trade.is_open: - if send_msg and not stoploss_order and not trade.open_order_id: - self._notify_exit(trade, '', True) self.handle_protections(trade.pair) + sub_trade = order.get('filled') != trade.amount + if order.get('side', None) == 'sell': + if send_msg and not stoploss_order and not trade.open_order_id: + self._notify_exit(trade, '', True, sub_trade=sub_trade, order=order) elif send_msg and not trade.open_order_id: # Buy fill - self._notify_enter(trade, order, fill=True) + self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) return False @@ -1460,6 +1492,8 @@ class FreqtradeBot(LoggingMixin): return order_amount return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', [])) + rpc_msg:Dict[Any, Any] = {} + def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float, trades: List) -> float: """ @@ -1472,6 +1506,13 @@ class FreqtradeBot(LoggingMixin): if len(trades) == 0: logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) + msg = { + 'type': RPCMessageType.WARNING, + 'status': f"fees bug for {trade.id}" + } + if not self.rpc_msg.get(trade.id): + self.rpc.send_msg(msg) + self.rpc_msg[trade.id] = 1 return order_amount fee_currency = None amount = 0 diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 559c7e94a..ee80a98b1 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -119,6 +119,7 @@ class Order(_DECL_BASE): ft_order_side: str = Column(String(25), nullable=False) ft_pair: str = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) + is_fully_realized = Column(Boolean, nullable=True, default=False) order_id = Column(String(255), nullable=False, index=True) status = Column(String(255), nullable=True) @@ -493,12 +494,18 @@ class LocalTrade(): self.amount = order.safe_amount_after_fee if self.is_open: logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.') - self.open_order_id = None + # condition to avoid reset value when updating fees + if self.open_order_id == order.order_id: + self.open_order_id = None self.recalc_trade_from_orders() elif order.ft_order_side == 'sell': if self.is_open: logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.') - self.close(order.safe_price) + # condition to avoid reset value when updating fees + if self.open_order_id == order.order_id: + self.open_order_id = None + self.process_sell_sub_trade(order) + return elif order.ft_order_side == 'stoploss': self.stoploss_order_id = None self.close_rate_requested = self.stop_loss @@ -510,6 +517,68 @@ class LocalTrade(): raise ValueError(f'Unknown order type: {order.order_type}') Trade.commit() + def process_sell_sub_trade(self, order: Order, is_closed: bool = True) -> None: + orders = (self.select_filled_orders('buy')) + + if len(orders) < 1: + # Todo /test_freqtradebot.py::test_execute_trade_exit_market_order + self.close(order.safe_price) + Trade.commit() + logger.info("*:"*500) + return + + sell_amount = order.filled if is_closed else order.amount + sell_rate = order.safe_price + sell_stake_amount = sell_rate * sell_amount * (1 - self.fee_close) + if is_closed: + if sell_amount == self.amount: + self.close(sell_rate) + Trade.commit() + return + profit = 0.0 + idx = -1 + while sell_amount: + b_order = orders[idx] + buy_amount = b_order.filled or b_order.amount + buy_rate = b_order.average or b_order.price + if sell_amount < buy_amount: + amount = sell_amount + if is_closed: + b_order.filled -= amount + else: + if is_closed: + b_order.is_fully_realized = True + b_order.order_update_date = datetime.now(timezone.utc) + self.update_order(b_order) + idx -= 1 + amount = buy_amount + sell_amount -= amount + profit += self.calc_profit2(buy_rate, sell_rate, amount) + if is_closed: + b_order2 = orders[idx] + amount2 = b_order2.filled or b_order2.amount + b_order2.average = (b_order2.average * amount2 - profit) / amount2 + b_order2.order_update_date = datetime.now(timezone.utc) + self.update_order(b_order2) + Order.query.session.commit() + self.recalc_trade_from_orders() + + self.close_profit_abs = profit + self.close_profit = sell_stake_amount / (sell_stake_amount - profit) - 1 + Trade.commit() + + def calc_profit2(self, open_rate: float, close_rate: float, + amount: float) -> float: + return float(Decimal(amount) * + (Decimal(1 - self.fee_close) * Decimal(close_rate) - + Decimal(1 + self.fee_open) * Decimal(open_rate))) + + def get_open_rate(self, profit: float, close_rate: float, + amount: float) -> float: + return float((Decimal(amount) * + (Decimal(1 - self.fee_close) * Decimal(close_rate)) - + Decimal(profit))/(Decimal(amount) * Decimal(1 + self.fee_open))) + def close(self, rate: float, *, show_msg: bool = True) -> None: """ Sets close_rate to the given rate, calculates total profit @@ -636,24 +705,17 @@ class LocalTrade(): return float(f"{profit_ratio:.8f}") def recalc_trade_from_orders(self): - # We need at least 2 entry orders for averaging amounts and rates. - if len(self.select_filled_orders('buy')) < 2: - # Just in case, still recalc open trade value - self.recalc_open_trade_value() - return - total_amount = 0.0 total_stake = 0.0 for o in self.orders: if (o.ft_is_open or (o.ft_order_side != 'buy') or + o.is_fully_realized or (o.status not in NON_OPEN_EXCHANGE_STATES)): continue tmp_amount = o.safe_amount_after_fee - tmp_price = o.average or o.price - if o.filled is not None: - tmp_amount = o.filled + tmp_price = o.safe_price if tmp_amount > 0.0 and tmp_price is not None: total_amount += tmp_amount total_stake += tmp_price * tmp_amount @@ -685,9 +747,9 @@ class LocalTrade(): :param is_open: Only search for open orders? :return: latest Order object if it exists, else None """ - orders = self.orders + orders = [o for o in self.orders if not o.is_fully_realized] if order_side: - orders = [o for o in self.orders if o.ft_order_side == order_side] + orders = [o for o in orders if o.ft_order_side == order_side] if is_open is not None: orders = [o for o in orders if o.ft_is_open == is_open] if len(orders) > 0: @@ -704,6 +766,7 @@ class LocalTrade(): return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None)) and o.ft_is_open is False and (o.filled or 0) > 0 and + not o.is_fully_realized and o.status in NON_OPEN_EXCHANGE_STATES] @property diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 69f7f2858..e8babea25 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -235,17 +235,31 @@ class Telegram(RPCHandler): if msg['type'] == RPCMessageType.BUY_FILL: message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" + total = msg['amount'] * msg['open_rate'] elif msg['type'] == RPCMessageType.BUY: message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\ f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - - message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}" + total = msg['amount'] * msg['limit'] + if self._rpc._fiat_converter: + total_fiat = self._rpc._fiat_converter.convert_amount( + total, msg['stake_currency'], msg['fiat_currency']) + else: + total_fiat = 0 + message += f"*Total:* `({round_coin_value(total, msg['stake_currency'])}" if msg.get('fiat_currency', None): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + message += f", {round_coin_value(total_fiat, msg['fiat_currency'])}" message += ")`" + if msg.get('sub_trade'): + bal = round_coin_value(msg['stake_amount'], msg['stake_currency']) + message += f"\n*Balance:* `({bal}" + + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + + message += ")`" return message def _format_sell_msg(self, msg: Dict[str, Any]) -> str: @@ -277,7 +291,6 @@ class Telegram(RPCHandler): f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"*Buy Tag:* `{msg['buy_tag']}`\n" f"*Sell Reason:* `{msg['sell_reason']}`\n" - f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" f"*Amount:* `{msg['amount']:.8f}`\n" f"*Open Rate:* `{msg['open_rate']:.8f}`\n") @@ -287,7 +300,21 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.SELL_FILL: message += f"*Close Rate:* `{msg['close_rate']:.8f}`" + if msg.get('sub_trade'): + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + bal = round_coin_value(msg['stake_amount'], msg['stake_currency']) + message += f"\n*Balance:* `({bal}" + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + + message += ")`" + else: + message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`" return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: @@ -380,8 +407,8 @@ class Telegram(RPCHandler): for x, order in enumerate(filled_orders): cur_entry_datetime = arrow.get(order["order_filled_date"]) - cur_entry_amount = order["amount"] - cur_entry_average = order["safe_price"] + cur_entry_amount = order["filled"] or order["amount"] + cur_entry_average = order["safeprice"] lines.append(" ") if x == 0: lines.append(f"*Entry #{x+1}:*") @@ -392,8 +419,9 @@ class Telegram(RPCHandler): sumA = 0 sumB = 0 for y in range(x): - sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"]) - sumB += filled_orders[y]["amount"] + amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] + sumA += amount * filled_orders[y]["safe_price"] + sumB += amount prev_avg_price = sumA / sumB price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) minus_on_entry = 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ccf61f91b..4ef4bc61b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1114,6 +1114,8 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False } == last_msg @@ -1169,6 +1171,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False } == msg @@ -1891,11 +1895,11 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `1:00:00 (60.0 min)`' ) msg_mock.reset_mock() @@ -1923,11 +1927,11 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount @@ -1994,10 +1998,10 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: '*Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) @@ -2093,11 +2097,11 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `2:35:03 (155.1 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `2:35:03 (155.1 min)`' ) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7e56a96e6..c8f74ec83 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -753,7 +753,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, # In case of closed order limit_buy_order_usdt['status'] = 'closed' limit_buy_order_usdt['price'] = 10 - limit_buy_order_usdt['cost'] = 100 + limit_buy_order_usdt['cost'] = 300 limit_buy_order_usdt['id'] = '444' mocker.patch('freqtrade.exchange.Exchange.create_order', @@ -763,7 +763,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, assert trade assert trade.open_order_id is None assert trade.open_rate == 10 - assert trade.stake_amount == 100 + assert trade.stake_amount == 300 # In case of rejected or expired order and partially filled limit_buy_order_usdt['status'] = 'expired' @@ -771,7 +771,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_buy_order_usdt['filled'] = 20.0 limit_buy_order_usdt['remaining'] = 10.00 limit_buy_order_usdt['price'] = 0.5 - limit_buy_order_usdt['cost'] = 15.0 + limit_buy_order_usdt['cost'] = 10.0 limit_buy_order_usdt['id'] = '555' mocker.patch('freqtrade.exchange.Exchange.create_order', MagicMock(return_value=limit_buy_order_usdt)) @@ -780,7 +780,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, assert trade assert trade.open_order_id == '555' assert trade.open_rate == 0.5 - assert trade.stake_amount == 15.0 + assert trade.stake_amount == 10.0 # Test with custom stake limit_buy_order_usdt['status'] = 'open' @@ -2666,6 +2666,8 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'stake_amount': 60.0, } == last_msg @@ -2720,7 +2722,9 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, - } == last_msg + 'sub_trade': False, + 'stake_amount': 60.0, + } == last_msg def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fee, @@ -2788,6 +2792,8 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'stake_amount': 60.0, } == last_msg @@ -2848,6 +2854,8 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'stake_amount': 60.0, } == last_msg @@ -3066,6 +3074,8 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'stake_amount': 60.0, } == last_msg @@ -4429,7 +4439,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'status': None, 'price': 9, 'amount': 12, - 'cost': 100, + 'cost': 108, 'ft_is_open': True, 'id': '651', 'order_id': '651' @@ -4576,6 +4586,173 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Make sure the closed order is found as the second order. order = trade.select_order('buy', False) assert order.order_id == '652' + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': 8, + 'average': 8, + 'amount': 15, + 'filled': 15, + 'cost': 120, + 'ft_is_open': False, + 'id': '653', + 'order_id': '653' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=8, + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS), + sub_trade_amt=15) + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.is_open + print(trade.is_open) + assert trade.amount == 22 + assert trade.stake_amount == 203.5625 + assert pytest.approx(trade.open_rate) == 9.252840909090908 + + orders = Order.query.all() + assert orders + assert len(orders) == 4 + + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '653' + + +def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: + return + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_wallet(mocker, free=10000) + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 200.0, + "dry_run_wallet": 1000.0, + }) + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + bid = 11 + amount = 100 + buy_rate_mock = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + # Initial buy + closed_successful_buy_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': bid, + 'average': bid, + 'cost': bid * amount, + 'amount': amount, + 'filled': amount, + 'ft_is_open': False, + 'id': '600', + 'order_id': '600' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_successful_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_successful_buy_order)) + assert freqtrade.execute_entry(pair, amount) + # Should create an closed trade with an no open order id + # Order is filled and trade is open + orders = Order.query.all() + assert orders + assert len(orders) == 1 + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 1100 + + # Assume it does nothing since order is closed and trade is open + freqtrade.update_closed_trades_without_assigned_fees() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 1100 + assert not trade.fee_updated('buy') + + freqtrade.check_handle_timedout() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 1100 + assert not trade.fee_updated('buy') + + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': 8, + 'average': 8, + 'amount': 50, + 'filled': 50, + 'cost': 120, + 'ft_is_open': False, + 'id': '601', + 'order_id': '601' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=8, + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS), + sub_trade_amt=50) + trades: List[Trade] = trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.amount == 100 + assert trade.stake_amount == 1100 + assert pytest.approx(trade.open_rate) == 11.0 + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '601' def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0f00bd4bb..34ef8e7ec 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -111,7 +111,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_profit is None assert trade.close_date is None - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_limit_buy_usdt' oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') trade.update_trade(oobj) assert trade.open_order_id is None @@ -123,7 +123,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca caplog) caplog.clear() - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_limit_sell_usdt' oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') trade.update_trade(oobj) assert trade.open_order_id is None @@ -151,8 +151,9 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, exchange='binance', ) - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_market_buy' oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') + print(market_buy_order_usdt) trade.update_trade(oobj) assert trade.open_order_id is None assert trade.open_rate == 2.0 @@ -164,7 +165,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog.clear() trade.is_open = True - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_market_sell' oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') trade.update_trade(oobj) assert trade.open_order_id is None