@@ -168,7 +168,25 @@ def init_user_inputs(self, inputs: dict) -> None:
168168 "fill. OctoBot won't create orders at startup: it will use the ones already on exchange instead. "
169169 "This mode allows grid orders to operate on user created orders. Can't work on trading simulator." ,
170170 )
171- self .UI .user_input (
171+ self .UI .user_input (
172+ self .CONFIG_ENABLE_TRAILING_UP , commons_enums .UserInputTypes .BOOLEAN ,
173+ default_config [self .CONFIG_ENABLE_TRAILING_UP ], inputs ,
174+ parent_input_name = self .CONFIG_PAIR_SETTINGS ,
175+ title = "Trailing up: when checked, the whole grid will be cancelled and recreated when price goes above the "
176+ "highest selling price. This might require the grid to perform a buy market order to be "
177+ "able to recreate the grid new sell orders at the updated price." ,
178+ )
179+ self .UI .user_input (
180+ self .CONFIG_ENABLE_TRAILING_DOWN , commons_enums .UserInputTypes .BOOLEAN ,
181+ default_config [self .CONFIG_ENABLE_TRAILING_DOWN ], inputs ,
182+ parent_input_name = self .CONFIG_PAIR_SETTINGS ,
183+ title = "Trailing down: when checked, the whole grid will be cancelled and recreated when price goes bellow"
184+ " the lowest buying price. This might require the grid to perform a sell market order to be "
185+ "able to recreate the grid new buy orders at the updated price. "
186+ "Warning: when trailing down, the sell order required to recreate the buying side of the grid "
187+ "might generate a loss." ,
188+ )
189+ self .UI .user_input (
172190 self .CONFIG_ALLOW_FUNDS_REDISPATCH , commons_enums .UserInputTypes .BOOLEAN ,
173191 default_config [self .CONFIG_ALLOW_FUNDS_REDISPATCH ], inputs ,
174192 parent_input_name = self .CONFIG_PAIR_SETTINGS ,
@@ -206,6 +224,8 @@ def get_default_pair_config(self, symbol, flat_spread, flat_increment) -> dict:
206224 self .CONFIG_USE_FIXED_VOLUMES_FOR_MIRROR_ORDERS : False ,
207225 self .CONFIG_USE_EXISTING_ORDERS_ONLY : False ,
208226 self .CONFIG_ALLOW_FUNDS_REDISPATCH : False ,
227+ self .CONFIG_ENABLE_TRAILING_UP : False ,
228+ self .CONFIG_ENABLE_TRAILING_DOWN : False ,
209229 self .CONFIG_FUNDS_REDISPATCH_INTERVAL : 24 ,
210230 }
211231
@@ -317,15 +337,31 @@ def read_config(self):
317337 self .compensate_for_missed_mirror_order = self .symbol_trading_config .get (
318338 self .trading_mode .COMPENSATE_FOR_MISSED_MIRROR_ORDER , self .compensate_for_missed_mirror_order
319339 )
340+ self .enable_trailing_up = self .symbol_trading_config .get (
341+ self .trading_mode .CONFIG_ENABLE_TRAILING_UP , self .enable_trailing_up
342+ )
343+ self .enable_trailing_down = self .symbol_trading_config .get (
344+ self .trading_mode .CONFIG_ENABLE_TRAILING_DOWN , self .enable_trailing_down
345+ )
320346
321- async def _handle_staggered_orders (self , current_price , ignore_mirror_orders_only , ignore_available_funds ):
347+ async def _handle_staggered_orders (
348+ self , current_price , ignore_mirror_orders_only , ignore_available_funds , trigger_trailing
349+ ):
322350 self ._init_allowed_price_ranges (current_price )
323351 if ignore_mirror_orders_only or not self .use_existing_orders_only :
324352 async with self .producer_exchange_wide_lock (self .exchange_manager ):
353+ if trigger_trailing and self .is_currently_trailing :
354+ self .logger .debug (
355+ f"{ self .symbol } on { self .exchange_name } : trailing signal ignored: "
356+ f"a trailing process is already running"
357+ )
358+ return
325359 # use exchange level lock to prevent funds double spend
326- buy_orders , sell_orders = await self ._generate_staggered_orders (current_price , ignore_available_funds )
360+ buy_orders , sell_orders , triggering_trailing = await self ._generate_staggered_orders (
361+ current_price , ignore_available_funds , trigger_trailing
362+ )
327363 grid_orders = self ._merged_and_sort_not_virtual_orders (buy_orders , sell_orders )
328- await self ._create_not_virtual_orders (grid_orders , current_price )
364+ await self ._create_not_virtual_orders (grid_orders , current_price , triggering_trailing )
329365
330366 async def trigger_staggered_orders_creation (self ):
331367 # reload configuration
@@ -360,15 +396,15 @@ def _apply_default_symbol_config(self) -> bool:
360396 )
361397 return True
362398
363- async def _generate_staggered_orders (self , current_price , ignore_available_funds ):
399+ async def _generate_staggered_orders (self , current_price , ignore_available_funds , trigger_trailing ):
364400 order_manager = self .exchange_manager .exchange_personal_data .orders_manager
365401 if not self .single_pair_setup :
366402 interfering_orders_pairs = self ._get_interfering_orders_pairs (order_manager .get_open_orders ())
367403 if interfering_orders_pairs :
368404 self .logger .error (f"Impossible to create grid orders for { self .symbol } with interfering orders "
369405 f"using pair(s): { interfering_orders_pairs } . Configure funds to use for each pairs "
370406 f"to be able to use interfering pairs." )
371- return [], []
407+ return [], [], False
372408 existing_orders = order_manager .get_open_orders (self .symbol )
373409
374410 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
395431 highest_buy = self .buy_price_range .higher_bound
396432 lowest_sell = self .sell_price_range .lower_bound
397433 highest_sell = self .sell_price_range .higher_bound
398- if sorted_orders :
399- buy_orders = [order for order in sorted_orders if order .side == trading_enums .TradeOrderSide .BUY ]
400- highest_buy = current_price
401- sell_orders = [order for order in sorted_orders if order .side == trading_enums .TradeOrderSide .SELL ]
402- lowest_sell = current_price
403- origin_created_buy_orders_count , origin_created_sell_orders_count = self ._get_origin_orders_count (
404- sorted_orders , recently_closed_trades
405- )
406-
407- min_max_total_order_price_delta = (
408- self .flat_increment * (origin_created_buy_orders_count - 1 + origin_created_sell_orders_count - 1 )
409- + self .flat_increment
410- )
411- if buy_orders :
412- lowest_buy = buy_orders [0 ].origin_price
413- if not sell_orders :
414- highest_buy = min (current_price , lowest_buy + min_max_total_order_price_delta )
415- # buy orders only
416- lowest_sell = highest_buy + self .flat_spread - self .flat_increment
417- highest_sell = lowest_buy + min_max_total_order_price_delta + self .flat_spread - self .flat_increment
418- else :
419- # use only open order prices when possible
420- _highest_sell = sell_orders [- 1 ].origin_price
421- highest_buy = min (current_price , _highest_sell - self .flat_spread + self .flat_increment )
422- if sell_orders :
423- highest_sell = sell_orders [- 1 ].origin_price
424- if not buy_orders :
425- lowest_sell = max (current_price , highest_sell - min_max_total_order_price_delta )
426- # sell orders only
427- lowest_buy = max (
428- 0 , highest_sell - min_max_total_order_price_delta - self .flat_spread + self .flat_increment
429- )
430- highest_buy = lowest_sell - self .flat_spread + self .flat_increment
431- else :
432- # use only open order prices when possible
433- _lowest_buy = buy_orders [0 ].origin_price
434- lowest_sell = max (current_price , _lowest_buy - self .flat_spread + self .flat_increment )
435-
436- missing_orders , state , _ = self ._analyse_current_orders_situation (
437- sorted_orders , recently_closed_trades , lowest_buy , highest_sell , current_price
438- )
439- if missing_orders :
440- self .logger .info (
441- f"{ len (missing_orders )} missing { self .symbol } orders on { self .exchange_name } : { missing_orders } "
434+ if sorted_orders and not trigger_trailing :
435+ if self ._should_trigger_trailing (sorted_orders , current_price , False ):
436+ trigger_trailing = True
437+ else :
438+ buy_orders = [order for order in sorted_orders if order .side == trading_enums .TradeOrderSide .BUY ]
439+ sell_orders = [order for order in sorted_orders if order .side == trading_enums .TradeOrderSide .SELL ]
440+ highest_buy = current_price
441+ lowest_sell = current_price
442+ origin_created_buy_orders_count , origin_created_sell_orders_count = self ._get_origin_orders_count (
443+ sorted_orders , recently_closed_trades
444+ )
445+
446+ min_max_total_order_price_delta = (
447+ self .flat_increment * (origin_created_buy_orders_count - 1 + origin_created_sell_orders_count - 1 )
448+ + self .flat_increment
449+ )
450+ if buy_orders :
451+ lowest_buy = buy_orders [0 ].origin_price
452+ if not sell_orders :
453+ highest_buy = min (current_price , lowest_buy + min_max_total_order_price_delta )
454+ # buy orders only
455+ lowest_sell = highest_buy + self .flat_spread - self .flat_increment
456+ highest_sell = lowest_buy + min_max_total_order_price_delta + self .flat_spread - self .flat_increment
457+ else :
458+ # use only open order prices when possible
459+ _highest_sell = sell_orders [- 1 ].origin_price
460+ highest_buy = min (current_price , _highest_sell - self .flat_spread + self .flat_increment )
461+ if sell_orders :
462+ highest_sell = sell_orders [- 1 ].origin_price
463+ if not buy_orders :
464+ lowest_sell = max (current_price , highest_sell - min_max_total_order_price_delta )
465+ # sell orders only
466+ lowest_buy = max (
467+ 0 , highest_sell - min_max_total_order_price_delta - self .flat_spread + self .flat_increment
468+ )
469+ highest_buy = lowest_sell - self .flat_spread + self .flat_increment
470+ else :
471+ # use only open order prices when possible
472+ _lowest_buy = buy_orders [0 ].origin_price
473+ lowest_sell = max (current_price , _lowest_buy - self .flat_spread + self .flat_increment )
474+ if trigger_trailing :
475+ await self ._prepare_trailing (sorted_orders , current_price )
476+ self .is_currently_trailing = True
477+ # trailing will cancel all orders: set state to NEW with no existing order
478+ missing_orders , state , sorted_orders = None , self .NEW , []
479+ else :
480+ # no trailing, process normal analysis
481+ missing_orders , state , _ = self ._analyse_current_orders_situation (
482+ sorted_orders , recently_closed_trades , lowest_buy , highest_sell , current_price
442483 )
443- await self ._handle_missed_mirror_orders_fills (recently_closed_trades , missing_orders , current_price )
484+ if missing_orders :
485+ self .logger .info (
486+ f"{ len (missing_orders )} missing { self .symbol } orders on { self .exchange_name } : { missing_orders } "
487+ )
488+ await self ._handle_missed_mirror_orders_fills (recently_closed_trades , missing_orders , current_price )
444489 try :
490+ # apply state and (re)create missing orders
445491 buy_orders = self ._create_orders (lowest_buy , highest_buy ,
446492 trading_enums .TradeOrderSide .BUY , sorted_orders ,
447493 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
461507 buy_orders , sell_orders , state = await self ._reset_orders (
462508 sorted_orders , lowest_buy , highest_buy , lowest_sell , highest_sell , current_price , ignore_available_funds
463509 )
510+ trigger_trailing = False
464511
465- return buy_orders , sell_orders
512+ return buy_orders , sell_orders , trigger_trailing
466513
467514 def _get_origin_orders_count (self , recent_trades , open_orders ):
468515 origin_created_buy_orders_count = self .buy_orders_count
0 commit comments