diff --git a/Trading/Exchange/binance/binance_exchange.py b/Trading/Exchange/binance/binance_exchange.py index a8755c4b9..2f3f1da20 100644 --- a/Trading/Exchange/binance/binance_exchange.py +++ b/Trading/Exchange/binance/binance_exchange.py @@ -116,14 +116,15 @@ def get_adapter_class(self): async def get_account_id(self, **kwargs: dict) -> str: try: - if self.exchange_manager.is_future: - raw_binance_balance = await self.connector.client.fapiPrivateV3GetBalance() - # accountAlias = unique account code - # from https://binance-docs.github.io/apidocs/futures/en/#futures-account-balance-v3-user_data - return raw_binance_balance[0]["accountAlias"] - else: - raw_balance = await self.connector.client.fetch_balance() - return raw_balance[ccxt_constants.CCXT_INFO]["uid"] + with self.connector.error_describer(): + if self.exchange_manager.is_future: + raw_binance_balance = await self.connector.client.fapiPrivateV3GetBalance() + # accountAlias = unique account code + # from https://binance-docs.github.io/apidocs/futures/en/#futures-account-balance-v3-user_data + return raw_binance_balance[0]["accountAlias"] + else: + raw_balance = await self.connector.client.fetch_balance() + return raw_balance[ccxt_constants.CCXT_INFO]["uid"] except (KeyError, IndexError): # should not happen raise diff --git a/Trading/Exchange/bingx/bingx_exchange.py b/Trading/Exchange/bingx/bingx_exchange.py index 851cab327..4addfd536 100644 --- a/Trading/Exchange/bingx/bingx_exchange.py +++ b/Trading/Exchange/bingx/bingx_exchange.py @@ -86,8 +86,9 @@ def get_name(cls) -> str: return 'bingx' async def get_account_id(self, **kwargs: dict) -> str: - resp = await self.connector.client.accountV1PrivateGetUid() - return resp["data"]["uid"] + with self.connector.error_describer(): + resp = await self.connector.client.accountV1PrivateGetUid() + return resp["data"]["uid"] async def get_my_recent_trades(self, symbol=None, since=None, limit=None, **kwargs): # On SPOT Bingx, account recent trades is available under fetch_closed_orders diff --git a/Trading/Exchange/coinbase/coinbase_exchange.py b/Trading/Exchange/coinbase/coinbase_exchange.py index ad04e2518..8d9b701b4 100644 --- a/Trading/Exchange/coinbase/coinbase_exchange.py +++ b/Trading/Exchange/coinbase/coinbase_exchange.py @@ -172,37 +172,39 @@ def get_adapter_class(self): async def get_account_id(self, **kwargs: dict) -> str: try: - # warning might become deprecated - # https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-users - portfolio_id = None - accounts = await self.connector.client.fetch_accounts() - # use portfolio id when possible to enable "coinbase subaccounts" which are called "portfolios" - # note: oldest portfolio portfolio id == user id (from previous v2PrivateGetUser) when using master account - portfolio_ids = set(account[ccxt_constants.CCXT_INFO]['retail_portfolio_id'] for account in accounts) - if len(portfolio_ids) != 1: - is_up_to_date_key = self._is_up_to_date_api_key() - if is_up_to_date_key: - self.logger.error( - f"Unexpected: failed to identify Coinbase portfolio id on up to date API keys: " - f"{portfolio_ids=}" + with self.connector.error_describer(): + # warning might become deprecated + # https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-users + portfolio_id = None + accounts = await self.connector.client.fetch_accounts() + # use portfolio id when possible to enable "coinbase subaccounts" which are called "portfolios" + # note: oldest portfolio portfolio id == user id (from previous v2PrivateGetUser) when + # using master account + portfolio_ids = set(account[ccxt_constants.CCXT_INFO]['retail_portfolio_id'] for account in accounts) + if len(portfolio_ids) != 1: + is_up_to_date_key = self._is_up_to_date_api_key() + if is_up_to_date_key: + self.logger.error( + f"Unexpected: failed to identify Coinbase portfolio id on up to date API keys: " + f"{portfolio_ids=}" + ) + sorted_portfolios = sorted( + [ + account[ccxt_constants.CCXT_INFO] + for account in accounts + ], + key=lambda account: account["created_at"], ) - sorted_portfolios = sorted( - [ - account[ccxt_constants.CCXT_INFO] - for account in accounts - ], - key=lambda account: account["created_at"], - ) - portfolio_id = sorted_portfolios[0]['retail_portfolio_id'] - self.logger.info( - f"{len(portfolio_ids)} portfolio found on Coinbase account. " - f"This can happen with non up-to-date API keys ({is_up_to_date_key=}). " - f"Using the oldest portfolio id to bind to main account: {portfolio_id=}." - ) - else: - portfolio_id = next(iter(portfolio_ids)) - return portfolio_id - except ccxt.BaseError as err: + portfolio_id = sorted_portfolios[0]['retail_portfolio_id'] + self.logger.info( + f"{len(portfolio_ids)} portfolio found on Coinbase account. " + f"This can happen with non up-to-date API keys ({is_up_to_date_key=}). " + f"Using the oldest portfolio id to bind to main account: {portfolio_id=}." + ) + else: + portfolio_id = next(iter(portfolio_ids)) + return portfolio_id + except (ccxt.BaseError, octobot_trading.errors.OctoBotExchangeError) as err: self.logger.exception( err, True, f"Error when fetching {self.get_name()} account id: {err} ({err.__class__.__name__}). " diff --git a/Trading/Exchange/hollaex/hollaex_exchange.py b/Trading/Exchange/hollaex/hollaex_exchange.py index fc603fbdd..b71e926cc 100644 --- a/Trading/Exchange/hollaex/hollaex_exchange.py +++ b/Trading/Exchange/hollaex/hollaex_exchange.py @@ -86,8 +86,9 @@ def is_configurable(cls): return True async def get_account_id(self, **kwargs: dict) -> str: - user_info = await self.connector.client.private_get_user() - return user_info["id"] + with self.connector.error_describer(): + user_info = await self.connector.client.private_get_user() + return user_info["id"] async def get_symbol_prices(self, symbol, time_frame, limit: int = None, **kwargs: dict): # ohlcv without limit is not supported, replaced by a default max limit diff --git a/Trading/Exchange/kucoin/kucoin_exchange.py b/Trading/Exchange/kucoin/kucoin_exchange.py index 604b92ae8..f8970a418 100644 --- a/Trading/Exchange/kucoin/kucoin_exchange.py +++ b/Trading/Exchange/kucoin/kucoin_exchange.py @@ -25,6 +25,7 @@ import octobot_trading.exchanges.connectors.ccxt.ccxt_connector as ccxt_connector import octobot_trading.exchanges.connectors.ccxt.enums as ccxt_enums import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants +import octobot_trading.exchanges.connectors.ccxt.ccxt_client_util as ccxt_client_util import octobot_commons.constants as commons_constants import octobot_trading.constants as constants import octobot_trading.enums as trading_enums @@ -175,56 +176,54 @@ async def get_account_id(self, **kwargs: dict) -> str: # It is currently impossible to fetch subaccounts account id, use a constant value to identify it. # updated: 21/05/2024 try: - account_id = None - subaccount_id = None - sub_accounts = await self.connector.client.private_get_sub_accounts() - accounts = sub_accounts.get("data", {}).get("items", {}) - has_subaccounts = bool(accounts) - if has_subaccounts: - if len(accounts) == 1: - # only 1 account: use its id or name - account = accounts[0] - # try using subUserId if available - # 'ex subUserId: 65d41ea409407d000160cc17 subName: octobot1' - account_id = account.get("subUserId") or account["subName"] - else: - # more than 1 account: consider other accounts - for account in accounts: - if account["subUserId"]: - subaccount_id = account["subName"] - else: - # only subaccounts have a subUserId: if this condition is True, we are on the main account - account_id = account["subName"] - if account_id and self.exchange_manager.is_future: - account_id = octobot.community.to_community_exchange_internal_name( - account_id, commons_constants.CONFIG_EXCHANGE_FUTURE + with self.connector.error_describer(): + account_id = None + subaccount_id = None + sub_accounts = await self.connector.client.private_get_sub_accounts() + accounts = sub_accounts.get("data", {}).get("items", {}) + has_subaccounts = bool(accounts) + if has_subaccounts: + if len(accounts) == 1: + # only 1 account: use its id or name + account = accounts[0] + # try using subUserId if available + # 'ex subUserId: 65d41ea409407d000160cc17 subName: octobot1' + account_id = account.get("subUserId") or account["subName"] + else: + # more than 1 account: consider other accounts + for account in accounts: + if account["subUserId"]: + subaccount_id = account["subName"] + else: + # only subaccounts have a subUserId: if this condition is True, we are on the main account + account_id = account["subName"] + if account_id and self.exchange_manager.is_future: + account_id = octobot.community.to_community_exchange_internal_name( + account_id, commons_constants.CONFIG_EXCHANGE_FUTURE + ) + if subaccount_id: + # there is at least a subaccount: ensure the current account is the main account as there is no way + # to know the id of the current account (only a list of existing accounts) + subaccount_api_key_details = await self.connector.client.private_get_sub_api_key( + {"subName": subaccount_id} ) - if subaccount_id: - # there is at least a subaccount: ensure the current account is the main account as there is no way - # to know the id of the current account (only a list of existing accounts) - subaccount_api_key_details = await self.connector.client.private_get_sub_api_key( - {"subName": subaccount_id} - ) - if "data" not in subaccount_api_key_details or "msg" in subaccount_api_key_details: - # subaccounts can't fetch other accounts data, if this is False, we are on a subaccount + if "data" not in subaccount_api_key_details or "msg" in subaccount_api_key_details: + # subaccounts can't fetch other accounts data, if this is False, we are on a subaccount + self.logger.error( + f"kucoin api changed: it is now possible to call private_get_sub_accounts on subaccounts. " + f"kucoin get_account_id has to be updated. " + f"sub_accounts={sub_accounts} subaccount_api_key_details={subaccount_api_key_details}" + ) + return constants.DEFAULT_ACCOUNT_ID + if has_subaccounts and account_id is None: self.logger.error( - f"kucoin api changed: it is now possible to call private_get_sub_accounts on subaccounts. " - f"kucoin get_account_id has to be updated. " - f"sub_accounts={sub_accounts} subaccount_api_key_details={subaccount_api_key_details}" + f"kucoin api changed: can't fetch master account account_id. " + f"kucoin get_account_id has to be updated." + f"sub_accounts={sub_accounts}" ) - return constants.DEFAULT_ACCOUNT_ID - if has_subaccounts and account_id is None: - self.logger.error( - f"kucoin api changed: can't fetch master account account_id. " - f"kucoin get_account_id has to be updated." - f"sub_accounts={sub_accounts}" - ) - account_id = constants.DEFAULT_ACCOUNT_ID - # we are on the master account - return account_id or constants.DEFAULT_ACCOUNT_ID - except ccxt.AuthenticationError: - # when api key is wrong - raise + account_id = constants.DEFAULT_ACCOUNT_ID + # we are on the master account + return account_id or constants.DEFAULT_ACCOUNT_ID except ccxt.ExchangeError as err: # ExchangeError('kucoin This user is not a master user') if "not a master user" not in str(err): diff --git a/Trading/Exchange/okx/okx_exchange.py b/Trading/Exchange/okx/okx_exchange.py index a209b9efc..5a7e8d6d2 100644 --- a/Trading/Exchange/okx/okx_exchange.py +++ b/Trading/Exchange/okx/okx_exchange.py @@ -198,7 +198,8 @@ def _fix_limit(self, limit: int) -> int: async def get_account_id(self, **kwargs: dict) -> str: accounts = await self.connector.client.fetch_accounts() try: - return accounts[0]["id"] + with self.connector.error_describer(): + return accounts[0]["id"] except IndexError: # should never happen as at least one account should be available return None diff --git a/Trading/Mode/grid_trading_mode/grid_trading.py b/Trading/Mode/grid_trading_mode/grid_trading.py index 8d4bbb2e2..e0d1a5b0f 100644 --- a/Trading/Mode/grid_trading_mode/grid_trading.py +++ b/Trading/Mode/grid_trading_mode/grid_trading.py @@ -440,7 +440,7 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds highest_buy = current_price lowest_sell = current_price origin_created_buy_orders_count, origin_created_sell_orders_count = self._get_origin_orders_count( - sorted_orders, recently_closed_trades + recently_closed_trades, sorted_orders ) min_max_total_order_price_delta = ( @@ -485,6 +485,12 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds self.logger.info( f"{len(missing_orders)} missing {self.symbol} orders on {self.exchange_name}: {missing_orders}" ) + else: + self.logger.info( + f"All {len(sorted_orders)} out of {self.buy_orders_count + self.sell_orders_count} {self.symbol} " + f"target orders are in place on {self.exchange_name}" + ) + await self._handle_missed_mirror_orders_fills(recently_closed_trades, missing_orders, current_price) try: # apply state and (re)create missing orders @@ -515,10 +521,13 @@ def _get_origin_orders_count(self, recent_trades, open_orders): origin_created_buy_orders_count = self.buy_orders_count origin_created_sell_orders_count = self.sell_orders_count if recent_trades: + # in case all initial orders didn't get created, try to infer the original value from open orders and trades buy_orders_count = len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) buy_trades_count = len([trade for trade in recent_trades if trade.side is trading_enums.TradeOrderSide.BUY]) origin_created_buy_orders_count = buy_orders_count + buy_trades_count - origin_created_sell_orders_count = len(open_orders) + len(recent_trades) - origin_created_buy_orders_count + origin_created_sell_orders_count = ( + len(open_orders) + len(recent_trades) - origin_created_buy_orders_count + ) return origin_created_buy_orders_count, origin_created_sell_orders_count def _get_grid_trades_or_orders(self, trades_or_orders): diff --git a/Trading/Mode/grid_trading_mode/tests/test_grid_trading_mode.py b/Trading/Mode/grid_trading_mode/tests/test_grid_trading_mode.py index dd625563c..957be02d5 100644 --- a/Trading/Mode/grid_trading_mode/tests/test_grid_trading_mode.py +++ b/Trading/Mode/grid_trading_mode/tests/test_grid_trading_mode.py @@ -911,6 +911,247 @@ async def test_start_after_offline_x_filled_and_price_back_should_buy_to_recreat _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 200) +async def test_start_after_offline_x_filled_and_missing_should_recreate_1_sell(): + symbol = "BTC/USDT" + async with _get_tools(symbol) as (producer, _, exchange_manager): + # forced config + producer.buy_funds = producer.sell_funds = 0 + producer.allow_order_funds_redispatch = True + producer.buy_orders_count = producer.sell_orders_count = 5 + producer.compensate_for_missed_mirror_order = True + producer.enable_trailing_down = False + producer.enable_trailing_up = True + producer.flat_increment = decimal.Decimal(100) + producer.flat_spread = decimal.Decimal(300) + producer.reinvest_profits = False + producer.sell_volume_per_order = producer.buy_volume_per_order = False + producer.starting_price = 0 + producer.use_existing_orders_only = False + producer.use_fixed_volume_for_mirror_orders = False + + orders_count = producer.buy_orders_count + producer.sell_orders_count + + + initial_price = decimal.Decimal("105278.1") + trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price) + btc_pf = trading_api.get_portfolio_currency(exchange_manager, "BTC") + usdt_pf = trading_api.get_portfolio_currency(exchange_manager, "USDT") + btc_pf.available = decimal.Decimal("0.00141858") + btc_pf.total = decimal.Decimal("0.00141858") + usdt_pf.available = decimal.Decimal("150.505098") + usdt_pf.total = decimal.Decimal("150.505098") + + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count)) + original_orders = copy.copy(trading_api.get_open_orders(exchange_manager)) + assert len(original_orders) == orders_count + assert sorted([ + order.origin_price for order in original_orders + ]) == [ + # buy orders + decimal.Decimal('104728.1'), decimal.Decimal('104828.1'), decimal.Decimal('104928.1'), + decimal.Decimal('105028.1'), decimal.Decimal('105128.1'), + # sell orders + decimal.Decimal('105428.1'), decimal.Decimal('105528.1'), decimal.Decimal('105628.1'), + decimal.Decimal('105728.1'), decimal.Decimal('105828.1') + ] + + # price goes down to 105120, 105128.1 order gets filled + price = decimal.Decimal("105120") + # offline simulation: price goes down to 105120, 105128.1 order gets filled + offline_filled = [order for order in original_orders if order.origin_price == decimal.Decimal('105128.1')] + assert len(offline_filled) == 1 + assert offline_filled[0].side == trading_enums.TradeOrderSide.BUY + for order in offline_filled: + await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer) + assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled) + assert btc_pf.available == decimal.Decimal('0.00028861409') + assert btc_pf.total == decimal.Decimal('0.00170420409') + assert usdt_pf.available == decimal.Decimal('0.247225519') + assert usdt_pf.total == decimal.Decimal('120.447922929') + + # back online: restore orders according to current price + trading_api.force_set_mark_price(exchange_manager, producer.symbol, price) + with _assert_missing_orders_count(producer, len(offline_filled)): + await producer._ensure_staggered_orders() + # restored orders + await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count)) + open_orders = trading_api.get_open_orders(exchange_manager) + # there is now 6 sell orders + assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 6 + # there is now 4 buy orders + assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 4 + # quantity is preserved + assert all( + decimal.Decimal("0.00028") < order.origin_quantity < decimal.Decimal("0.00029") + for order in open_orders + ) + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), initial_price) + + +async def test_start_after_offline_x_filled_and_missing_should_recreate_5_sell_orders_no_recent_trade(): + symbol = "BTC/USDT" + async with _get_tools(symbol) as (producer, _, exchange_manager): + # forced config + producer.buy_funds = producer.sell_funds = 0 + producer.allow_order_funds_redispatch = True + producer.buy_orders_count = producer.sell_orders_count = 5 + producer.compensate_for_missed_mirror_order = True + producer.enable_trailing_down = False + producer.enable_trailing_up = True + producer.flat_increment = decimal.Decimal(100) + producer.flat_spread = decimal.Decimal(300) + producer.reinvest_profits = False + producer.sell_volume_per_order = producer.buy_volume_per_order = False + producer.starting_price = 0 + producer.use_existing_orders_only = False + producer.use_fixed_volume_for_mirror_orders = False + + orders_count = producer.buy_orders_count + producer.sell_orders_count + + initial_price = decimal.Decimal("105278.1") + trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price) + btc_pf = trading_api.get_portfolio_currency(exchange_manager, "BTC") + usdt_pf = trading_api.get_portfolio_currency(exchange_manager, "USDT") + btc_pf.available = decimal.Decimal("0.00141858") + btc_pf.total = decimal.Decimal("0.00141858") + usdt_pf.available = decimal.Decimal("150.505098") + usdt_pf.total = decimal.Decimal("150.505098") + + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count)) + original_orders = copy.copy(trading_api.get_open_orders(exchange_manager)) + assert len(original_orders) == orders_count + assert sorted([ + order.origin_price for order in original_orders + ]) == [ + # buy orders + decimal.Decimal('104728.1'), decimal.Decimal('104828.1'), decimal.Decimal('104928.1'), + decimal.Decimal('105028.1'), decimal.Decimal('105128.1'), + # sell orders + decimal.Decimal('105428.1'), decimal.Decimal('105528.1'), decimal.Decimal('105628.1'), + decimal.Decimal('105728.1'), decimal.Decimal('105828.1') + ] + + # price goes down to 104720, all buy order get filled + price = decimal.Decimal("104720") + offline_filled = [order for order in original_orders if order.origin_price <= decimal.Decimal('105128.1')] + assert len(offline_filled) == 5 + assert all(o.side == trading_enums.TradeOrderSide.BUY for o in offline_filled) + for order in offline_filled: + await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer) + assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled) + assert btc_pf.available == decimal.Decimal("0.00143356799") + assert btc_pf.total == decimal.Decimal("0.00284915799") + assert usdt_pf.available == decimal.Decimal("0.247225519") + assert usdt_pf.total == decimal.Decimal("0.247225519") + + # clear trades + exchange_manager.exchange_personal_data.trades_manager.trades.clear() + + # back online: restore orders according to current price + trading_api.force_set_mark_price(exchange_manager, producer.symbol, price) + with _assert_missing_orders_count(producer, len(offline_filled)): + await producer._ensure_staggered_orders() + # create buy orders equivalent sell orders + await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count)) + open_orders = trading_api.get_open_orders(exchange_manager) + # there is now 10 sell orders + assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 10 + # quantity is preserved + assert all( + decimal.Decimal("0.00028") < order.origin_quantity < decimal.Decimal("0.00029") + for order in open_orders + ) + # there is now 0 buy order + assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 0 + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), initial_price) + + assert btc_pf.available == decimal.Decimal("0.00001571799") + assert btc_pf.total == decimal.Decimal("0.00284915799") + assert usdt_pf.available == decimal.Decimal("0.247225519") + assert usdt_pf.total == decimal.Decimal("0.247225519") + + +async def test_start_after_offline_x_filled_and_missing_should_recreate_5_buy_orders_no_recent_trade(): + symbol = "BTC/USDT" + async with _get_tools(symbol) as (producer, _, exchange_manager): + # forced config + producer.buy_funds = producer.sell_funds = 0 + producer.allow_order_funds_redispatch = True + producer.buy_orders_count = producer.sell_orders_count = 5 + producer.compensate_for_missed_mirror_order = True + producer.enable_trailing_down = False + producer.enable_trailing_up = True + producer.flat_increment = decimal.Decimal(100) + producer.flat_spread = decimal.Decimal(300) + producer.reinvest_profits = False + producer.sell_volume_per_order = producer.buy_volume_per_order = False + producer.starting_price = 0 + producer.use_existing_orders_only = False + producer.use_fixed_volume_for_mirror_orders = False + + orders_count = producer.buy_orders_count + producer.sell_orders_count + + initial_price = decimal.Decimal("105278.1") + trading_api.force_set_mark_price(exchange_manager, producer.symbol, initial_price) + btc_pf = trading_api.get_portfolio_currency(exchange_manager, "BTC") + usdt_pf = trading_api.get_portfolio_currency(exchange_manager, "USDT") + btc_pf.available = decimal.Decimal("0.00141858") + btc_pf.total = decimal.Decimal("0.00141858") + usdt_pf.available = decimal.Decimal("150.505098") + usdt_pf.total = decimal.Decimal("150.505098") + + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count)) + original_orders = copy.copy(trading_api.get_open_orders(exchange_manager)) + assert len(original_orders) == orders_count + assert sorted([ + order.origin_price for order in original_orders + ]) == [ + # buy orders + decimal.Decimal('104728.1'), decimal.Decimal('104828.1'), decimal.Decimal('104928.1'), + decimal.Decimal('105028.1'), decimal.Decimal('105128.1'), + # sell orders + decimal.Decimal('105428.1'), decimal.Decimal('105528.1'), decimal.Decimal('105628.1'), + decimal.Decimal('105728.1'), decimal.Decimal('105828.1') + ] + + # price goes up to 105838, all sell order get filled + price = decimal.Decimal("105838") + offline_filled = [order for order in original_orders if order.origin_price > decimal.Decimal('105128.1')] + assert len(offline_filled) == 5 + assert all(o.side == trading_enums.TradeOrderSide.SELL for o in offline_filled) + for order in offline_filled: + await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer) + assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled) + assert btc_pf.available == decimal.Decimal("0.00000299") + assert btc_pf.total == decimal.Decimal("0.00000299") + assert usdt_pf.available == decimal.Decimal("149.623458838921") + assert usdt_pf.total == decimal.Decimal("299.881331319921") + + # clear trades + exchange_manager.exchange_personal_data.trades_manager.trades.clear() + + # back online: restore orders according to current price + trading_api.force_set_mark_price(exchange_manager, producer.symbol, price) + with _assert_missing_orders_count(producer, len(offline_filled)): + await producer._ensure_staggered_orders() + # create buy orders equivalent sell orders + await asyncio.create_task(_check_open_orders_count(exchange_manager, orders_count)) + open_orders = trading_api.get_open_orders(exchange_manager) + # there is now 0 sell order + assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.SELL]) == 0 + # there is now 10 buy orders + assert len([order for order in open_orders if order.side is trading_enums.TradeOrderSide.BUY]) == 10 + # quantity is preserved + assert all( + decimal.Decimal("0.00028") < order.origin_quantity < decimal.Decimal("0.00029") + for order in open_orders + ) + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), initial_price) + + async def test_start_after_offline_1_filled_should_create_buy(): symbol = "BTC/USDT" async with _get_tools(symbol) as (producer, _, exchange_manager): diff --git a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py index b39aa6439..503b33c3b 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -1502,7 +1502,7 @@ def _fill_missing_orders( # missing order between similar orders quantity = self._get_surrounded_missing_order_quantity( previous_o, following_o, max_quant_per_order, decimal_missing_order_price, recent_trades, - current_price, sorted_orders + current_price, sorted_orders, side ) orders.append(OrderData(missing_order_side, quantity, decimal_missing_order_price, self.symbol, False)) @@ -1578,9 +1578,9 @@ def _fill_missing_orders( def _get_surrounded_missing_order_quantity( self, previous_order, following_order, max_quant_per_order, order_price, recent_trades, - current_price, sorted_orders + current_price, sorted_orders, side ): - selling = previous_order.side == trading_enums.TradeOrderSide.SELL + selling = side == trading_enums.TradeOrderSide.SELL if sorted_orders: if quantity := self._get_quantity_from_existing_orders( order_price, sorted_orders, selling @@ -1594,10 +1594,9 @@ def _get_surrounded_missing_order_quantity( min( data_util.mean([previous_order.origin_quantity, following_order.origin_quantity]) if following_order else previous_order.origin_quantity, - max_quant_per_order / order_price + (max_quant_per_order if selling else max_quant_per_order / order_price) ) - ) - ) + )) def _get_spread_missing_order_quantity( self, average_order_quantity, side, i, orders_count, price, selling, limiting_amount_from_this_order,