Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Grid] add trailing option #1421

Merged
merged 8 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Comment on lines +227 to +228
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why False as default value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailing up could be true by default indeed.
However, trailing down will sell at a loss, I don't think it should be true by default

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should enable it by default ? I would say no but we create a default profile with a trailing grid instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea (only for trailing UP I think)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, only trailing up, i'll add it

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
Loading