From bc8a87b4503d880a72ec412eecd59b9fddfd4dd4 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sun, 20 Oct 2024 23:58:01 +0200 Subject: [PATCH 1/8] [DisplayElements] fix symbol filter --- .../UI/plots/displayed_elements.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Meta/Keywords/scripting_library/UI/plots/displayed_elements.py b/Meta/Keywords/scripting_library/UI/plots/displayed_elements.py index 6bd2d8d00..53f9e27d4 100644 --- a/Meta/Keywords/scripting_library/UI/plots/displayed_elements.py +++ b/Meta/Keywords/scripting_library/UI/plots/displayed_elements.py @@ -67,7 +67,7 @@ async def fill_from_database(self, trading_mode, database_manager, exchange_name meta_db.get_trades_db(account_type, exchange_name), meta_db.get_symbol_db(exchange_name, symbol) ] - for db in dbs: + for index, db in enumerate(dbs): for table_name in await db.tables(): display_data = await db.all(table_name) if table_name == commons_enums.DBTables.INPUTS.value: @@ -78,7 +78,10 @@ async def fill_from_database(self, trading_mode, database_manager, exchange_name cached_values += display_data else: try: - filtered_data = self._filter_and_adapt_displayed_elements(display_data, symbol, time_frame, table_name) + filter_symbol = index != len(dbs) - 1 # don't filter symbol for symbol db + filtered_data = self._filter_and_adapt_displayed_elements( + display_data, symbol, time_frame, table_name, filter_symbol + ) chart = display_data[0][commons_enums.DisplayedElementTypes.CHART.value] if chart is None: continue @@ -258,16 +261,17 @@ def _adapt_for_display(self, table_name, filtered_elements): commons_enums.PlotCharts.MAIN_CHART.value return filtered_elements - def _filter_and_adapt_displayed_elements(self, elements, symbol, time_frame, table_name): + def _filter_and_adapt_displayed_elements(self, elements, symbol, time_frame, table_name, filter_symbol): + default_symbol = None if filter_symbol else symbol filtered_elements = [ display_element for display_element in elements if ( - display_element.get(commons_enums.DBRows.SYMBOL.value) == symbol + display_element.get(commons_enums.DBRows.SYMBOL.value, default_symbol) == symbol and display_element.get(commons_enums.DBRows.TIME_FRAME.value) == time_frame ) or ( display_element.get(trading_constants.STORAGE_ORIGIN_VALUE, {}) - .get(trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value, None) == symbol + .get(trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value, default_symbol) == symbol ) ] return self._adapt_for_display(table_name, filtered_elements) From 8090f0a7ffbb7bf1768a93e4dcd4027abab6e334 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 21 Oct 2024 16:11:33 +0200 Subject: [PATCH 2/8] [TradingView] add generator link in description --- .../Interfaces/web_interface/static/css/style.css | 7 +++++++ .../static/js/common/resources_rendering.js | 4 +++- .../resources/TradingViewSignalsTradingMode.md | 14 +++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Services/Interfaces/web_interface/static/css/style.css b/Services/Interfaces/web_interface/static/css/style.css index 0a2827279..dbf49bf43 100644 --- a/Services/Interfaces/web_interface/static/css/style.css +++ b/Services/Interfaces/web_interface/static/css/style.css @@ -719,4 +719,11 @@ table.dataTable tfoot th { .introjs-tooltip-title { color: #fff; /* avoid using h1 color */ +} + +/* markdown fixes */ +pre code { + font-size: inherit; + color: var(--mdb-code-color); /* 'inherit' overridden for themes compatibility */ + word-break: normal; } \ No newline at end of file diff --git a/Services/Interfaces/web_interface/static/js/common/resources_rendering.js b/Services/Interfaces/web_interface/static/js/common/resources_rendering.js index 1f5abb8c0..1a2fa7012 100644 --- a/Services/Interfaces/web_interface/static/js/common/resources_rendering.js +++ b/Services/Interfaces/web_interface/static/js/common/resources_rendering.js @@ -20,7 +20,9 @@ const mardownConverter = new showdown.Converter(); const currentURL = `${window.location.protocol}//${window.location.host}`; function markdown_to_html(text) { - return mardownConverter.makeHtml(text?.trim().replaceAll("

", "\n\n")) + return mardownConverter.makeHtml( + text?.trim().replaceAll("

", "\n\n") + ) } function fetch_images() { diff --git a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md index f0a70dfa0..256f9265e 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md +++ b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md @@ -10,6 +10,16 @@ To know more, checkout the full TradingView trading mode guide. +### Generate your own strategy using AI +Describe your trading strategy to the OctoBot AI strategy generator and get your strategy as Pine Script in seconds. +

+ + Generate my strategy with AI + +

+ ### Alert format cheatsheet Basic signals have the following format: @@ -57,7 +67,9 @@ SIGNAL=CANCEL Additional cancel parameters: - `PARAM_SIDE` is the side of the orders to cancel, it can be `buy` or `sell` to only cancel buy or sell orders. - - `TAG` is the tag of the order(s) to cancel. It can be used to only cancel orders that have been created with a specific tag. +- `TAG` is the tag of the order(s) to cancel. It can be used to only cancel orders that have been created with a specific tag. + +Note: `;` can also be used to separate signal parameters, exemple: `EXCHANGE=binance;SYMBOL=ETHBTC;SIGNAL=CANCEL` is equivalent to the previous example. Find the full TradingView alerts format on From c0ccca495b5882e50ab1aa91cbaca62b15007987 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 21 Oct 2024 16:38:43 +0200 Subject: [PATCH 3/8] [GPT] handle custom url --- Services/Services_bases/gpt_service/gpt.py | 29 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Services/Services_bases/gpt_service/gpt.py b/Services/Services_bases/gpt_service/gpt.py index fa61c14d0..d30192879 100644 --- a/Services/Services_bases/gpt_service/gpt.py +++ b/Services/Services_bases/gpt_service/gpt.py @@ -29,6 +29,7 @@ import octobot_commons.time_frame_manager as time_frame_manager import octobot_commons.authentication as authentication import octobot_commons.tree as tree +import octobot_commons.configuration.fields_utils as fields_utils import octobot.constants as constants import octobot.community as community @@ -46,6 +47,7 @@ def get_fields_description(self): if self._env_secret_key is None: return { services_constants.CONIG_OPENAI_SECRET_KEY: "Your openai API secret key", + services_constants.CONIG_LLM_CUSTOM_BASE_URL: "Custom LLM base url to use. Leave empty to use openai.com", } return {} @@ -53,6 +55,7 @@ def get_default_value(self): if self._env_secret_key is None: return { services_constants.CONIG_OPENAI_SECRET_KEY: "", + services_constants.CONIG_LLM_CUSTOM_BASE_URL: "", } return {} @@ -104,7 +107,10 @@ async def get_chat_completion( return await self._get_signal_from_gpt(messages, model, max_tokens, n, stop, temperature) def _get_client(self) -> openai.AsyncOpenAI: - return openai.AsyncOpenAI(api_key=self._get_api_key()) + return openai.AsyncOpenAI( + api_key=self._get_api_key(), + base_url=self._get_base_url(), + ) async def _get_signal_from_gpt( self, @@ -128,7 +134,10 @@ async def _get_signal_from_gpt( ) self._update_token_usage(completions.usage.total_tokens) return completions.choices[0].message.content - except openai.BadRequestError as err: + except ( + openai.BadRequestError, # error in request + openai.UnprocessableEntityError # error in model (ex: model not found) + )as err: raise errors.InvalidRequestError( f"Error when running request with model {model} (invalid request): {err}" ) from err @@ -315,6 +324,14 @@ def _get_api_key(self): services_constants.CONIG_OPENAI_SECRET_KEY ] + def _get_base_url(self): + value = self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT].get( + services_constants.CONIG_LLM_CUSTOM_BASE_URL + ) + if fields_utils.has_invalid_default_config_value(value): + return None + return value or None + async def prepare(self) -> None: try: if self.use_stored_signals_only(): @@ -323,8 +340,12 @@ async def prepare(self) -> None: fetched_models = await self._get_client().models.list() self.models = [d.id for d in fetched_models.data] if self.model not in self.models: - self.logger.warning(f"Warning: selected '{self.model}' model is not in GPT available models. " - f"Available models are: {self.models}") + self.logger.warning( + f"Warning: the default '{self.model}' model is not in available LLM models from the " + f"selected LLM provider. " + f"Available models are: {self.models}. Please select an available model when configuring your " + f"evaluators." + ) except openai.AuthenticationError as err: self.logger.error(f"Invalid OpenAI api key: {err}") self.creation_error_message = err From 60da13b4577561717d1c80e414800d286bc80b24 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 21 Oct 2024 19:21:28 +0200 Subject: [PATCH 4/8] [DailyTrading] support multiple take profit --- .../Mode/daily_trading_mode/daily_trading.py | 114 +++++++++++++----- .../tests/test_daily_trading_mode_consumer.py | 91 ++++++++++++++ 2 files changed, 176 insertions(+), 29 deletions(-) diff --git a/Trading/Mode/daily_trading_mode/daily_trading.py b/Trading/Mode/daily_trading_mode/daily_trading.py index 589bf06a0..e035ed3fd 100644 --- a/Trading/Mode/daily_trading_mode/daily_trading.py +++ b/Trading/Mode/daily_trading_mode/daily_trading.py @@ -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 @@ -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: @@ -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" @@ -468,10 +477,22 @@ 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 = {} @@ -479,37 +500,52 @@ async def _create_order( 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) @@ -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)) @@ -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( @@ -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) @@ -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: @@ -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: @@ -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) diff --git a/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py b/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py index f239417ff..a1848ca24 100644 --- a/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py +++ b/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py @@ -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 From 1a40c84cd9d8eaec6c285399068b6fbf86ec493c Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Mon, 21 Oct 2024 19:43:28 +0200 Subject: [PATCH 5/8] [TradingViewTradingMode] support multiple take profits --- .../Mode/daily_trading_mode/daily_trading.py | 2 +- .../tests/test_daily_trading_mode_consumer.py | 18 ++++++++++++++++++ .../resources/TradingViewSignalsTradingMode.md | 3 ++- .../tests/test_trading_view_signals_trading.py | 16 +++++++++++++--- .../trading_view_signals_trading.py | 11 +++++++++++ 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Trading/Mode/daily_trading_mode/daily_trading.py b/Trading/Mode/daily_trading_mode/daily_trading.py index e035ed3fd..a658fc1c2 100644 --- a/Trading/Mode/daily_trading_mode/daily_trading.py +++ b/Trading/Mode/daily_trading_mode/daily_trading.py @@ -609,7 +609,7 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs): symbol_market, price ) - for price in data.get(self.ADDITIONAL_TAKE_PROFIT_PRICES_KEY, []) + for price in (data.get(self.ADDITIONAL_TAKE_PROFIT_PRICES_KEY) or []) ] user_stop_price = trading_personal_data.decimal_adapt_price( symbol_market, diff --git a/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py b/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py index a1848ca24..1c12a772a 100644 --- a/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py +++ b/Trading/Mode/daily_trading_mode/tests/test_daily_trading_mode_consumer.py @@ -963,6 +963,24 @@ async def test_chained_stop_loss_and_take_profit_orders(tools): # take profit only data = { consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal("100000"), + consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [], + consumer.VOLUME_KEY: decimal.Decimal("0.01"), + } + orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data) + buy_order = orders_with_tp[0] + assert len(buy_order.chained_orders) == 1 + take_profit_order = buy_order.chained_orders[0] + 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) + assert take_profit_order.origin_price == decimal.Decimal("100000") + 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() + # take profit only using ADDITIONAL_TAKE_PROFIT_PRICES_KEY + data = { + consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [decimal.Decimal("100000")], consumer.VOLUME_KEY: decimal.Decimal("0.01"), } orders_with_tp = await consumer.create_new_orders(symbol, decimal.Decimal(str(-1)), state, data=data) diff --git a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md index 256f9265e..648b96170 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md +++ b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md @@ -51,7 +51,8 @@ orders price syntax. - `STOP_PRICE` is the price of the stop order to create. Can also be a delta or % delta like `PRICE`. When increasing the position or buying in spot trading, the stop loss will automatically be created once the initial order is filled. When decreasing the position (or selling in spot) using a LIMIT `ORDER_TYPE`, the stop loss will be created instantly. *Orders crated this way are compatible with PNL history.* It follows the orders price syntax. - `TAKE_PROFIT_PRICE` is the price of the take profit order to create. Can also be a delta or % delta like `PRICE`. When increasing the position or buying in spot trading, the take profit will automatically be created once the initial order is filled. When decreasing the position (or selling in spot) using a LIMIT `ORDER_TYPE`, the take profit will be created instantly. *Orders crated this way are compatible with PNL history.* It follows the -orders price syntax. +orders price syntax. +Multiple take profit prices can be used from `TAKE_PROFIT_PRICE_1`, `TAKE_PROFIT_PRICE_2`, ... - `REDUCE_ONLY` when true, only reduce the current position (avoid accidental short position opening when reducing a long position). **Only used in futures trading**. Default is false - `TAG` is an identifier to give to the orders to create. diff --git a/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py b/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py index 4351625f8..c6fb0d99c 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py +++ b/Trading/Mode/trading_view_signals_trading_mode/tests/test_trading_view_signals_trading.py @@ -162,11 +162,13 @@ async def test_parse_signal_data(): errors = [] assert Mode.TradingViewSignalsTradingMode.parse_signal_data( - "KEY=value;EXCHANGE;PLOp=ABC", + "KEY=value;EXCHANGE;PLOp=ABC;TAKE_PROFIT_PRICE=1;TAKE_PROFIT_PRICE_2=3", errors ) == { "KEY": "value", "PLOp": "ABC", + "TAKE_PROFIT_PRICE": "1", + "TAKE_PROFIT_PRICE_2": "3", } assert len(errors) == 1 assert "EXCHANGE" in str(errors[0]) @@ -301,6 +303,7 @@ async def test_signal_callback(tools): consumer.STOP_PRICE_KEY: decimal.Decimal(math.nan), consumer.STOP_ONLY: False, consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(math.nan), + consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [], consumer.REDUCE_ONLY_KEY: False, consumer.TAG_KEY: None, consumer.EXCHANGE_ORDER_IDS: None, @@ -327,6 +330,7 @@ async def test_signal_callback(tools): consumer.STOP_PRICE_KEY: decimal.Decimal("25000"), consumer.STOP_ONLY: True, consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal(math.nan), + consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [], consumer.REDUCE_ONLY_KEY: False, consumer.TAG_KEY: "stop_1_tag", consumer.EXCHANGE_ORDER_IDS: None, @@ -357,6 +361,7 @@ async def test_signal_callback(tools): consumer.STOP_PRICE_KEY: decimal.Decimal("12"), consumer.STOP_ONLY: False, consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal("22222"), + consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [], consumer.REDUCE_ONLY_KEY: True, consumer.TAG_KEY: None, mode.EXCHANGE_ORDER_IDS: ["ab1", "aaaaa"], @@ -376,7 +381,9 @@ async def test_signal_callback(tools): mode.REDUCE_ONLY_KEY: True, mode.ORDER_TYPE_SIGNAL: "LiMiT", mode.STOP_PRICE_KEY: "-10%", # price - 10% - mode.TAKE_PROFIT_PRICE_KEY: "120.333333333333333d", # price + 120.333333333333333 + f"{mode.TAKE_PROFIT_PRICE_KEY}_0": "120.333333333333333d", # price + 120.333333333333333 + f"{mode.TAKE_PROFIT_PRICE_KEY}_1": "122.333333333333333d", # price + 122.333333333333333 + f"{mode.TAKE_PROFIT_PRICE_KEY}_2": "4444d", # price + 4444 mode.EXCHANGE_ORDER_IDS: ["ab1", "aaaaa"], "PARAM_TAG_1": "ttt", "PARAM_Plop": False, @@ -389,7 +396,10 @@ async def test_signal_callback(tools): consumer.VOLUME_KEY: decimal.Decimal("1"), consumer.STOP_PRICE_KEY: decimal.Decimal("6308.27549999"), consumer.STOP_ONLY: False, - consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal("7129.52833333"), + consumer.TAKE_PROFIT_PRICE_KEY: decimal.Decimal("nan"), # only additional TP orders are provided + consumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: [ + decimal.Decimal("7129.52833333"), decimal.Decimal("7131.52833333"), decimal.Decimal('11453.19499999') + ], consumer.REDUCE_ONLY_KEY: True, consumer.TAG_KEY: None, mode.EXCHANGE_ORDER_IDS: ["ab1", "aaaaa"], diff --git a/Trading/Mode/trading_view_signals_trading_mode/trading_view_signals_trading.py b/Trading/Mode/trading_view_signals_trading_mode/trading_view_signals_trading.py index 1582d58d8..0a2265f22 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/trading_view_signals_trading.py +++ b/Trading/Mode/trading_view_signals_trading_mode/trading_view_signals_trading.py @@ -273,6 +273,9 @@ async def _parse_order_details(self, ctx, parsed_data): tp_price = await self._parse_price( ctx, parsed_data, TradingViewSignalsTradingMode.TAKE_PROFIT_PRICE_KEY, math.nan ) + additional_tp_prices = await self._parse_additional_prices( + ctx, parsed_data, f"{TradingViewSignalsTradingMode.TAKE_PROFIT_PRICE_KEY}_", math.nan + ) allow_holdings_adaptation = parsed_data.get(TradingViewSignalsTradingMode.ALLOW_HOLDINGS_ADAPTATION_KEY, False) order_data = { @@ -283,6 +286,7 @@ async def _parse_order_details(self, ctx, parsed_data): TradingViewSignalsModeConsumer.STOP_PRICE_KEY: stop_price, TradingViewSignalsModeConsumer.STOP_ONLY: order_type == TradingViewSignalsTradingMode.STOP_SIGNAL, TradingViewSignalsModeConsumer.TAKE_PROFIT_PRICE_KEY: tp_price, + TradingViewSignalsModeConsumer.ADDITIONAL_TAKE_PROFIT_PRICES_KEY: additional_tp_prices, TradingViewSignalsModeConsumer.REDUCE_ONLY_KEY: parsed_data.get(TradingViewSignalsTradingMode.REDUCE_ONLY_KEY, False), TradingViewSignalsModeConsumer.TAG_KEY: @@ -293,6 +297,13 @@ async def _parse_order_details(self, ctx, parsed_data): } return state, order_data + async def _parse_additional_prices(self, ctx, parsed_data, price_prefix, default): + prices = [] + for key, value in parsed_data.items(): + if key.startswith(price_prefix) and len(key.split(price_prefix)) == 2: + prices.append(await self._parse_price(ctx, parsed_data, key, default)) + return prices + async def _parse_price(self, ctx, parsed_data, key, default): target_price = decimal.Decimal(str(default)) if input_price_or_offset := parsed_data.get(key, 0): From a5d1b070d2f49e577171326038ddd9c64179d2be Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 23 Oct 2024 23:02:46 +0200 Subject: [PATCH 6/8] [TradingView] improve generated strategies docs --- .../resources/TradingViewSignalsTradingMode.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md index 648b96170..f1fa1e40f 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md +++ b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md @@ -12,7 +12,11 @@ full TradingView trading mode guide. ### Generate your own strategy using AI Describe your trading strategy to the OctoBot AI strategy generator and get your strategy as Pine Script in seconds. -

+Automate it with your self-hosted OctoBot or a + TradingView OctoBot. +

From b5da49a6b625fa3db73b0dde80b14ee60d03ccf8 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 23 Oct 2024 23:03:05 +0200 Subject: [PATCH 7/8] [Exchanges] handle oauth keys --- Trading/Exchange/bitmart/bitmart_exchange.py | 4 ++-- Trading/Exchange/coinbase/coinbase_exchange.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Trading/Exchange/bitmart/bitmart_exchange.py b/Trading/Exchange/bitmart/bitmart_exchange.py index ff8bc7579..ea8c12800 100644 --- a/Trading/Exchange/bitmart/bitmart_exchange.py +++ b/Trading/Exchange/bitmart/bitmart_exchange.py @@ -27,9 +27,9 @@ class BitMartConnector(exchanges.CCXTConnector): def _client_factory(self, force_unauth, keys_adapter=None) -> tuple: return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter) - def _keys_adapter(self, key, secret, password, uid): + def _keys_adapter(self, key, secret, password, uid, auth_token): # use password as uid - return key, secret, "", password + return key, secret, "", password, None, None class BitMart(exchanges.RestExchange): diff --git a/Trading/Exchange/coinbase/coinbase_exchange.py b/Trading/Exchange/coinbase/coinbase_exchange.py index 34b4980c6..06a61353c 100644 --- a/Trading/Exchange/coinbase/coinbase_exchange.py +++ b/Trading/Exchange/coinbase/coinbase_exchange.py @@ -65,12 +65,15 @@ class CoinbaseConnector(ccxt_connector.CCXTConnector): def _client_factory(self, force_unauth, keys_adapter=None) -> tuple: return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter) - def _keys_adapter(self, key, secret, password, uid): + def _keys_adapter(self, key, secret, password, uid, auth_token): + if auth_token: + # when auth token is provided, force invalid keys + return "ANY_KEY", "ANY_SECRET", password, uid, auth_token, "Bearer " # CCXT pem key reader is not expecting users to under keys pasted as text from the coinbase UI # convert \\n to \n to make this format compatible as well if secret and "\\n" in secret: secret = secret.replace("\\n", "\n") - return key, secret, password, uid + return key, secret, password, uid, None, None @_coinbase_retrier async def _load_markets(self, client, reload: bool): From 8f22149bb0cdc72d64d4dc70ac638b1de6d0691e Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sat, 26 Oct 2024 17:42:30 +0200 Subject: [PATCH 8/8] [TradingView] add custom automations video --- Trading/Mode/dca_trading_mode/resources/DCATradingMode.md | 3 --- .../resources/TradingViewSignalsTradingMode.md | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Trading/Mode/dca_trading_mode/resources/DCATradingMode.md b/Trading/Mode/dca_trading_mode/resources/DCATradingMode.md index f2bd870f6..95b24e3ac 100644 --- a/Trading/Mode/dca_trading_mode/resources/DCATradingMode.md +++ b/Trading/Mode/dca_trading_mode/resources/DCATradingMode.md @@ -4,12 +4,9 @@ in smaller amounts at regular intervals.

diff --git a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md index f1fa1e40f..2860a13c7 100644 --- a/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md +++ b/Trading/Mode/trading_view_signals_trading_mode/resources/TradingViewSignalsTradingMode.md @@ -6,6 +6,14 @@ Free TradingView webhook alerts can be used to automate trades based on TradingView alerts. +
+
+ +
+
+ To know more, checkout the full TradingView trading mode guide.