Skip to content

Commit aecc0bc

Browse files
committed
[TradingView] handle TRAILING_PROFILE for balanced order groups
1 parent 1c07407 commit aecc0bc

File tree

7 files changed

+297
-15
lines changed

7 files changed

+297
-15
lines changed

Trading/Mode/daily_trading_mode/daily_trading.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# You should have received a copy of the GNU Lesser General Public
1515
# License along with this library.
1616
import asyncio
17+
import copy
1718
import decimal
1819
import math
1920
import dataclasses
@@ -28,13 +29,13 @@
2829
import octobot_evaluators.constants as evaluators_constants
2930
import octobot_evaluators.enums as evaluators_enums
3031
import octobot_evaluators.matrix as matrix
31-
import octobot_trading.personal_data as trading_personal_data
3232
import octobot_trading.constants as trading_constants
3333
import octobot_trading.errors as trading_errors
34+
import octobot_trading.api as trading_api
3435
import octobot_trading.modes as trading_modes
3536
import octobot_trading.modes.script_keywords as script_keywords
3637
import octobot_trading.enums as trading_enums
37-
import octobot_trading.api as trading_api
38+
import octobot_trading.personal_data as trading_personal_data
3839

3940

4041
@dataclasses.dataclass
@@ -194,6 +195,7 @@ class DailyTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):
194195
TAKE_PROFIT_PRICE_KEY = "TAKE_PROFIT_PRICE"
195196
ADDITIONAL_TAKE_PROFIT_PRICES_KEY = "ADDITIONAL_TAKE_PROFIT_PRICES"
196197
STOP_ONLY = "STOP_ONLY"
198+
TRAILING_PROFILE = "TRAILING_PROFILE"
197199
REDUCE_ONLY_KEY = "REDUCE_ONLY"
198200
TAG_KEY = "TAG"
199201
EXCHANGE_ORDER_IDS = "EXCHANGE_ORDER_IDS"
@@ -494,7 +496,8 @@ async def _create_order(
494496
self, current_order,
495497
use_take_profit_orders, take_profits_details: list[OrderDetails],
496498
use_stop_loss_orders, stop_loss_details: list[OrderDetails],
497-
symbol_market, tag
499+
symbol_market, tag,
500+
trailing_profile_type: typing.Optional[trading_personal_data.TrailingProfileTypes]
498501
):
499502
params = {}
500503
chained_orders = []
@@ -548,10 +551,20 @@ async def _create_order(
548551
params.update(param_update)
549552
chained_orders.append(chained_order)
550553
if len(chained_orders) > 1:
551-
stop_count = len([o for o in chained_orders if trading_personal_data.is_stop_order(o.order_type)])
552-
tp_count = len(chained_orders) - stop_count
553-
group_type = trading_personal_data.OneCancelsTheOtherOrderGroup if stop_count == tp_count \
554-
else trading_personal_data.BalancedTakeProfitAndStopOrderGroup
554+
stop_orders = [o for o in chained_orders if trading_personal_data.is_stop_order(o.order_type)]
555+
tp_orders = [o for o in chained_orders if not trading_personal_data.is_stop_order(o.order_type)]
556+
if len(stop_orders) == len(tp_orders):
557+
group_type = trading_personal_data.OneCancelsTheOtherOrderGroup
558+
elif trailing_profile_type == trading_personal_data.TrailingProfileTypes.FILLED_TAKE_PROFIT:
559+
group_type = trading_personal_data.TrailingOnFilledTPBalancedOrderGroup
560+
entry_price = current_order.origin_price
561+
for stop_order in stop_orders:
562+
# register trailing profile in stop orders
563+
stop_order.trailing_profile = trading_personal_data.create_filled_take_profit_trailing_profile(
564+
entry_price, tp_orders
565+
)
566+
else:
567+
group_type = trading_personal_data.BalancedTakeProfitAndStopOrderGroup
555568
oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(group_type)
556569
for order in chained_orders:
557570
order.add_to_order_group(oco_group)
@@ -627,6 +640,9 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
627640
if create_stop_only and (not user_stop_price or user_stop_price.is_nan()):
628641
self.logger.error("Stop price is required to create a stop order")
629642
return []
643+
trailing_profile_type = trading_personal_data.TrailingProfileTypes(
644+
data[self.TRAILING_PROFILE]
645+
) if data.get(self.TRAILING_PROFILE) else None
630646
is_reducing_position = not increasing_position
631647
if self.USE_TARGET_PROFIT_MODE:
632648
if is_reducing_position:
@@ -697,7 +713,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
697713
current_order,
698714
use_chained_take_profit_orders, take_profit_order_details,
699715
use_chained_stop_loss_orders, stop_loss_order_details,
700-
symbol_market, tag
716+
symbol_market, tag,
717+
trailing_profile_type
701718
):
702719
created_orders.append(current_order)
703720

@@ -733,7 +750,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
733750
current_order,
734751
use_chained_take_profit_orders, take_profit_order_details,
735752
use_chained_stop_loss_orders, stop_loss_order_details,
736-
symbol_market, tag
753+
symbol_market, tag,
754+
trailing_profile_type
737755
)):
738756
if updated_limit:
739757
created_orders.append(updated_limit)
@@ -801,7 +819,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
801819
current_order,
802820
use_chained_take_profit_orders, take_profit_order_details,
803821
use_chained_stop_loss_orders, stop_loss_order_details,
804-
symbol_market, tag
822+
symbol_market, tag,
823+
trailing_profile_type
805824
)):
806825
if updated_limit:
807826
created_orders.append(updated_limit)
@@ -863,7 +882,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
863882
current_order,
864883
use_chained_take_profit_orders, take_profit_order_details,
865884
use_chained_stop_loss_orders, stop_loss_order_details,
866-
symbol_market, tag
885+
symbol_market, tag,
886+
trailing_profile_type
867887
):
868888
created_orders.append(current_order)
869889
if created_orders:

Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,7 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
958958
assert stop_order.associated_entry_ids == [buy_order.order_id]
959959
assert stop_order.tag == "super"
960960
assert stop_order.reduce_only is False
961+
assert stop_order.trailing_profile is None
961962
assert stop_order.is_open()
962963

963964
state = trading_enums.EvaluatorStates.LONG.value
@@ -977,6 +978,7 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
977978
assert take_profit_order.origin_price == decimal.Decimal("100000")
978979
assert take_profit_order.is_waiting_for_chained_trigger
979980
assert take_profit_order.associated_entry_ids == [buy_order.order_id]
981+
assert take_profit_order.trailing_profile is None
980982
assert not take_profit_order.is_open()
981983
assert not take_profit_order.is_created()
982984
assert take_profit_order.reduce_only is False
@@ -998,6 +1000,7 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
9981000
assert not take_profit_order.is_open()
9991001
assert not take_profit_order.is_created()
10001002
assert take_profit_order.reduce_only is False
1003+
assert take_profit_order.trailing_profile is None
10011004

10021005
# stop loss and take profit
10031006
data = {
@@ -1015,6 +1018,7 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
10151018
assert stop_order.origin_price == decimal.Decimal("123")
10161019
assert stop_order.is_waiting_for_chained_trigger
10171020
assert stop_order.associated_entry_ids == [buy_order.order_id]
1021+
assert stop_order.trailing_profile is None
10181022
assert not take_profit_order.is_open()
10191023
assert not take_profit_order.is_created()
10201024
take_profit_order = buy_order.chained_orders[1]
@@ -1029,6 +1033,7 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
10291033
assert take_profit_order.reduce_only is False
10301034
assert isinstance(stop_order.order_group, trading_personal_data.OneCancelsTheOtherOrderGroup)
10311035
assert take_profit_order.order_group is stop_order.order_group
1036+
assert take_profit_order.trailing_profile is None
10321037

10331038
# stop loss and take profit but decreasing position size: create stop loss and no take profit
10341039
# (this initial order is a take profit already)
@@ -1050,6 +1055,7 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
10501055
assert stop_loss.chained_orders == []
10511056
assert stop_loss.reduce_only is True # True as force stop loss
10521057
assert stop_loss.origin_price == decimal.Decimal("123")
1058+
assert stop_loss.trailing_profile is None
10531059
assert stop_loss.origin_quantity == decimal.Decimal("0.01") \
10541060
- trading_personal_data.get_fees_for_currency(sell_limit.fee, stop_loss.quantity_currency)
10551061

@@ -1087,11 +1093,12 @@ async def test_chained_multiple_take_profit_orders(tools):
10871093
assert not take_profit_order.is_open()
10881094
assert not take_profit_order.is_created()
10891095
assert take_profit_order.update_with_triggering_order_fees == is_last
1096+
assert take_profit_order.trailing_profile is None
10901097

10911098
# only 2 additional (2 in total)
10921099
data = {
10931100
consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal("110000"), decimal.Decimal("120000")],
1094-
consumer.VOLUME_KEY: decimal.Decimal("0.01"),
1101+
consumer.VOLUME_KEY: decimal.Decimal("0.01"), consumer.TRAILING_PROFILE: None
10951102
}
10961103
orders_with_tps = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)
10971104
buy_order = orders_with_tps[0]
@@ -1110,6 +1117,7 @@ async def test_chained_multiple_take_profit_orders(tools):
11101117
assert not take_profit_order.is_open()
11111118
assert not take_profit_order.is_created()
11121119
assert take_profit_order.update_with_triggering_order_fees == is_last
1120+
assert take_profit_order.trailing_profile is None
11131121

11141122
# stop loss and 1 take profit and 5 additional (6 TP in total)
11151123
tp_prices = [
@@ -1134,6 +1142,7 @@ async def test_chained_multiple_take_profit_orders(tools):
11341142
assert stop_order.is_waiting_for_chained_trigger
11351143
assert stop_order.associated_entry_ids == [buy_order.order_id]
11361144
assert stop_order.update_with_triggering_order_fees is True
1145+
assert stop_order.trailing_profile is None
11371146
assert len(buy_order.chained_orders[1:]) == len(tp_prices)
11381147
for i, take_profit_order in enumerate(buy_order.chained_orders[1:]):
11391148
is_last = i == len(buy_order.chained_orders[1:]) - 1
@@ -1150,6 +1159,64 @@ async def test_chained_multiple_take_profit_orders(tools):
11501159
assert isinstance(stop_order.order_group, trading_personal_data.BalancedTakeProfitAndStopOrderGroup)
11511160
assert take_profit_order.order_group is stop_order.order_group
11521161
assert take_profit_order.update_with_triggering_order_fees == is_last
1162+
assert take_profit_order.trailing_profile is None
1163+
1164+
1165+
async def test_chained_multiple_take_profit_with_filled_tp_trailing_stop_orders(tools):
1166+
exchange_manager, trader, symbol, consumer, last_btc_price = tools
1167+
1168+
# with BTC/USDT
1169+
exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \
1170+
last_btc_price
1171+
exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \
1172+
decimal.Decimal(str(10 + 1000 / last_btc_price))
1173+
1174+
state = trading_enums.EvaluatorStates.LONG.value
1175+
# stop loss and 1 take profit and 5 additional (6 TP in total)
1176+
tp_prices = [
1177+
decimal.Decimal("100012"),
1178+
decimal.Decimal("110000"), decimal.Decimal("120000"), decimal.Decimal("130000"),
1179+
decimal.Decimal("140000"), decimal.Decimal("150000")
1180+
]
1181+
data = {
1182+
consumer.STOP_PRICE_KEY: decimal.Decimal("123"),
1183+
consumer.TAKE_PROFIT_PRICE_KEY: tp_prices[0],
1184+
consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: tp_prices[1:],
1185+
consumer.VOLUME_KEY: decimal.Decimal("0.01"),
1186+
consumer.TRAILING_PROFILE: trading_personal_data.TrailingProfileTypes.FILLED_TAKE_PROFIT.value,
1187+
}
1188+
orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state, data=data)
1189+
buy_order = orders_with_tp[0]
1190+
assert len(buy_order.chained_orders) == 1 + len(tp_prices)
1191+
stop_order = buy_order.chained_orders[0]
1192+
assert isinstance(stop_order, trading_personal_data.StopLossOrder)
1193+
assert stop_order.origin_quantity == decimal.Decimal("0.01") \
1194+
- trading_personal_data.get_fees_for_currency(buy_order.fee, stop_order.quantity_currency)
1195+
assert stop_order.origin_price == decimal.Decimal("123")
1196+
assert stop_order.is_waiting_for_chained_trigger
1197+
assert stop_order.associated_entry_ids == [buy_order.order_id]
1198+
assert stop_order.update_with_triggering_order_fees is True
1199+
assert stop_order.trailing_profile == trading_personal_data.FilledTakeProfitTrailingProfile([
1200+
trading_personal_data.TrailingPriceStep(float(trailing_price), float(trigger_price), True)
1201+
for trailing_price, trigger_price in zip([buy_order.origin_price] + tp_prices[:-1], tp_prices)
1202+
])
1203+
assert len(buy_order.chained_orders[1:]) == len(tp_prices)
1204+
for i, take_profit_order in enumerate(buy_order.chained_orders[1:]):
1205+
is_last = i == len(buy_order.chained_orders[1:]) - 1
1206+
assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)
1207+
assert take_profit_order.origin_quantity == (
1208+
decimal.Decimal("0.01")
1209+
- trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)
1210+
) / decimal.Decimal(str(len(tp_prices)))
1211+
assert take_profit_order.origin_price == tp_prices[i]
1212+
assert take_profit_order.is_waiting_for_chained_trigger
1213+
assert take_profit_order.associated_entry_ids == [buy_order.order_id]
1214+
assert not take_profit_order.is_open()
1215+
assert not take_profit_order.is_created()
1216+
assert isinstance(stop_order.order_group, trading_personal_data.TrailingOnFilledTPBalancedOrderGroup)
1217+
assert take_profit_order.order_group is stop_order.order_group
1218+
assert take_profit_order.update_with_triggering_order_fees == is_last
1219+
assert take_profit_order.trailing_profile is None
11531220

11541221

11551222
async def test_create_stop_loss_orders(tools):

Trading/Mode/remote_trading_signals_trading_mode/remote_trading_signals_trading.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,16 @@ async def _create_order(self, order_description, symbol, created_groups, fees_cu
383383
trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value, None
384384
)
385385
trigger_above = order_description.get(trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value, None)
386+
trailing_profile = None
387+
if trailing_profile_details := order_description.get(
388+
trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE.value
389+
):
390+
trailing_profile = personal_data.create_trailing_profile(
391+
personal_data.TrailingProfileTypes(
392+
order_description[trading_enums.TradingSignalOrdersAttrs.TRAILING_PROFILE_TYPE.value],
393+
),
394+
trailing_profile_details
395+
)
386396
order = personal_data.create_order_instance(
387397
trader=self.exchange_manager.trader,
388398
order_type=order_type,
@@ -397,7 +407,8 @@ async def _create_order(self, order_description, symbol, created_groups, fees_cu
397407
group=group,
398408
fees_currency_side=fees_currency_side,
399409
reduce_only=reduce_only,
400-
associated_entry_id=associated_entries[0] if associated_entries else None
410+
associated_entry_id=associated_entries[0] if associated_entries else None,
411+
trailing_profile=trailing_profile,
401412
)
402413
if associated_entries and len(associated_entries) > 1:
403414
for associated_entry in associated_entries[1:]:
@@ -409,7 +420,7 @@ async def _create_order(self, order_description, symbol, created_groups, fees_cu
409420

410421
def _get_or_create_order_group(self, order_description, group_id):
411422
group_type = order_description[trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value]
412-
group_class = tentacles_management.get_class_from_parent_subclasses(group_type, personal_data.OrderGroup)
423+
group_class = tentacles_management.get_deep_class_from_parent_subclasses(group_type, personal_data.OrderGroup)
413424
return self.exchange_manager.exchange_personal_data.orders_manager.get_or_create_group(group_class, group_id)
414425

415426
async def _create_orders(self, orders_descriptions, symbol):

0 commit comments

Comments
 (0)