Skip to content

Commit

Permalink
[DailyTrading] support multiple take profit
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDSM committed Oct 21, 2024
1 parent c0ccca4 commit 60da13b
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 29 deletions.
114 changes: 85 additions & 29 deletions Trading/Mode/daily_trading_mode/daily_trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import asyncio
import decimal
import math
import dataclasses
import typing

import octobot_commons.constants as commons_constants
import octobot_commons.enums as commons_enums
Expand All @@ -35,6 +37,12 @@
import octobot_trading.api as trading_api


@dataclasses.dataclass
class OrderDetails:
price: decimal.Decimal
quantity: typing.Optional[decimal.Decimal]


class DailyTradingMode(trading_modes.AbstractTradingMode):

def init_user_inputs(self, inputs: dict) -> None:
Expand Down Expand Up @@ -184,6 +192,7 @@ class DailyTradingModeConsumer(trading_modes.AbstractTradingModeConsumer):
VOLUME_KEY = "VOLUME"
STOP_PRICE_KEY = "STOP_PRICE"
TAKE_PROFIT_PRICE_KEY = "TAKE_PROFIT_PRICE"
ADDITIONAL_TAKE_PROFIT_PRICES_KEY = "ADDITIONAL_TAKE_PROFIT_PRICES"
STOP_ONLY = "STOP_ONLY"
REDUCE_ONLY_KEY = "REDUCE_ONLY"
TAG_KEY = "TAG"
Expand Down Expand Up @@ -468,48 +477,75 @@ def _get_max_amount_from_max_ratio(self, max_ratio, quantity, quote, default_rat
f"Set it to 100 to buy anyway.")
return trading_constants.ZERO

def _get_split_take_profit_details(
self, order_details: list[OrderDetails], total_quantity: decimal.Decimal, symbol_market
):
prices = [order_detail.price for order_detail in order_details]
quantities, prices = trading_personal_data.get_valid_split_orders(
total_quantity, prices, symbol_market
)
return [
OrderDetails(price, quantity)
for quantity, price in zip(quantities, prices)
]

async def _create_order(
self, current_order,
use_take_profit_orders, take_profit_price,
use_stop_loss_orders, stop_price,
use_take_profit_orders, take_profits_details: list[OrderDetails],
use_stop_loss_orders, stop_loss_details: list[OrderDetails],
symbol_market, tag
):
params = {}
chained_orders = []
is_long = current_order.side is trading_enums.TradeOrderSide.BUY
exit_side = trading_enums.TradeOrderSide.SELL if is_long else trading_enums.TradeOrderSide.BUY
if use_stop_loss_orders:
if len(stop_loss_details) > 1:
self.logger.error(f"Multiple stop loss orders is not supported.")
stop_price = trading_personal_data.decimal_adapt_price(
symbol_market,
current_order.origin_price * (
trading_constants.ONE + (self.TARGET_PROFIT_STOP_LOSS * (-1 if is_long else 1))
)
) if stop_price.is_nan() else stop_price
) if (not stop_loss_details or stop_loss_details[0].price.is_nan()) else stop_loss_details[0].price
param_update, chained_order = await self.register_chained_order(
current_order, stop_price, trading_enums.TraderOrderType.STOP_LOSS, exit_side, tag=tag
)
params.update(param_update)
chained_orders.append(chained_order)
if use_take_profit_orders:
take_profit_price = trading_personal_data.decimal_adapt_price(
symbol_market,
current_order.origin_price * (
trading_constants.ONE + (self.TARGET_PROFIT_TAKE_PROFIT * (1 if is_long else -1))
if take_profits_details:
local_take_profits_details = self._get_split_take_profit_details(
take_profits_details, current_order.origin_quantity, symbol_market
)
) if take_profit_price.is_nan() else take_profit_price
order_type = self.exchange_manager.trader.get_take_profit_order_type(
current_order,
trading_enums.TraderOrderType.SELL_LIMIT if exit_side is trading_enums.TradeOrderSide.SELL
else trading_enums.TraderOrderType.BUY_LIMIT
)
param_update, chained_order = await self.register_chained_order(
current_order, take_profit_price, order_type, exit_side, tag=tag
)
params.update(param_update)
chained_orders.append(chained_order)
else:
local_take_profits_details = [
OrderDetails(decimal.Decimal("nan"), current_order.origin_quantity)
]
for take_profits_detail in local_take_profits_details:
take_profit_price = trading_personal_data.decimal_adapt_price(
symbol_market,
current_order.origin_price * (
trading_constants.ONE + (self.TARGET_PROFIT_TAKE_PROFIT * (1 if is_long else -1))
)
) if take_profits_detail.price.is_nan() else take_profits_detail.price
order_type = self.exchange_manager.trader.get_take_profit_order_type(
current_order,
trading_enums.TraderOrderType.SELL_LIMIT if exit_side is trading_enums.TradeOrderSide.SELL
else trading_enums.TraderOrderType.BUY_LIMIT
)
param_update, chained_order = await self.register_chained_order(
current_order, take_profit_price, order_type, exit_side,
quantity=take_profits_detail.quantity, tag=tag
)
params.update(param_update)
chained_orders.append(chained_order)
if len(chained_orders) > 1:
oco_group = self.exchange_manager.exchange_personal_data.orders_manager \
.create_group(trading_personal_data.OneCancelsTheOtherOrderGroup)
stop_count = len([o for o in chained_orders if trading_personal_data.is_stop_order(o.order_type)])
tp_count = len(chained_orders) - stop_count
group_type = trading_personal_data.OneCancelsTheOtherOrderGroup if stop_count == tp_count \
else trading_personal_data.BalancedTakeProfitAndStopOrderGroup
oco_group = self.exchange_manager.exchange_personal_data.orders_manager.create_group(group_type)
for order in chained_orders:
order.add_to_order_group(oco_group)
return await self.trading_mode.create_order(current_order, params=params or None)
Expand Down Expand Up @@ -568,6 +604,13 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
symbol_market,
data.get(self.TAKE_PROFIT_PRICE_KEY, decimal.Decimal(math.nan))
)
additional_user_take_profit_prices = [
trading_personal_data.decimal_adapt_price(
symbol_market,
price
)
for price in data.get(self.ADDITIONAL_TAKE_PROFIT_PRICES_KEY, [])
]
user_stop_price = trading_personal_data.decimal_adapt_price(
symbol_market,
data.get(self.STOP_PRICE_KEY, decimal.Decimal(math.nan))
Expand Down Expand Up @@ -599,11 +642,24 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
use_stop_orders = is_reducing_position and (self.USE_STOP_ORDERS or not user_stop_price.is_nan())
# use stop loss when increasing the position and the user explicitly asks for one
use_chained_take_profit_orders = increasing_position and (
not user_take_profit_price.is_nan() or self.USE_TARGET_PROFIT_MODE
(not user_take_profit_price.is_nan() or additional_user_take_profit_prices)
or self.USE_TARGET_PROFIT_MODE
)
use_chained_stop_loss_orders = increasing_position and (
not user_stop_price.is_nan() or (self.USE_TARGET_PROFIT_MODE and self.USE_STOP_ORDERS)
)
stop_loss_order_details = take_profit_order_details = []
if use_chained_take_profit_orders:
take_profit_order_details = [] if user_take_profit_price.is_nan() else [
OrderDetails(user_take_profit_price, None)
]
take_profit_order_details += [
OrderDetails(price, None)
for price in additional_user_take_profit_prices
]
if use_chained_stop_loss_orders:
stop_loss_order_details = [OrderDetails(user_stop_price, None)]

if state == trading_enums.EvaluatorStates.VERY_SHORT.value and not self.DISABLE_SELL_ORDERS:
quantity = user_volume \
or await self._get_market_quantity_from_risk(
Expand Down Expand Up @@ -631,8 +687,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
)
if current_order := await self._create_order(
current_order,
use_chained_take_profit_orders, user_take_profit_price,
use_chained_stop_loss_orders, user_stop_price,
use_chained_take_profit_orders, take_profit_order_details,
use_chained_stop_loss_orders, stop_loss_order_details,
symbol_market, tag
):
created_orders.append(current_order)
Expand Down Expand Up @@ -667,8 +723,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
updated_limit = None
if create_stop_only or (updated_limit := await self._create_order(
current_order,
use_chained_take_profit_orders, user_take_profit_price,
use_chained_stop_loss_orders, user_stop_price,
use_chained_take_profit_orders, take_profit_order_details,
use_chained_stop_loss_orders, stop_loss_order_details,
symbol_market, tag
)):
if updated_limit:
Expand Down Expand Up @@ -735,8 +791,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
updated_limit = None
if create_stop_only or (updated_limit := await self._create_order(
current_order,
use_chained_take_profit_orders, user_take_profit_price,
use_chained_stop_loss_orders, user_stop_price,
use_chained_take_profit_orders, take_profit_order_details,
use_chained_stop_loss_orders, stop_loss_order_details,
symbol_market, tag
)):
if updated_limit:
Expand Down Expand Up @@ -797,8 +853,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
)
if current_order := await self._create_order(
current_order,
use_chained_take_profit_orders, user_take_profit_price,
use_chained_stop_loss_orders, user_stop_price,
use_chained_take_profit_orders, take_profit_order_details,
use_chained_stop_loss_orders, stop_loss_order_details,
symbol_market, tag
):
created_orders.append(current_order)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,97 @@ async def test_chained_stop_loss_and_take_profit_orders(tools):
- trading_personal_data.get_fees_for_currency(sell_limit.fee, stop_loss.quantity_currency)


async def test_chained_multiple_take_profit_orders(tools):
exchange_manager, trader, symbol, consumer, last_btc_price = tools

# with BTC/USDT
exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.value_converter.last_prices_by_trading_pair[symbol] = \
last_btc_price
exchange_manager.exchange_personal_data.portfolio_manager.portfolio_value_holder.portfolio_current_value = \
decimal.Decimal(str(10 + 1000 / last_btc_price))

state = trading_enums.EvaluatorStates.LONG.value
# 1 take profit and 2 additional (3 in total)
data = {
consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal("100000"),
consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal("110000"), decimal.Decimal("120000")],
consumer.VOLUME_KEY: decimal.Decimal("0.01"),
}
orders_with_tps = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)
buy_order = orders_with_tps[0]
tp_prices = [decimal.Decimal("100000"), decimal.Decimal("110000"), decimal.Decimal("120000")]
assert len(buy_order.chained_orders) == len(tp_prices)
for i, take_profit_order in enumerate(buy_order.chained_orders):
assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)
assert take_profit_order.origin_quantity == (
decimal.Decimal("0.01")
- trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)
) / decimal.Decimal(str(len(tp_prices)))
assert take_profit_order.origin_price == tp_prices[i]
assert take_profit_order.is_waiting_for_chained_trigger
assert take_profit_order.associated_entry_ids == [buy_order.order_id]
assert not take_profit_order.is_open()
assert not take_profit_order.is_created()

# only 2 additional (2 in total)
data = {
consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal("110000"), decimal.Decimal("120000")],
consumer.VOLUME_KEY: decimal.Decimal("0.01"),
}
orders_with_tps = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data)
buy_order = orders_with_tps[0]
tp_prices = [decimal.Decimal("110000"), decimal.Decimal("120000")]
assert len(buy_order.chained_orders) == len(tp_prices)
for i, take_profit_order in enumerate(buy_order.chained_orders):
assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)
assert take_profit_order.origin_quantity == (
decimal.Decimal("0.01")
- trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)
) / decimal.Decimal(str(len(tp_prices)))
assert take_profit_order.origin_price == tp_prices[i]
assert take_profit_order.is_waiting_for_chained_trigger
assert take_profit_order.associated_entry_ids == [buy_order.order_id]
assert not take_profit_order.is_open()
assert not take_profit_order.is_created()

# stop loss and 1 take profit and 5 additional (6 TP in total)
tp_prices = [
decimal.Decimal("100012"),
decimal.Decimal("110000"), decimal.Decimal("120000"), decimal.Decimal("130000"),
decimal.Decimal("140000"), decimal.Decimal("150000")
]
data = {
consumer.STOP_PRICE_KEY: decimal.Decimal("123"),
consumer.TAKE_PROFIT_PRICE_KEY: tp_prices[0],
consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: tp_prices[1:],
consumer.VOLUME_KEY: decimal.Decimal("0.01"),
}
orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(0.4)), state, data=data)
buy_order = orders_with_tp[0]
assert len(buy_order.chained_orders) == 1 + len(tp_prices)
stop_order = buy_order.chained_orders[0]
assert isinstance(stop_order, trading_personal_data.StopLossOrder)
assert stop_order.origin_quantity == decimal.Decimal("0.01") \
- trading_personal_data.get_fees_for_currency(buy_order.fee, stop_order.quantity_currency)
assert stop_order.origin_price == decimal.Decimal("123")
assert stop_order.is_waiting_for_chained_trigger
assert stop_order.associated_entry_ids == [buy_order.order_id]
assert len(buy_order.chained_orders[1:]) == len(tp_prices)
for i, take_profit_order in enumerate(buy_order.chained_orders[1:]):
assert isinstance(take_profit_order, trading_personal_data.SellLimitOrder)
assert take_profit_order.origin_quantity == (
decimal.Decimal("0.01")
- trading_personal_data.get_fees_for_currency(buy_order.fee, take_profit_order.quantity_currency)
) / decimal.Decimal(str(len(tp_prices)))
assert take_profit_order.origin_price == tp_prices[i]
assert take_profit_order.is_waiting_for_chained_trigger
assert take_profit_order.associated_entry_ids == [buy_order.order_id]
assert not take_profit_order.is_open()
assert not take_profit_order.is_created()
assert isinstance(stop_order.order_group, trading_personal_data.BalancedTakeProfitAndStopOrderGroup)
assert take_profit_order.order_group is stop_order.order_group


async def test_create_stop_loss_orders(tools):
exchange_manager, trader, symbol, consumer, last_btc_price = tools

Expand Down

0 comments on commit 60da13b

Please sign in to comment.