diff --git a/Meta/Keywords/scripting_library/tests/orders/order_types/test_multiple_orders_creation.py b/Meta/Keywords/scripting_library/tests/orders/order_types/test_multiple_orders_creation.py index e26422622..c050f3b92 100644 --- a/Meta/Keywords/scripting_library/tests/orders/order_types/test_multiple_orders_creation.py +++ b/Meta/Keywords/scripting_library/tests/orders/order_types/test_multiple_orders_creation.py @@ -107,7 +107,8 @@ async def test_orders_amount_then_position_sequence(mock_context): api.load_pair_contract( mock_context.exchange_manager, api.create_default_future_contract( - mock_context.symbol, decimal.Decimal(1), trading_enums.FutureContractType.LINEAR_PERPETUAL + mock_context.symbol, decimal.Decimal(1), trading_enums.FutureContractType.LINEAR_PERPETUAL, + trading_constants.DEFAULT_SYMBOL_POSITION_MODE ).to_dict() ) diff --git a/Trading/Mode/daily_trading_mode/daily_trading.py b/Trading/Mode/daily_trading_mode/daily_trading.py index 585cfe9bb..f49094021 100644 --- a/Trading/Mode/daily_trading_mode/daily_trading.py +++ b/Trading/Mode/daily_trading_mode/daily_trading.py @@ -587,6 +587,7 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): spot_increasing_position = state in (trading_enums.EvaluatorStates.VERY_LONG.value, trading_enums.EvaluatorStates.LONG.value) if self.exchange_manager.is_future: + self.trading_mode.ensure_supported(symbol) # on futures, current_symbol_holding = current_market_holding = market_quantity max_buy_size, buy_increasing_position = trading_personal_data.get_futures_max_order_size( self.exchange_manager, symbol, trading_enums.TradeOrderSide.BUY, @@ -868,9 +869,13 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): raise trading_errors.OrderCreationError() raise trading_errors.MissingMinimalExchangeTradeVolume() - except (trading_errors.MissingFunds, - trading_errors.MissingMinimalExchangeTradeVolume, - trading_errors.OrderCreationError): + except ( + trading_errors.MissingFunds, + trading_errors.MissingMinimalExchangeTradeVolume, + trading_errors.OrderCreationError, + trading_errors.InvalidPositionSide, + trading_errors.UnsupportedContractConfigurationError + ): raise except asyncio.TimeoutError as e: self.logger.error(f"Impossible to create order for {symbol} on {self.exchange_manager.exchange_name}: {e} " diff --git a/Trading/Mode/dca_trading_mode/dca_trading.py b/Trading/Mode/dca_trading_mode/dca_trading.py index 2bc2c9b0d..af3aac521 100644 --- a/Trading/Mode/dca_trading_mode/dca_trading.py +++ b/Trading/Mode/dca_trading_mode/dca_trading.py @@ -134,6 +134,7 @@ async def create_new_orders(self, symbol, _, state, **kwargs): ) ) if self.exchange_manager.is_future: + self.trading_mode.ensure_supported(symbol) # on futures, current_symbol_holding = current_market_holding = market_quantity initial_available_funds, _ = trading_personal_data.get_futures_max_order_size( self.exchange_manager, symbol, side, diff --git a/Trading/Mode/grid_trading_mode/config/GridTradingMode.json b/Trading/Mode/grid_trading_mode/config/GridTradingMode.json index 7c426fc4a..9a09ca957 100644 --- a/Trading/Mode/grid_trading_mode/config/GridTradingMode.json +++ b/Trading/Mode/grid_trading_mode/config/GridTradingMode.json @@ -17,6 +17,8 @@ "mirror_order_delay": 0, "use_existing_orders_only": false, "allow_funds_redispatch": false, + "enable_trailing_up": false, + "enable_trailing_down": false, "funds_redispatch_interval": 24 }, { @@ -34,6 +36,9 @@ "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false, + "allow_funds_redispatch": false, + "enable_trailing_up": false, + "enable_trailing_down": false, "funds_redispatch_interval": 24 }, { @@ -51,6 +56,9 @@ "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false, + "allow_funds_redispatch": false, + "enable_trailing_up": false, + "enable_trailing_down": false, "funds_redispatch_interval": 24 } ] diff --git a/Trading/Mode/grid_trading_mode/grid_trading.py b/Trading/Mode/grid_trading_mode/grid_trading.py index 3961f0d8a..8d4bbb2e2 100644 --- a/Trading/Mode/grid_trading_mode/grid_trading.py +++ b/Trading/Mode/grid_trading_mode/grid_trading.py @@ -168,7 +168,25 @@ def init_user_inputs(self, inputs: dict) -> None: "fill. OctoBot won't create orders at startup: it will use the ones already on exchange instead. " "This mode allows grid orders to operate on user created orders. Can't work on trading simulator.", ) - self.UI.user_input( + self.UI.user_input( + self.CONFIG_ENABLE_TRAILING_UP, commons_enums.UserInputTypes.BOOLEAN, + default_config[self.CONFIG_ENABLE_TRAILING_UP], inputs, + parent_input_name=self.CONFIG_PAIR_SETTINGS, + title="Trailing up: when checked, the whole grid will be cancelled and recreated when price goes above the " + "highest selling price. This might require the grid to perform a buy market order to be " + "able to recreate the grid new sell orders at the updated price.", + ) + self.UI.user_input( + self.CONFIG_ENABLE_TRAILING_DOWN, commons_enums.UserInputTypes.BOOLEAN, + default_config[self.CONFIG_ENABLE_TRAILING_DOWN], inputs, + parent_input_name=self.CONFIG_PAIR_SETTINGS, + title="Trailing down: when checked, the whole grid will be cancelled and recreated when price goes bellow" + " the lowest buying price. This might require the grid to perform a sell market order to be " + "able to recreate the grid new buy orders at the updated price. " + "Warning: when trailing down, the sell order required to recreate the buying side of the grid " + "might generate a loss.", + ) + self.UI.user_input( self.CONFIG_ALLOW_FUNDS_REDISPATCH, commons_enums.UserInputTypes.BOOLEAN, default_config[self.CONFIG_ALLOW_FUNDS_REDISPATCH], inputs, parent_input_name=self.CONFIG_PAIR_SETTINGS, @@ -206,6 +224,8 @@ def get_default_pair_config(self, symbol, flat_spread, flat_increment) -> dict: self.CONFIG_USE_FIXED_VOLUMES_FOR_MIRROR_ORDERS: False, self.CONFIG_USE_EXISTING_ORDERS_ONLY: False, self.CONFIG_ALLOW_FUNDS_REDISPATCH: False, + self.CONFIG_ENABLE_TRAILING_UP: False, + self.CONFIG_ENABLE_TRAILING_DOWN: False, self.CONFIG_FUNDS_REDISPATCH_INTERVAL: 24, } @@ -317,15 +337,31 @@ def read_config(self): self.compensate_for_missed_mirror_order = self.symbol_trading_config.get( self.trading_mode.COMPENSATE_FOR_MISSED_MIRROR_ORDER, self.compensate_for_missed_mirror_order ) + self.enable_trailing_up = self.symbol_trading_config.get( + self.trading_mode.CONFIG_ENABLE_TRAILING_UP, self.enable_trailing_up + ) + self.enable_trailing_down = self.symbol_trading_config.get( + self.trading_mode.CONFIG_ENABLE_TRAILING_DOWN, self.enable_trailing_down + ) - async def _handle_staggered_orders(self, current_price, ignore_mirror_orders_only, ignore_available_funds): + async def _handle_staggered_orders( + self, current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing + ): self._init_allowed_price_ranges(current_price) if ignore_mirror_orders_only or not self.use_existing_orders_only: async with self.producer_exchange_wide_lock(self.exchange_manager): + if trigger_trailing and self.is_currently_trailing: + self.logger.debug( + f"{self.symbol} on {self.exchange_name}: trailing signal ignored: " + f"a trailing process is already running" + ) + return # use exchange level lock to prevent funds double spend - buy_orders, sell_orders = await self._generate_staggered_orders(current_price, ignore_available_funds) + buy_orders, sell_orders, triggering_trailing = await self._generate_staggered_orders( + current_price, ignore_available_funds, trigger_trailing + ) grid_orders = self._merged_and_sort_not_virtual_orders(buy_orders, sell_orders) - await self._create_not_virtual_orders(grid_orders, current_price) + await self._create_not_virtual_orders(grid_orders, current_price, triggering_trailing) async def trigger_staggered_orders_creation(self): # reload configuration @@ -360,7 +396,7 @@ def _apply_default_symbol_config(self) -> bool: ) return True - async def _generate_staggered_orders(self, current_price, ignore_available_funds): + async def _generate_staggered_orders(self, current_price, ignore_available_funds, trigger_trailing): order_manager = self.exchange_manager.exchange_personal_data.orders_manager if not self.single_pair_setup: interfering_orders_pairs = self._get_interfering_orders_pairs(order_manager.get_open_orders()) @@ -368,7 +404,7 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds self.logger.error(f"Impossible to create grid orders for {self.symbol} with interfering orders " f"using pair(s): {interfering_orders_pairs}. Configure funds to use for each pairs " f"to be able to use interfering pairs.") - return [], [] + return [], [], False existing_orders = order_manager.get_open_orders(self.symbol) sorted_orders = self._get_grid_trades_or_orders(existing_orders) @@ -395,53 +431,63 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds highest_buy = self.buy_price_range.higher_bound lowest_sell = self.sell_price_range.lower_bound highest_sell = self.sell_price_range.higher_bound - if sorted_orders: - buy_orders = [order for order in sorted_orders if order.side == trading_enums.TradeOrderSide.BUY] - highest_buy = current_price - sell_orders = [order for order in sorted_orders if order.side == trading_enums.TradeOrderSide.SELL] - lowest_sell = current_price - origin_created_buy_orders_count, origin_created_sell_orders_count = self._get_origin_orders_count( - sorted_orders, recently_closed_trades - ) - - min_max_total_order_price_delta = ( - self.flat_increment * (origin_created_buy_orders_count - 1 + origin_created_sell_orders_count - 1) - + self.flat_increment - ) - if buy_orders: - lowest_buy = buy_orders[0].origin_price - if not sell_orders: - highest_buy = min(current_price, lowest_buy + min_max_total_order_price_delta) - # buy orders only - lowest_sell = highest_buy + self.flat_spread - self.flat_increment - highest_sell = lowest_buy + min_max_total_order_price_delta + self.flat_spread - self.flat_increment - else: - # use only open order prices when possible - _highest_sell = sell_orders[-1].origin_price - highest_buy = min(current_price, _highest_sell - self.flat_spread + self.flat_increment) - if sell_orders: - highest_sell = sell_orders[-1].origin_price - if not buy_orders: - lowest_sell = max(current_price, highest_sell - min_max_total_order_price_delta) - # sell orders only - lowest_buy = max( - 0, highest_sell - min_max_total_order_price_delta - self.flat_spread + self.flat_increment - ) - highest_buy = lowest_sell - self.flat_spread + self.flat_increment - else: - # use only open order prices when possible - _lowest_buy = buy_orders[0].origin_price - lowest_sell = max(current_price, _lowest_buy - self.flat_spread + self.flat_increment) - - missing_orders, state, _ = self._analyse_current_orders_situation( - sorted_orders, recently_closed_trades, lowest_buy, highest_sell, current_price - ) - if missing_orders: - self.logger.info( - f"{len(missing_orders)} missing {self.symbol} orders on {self.exchange_name}: {missing_orders}" + if sorted_orders and not trigger_trailing: + if self._should_trigger_trailing(sorted_orders, current_price, False): + trigger_trailing = True + else: + buy_orders = [order for order in sorted_orders if order.side == trading_enums.TradeOrderSide.BUY] + sell_orders = [order for order in sorted_orders if order.side == trading_enums.TradeOrderSide.SELL] + 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 + ) + + min_max_total_order_price_delta = ( + self.flat_increment * (origin_created_buy_orders_count - 1 + origin_created_sell_orders_count - 1) + + self.flat_increment + ) + if buy_orders: + lowest_buy = buy_orders[0].origin_price + if not sell_orders: + highest_buy = min(current_price, lowest_buy + min_max_total_order_price_delta) + # buy orders only + lowest_sell = highest_buy + self.flat_spread - self.flat_increment + highest_sell = lowest_buy + min_max_total_order_price_delta + self.flat_spread - self.flat_increment + else: + # use only open order prices when possible + _highest_sell = sell_orders[-1].origin_price + highest_buy = min(current_price, _highest_sell - self.flat_spread + self.flat_increment) + if sell_orders: + highest_sell = sell_orders[-1].origin_price + if not buy_orders: + lowest_sell = max(current_price, highest_sell - min_max_total_order_price_delta) + # sell orders only + lowest_buy = max( + 0, highest_sell - min_max_total_order_price_delta - self.flat_spread + self.flat_increment + ) + highest_buy = lowest_sell - self.flat_spread + self.flat_increment + else: + # use only open order prices when possible + _lowest_buy = buy_orders[0].origin_price + lowest_sell = max(current_price, _lowest_buy - self.flat_spread + self.flat_increment) + if trigger_trailing: + await self._prepare_trailing(sorted_orders, current_price) + self.is_currently_trailing = True + # trailing will cancel all orders: set state to NEW with no existing order + missing_orders, state, sorted_orders = None, self.NEW, [] + else: + # no trailing, process normal analysis + missing_orders, state, _ = self._analyse_current_orders_situation( + sorted_orders, recently_closed_trades, lowest_buy, highest_sell, current_price ) - await self._handle_missed_mirror_orders_fills(recently_closed_trades, missing_orders, current_price) + if missing_orders: + self.logger.info( + f"{len(missing_orders)} missing {self.symbol} orders on {self.exchange_name}: {missing_orders}" + ) + await self._handle_missed_mirror_orders_fills(recently_closed_trades, missing_orders, current_price) try: + # apply state and (re)create missing orders buy_orders = self._create_orders(lowest_buy, highest_buy, trading_enums.TradeOrderSide.BUY, sorted_orders, current_price, missing_orders, state, self.buy_funds, ignore_available_funds, @@ -461,8 +507,9 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds buy_orders, sell_orders, state = await self._reset_orders( sorted_orders, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds ) + trigger_trailing = False - return buy_orders, sell_orders + return buy_orders, sell_orders, trigger_trailing def _get_origin_orders_count(self, recent_trades, open_orders): origin_created_buy_orders_count = self.buy_orders_count diff --git a/Trading/Mode/grid_trading_mode/resources/GridTradingMode.md b/Trading/Mode/grid_trading_mode/resources/GridTradingMode.md index 945941199..35df11b59 100644 --- a/Trading/Mode/grid_trading_mode/resources/GridTradingMode.md +++ b/Trading/Mode/grid_trading_mode/resources/GridTradingMode.md @@ -20,6 +20,12 @@ You can customize the grid for each trading pair. To configure a pair, enter: - The interval between each order (increment) - The amount of initial buy and sell orders to create +#### Trailing options +A grid can only operate within its price range. However, when trailing options are enabled, +the whole grid can be automatically cancelled and recreated +when the traded asset's price moves beyond the grid range. In this case, a market order can be executed in order to +have the necessary funds to create the grid buy and sell orders. + #### Profits Profits will be made from price movements within the covered price area. It never "sells at a loss", but always at a profit, therefore OctoBot never cancels any orders when using the Grid Trading Mode. 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 670f59385..dd625563c 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 @@ -1360,6 +1360,113 @@ def _get_fees_for_currency(fee, currency): assert sorted(new_orders, key=lambda x: x.origin_price)[-1] is sorted(open_orders, key=lambda x: x.origin_price)[-1] +async def test_trailing_up(): + symbol = "BTC/USDT" + async with _get_tools(symbol) as (producer, _, exchange_manager): + # first start: setup orders + producer.sell_funds = decimal.Decimal("1") # 25 sell orders + producer.buy_funds = decimal.Decimal("1") # 19 buy orders + orders_count = 19 + 25 + + price = 100 + trading_api.force_set_mark_price(exchange_manager, producer.symbol, price) + 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 + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 100) + # A. price moves up + pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, "USDT").available + + # offline simulation: orders get filled but not replaced => price got up to more than the max price + open_orders = copy.copy(trading_api.get_open_orders(exchange_manager)) + offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL] + for order in offline_filled: + await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer) + # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS + staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None) + post_portfolio = trading_api.get_portfolio_currency(exchange_manager, "USDT").available + assert pre_portfolio < post_portfolio + assert len(trading_api.get_open_orders(exchange_manager)) == orders_count - len(offline_filled) + producer.enable_trailing_up = True + + # top filled sell order price = 225 + assert max(o.origin_price for o in offline_filled) == decimal.Decimal("225") + new_price = decimal.Decimal(250) + trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price) + # will trail up + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 250) + + # B. orders get filled but not enough to trigger a trailing reset + # offline simulation: orders get filled but not replaced => price got up to more than the max price + open_orders = trading_api.get_open_orders(exchange_manager) + # all but 1 sell orders is filled + offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.SELL][:-1] + for order in offline_filled: + await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer) + # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS + staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None) + post_portfolio = trading_api.get_portfolio_currency(exchange_manager, "USDT").available + assert pre_portfolio < post_portfolio + assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(offline_filled) + producer.enable_trailing_up = True + producer.enable_trailing_down = True + # doesn't trail up: a sell order still remains + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 250) + # all buy orders are still here + # not cancelled sell order is still here + offline_filled_ids = [o.order_id for o in offline_filled] + for order in open_orders: + if order.order_id in offline_filled_ids: + assert order.is_closed() + else: + assert order.is_open() + + # C. price moves down, trailing down is disabled + pre_portfolio = trading_api.get_portfolio_currency(exchange_manager, "BTC").available + + # offline simulation: orders get filled but not replaced => price got up to more than the max price + open_orders = copy.copy(trading_api.get_open_orders(exchange_manager)) + offline_filled = [o for o in open_orders if o.side == trading_enums.TradeOrderSide.BUY] + for order in offline_filled: + await _fill_order(order, exchange_manager, trigger_update_callback=False, producer=producer) + # simulate a start without StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS + staggered_orders_trading.StaggeredOrdersTradingModeProducer.AVAILABLE_FUNDS.pop(exchange_manager.id, None) + post_portfolio = trading_api.get_portfolio_currency(exchange_manager, "BTC").available + assert pre_portfolio < post_portfolio + assert len(trading_api.get_open_orders(exchange_manager)) == producer.operational_depth - len(offline_filled) + producer.enable_trailing_down = False + + # top filled sell order price = 125 + assert min(o.origin_price for o in offline_filled) == decimal.Decimal("125") + new_price = decimal.Decimal(125) + trading_api.force_set_mark_price(exchange_manager, producer.symbol, new_price) + # will not trail down + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 250) + # only contains sell orders + open_orders = copy.copy(trading_api.get_open_orders(exchange_manager)) + assert all (order.side == trading_enums.TradeOrderSide.SELL for order in open_orders) + + # D. price is still down, trailing down is enabled + producer.enable_trailing_down = True + + # will trail down + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth - 1)) # -1 because the very first order can't be at a price <0 + # orders are recreated around 125 + _check_created_orders(producer, trading_api.get_open_orders(exchange_manager), 125) + # now contains buy and sell orders + open_orders = trading_api.get_open_orders(exchange_manager) + assert len([order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL]) == producer.sell_orders_count + assert len([order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY]) == producer.buy_orders_count - 1 + + @contextlib.contextmanager def _assert_adapt_order_quantity_because_fees(get_fees_for_currency=False): _origin_decimal_adapt_order_quantity_because_fees = trading_personal_data.decimal_adapt_order_quantity_because_fees 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 3b9fc0a32..b39aa6439 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -19,6 +19,7 @@ import math import asyncio import decimal +import typing import async_channel.constants as channel_constants import octobot_commons.constants as commons_constants @@ -113,6 +114,8 @@ class StaggeredOrdersTradingMode(trading_modes.AbstractTradingMode): CONFIG_OPERATIONAL_DEPTH = "operational_depth" CONFIG_MIRROR_ORDER_DELAY = "mirror_order_delay" CONFIG_ALLOW_FUNDS_REDISPATCH = "allow_funds_redispatch" + CONFIG_ENABLE_TRAILING_UP = "enable_trailing_up" + CONFIG_ENABLE_TRAILING_DOWN = "enable_trailing_down" CONFIG_FUNDS_REDISPATCH_INTERVAL = "funds_redispatch_interval" COMPENSATE_FOR_MISSED_MIRROR_ORDER = "compensate_for_missed_mirror_order" CONFIG_STARTING_PRICE = "starting_price" @@ -245,8 +248,7 @@ async def _order_notification_callback(self, exchange, exchange_id, cryptocurren ) and is_from_bot ): - async with self.producers[0].get_lock(): - await self.producers[0].order_filled_callback(order) + await self.producers[0].order_filled_callback(order) @classmethod def get_is_symbol_wildcard(cls) -> bool: @@ -388,6 +390,7 @@ class StaggeredOrdersTradingModeConsumer(trading_modes.AbstractTradingModeConsum ORDER_DATA_KEY = "order_data" CURRENT_PRICE_KEY = "current_price" SYMBOL_MARKET_KEY = "symbol_market" + COMPLETING_TRAILING_KEY = "completing_trailing" def __init__(self, trading_mode): super().__init__(trading_mode) @@ -406,13 +409,19 @@ async def cancel_orders_creation(self): async def create_new_orders(self, symbol, final_note, state, **kwargs): # use dict default getter: can't afford missing data data = kwargs["data"] - if not self.skip_orders_creation: - order_data = data[self.ORDER_DATA_KEY] - current_price = data[self.CURRENT_PRICE_KEY] - symbol_market = data[self.SYMBOL_MARKET_KEY] - return await self.create_order(order_data, current_price, symbol_market) - else: - self.logger.info(f"Skipped {data.get(self.ORDER_DATA_KEY, '')}") + try: + if not self.skip_orders_creation: + order_data = data[self.ORDER_DATA_KEY] + current_price = data[self.CURRENT_PRICE_KEY] + symbol_market = data[self.SYMBOL_MARKET_KEY] + return await self.create_order(order_data, current_price, symbol_market) + else: + self.logger.info(f"Skipped {data.get(self.ORDER_DATA_KEY, '')}") + finally: + if data[self.COMPLETING_TRAILING_KEY]: + for producer in self.trading_mode.producers: + # trailing process complete + producer.is_currently_trailing = False async def create_order(self, order_data, current_price, symbol_market): created_order = None @@ -524,6 +533,8 @@ def __init__(self, channel, config, trading_mode, exchange_manager): self.mirror_orders_tasks = [] self.mirroring_pause_task = None self.allow_order_funds_redispatch = False + self.enable_trailing_up = False + self.enable_trailing_down = False self.funds_redispatch_interval = 24 self._expect_missing_orders = False self._skip_order_restore_on_recently_closed_orders = True @@ -551,6 +562,7 @@ def __init__(self, channel, config, trading_mode, exchange_manager): self.allow_virtual_orders = True self.health_check_interval_secs = self.__class__.HEALTH_CHECK_INTERVAL_SECS self.healthy = False + self.is_currently_trailing = False try: self._load_symbol_trading_config() @@ -713,28 +725,50 @@ async def stop_mirror_orders(self, delay): self.allowed_mirror_orders.set() self.logger.info(f"Resuming {self.symbol} mirror orders creation after a {delay} seconds pause") - async def _ensure_staggered_orders(self, ignore_mirror_orders_only=False, ignore_available_funds=False): + async def _ensure_staggered_orders( + self, ignore_mirror_orders_only=False, ignore_available_funds=False, trigger_trailing=False + ): _, _, _, self.current_price, self.symbol_market = await trading_personal_data.get_pre_order_data( self.exchange_manager, symbol=self.symbol, timeout=self.PRICE_FETCHING_TIMEOUT ) self.logger.debug(f"{self.symbol} symbol_market initialized") - await self.create_state(self._get_new_state_price(), ignore_mirror_orders_only, ignore_available_funds) + await self.create_state( + self._get_new_state_price(), ignore_mirror_orders_only, ignore_available_funds, trigger_trailing + ) def _get_new_state_price(self): return decimal.Decimal(str(self.current_price if self.starting_price == 0 else self.starting_price)) - async def create_state(self, current_price, ignore_mirror_orders_only, ignore_available_funds): + async def create_state(self, current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing): if current_price is not None: self._refresh_symbol_data(self.symbol_market) async with self.get_lock(), self.trading_mode_trigger(skip_health_check=True): if self.exchange_manager.trader.is_enabled: - await self._handle_staggered_orders(current_price, ignore_mirror_orders_only, ignore_available_funds) + await self._handle_staggered_orders( + current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing + ) self.logger.debug(f"{self.symbol} orders updated on {self.exchange_name}") - async def order_filled_callback(self, filled_order): + async def order_filled_callback(self, filled_order: dict): # create order on the order side + new_order = self._create_mirror_order(filled_order) + self.logger.debug(f"Creating mirror order: {new_order} after filled order: {filled_order}") + filled_price = decimal.Decimal(str( + filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] + )) + if self.mirror_order_delay == 0 or trading_api.get_is_backtesting(self.exchange_manager): + await self._ensure_trailing_and_create_order_when_possible(new_order, filled_price) + else: + # create order after waiting time + self.mirror_orders_tasks.append(asyncio.get_event_loop().call_later( + self.mirror_order_delay, + asyncio.create_task, + self._ensure_trailing_and_create_order_when_possible(new_order, filled_price) + )) + + def _create_mirror_order(self, filled_order: dict): now_selling = filled_order[ trading_enums.ExchangeConstantsOrderColumns.SIDE.value ] == trading_enums.TradeOrderSide.BUY.value @@ -756,23 +790,33 @@ async def order_filled_callback(self, filled_order): self.flat_spread = trading_personal_data.decimal_adapt_price( self.symbol_market, self.spread * self.flat_increment / self.increment ) - price_increment = self.flat_spread - self.flat_increment - filled_price = decimal.Decimal(str(filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value])) + mirror_price_difference = self.flat_spread - self.flat_increment + # try to get the order origin price to compute mirror order price + filled_price = decimal.Decimal(str( + filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] + )) + maybe_trade, maybe_order = self.exchange_manager.exchange_personal_data.get_trade_or_open_order( + filled_order[trading_enums.ExchangeConstantsOrderColumns.ID.value] + ) + if maybe_trade: + # normal case + order_origin_price = maybe_trade.origin_price + elif maybe_order: + # should not happen but still handle it just in case + order_origin_price = maybe_order.origin_price + else: + # can't find order: default to filled price, even though it might be different from origin price + self.logger.warning( + f"Computing mirror order price using filled order price: no associated trade or order has been " + f"found, this can lead to inconsistent order intervals (order: {filled_order})" + ) + order_origin_price = filled_price + price = order_origin_price + mirror_price_difference if now_selling else order_origin_price - mirror_price_difference + filled_volume = decimal.Decimal(str(filled_order[trading_enums.ExchangeConstantsOrderColumns.FILLED.value])) - price = filled_price + price_increment if now_selling else filled_price - price_increment fee = filled_order[trading_enums.ExchangeConstantsOrderColumns.FEE.value] volume = self._compute_mirror_order_volume(now_selling, filled_price, price, filled_volume, fee) - new_order = OrderData(new_side, volume, price, self.symbol, False, associated_entry_id) - self.logger.debug(f"Creating mirror order: {new_order} after filled order: {filled_order}") - if self.mirror_order_delay == 0 or trading_api.get_is_backtesting(self.exchange_manager): - await self._lock_portfolio_and_create_order_when_possible(new_order, filled_price) - else: - # create order after waiting time - self.mirror_orders_tasks.append(asyncio.get_event_loop().call_later( - self.mirror_order_delay, - asyncio.create_task, - self._lock_portfolio_and_create_order_when_possible(new_order, filled_price) - )) + return OrderData(new_side, volume, price, self.symbol, False, associated_entry_id) def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, filled_volume, paid_fees: dict): # use target volumes if set @@ -804,12 +848,85 @@ def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, fees_in_base = new_order_quantity * self.max_fees return new_order_quantity - fees_in_base - async def _lock_portfolio_and_create_order_when_possible(self, new_order, filled_price): + async def _ensure_trailing_and_create_order_when_possible(self, new_order, current_price): + if self._should_trigger_trailing(None, None, True): + # do not give current price as in this context, having only one-sided orders requires trailing + await self._ensure_staggered_orders(trigger_trailing=True) + else: + async with self.get_lock(): + await self._lock_portfolio_and_create_order_when_possible(new_order, current_price) + + async def _lock_portfolio_and_create_order_when_possible(self, new_order, current_price): await asyncio.wait_for(self.allowed_mirror_orders.wait(), timeout=None) async with self.exchange_manager.exchange_personal_data.portfolio_manager.portfolio.lock: - await self._create_order(new_order, filled_price) + await self._create_order(new_order, current_price, False) + + def _should_trigger_trailing( + self, + orders: typing.Optional[list], + current_price: typing.Optional[decimal.Decimal], + trail_on_missing_orders: bool + ) -> bool: + if not (self.enable_trailing_up or self.enable_trailing_down): + return False + existing_orders = ( + orders or self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(self.symbol) + ) + buy_orders = sorted( + [order for order in existing_orders if order.side == trading_enums.TradeOrderSide.BUY], + key=lambda o: -o.origin_price + ) + sell_orders = sorted( + [order for order in existing_orders if order.side == trading_enums.TradeOrderSide.SELL], + key=lambda o: o.origin_price + ) + # 3 to allow trailing even if a few order from the other side have also been filled + one_sided_orders_trailing_threshold = self.operational_depth / 3 + if ( + self.enable_trailing_up + and (len(buy_orders) >= one_sided_orders_trailing_threshold or trail_on_missing_orders) + and not sell_orders + ): + # only buy orders remaining: everything has been sold, trigger tailing up when enabled if price is + # beyond range + if current_price and buy_orders: + missing_orders_count = self.operational_depth - len(buy_orders) + price_delta = missing_orders_count * self.flat_increment + first_order = buy_orders[0] + approximated_highest_buy_price = first_order.origin_price + price_delta + if current_price >= approximated_highest_buy_price: + # current price is beyond grid maximum buy price: trigger trailing + return True + last_order = buy_orders[-1] + if last_order.origin_price - self.flat_increment < trading_constants.ZERO: + # not all buy orders could have been created: trigger trailing as there is no way to check + # the theoretical max price of the grid + return len(buy_orders) >= self.operational_depth / 2 and current_price > first_order.origin_price + elif trail_on_missing_orders: + # trail too much + return True + if ( + self.enable_trailing_down + and (len(sell_orders) >= one_sided_orders_trailing_threshold or trail_on_missing_orders) + and not buy_orders + ): + # only sell orders remaining: everything has been bought, trigger tailing up when enabled if price is + # beyond range + if current_price: + missing_orders_count = self.operational_depth - len(sell_orders) + price_delta = missing_orders_count * self.flat_increment + first_order = sell_orders[0] + approximated_lowest_sell_price = first_order.origin_price - price_delta + if current_price <= approximated_lowest_sell_price: + # current price is beyond grid minimum sell price: trigger trailing + return True + elif trail_on_missing_orders: + return True + return False - async def _handle_staggered_orders(self, current_price, ignore_mirror_orders_only, ignore_available_funds): + async def _handle_staggered_orders( + self, current_price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing + ): self._ensure_current_price_in_limit_parameters(current_price) if not ignore_mirror_orders_only and self.use_existing_orders_only: # when using existing orders only, no need to check existing orders (they can't be wrong since they are @@ -817,10 +934,18 @@ async def _handle_staggered_orders(self, current_price, ignore_mirror_orders_onl self._set_increment_and_spread(current_price) else: async with self.producer_exchange_wide_lock(self.exchange_manager): + if trigger_trailing and self.is_currently_trailing: + self.logger.debug( + f"{self.symbol} on {self.exchange_name}: trailing signal ignored: " + f"a trailing process is already running" + ) + return # use exchange level lock to prevent funds double spend - buy_orders, sell_orders = await self._generate_staggered_orders(current_price, ignore_available_funds) + buy_orders, sell_orders, triggering_trailing = await self._generate_staggered_orders( + current_price, ignore_available_funds, trigger_trailing + ) staggered_orders = self._merged_and_sort_not_virtual_orders(buy_orders, sell_orders) - await self._create_not_virtual_orders(staggered_orders, current_price) + await self._create_not_virtual_orders(staggered_orders, current_price, triggering_trailing) def _ensure_current_price_in_limit_parameters(self, current_price): message = None @@ -845,14 +970,14 @@ def _log_window_error_or_warning(self, message, using_error): log_func = self.logger.error if using_error else self.logger.warning log_func(message) - async def _generate_staggered_orders(self, current_price, ignore_available_funds): + async def _generate_staggered_orders(self, current_price, ignore_available_funds, trigger_trailing): order_manager = self.exchange_manager.exchange_personal_data.orders_manager interfering_orders_pairs = self._get_interfering_orders_pairs(order_manager.get_open_orders()) if interfering_orders_pairs: self.logger.error(f"Impossible to create {self.ORDERS_DESC} orders for {self.symbol} with " f"interfering orders using pair(s): {interfering_orders_pairs}. " f"{self.ORDERS_DESC.capitalize()} orders require no other orders in both base and quote.") - return [], [] + return [], [], False existing_orders = order_manager.get_open_orders(self.symbol) sorted_orders = sorted(existing_orders, key=lambda order: order.origin_price) @@ -862,10 +987,19 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds recently_closed_trades = trading_api.get_trade_history(self.exchange_manager, symbol=self.symbol, since=recent_trades_time) recently_closed_trades = sorted(recently_closed_trades, key=lambda trade: trade.executed_price) - - missing_orders, state, candidate_flat_increment = self._analyse_current_orders_situation( - sorted_orders, recently_closed_trades, self.lowest_buy, self.highest_sell, current_price + candidate_flat_increment = None + trigger_trailing = trigger_trailing or bool( + sorted_orders and self._should_trigger_trailing(sorted_orders, current_price, False) ) + if trigger_trailing: + await self._prepare_trailing(sorted_orders, current_price) + self.is_currently_trailing = True + # trailing will cancel all orders: set state to NEW with no existing order + missing_orders, state, sorted_orders = None, self.NEW, [] + else: + missing_orders, state, candidate_flat_increment = self._analyse_current_orders_situation( + sorted_orders, recently_closed_trades, self.lowest_buy, self.highest_sell, current_price + ) self._set_increment_and_spread(current_price, candidate_flat_increment) highest_buy = min(current_price, self.highest_sell) @@ -888,22 +1022,14 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds if state == self.NEW: self._set_virtual_orders(buy_orders, sell_orders, self.operational_depth) - return buy_orders, sell_orders + return buy_orders, sell_orders, trigger_trailing async def _reset_orders( self, sorted_orders, lowest_buy, highest_buy, lowest_sell, highest_sell, current_price, ignore_available_funds ): self.logger.info("Resetting orders") await asyncio.gather(*(self.trading_mode.cancel_order(order) for order in sorted_orders)) - base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote() - self._set_initially_available_funds( - base, - trading_api.get_portfolio_currency(self.exchange_manager, base).available, - ) - self._set_initially_available_funds( - quote, - trading_api.get_portfolio_currency(self.exchange_manager, quote).available, - ) + self._reset_available_funds() state = self.NEW buy_orders = self._create_orders( lowest_buy, highest_buy, trading_enums.TradeOrderSide.BUY, sorted_orders, @@ -915,6 +1041,17 @@ async def _reset_orders( ) return buy_orders, sell_orders, state + def _reset_available_funds(self): + base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote() + self._set_initially_available_funds( + base, + trading_api.get_portfolio_currency(self.exchange_manager, base).available, + ) + self._set_initially_available_funds( + quote, + trading_api.get_portfolio_currency(self.exchange_manager, quote).available, + ) + def _ensure_used_funds(self, new_buy_orders, new_sell_orders, existing_orders, recently_closed_trades): if not self.allow_order_funds_redispatch: return @@ -1102,7 +1239,7 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde ) if fees_adapted_target_amount != target_amount: self.logger.info( - f"Adapted balancing quantity to compy with exchange fees. Using {fees_adapted_target_amount} " + f"Adapted balancing quantity to comply with exchange fees. Using {fees_adapted_target_amount} " f"instead of {target_amount}" ) target_amount = fees_adapted_target_amount @@ -1112,7 +1249,7 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde self.symbol_market ) if not to_create_details: - self.logger.error( + self.logger.warning( f"No enough computed funds to recreate packed missed [{self.exchange_manager.exchange_name}] " f"mirror order balancing order on {self.symbol}: target_amount: {target_amount} is not enough " f"for exchange minimal trading amounts rules" @@ -1126,7 +1263,7 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde else market_quantity other_currency = base if order_type is trading_enums.TraderOrderType.BUY_MARKET \ else quote - self.logger.error( + self.logger.warning( f"No enough available funds to create missed [{self.exchange_manager.exchange_name}] mirror " f"order {order_type.value} balancing order on {self.symbol}. " f"Required {float(order_amount)} {limiting_currency}, available {float(limiting_amount)} " @@ -1189,6 +1326,88 @@ def _printable_trade(trade): ) return trades_with_missing_mirror_order_fills + async def _prepare_trailing(self, open_orders: list, current_price: decimal.Decimal): + log_header = f"[{self.exchange_manager.exchange_name}] {self.symbol} @ {current_price} trailing process: " + if current_price <= trading_constants.ZERO: + self.logger.error( + f"Aborting {log_header}current price is {current_price}") + return + # 1. cancel all open orders + try: + cancelled_orders = [] + self.logger.info(f"{log_header}cancelling {len(open_orders)} open orders on {self.symbol}") + for order in open_orders: + if not (order.is_cancelled() or order.is_closed()): + await self.trading_mode.cancel_order(order) + cancelled_orders.append(order) + except Exception as err: + self.logger.exception(err, True, f"Error in {log_header} cancel orders step: {err}") + cancelled_orders = [] + + # 2. if necessary, convert a part of the funds to be able to create buy and sell orders + orders = [] + try: + parsed_symbol = symbol_util.parse_symbol(self.symbol) + available_base_amount = trading_api.get_portfolio_currency(self.exchange_manager, parsed_symbol.base).available + available_quote_amount = trading_api.get_portfolio_currency(self.exchange_manager, parsed_symbol.quote).available + usable_amount_in_quote = available_quote_amount + (available_base_amount * current_price) + config_max_amount = self.buy_funds + (self.sell_funds * current_price) + if config_max_amount > trading_constants.ZERO: + usable_amount_in_quote = min(usable_amount_in_quote, config_max_amount) + # amount = the total amount (in base) to put into the grid at the current price + usable_amount_in_base = usable_amount_in_quote / current_price + + target_base = usable_amount_in_base / decimal.Decimal(2) + target_quote = usable_amount_in_quote / decimal.Decimal(2) + + amount = trading_constants.ZERO + if available_base_amount < target_base: + # buy order + to_buy = parsed_symbol.base + to_sell = parsed_symbol.quote + amount = (target_base - available_base_amount) * current_price + if available_quote_amount < target_quote: + if amount != trading_constants.ZERO: + # can't buy with currencies, this should never happen: log error + self.logger.error(f"{log_header}can't buy and sell {parsed_symbol} at the same time.") + else: + # sell order + to_buy = parsed_symbol.quote + to_sell = parsed_symbol.base + amount = (target_quote - available_quote_amount) / current_price + + if amount > trading_constants.ZERO: + self.logger.info(f"{log_header}selling {amount} {to_sell} to buy {to_buy}") + # need portfolio available to be up-to-date with cancelled orders + orders = await trading_modes.convert_asset_to_target_asset( + self.trading_mode, to_sell, to_buy, { + self.symbol: { + trading_enums.ExchangeConstantsTickersColumns.CLOSE.value: current_price, + } + }, asset_amount=amount + ) + if orders: + await asyncio.gather(*[ + trading_personal_data.wait_for_order_fill( + order, self.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT, True + ) for order in orders + ]) + else: + self.logger.info(f"{log_header}nothing to buy or sell. Current funds are enough") + except Exception as err: + self.logger.exception( + err, True, f"Error in {log_header}convert into target step: {err}" + ) + + # 3. reset available funds (free funds from cancelled orders) + self._reset_available_funds() + + self.logger.info( + f"Completed {log_header} {len(cancelled_orders)} cancelled orders, {len(orders)} " + f"created conversion orders" + ) + return cancelled_orders, orders + def _analyse_current_orders_situation(self, sorted_orders, recently_closed_trades, lower_bound, higher_bound, current_price): if not sorted_orders: return None, self.NEW, None @@ -1565,8 +1784,10 @@ def _bootstrap_parameters(self, sorted_orders, recently_closed_trades, lower_bou only_sell = True if sorted_orders[-1].side == trading_enums.TradeOrderSide.BUY: # only buy orders - self.logger.warning(f"Only buy orders are online for {self.symbol}, now waiting for the price to " - f"go down to create new sell orders.") + self.logger.warning( + f"Only buy orders are online ({len(sorted_orders)} orders) for {self.symbol}, " + f"now waiting for the price to go down to create new sell orders." + ) only_buy = True for order in sorted_orders: if order.symbol != self.symbol: @@ -1941,11 +2162,12 @@ def _get_min_max_quantity(average_order_quantity, mode): max_quantity = average_order_quantity * (1 + mode_multiplier / 2) return min_quantity, max_quantity - async def _create_order(self, order, current_price): + async def _create_order(self, order, current_price, completing_trailing): data = { StaggeredOrdersTradingModeConsumer.ORDER_DATA_KEY: order, StaggeredOrdersTradingModeConsumer.CURRENT_PRICE_KEY: current_price, StaggeredOrdersTradingModeConsumer.SYMBOL_MARKET_KEY: self.symbol_market, + StaggeredOrdersTradingModeConsumer.COMPLETING_TRAILING_KEY: completing_trailing, } state = trading_enums.EvaluatorStates.LONG if order.side is trading_enums.TradeOrderSide.BUY else trading_enums.EvaluatorStates.SHORT await self.submit_trading_evaluation(cryptocurrency=self.trading_mode.cryptocurrency, @@ -1954,9 +2176,12 @@ async def _create_order(self, order, current_price): state=state, data=data) - async def _create_not_virtual_orders(self, orders_to_create, current_price): - for order in orders_to_create: - await self._create_order(order, current_price) + async def _create_not_virtual_orders( + self, orders_to_create: list, current_price: decimal.Decimal, triggering_trailing: bool + ): + for index, order in enumerate(orders_to_create): + is_completing_trailing = triggering_trailing and (index == len(orders_to_create) - 1) + await self._create_order(order, current_price, is_completing_trailing) base, quote = symbol_util.parse_symbol(order.symbol).base_and_quote() # keep track of the required funds volume = order.quantity if order.side is trading_enums.TradeOrderSide.SELL \ diff --git a/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py b/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py index a71cd07ee..f6b76aed0 100644 --- a/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py +++ b/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py @@ -104,6 +104,8 @@ async def _get_tools(symbol, btc_holdings=None, additional_portfolio={}, fees=No await trader.initialize() # set BTC/USDT price at 1000 USDT + if symbol not in exchange_manager.client_symbols: + exchange_manager.client_symbols.append(symbol) trading_api.force_set_mark_price(exchange_manager, symbol, 1000) mode, producer = await _init_trading_mode(config, exchange_manager, symbol) @@ -1371,6 +1373,70 @@ def _decimal_adapt_order_quantity_because_fees( assert created_orders == [] +async def test_create_state(): + symbol = "BTC/USD" + async with _get_tools(symbol) as tools: + producer, consumer, exchange_manager = tools + price = decimal.Decimal(1000) + ignore_mirror_orders_only = False + ignore_available_funds = False + trigger_trailing = False + _, _, _, _, producer.symbol_market = await trading_personal_data.get_pre_order_data(exchange_manager, symbol) + # not triggering trailing + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], False))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing) + _create_not_virtual_orders_mock.assert_awaited_once_with([], price, False) + + # triggering trailing + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], True))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing) + _create_not_virtual_orders_mock.assert_awaited_once_with([], price, True) + trigger_trailing = True + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], True))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing) + _create_not_virtual_orders_mock.assert_awaited_once_with([], price, True) + + # already trailing: skip call + producer.is_currently_trailing = True + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], True))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_not_called() + _create_not_virtual_orders_mock.assert_not_called() + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], False))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_not_called() + _create_not_virtual_orders_mock.assert_not_called() + + # not tailing anymore: can now call + producer.is_currently_trailing = False + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], True))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing) + _create_not_virtual_orders_mock.assert_awaited_once_with([], price, True) + trigger_trailing = True + with mock.patch.object(producer, "_generate_staggered_orders", mock.AsyncMock(return_value=([], [], False))) \ + as _generate_staggered_orders_mock, mock.patch.object(producer, "_create_not_virtual_orders", mock.AsyncMock()) \ + as _create_not_virtual_orders_mock: + await producer.create_state(price, ignore_mirror_orders_only, ignore_available_funds, trigger_trailing) + _generate_staggered_orders_mock.assert_awaited_once_with(price, ignore_available_funds, trigger_trailing) + _create_not_virtual_orders_mock.assert_awaited_once_with([], price, False) + + async def test_create_new_orders(): symbol = "BTC/USD" async with _get_tools(symbol) as tools: @@ -1383,15 +1449,26 @@ async def test_create_new_orders(): # valid input price = decimal.Decimal(205) - quantity = decimal.Decimal(4) + quantity = decimal.Decimal(1) side = trading_enums.TradeOrderSide.BUY to_create_order = staggered_orders_trading.OrderData(side, quantity, price, symbol, False) + producer.is_currently_trailing = True + data = { + consumer.ORDER_DATA_KEY: to_create_order, + consumer.CURRENT_PRICE_KEY: price, + consumer.SYMBOL_MARKET_KEY: symbol_market, + consumer.COMPLETING_TRAILING_KEY: False, + } + assert await consumer.create_new_orders(symbol, None, None, data=data) + assert producer.is_currently_trailing is True data = { consumer.ORDER_DATA_KEY: to_create_order, consumer.CURRENT_PRICE_KEY: price, - consumer.SYMBOL_MARKET_KEY: symbol_market + consumer.SYMBOL_MARKET_KEY: symbol_market, + consumer.COMPLETING_TRAILING_KEY: True, # will update producer.is_currently_trailing } assert await consumer.create_new_orders(symbol, None, None, data=data) + assert producer.is_currently_trailing is False # updated to false # invalid input 1 data = { @@ -1489,6 +1566,313 @@ async def test_single_exchange_process_optimize_initial_portfolio(): assert final_portfolio["USD"].available == decimal.Decimal("5545") +async def test_prepare_trailing(): + symbol = "BTC/USD" + async with _get_tools(symbol) as tools: + producer, _, exchange_manager = tools + + trading_api.force_set_mark_price(exchange_manager, symbol, 4000) + with mock.patch.object(producer, "_ensure_current_price_in_limit_parameters", mock.Mock()) \ + as _ensure_current_price_in_limit_parameters_mock: + await producer._ensure_staggered_orders() + _ensure_current_price_in_limit_parameters_mock.assert_called_once() + # price info: create orders + assert producer.current_price == 4000 + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + # now has buy and sell orders + open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + # simulate price being stable + cancelled_orders, created_orders = await producer._prepare_trailing(open_orders, current_price=4000) + assert len(cancelled_orders) == len(open_orders) + # cancelled orders + updated_open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + assert updated_open_orders == [] + + # created order to balance BTC and USD (sell BTC) + assert len(created_orders) == 1 + assert created_orders[0].symbol == symbol + assert created_orders[0].origin_quantity == decimal.Decimal("4.87500000") + fees = created_orders[0].fee[trading_enums.FeePropertyColumns.COST.value] + assert fees == decimal.Decimal("19.5") + assert isinstance(created_orders[0], trading_personal_data.SellMarketOrder) + + portfolio = exchange_manager.exchange_personal_data.portfolio_manager.portfolio.portfolio + # portfolio is now balanced + assert portfolio["BTC"].available == decimal.Decimal("5.125") # 5.125 x 4000 = 20500 + assert portfolio["USD"].available == decimal.Decimal("20480.5") == decimal.Decimal("20500") - fees + + # price change (going down), no order to cancel: just adapt pf + trading_api.force_set_mark_price(exchange_manager, symbol, 3000) + cancelled_orders, created_orders = await producer._prepare_trailing(open_orders, current_price=3000) + # no order to cancel (orders are already cancelled) + assert len(cancelled_orders) == 0 + # created order to balance BTC and USD (buy BTC) + assert len(created_orders) == 1 + assert created_orders[0].symbol == symbol + assert created_orders[0].origin_quantity == decimal.Decimal('0.85091666') + fees = created_orders[0].fee[trading_enums.FeePropertyColumns.COST.value] + assert fees == decimal.Decimal('0.00085091666') + assert isinstance(created_orders[0], trading_personal_data.BuyMarketOrder) + + assert portfolio["BTC"].available == decimal.Decimal('5.97506574334') # Decimal('5.97506574334') x 3000 = Decimal('17925.19723002000') + assert portfolio["USD"].available == decimal.Decimal('17927.75002000000') + + # price change (going up), no order to cancel: just adapt pf + trading_api.force_set_mark_price(exchange_manager, symbol, 8000) + cancelled_orders, created_orders = await producer._prepare_trailing([], current_price=8000) + # no order to cancel + assert len(cancelled_orders) == 0 + # created order to balance BTC and USD (buy BTC) + assert len(created_orders) == 1 + assert created_orders[0].symbol == symbol + assert created_orders[0].origin_quantity == decimal.Decimal('1.86704849') + fees = created_orders[0].fee[trading_enums.FeePropertyColumns.COST.value] + assert fees == decimal.Decimal('14.93638792000') + assert isinstance(created_orders[0], trading_personal_data.SellMarketOrder) + + assert portfolio["BTC"].available == decimal.Decimal('4.10801725334') # Decimal('4.10801725334') x 8000 = Decimal('32864.13802672000') + assert portfolio["USD"].available == decimal.Decimal('32849.20155208000') + + +async def test_should_trigger_trailing_not_all_buy_order_created(): + symbol = "BTC/USD" + async with _get_tools(symbol) as tools: + current_price = decimal.Decimal(4000) + producer, _, exchange_manager = tools + # A. no open order: no trailing + assert producer._should_trigger_trailing([], current_price, False) is False + producer.enable_trailing_up = producer.enable_trailing_down = True + assert producer._should_trigger_trailing([], current_price, False) is False + + # create orders + trading_api.force_set_mark_price(exchange_manager, symbol, 4000) + with mock.patch.object(producer, "_ensure_current_price_in_limit_parameters", mock.Mock()) \ + as _ensure_current_price_in_limit_parameters_mock: + await producer._ensure_staggered_orders() + _ensure_current_price_in_limit_parameters_mock.assert_called_once() + + assert producer.current_price == 4000 + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + # now has buy and sell orders + + # B. trailing disabled + producer.enable_trailing_up = producer.enable_trailing_down = False + assert producer._should_trigger_trailing([], current_price, False) is False + open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY] + sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL] + assert len(buy_orders) > 10 + assert len(sell_orders) > 10 + assert producer._should_trigger_trailing(buy_orders, current_price, False) is False + assert producer._should_trigger_trailing(sell_orders, current_price, False) is False + + # C. trailing enabled + producer.enable_trailing_up = True + assert producer._should_trigger_trailing(sell_orders, current_price, False) is False + # True because all buy orders couldn't be created: impossible to check accurately + assert producer._should_trigger_trailing(buy_orders, current_price, False) is True + + producer.enable_trailing_down = True + assert producer._should_trigger_trailing(sell_orders, current_price, False) is False + assert producer._should_trigger_trailing(sell_orders, decimal.Decimal(100), False) is True + assert producer._should_trigger_trailing(buy_orders, current_price, False) is True + + # D. no trailing if at least 1 order on each side + assert producer._should_trigger_trailing(buy_orders + sell_orders, current_price, False) is False + assert producer._should_trigger_trailing([buy_orders[0]] + sell_orders, current_price, False) is False + + # E. use open orders + assert producer._should_trigger_trailing([], current_price, False) is False + assert producer._should_trigger_trailing([], current_price, True) is False + + +async def test_should_trigger_trailing_all_buy_order_created(): + symbol = "BTC/USD" + async with _get_tools(symbol) as tools: + current_price = decimal.Decimal(4000) + producer, _, exchange_manager = tools + producer.increment = decimal.Decimal("0.02") # instead of 0.04 + # A. no open order: no trailing + assert producer._should_trigger_trailing([], current_price, False) is False + producer.enable_trailing_up = producer.enable_trailing_down = True + assert producer._should_trigger_trailing([], current_price, False) is False + + # create orders + trading_api.force_set_mark_price(exchange_manager, symbol, 4000) + with mock.patch.object(producer, "_ensure_current_price_in_limit_parameters", mock.Mock()) \ + as _ensure_current_price_in_limit_parameters_mock: + await producer._ensure_staggered_orders() + _ensure_current_price_in_limit_parameters_mock.assert_called_once() + + assert producer.current_price == 4000 + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + # now has buy and sell orders + + # B. trailing disabled + producer.enable_trailing_up = producer.enable_trailing_down = False + assert producer._should_trigger_trailing([], current_price, False) is False + open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY] + sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL] + assert len(buy_orders) > 10 + assert len(sell_orders) > 10 + assert producer._should_trigger_trailing(buy_orders, current_price, False) is False + assert producer._should_trigger_trailing(sell_orders, current_price, False) is False + + # C. trailing enabled + producer.enable_trailing_up = True + assert producer._should_trigger_trailing(sell_orders, current_price, False) is False + assert producer._should_trigger_trailing(sell_orders, None, False) is False + assert producer._should_trigger_trailing(buy_orders, current_price, False) is False + assert producer._should_trigger_trailing(buy_orders, decimal.Decimal(6000), False) is True + assert producer._should_trigger_trailing(buy_orders, None, True) is True + assert producer._should_trigger_trailing(buy_orders, None, False) is False + + producer.enable_trailing_down = True + assert producer._should_trigger_trailing(sell_orders, current_price, False) is False + assert producer._should_trigger_trailing(sell_orders, decimal.Decimal(2000), False) is True + assert producer._should_trigger_trailing(sell_orders, None, True) is True + assert producer._should_trigger_trailing(buy_orders, current_price, False) is False + assert producer._should_trigger_trailing(buy_orders, None, False) is False + assert producer._should_trigger_trailing(buy_orders, None, True) is True + + # D. no trailing if at least 1 order on each side + assert producer._should_trigger_trailing(buy_orders + sell_orders, current_price, False) is False + assert producer._should_trigger_trailing([buy_orders[0]] + sell_orders, current_price, False) is False + + # E. use open orders + assert producer._should_trigger_trailing([], current_price, False) is False + assert producer._should_trigger_trailing([], current_price, True) is False # has open orders on the other side + + +async def test_order_notification_callback(): + symbol = "BTC/USD" + async with _get_tools(symbol) as tools: + producer, _, exchange_manager = tools + producer.increment = decimal.Decimal("0.02") # replaces 0.04 + + # create orders + trading_api.force_set_mark_price(exchange_manager, symbol, 4000) + with mock.patch.object(producer, "_ensure_current_price_in_limit_parameters", mock.Mock()) \ + as _ensure_current_price_in_limit_parameters_mock: + await producer._ensure_staggered_orders() + _ensure_current_price_in_limit_parameters_mock.assert_called_once() + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + + # cancel sell orders and change reference price to 6000: should trigger trailing + open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY] + sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL] + for order in sell_orders: + await exchange_manager.trader.cancel_order(order) + trading_api.force_set_mark_price(exchange_manager, symbol, 6000) + filled_order = buy_orders[0] + filled_order.filled_price = 6000 + producer.enable_trailing_up = producer.enable_trailing_down = False + with mock.patch.object(producer, "_lock_portfolio_and_create_order_when_possible", mock.AsyncMock()) as _lock_portfolio_and_create_order_when_possible: + await _fill_order(filled_order, exchange_manager, trigger_update_callback=True) + # trailing disabled + _lock_portfolio_and_create_order_when_possible.assert_called_once() + assert len(exchange_manager.exchange_personal_data.orders_manager.get_open_orders()) == len(sell_orders) - 1 + + # will trail + filled_order = buy_orders[1] + filled_order.filled_price = 6000 + producer.enable_trailing_up = producer.enable_trailing_down = True + with mock.patch.object(producer, "_lock_portfolio_and_create_order_when_possible", mock.AsyncMock()) as _lock_portfolio_and_create_order_when_possible: + await _fill_order(filled_order, exchange_manager, trigger_update_callback=True) + # trailing trigger + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + updated_open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + buy_orders = [order for order in updated_open_orders if order.side == trading_enums.TradeOrderSide.BUY] + sell_orders = [order for order in updated_open_orders if order.side == trading_enums.TradeOrderSide.SELL] + assert len(buy_orders) == 25 + assert len(sell_orders) == 25 + # trailed instead + _lock_portfolio_and_create_order_when_possible.assert_not_called() + + filled_order = buy_orders[0] + with mock.patch.object(producer, "_lock_portfolio_and_create_order_when_possible", mock.AsyncMock()) as _lock_portfolio_and_create_order_when_possible: + await _fill_order(filled_order, exchange_manager, trigger_update_callback=True) + # do not trail again, create mirror order instead + _lock_portfolio_and_create_order_when_possible.assert_called_once() + + +async def test_create_mirror_order(): + symbol = "BTC/USD" + async with _get_tools(symbol) as tools: + producer, _, exchange_manager = tools + # create orders + price = 100 + producer.mode = staggered_orders_trading.StrategyModes.NEUTRAL + trading_api.force_set_mark_price(exchange_manager, producer.symbol, price) + await producer._ensure_staggered_orders() + await asyncio.create_task(_check_open_orders_count(exchange_manager, producer.operational_depth)) + + open_orders = exchange_manager.exchange_personal_data.orders_manager.get_open_orders() + buy_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.BUY] + sell_orders = [order for order in open_orders if order.side == trading_enums.TradeOrderSide.SELL] + buy_sell_increment = producer.flat_spread - producer.flat_increment + + # mirroring buy order + buy_1 = buy_orders[0] + assert buy_1.origin_price == decimal.Decimal("97") + assert buy_1.origin_quantity == decimal.Decimal("0.46") + assert buy_1.side == trading_enums.TradeOrderSide.BUY + buy_1_mirror_order = producer._create_mirror_order(buy_1.to_dict()) + assert isinstance(buy_1_mirror_order, staggered_orders_trading.OrderData) + assert buy_1_mirror_order.associated_entry_id == buy_1.order_id + assert buy_1_mirror_order.side == trading_enums.TradeOrderSide.SELL + assert buy_1_mirror_order.symbol == symbol + assert buy_1_mirror_order.price == decimal.Decimal("99") == buy_1.origin_price + buy_sell_increment + assert buy_1_mirror_order.quantity < buy_1.origin_quantity + assert buy_1_mirror_order.quantity == decimal.Decimal('0.4595400') + + # mirroring sell order + sell_1 = sell_orders[0] + assert sell_1.origin_price == decimal.Decimal("103") + assert sell_1.origin_quantity == decimal.Decimal('0.00464646') + assert sell_1.side == trading_enums.TradeOrderSide.SELL + sell_1_mirror_order = producer._create_mirror_order(sell_1.to_dict()) + assert isinstance(sell_1_mirror_order, staggered_orders_trading.OrderData) + assert sell_1_mirror_order.associated_entry_id == sell_1.order_id + assert sell_1_mirror_order.side == trading_enums.TradeOrderSide.BUY + assert sell_1_mirror_order.symbol == symbol + assert sell_1_mirror_order.price == decimal.Decimal("101") == sell_1.origin_price - buy_sell_increment + assert sell_1_mirror_order.quantity > sell_1.origin_quantity + assert sell_1_mirror_order.quantity == decimal.Decimal('0.004733730639801980198019801981') + + # fill price is != from origin price => use origin price to avoid moving grid orders + assert buy_1.origin_price == decimal.Decimal("97") + buy_1.filled_price = decimal.Decimal("96") # simulate fill at 96 + buy_2_mirror_order = producer._create_mirror_order(buy_1.to_dict()) + assert isinstance(buy_2_mirror_order, staggered_orders_trading.OrderData) + # mirror order price is still 99, even if fill price is not 97 + assert buy_2_mirror_order.price == decimal.Decimal("99") == buy_1.origin_price + buy_sell_increment + assert buy_2_mirror_order.side == trading_enums.TradeOrderSide.SELL + # new sell order quantity is equal to previous mirror order quantity: only the amount of USDT spend is smaller + assert buy_2_mirror_order.quantity == buy_1_mirror_order.quantity + assert buy_2_mirror_order.quantity == decimal.Decimal('0.4595400') + + # sell_1 will be found in trades + assert sell_1.origin_price == decimal.Decimal("103") + sell_1.filled_price = decimal.Decimal("110") # simulate fill at 110 + await _fill_order(sell_1, exchange_manager, trigger_update_callback=False, producer=producer) + maybe_trade, maybe_order = exchange_manager.exchange_personal_data.get_trade_or_open_order( + sell_1.order_id + ) + assert maybe_trade + assert maybe_trade.origin_price == decimal.Decimal("103") + assert maybe_order is None + sell_2_mirror_order = producer._create_mirror_order(sell_1.to_dict()) + # mirror order price is still 101, even if fill price is not 110 + assert sell_2_mirror_order.price == decimal.Decimal("101") == sell_1.origin_price - buy_sell_increment + assert sell_2_mirror_order.side == trading_enums.TradeOrderSide.BUY + # new buy order quantity is larger than previous one as sell order was filled at a higher price + assert sell_2_mirror_order.quantity > sell_1_mirror_order.quantity + assert sell_2_mirror_order.quantity == decimal.Decimal('0.005055854530099009900990099009') + + async def _wait_for_orders_creation(orders_count=1): for _ in range(orders_count): await asyncio_tools.wait_asyncio_next_cycle() @@ -1559,9 +1943,10 @@ async def _check_generate_orders(exchange_manager, producer, expected_buy_count, expected_sell_count, price, symbol_market): async with exchange_manager.exchange_personal_data.portfolio_manager.portfolio.lock: producer._refresh_symbol_data(symbol_market) - buy_orders, sell_orders = await producer._generate_staggered_orders(decimal.Decimal(str(price)), False) + buy_orders, sell_orders, triggering_trailing = await producer._generate_staggered_orders(decimal.Decimal(str(price)), False, False) assert len(buy_orders) == expected_buy_count assert len(sell_orders) == expected_sell_count + assert triggering_trailing is False assert all(o.price < price for o in buy_orders) assert all(o.price > price for o in sell_orders) @@ -1582,7 +1967,7 @@ async def _check_generate_orders(exchange_manager, producer, expected_buy_count, if staggered_orders: assert not any(order for order in staggered_orders if order.is_virtual) - await producer._create_not_virtual_orders(staggered_orders, price) + await producer._create_not_virtual_orders(staggered_orders, price, triggering_trailing) assert all(producer.highest_sell >= o.price >= producer.lowest_buy for o in sell_orders) @@ -1697,9 +2082,10 @@ def _check_orders(orders, strategy_mode, producer, exchange_manager): async def _light_check_orders(producer, exchange_manager, expected_buy_count, expected_sell_count, price): - buy_orders, sell_orders = await producer._generate_staggered_orders(decimal.Decimal(str(price)), False) + buy_orders, sell_orders, triggering_trailing = await producer._generate_staggered_orders(decimal.Decimal(str(price)), False, False) assert len(buy_orders) == expected_buy_count assert len(sell_orders) == expected_sell_count + assert triggering_trailing is False assert all(o.price < price for o in buy_orders) assert all(o.price > price for o in sell_orders) @@ -1714,7 +2100,7 @@ async def _light_check_orders(producer, exchange_manager, expected_buy_count, ex if staggered_orders: assert not any(order for order in staggered_orders if order.is_virtual) - await producer._create_not_virtual_orders(staggered_orders, price) + await producer._create_not_virtual_orders(staggered_orders, price, triggering_trailing) await asyncio.create_task(_wait_for_orders_creation(len(staggered_orders))) open_orders = trading_api.get_open_orders(exchange_manager) diff --git a/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py b/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py index cf2768caa..ebd8ea8cd 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py +++ b/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py @@ -387,7 +387,8 @@ async def test_signal_callback(tools): trading_api.load_pair_contract( exchange_manager, trading_api.create_default_future_contract( - "BTC/USDT", decimal.Decimal(4), trading_enums.FutureContractType.LINEAR_PERPETUAL + "BTC/USDT", decimal.Decimal(4), trading_enums.FutureContractType.LINEAR_PERPETUAL, + trading_constants.DEFAULT_SYMBOL_POSITION_MODE ).to_dict() ) await producer.signal_callback({ diff --git a/profiles/grid_trading/specific_config/GridTradingMode.json b/profiles/grid_trading/specific_config/GridTradingMode.json index 82435312e..4dfc03909 100644 --- a/profiles/grid_trading/specific_config/GridTradingMode.json +++ b/profiles/grid_trading/specific_config/GridTradingMode.json @@ -16,6 +16,9 @@ "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false, + "allow_funds_redispatch": false, + "enable_trailing_up": false, + "enable_trailing_down": false, "funds_redispatch_interval": 24 }, { @@ -33,6 +36,9 @@ "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false, + "allow_funds_redispatch": false, + "enable_trailing_up": false, + "enable_trailing_down": false, "funds_redispatch_interval": 24 }, { @@ -50,6 +56,9 @@ "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false, + "allow_funds_redispatch": false, + "enable_trailing_up": false, + "enable_trailing_down": false, "funds_redispatch_interval": 24 } ] diff --git a/profiles/trailing_grid_trading/profile.json b/profiles/trailing_grid_trading/profile.json new file mode 100644 index 000000000..31f10da2a --- /dev/null +++ b/profiles/trailing_grid_trading/profile.json @@ -0,0 +1,45 @@ +{ + "config": { + "crypto-currencies": { + "Bitcoin": { + "enabled": true, + "pairs": [ + "BTC/USDT" + ] + } + }, + "exchanges": { + "binance": { + "enabled": true + } + }, + "trader": { + "enabled": false, + "load-trade-history": false + }, + "trader-simulator": { + "enabled": true, + "fees": { + "maker": 0.1, + "taker": 0.1 + }, + "starting-portfolio": { + "BTC": 10, + "USDT": 1000 + } + }, + "trading": { + "reference-market": "USDT", + "risk": 0.5 + } + }, + "profile": { + "avatar": "default_profile.png", + "risk": 1, + "complexity": 1, + "description": "Trailing Grid Trading is a profile similar to the Grid Trading profile, except that it will create a trailing grid. When the BTC price will rise beyond the initial grid sell orders, the grid will automatically adapt to trade according to the updated price.", + "id": "trailing_grid_trading", + "name": "Trailing Grid Trading", + "read_only": true + } +} \ No newline at end of file diff --git a/profiles/trailing_grid_trading/specific_config/GridTradingMode.json b/profiles/trailing_grid_trading/specific_config/GridTradingMode.json new file mode 100644 index 000000000..71360f048 --- /dev/null +++ b/profiles/trailing_grid_trading/specific_config/GridTradingMode.json @@ -0,0 +1,25 @@ +{ + "required_strategies": [], + "pair_settings": [ + { + "pair": "BTC/USDT", + "flat_spread": 1300, + "flat_increment": 800, + "buy_orders_count": 15, + "sell_orders_count": 15, + "sell_funds": 0, + "buy_funds": 0, + "starting_price": 0, + "buy_volume_per_order": 0, + "sell_volume_per_order": 0, + "ignore_exchange_fees": false, + "use_fixed_volume_for_mirror_orders": false, + "mirror_order_delay": 0, + "use_existing_orders_only": false, + "allow_funds_redispatch": false, + "enable_trailing_up": true, + "enable_trailing_down": false, + "funds_redispatch_interval": 24 + } + ] +} \ No newline at end of file diff --git a/profiles/trailing_grid_trading/tentacles_config.json b/profiles/trailing_grid_trading/tentacles_config.json new file mode 100644 index 000000000..cb8f8cef6 --- /dev/null +++ b/profiles/trailing_grid_trading/tentacles_config.json @@ -0,0 +1,7 @@ +{ + "tentacle_activation": { + "Trading": { + "GridTradingMode": true + } + } +} \ No newline at end of file