Skip to content

Commit 7e23494

Browse files
authored
Merge pull request #1425 from Drakkar-Software/dev
Dev merge
2 parents 15d8df9 + a9ea307 commit 7e23494

File tree

14 files changed

+993
-120
lines changed

14 files changed

+993
-120
lines changed

Meta/Keywords/scripting_library/tests/orders/order_types/test_multiple_orders_creation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ async def test_orders_amount_then_position_sequence(mock_context):
107107
api.load_pair_contract(
108108
mock_context.exchange_manager,
109109
api.create_default_future_contract(
110-
mock_context.symbol, decimal.Decimal(1), trading_enums.FutureContractType.LINEAR_PERPETUAL
110+
mock_context.symbol, decimal.Decimal(1), trading_enums.FutureContractType.LINEAR_PERPETUAL,
111+
trading_constants.DEFAULT_SYMBOL_POSITION_MODE
111112
).to_dict()
112113
)
113114

Trading/Mode/daily_trading_mode/daily_trading.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
587587
spot_increasing_position = state in (trading_enums.EvaluatorStates.VERY_LONG.value,
588588
trading_enums.EvaluatorStates.LONG.value)
589589
if self.exchange_manager.is_future:
590+
self.trading_mode.ensure_supported(symbol)
590591
# on futures, current_symbol_holding = current_market_holding = market_quantity
591592
max_buy_size, buy_increasing_position = trading_personal_data.get_futures_max_order_size(
592593
self.exchange_manager, symbol, trading_enums.TradeOrderSide.BUY,
@@ -868,9 +869,13 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
868869
raise trading_errors.OrderCreationError()
869870
raise trading_errors.MissingMinimalExchangeTradeVolume()
870871

871-
except (trading_errors.MissingFunds,
872-
trading_errors.MissingMinimalExchangeTradeVolume,
873-
trading_errors.OrderCreationError):
872+
except (
873+
trading_errors.MissingFunds,
874+
trading_errors.MissingMinimalExchangeTradeVolume,
875+
trading_errors.OrderCreationError,
876+
trading_errors.InvalidPositionSide,
877+
trading_errors.UnsupportedContractConfigurationError
878+
):
874879
raise
875880
except asyncio.TimeoutError as e:
876881
self.logger.error(f"Impossible to create order for {symbol} on {self.exchange_manager.exchange_name}: {e} "

Trading/Mode/dca_trading_mode/dca_trading.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ async def create_new_orders(self, symbol, _, state, **kwargs):
134134
)
135135
)
136136
if self.exchange_manager.is_future:
137+
self.trading_mode.ensure_supported(symbol)
137138
# on futures, current_symbol_holding = current_market_holding = market_quantity
138139
initial_available_funds, _ = trading_personal_data.get_futures_max_order_size(
139140
self.exchange_manager, symbol, side,

Trading/Mode/grid_trading_mode/config/GridTradingMode.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"mirror_order_delay": 0,
1818
"use_existing_orders_only": false,
1919
"allow_funds_redispatch": false,
20+
"enable_trailing_up": false,
21+
"enable_trailing_down": false,
2022
"funds_redispatch_interval": 24
2123
},
2224
{
@@ -34,6 +36,9 @@
3436
"use_fixed_volume_for_mirror_orders": false,
3537
"mirror_order_delay": 0,
3638
"use_existing_orders_only": false,
39+
"allow_funds_redispatch": false,
40+
"enable_trailing_up": false,
41+
"enable_trailing_down": false,
3742
"funds_redispatch_interval": 24
3843
},
3944
{
@@ -51,6 +56,9 @@
5156
"use_fixed_volume_for_mirror_orders": false,
5257
"mirror_order_delay": 0,
5358
"use_existing_orders_only": false,
59+
"allow_funds_redispatch": false,
60+
"enable_trailing_up": false,
61+
"enable_trailing_down": false,
5462
"funds_redispatch_interval": 24
5563
}
5664
]

Trading/Mode/grid_trading_mode/grid_trading.py

Lines changed: 99 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Trading/Mode/grid_trading_mode/resources/GridTradingMode.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ You can customize the grid for each trading pair. To configure a pair, enter:
2020
- The interval between each order (increment)
2121
- The amount of initial buy and sell orders to create
2222

23+
#### Trailing options
24+
A grid can only operate within its price range. However, when trailing options are enabled,
25+
the whole grid can be automatically cancelled and recreated
26+
when the traded asset's price moves beyond the grid range. In this case, a market order can be executed in order to
27+
have the necessary funds to create the grid buy and sell orders.
28+
2329
#### Profits
2430
Profits will be made from price movements within the covered price area.
2531
It never "sells at a loss", but always at a profit, therefore OctoBot never cancels any orders when using the Grid Trading Mode.

0 commit comments

Comments
 (0)