|
16 | 16 | import asyncio |
17 | 17 | import decimal |
18 | 18 | import math |
| 19 | +import dataclasses |
| 20 | +import typing |
19 | 21 |
|
20 | 22 | import octobot_commons.constants as commons_constants |
21 | 23 | import octobot_commons.enums as commons_enums |
|
35 | 37 | import octobot_trading.api as trading_api |
36 | 38 |
|
37 | 39 |
|
| 40 | +@dataclasses.dataclass |
| 41 | +class OrderDetails: |
| 42 | + price: decimal.Decimal |
| 43 | + quantity: typing.Optional[decimal.Decimal] |
| 44 | + |
| 45 | + |
38 | 46 | class DailyTradingMode(trading_modes.AbstractTradingMode): |
39 | 47 |
|
40 | 48 | def init_user_inputs(self, inputs: dict) -> None: |
@@ -184,6 +192,7 @@ class DailyTradingModeConsumer(trading_modes.AbstractTradingModeConsumer): |
184 | 192 | VOLUME_KEY = "VOLUME" |
185 | 193 | STOP_PRICE_KEY = "STOP_PRICE" |
186 | 194 | TAKE_PROFIT_PRICE_KEY = "TAKE_PROFIT_PRICE" |
| 195 | + ADDITIONAL_TAKE_PROFIT_PRICES_KEY = "ADDITIONAL_TAKE_PROFIT_PRICES" |
187 | 196 | STOP_ONLY = "STOP_ONLY" |
188 | 197 | REDUCE_ONLY_KEY = "REDUCE_ONLY" |
189 | 198 | TAG_KEY = "TAG" |
@@ -468,48 +477,75 @@ def _get_max_amount_from_max_ratio(self, max_ratio, quantity, quote, default_rat |
468 | 477 | f"Set it to 100 to buy anyway.") |
469 | 478 | return trading_constants.ZERO |
470 | 479 |
|
| 480 | + def _get_split_take_profit_details( |
| 481 | + self, order_details: list[OrderDetails], total_quantity: decimal.Decimal, symbol_market |
| 482 | + ): |
| 483 | + prices = [order_detail.price for order_detail in order_details] |
| 484 | + quantities, prices = trading_personal_data.get_valid_split_orders( |
| 485 | + total_quantity, prices, symbol_market |
| 486 | + ) |
| 487 | + return [ |
| 488 | + OrderDetails(price, quantity) |
| 489 | + for quantity, price in zip(quantities, prices) |
| 490 | + ] |
| 491 | + |
471 | 492 | async def _create_order( |
472 | 493 | self, current_order, |
473 | | - use_take_profit_orders, take_profit_price, |
474 | | - use_stop_loss_orders, stop_price, |
| 494 | + use_take_profit_orders, take_profits_details: list[OrderDetails], |
| 495 | + use_stop_loss_orders, stop_loss_details: list[OrderDetails], |
475 | 496 | symbol_market, tag |
476 | 497 | ): |
477 | 498 | params = {} |
478 | 499 | chained_orders = [] |
479 | 500 | is_long = current_order.side is trading_enums.TradeOrderSide.BUY |
480 | 501 | exit_side = trading_enums.TradeOrderSide.SELL if is_long else trading_enums.TradeOrderSide.BUY |
481 | 502 | if use_stop_loss_orders: |
| 503 | + if len(stop_loss_details) > 1: |
| 504 | + self.logger.error(f"Multiple stop loss orders is not supported.") |
482 | 505 | stop_price = trading_personal_data.decimal_adapt_price( |
483 | 506 | symbol_market, |
484 | 507 | current_order.origin_price * ( |
485 | 508 | trading_constants.ONE + (self.TARGET_PROFIT_STOP_LOSS * (-1 if is_long else 1)) |
486 | 509 | ) |
487 | | - ) if stop_price.is_nan() else stop_price |
| 510 | + ) if (not stop_loss_details or stop_loss_details[0].price.is_nan()) else stop_loss_details[0].price |
488 | 511 | param_update, chained_order = await self.register_chained_order( |
489 | 512 | current_order, stop_price, trading_enums.TraderOrderType.STOP_LOSS, exit_side, tag=tag |
490 | 513 | ) |
491 | 514 | params.update(param_update) |
492 | 515 | chained_orders.append(chained_order) |
493 | 516 | if use_take_profit_orders: |
494 | | - take_profit_price = trading_personal_data.decimal_adapt_price( |
495 | | - symbol_market, |
496 | | - current_order.origin_price * ( |
497 | | - trading_constants.ONE + (self.TARGET_PROFIT_TAKE_PROFIT * (1 if is_long else -1)) |
| 517 | + if take_profits_details: |
| 518 | + local_take_profits_details = self._get_split_take_profit_details( |
| 519 | + take_profits_details, current_order.origin_quantity, symbol_market |
498 | 520 | ) |
499 | | - ) if take_profit_price.is_nan() else take_profit_price |
500 | | - order_type = self.exchange_manager.trader.get_take_profit_order_type( |
501 | | - current_order, |
502 | | - trading_enums.TraderOrderType.SELL_LIMIT if exit_side is trading_enums.TradeOrderSide.SELL |
503 | | - else trading_enums.TraderOrderType.BUY_LIMIT |
504 | | - ) |
505 | | - param_update, chained_order = await self.register_chained_order( |
506 | | - current_order, take_profit_price, order_type, exit_side, tag=tag |
507 | | - ) |
508 | | - params.update(param_update) |
509 | | - chained_orders.append(chained_order) |
| 521 | + else: |
| 522 | + local_take_profits_details = [ |
| 523 | + OrderDetails(decimal.Decimal("nan"), current_order.origin_quantity) |
| 524 | + ] |
| 525 | + for take_profits_detail in local_take_profits_details: |
| 526 | + take_profit_price = trading_personal_data.decimal_adapt_price( |
| 527 | + symbol_market, |
| 528 | + current_order.origin_price * ( |
| 529 | + trading_constants.ONE + (self.TARGET_PROFIT_TAKE_PROFIT * (1 if is_long else -1)) |
| 530 | + ) |
| 531 | + ) if take_profits_detail.price.is_nan() else take_profits_detail.price |
| 532 | + order_type = self.exchange_manager.trader.get_take_profit_order_type( |
| 533 | + current_order, |
| 534 | + trading_enums.TraderOrderType.SELL_LIMIT if exit_side is trading_enums.TradeOrderSide.SELL |
| 535 | + else trading_enums.TraderOrderType.BUY_LIMIT |
| 536 | + ) |
| 537 | + param_update, chained_order = await self.register_chained_order( |
| 538 | + current_order, take_profit_price, order_type, exit_side, |
| 539 | + quantity=take_profits_detail.quantity, tag=tag |
| 540 | + ) |
| 541 | + params.update(param_update) |
| 542 | + chained_orders.append(chained_order) |
510 | 543 | if len(chained_orders) > 1: |
511 | | - oco_group = self.exchange_manager.exchange_personal_data.orders_manager \ |
512 | | - .create_group(trading_personal_data.OneCancelsTheOtherOrderGroup) |
| 544 | + stop_count = len([o for o in chained_orders if trading_personal_data.is_stop_order(o.order_type)]) |
| 545 | + tp_count = len(chained_orders) - stop_count |
| 546 | + group_type = trading_personal_data.OneCancelsTheOtherOrderGroup if stop_count == tp_count \ |
| 547 | + else trading_personal_data.BalancedTakeProfitAndStopOrderGroup |
| 548 | + oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(group_type) |
513 | 549 | for order in chained_orders: |
514 | 550 | order.add_to_order_group(oco_group) |
515 | 551 | return await self.trading_mode.create_order(current_order, params=params or None) |
@@ -568,6 +604,13 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): |
568 | 604 | symbol_market, |
569 | 605 | data.get(self.TAKE_PROFIT_PRICE_KEY, decimal.Decimal(math.nan)) |
570 | 606 | ) |
| 607 | + additional_user_take_profit_prices = [ |
| 608 | + trading_personal_data.decimal_adapt_price( |
| 609 | + symbol_market, |
| 610 | + price |
| 611 | + ) |
| 612 | + for price in (data.get(self.ADDITIONAL_TAKE_PROFIT_PRICES_KEY) or []) |
| 613 | + ] |
571 | 614 | user_stop_price = trading_personal_data.decimal_adapt_price( |
572 | 615 | symbol_market, |
573 | 616 | data.get(self.STOP_PRICE_KEY, decimal.Decimal(math.nan)) |
@@ -599,11 +642,24 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): |
599 | 642 | use_stop_orders = is_reducing_position and (self.USE_STOP_ORDERS or not user_stop_price.is_nan()) |
600 | 643 | # use stop loss when increasing the position and the user explicitly asks for one |
601 | 644 | use_chained_take_profit_orders = increasing_position and ( |
602 | | - not user_take_profit_price.is_nan() or self.USE_TARGET_PROFIT_MODE |
| 645 | + (not user_take_profit_price.is_nan() or additional_user_take_profit_prices) |
| 646 | + or self.USE_TARGET_PROFIT_MODE |
603 | 647 | ) |
604 | 648 | use_chained_stop_loss_orders = increasing_position and ( |
605 | 649 | not user_stop_price.is_nan() or (self.USE_TARGET_PROFIT_MODE and self.USE_STOP_ORDERS) |
606 | 650 | ) |
| 651 | + stop_loss_order_details = take_profit_order_details = [] |
| 652 | + if use_chained_take_profit_orders: |
| 653 | + take_profit_order_details = [] if user_take_profit_price.is_nan() else [ |
| 654 | + OrderDetails(user_take_profit_price, None) |
| 655 | + ] |
| 656 | + take_profit_order_details += [ |
| 657 | + OrderDetails(price, None) |
| 658 | + for price in additional_user_take_profit_prices |
| 659 | + ] |
| 660 | + if use_chained_stop_loss_orders: |
| 661 | + stop_loss_order_details = [OrderDetails(user_stop_price, None)] |
| 662 | + |
607 | 663 | if state == trading_enums.EvaluatorStates.VERY_SHORT.value and not self.DISABLE_SELL_ORDERS: |
608 | 664 | quantity = user_volume \ |
609 | 665 | or await self._get_market_quantity_from_risk( |
@@ -631,8 +687,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): |
631 | 687 | ) |
632 | 688 | if current_order := await self._create_order( |
633 | 689 | current_order, |
634 | | - use_chained_take_profit_orders, user_take_profit_price, |
635 | | - use_chained_stop_loss_orders, user_stop_price, |
| 690 | + use_chained_take_profit_orders, take_profit_order_details, |
| 691 | + use_chained_stop_loss_orders, stop_loss_order_details, |
636 | 692 | symbol_market, tag |
637 | 693 | ): |
638 | 694 | created_orders.append(current_order) |
@@ -667,8 +723,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): |
667 | 723 | updated_limit = None |
668 | 724 | if create_stop_only or (updated_limit := await self._create_order( |
669 | 725 | current_order, |
670 | | - use_chained_take_profit_orders, user_take_profit_price, |
671 | | - use_chained_stop_loss_orders, user_stop_price, |
| 726 | + use_chained_take_profit_orders, take_profit_order_details, |
| 727 | + use_chained_stop_loss_orders, stop_loss_order_details, |
672 | 728 | symbol_market, tag |
673 | 729 | )): |
674 | 730 | if updated_limit: |
@@ -735,8 +791,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): |
735 | 791 | updated_limit = None |
736 | 792 | if create_stop_only or (updated_limit := await self._create_order( |
737 | 793 | current_order, |
738 | | - use_chained_take_profit_orders, user_take_profit_price, |
739 | | - use_chained_stop_loss_orders, user_stop_price, |
| 794 | + use_chained_take_profit_orders, take_profit_order_details, |
| 795 | + use_chained_stop_loss_orders, stop_loss_order_details, |
740 | 796 | symbol_market, tag |
741 | 797 | )): |
742 | 798 | if updated_limit: |
@@ -797,8 +853,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): |
797 | 853 | ) |
798 | 854 | if current_order := await self._create_order( |
799 | 855 | current_order, |
800 | | - use_chained_take_profit_orders, user_take_profit_price, |
801 | | - use_chained_stop_loss_orders, user_stop_price, |
| 856 | + use_chained_take_profit_orders, take_profit_order_details, |
| 857 | + use_chained_stop_loss_orders, stop_loss_order_details, |
802 | 858 | symbol_market, tag |
803 | 859 | ): |
804 | 860 | created_orders.append(current_order) |
|
0 commit comments