Skip to content

Commit

Permalink
Merge pull request #1425 from Drakkar-Software/dev
Browse files Browse the repository at this point in the history
Dev merge
  • Loading branch information
GuillaumeDSM authored Jan 25, 2025
2 parents 15d8df9 + a9ea307 commit 7e23494
Show file tree
Hide file tree
Showing 14 changed files with 993 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)

Expand Down
11 changes: 8 additions & 3 deletions Trading/Mode/daily_trading_mode/daily_trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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} "
Expand Down
1 change: 1 addition & 0 deletions Trading/Mode/dca_trading_mode/dca_trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions Trading/Mode/grid_trading_mode/config/GridTradingMode.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
{
Expand All @@ -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
},
{
Expand All @@ -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
}
]
Expand Down
151 changes: 99 additions & 52 deletions Trading/Mode/grid_trading_mode/grid_trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -360,15 +396,15 @@ 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())
if interfering_orders_pairs:
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)
Expand All @@ -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,
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Trading/Mode/grid_trading_mode/resources/GridTradingMode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 7e23494

Please sign in to comment.