Skip to content

Commit 004a330

Browse files
authored
Merge pull request #1376 from Drakkar-Software/dev
Dev merge
2 parents d9e9ce8 + 8f22149 commit 004a330

File tree

12 files changed

+296
-51
lines changed

12 files changed

+296
-51
lines changed

Meta/Keywords/scripting_library/UI/plots/displayed_elements.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async def fill_from_database(self, trading_mode, database_manager, exchange_name
6767
meta_db.get_trades_db(account_type, exchange_name),
6868
meta_db.get_symbol_db(exchange_name, symbol)
6969
]
70-
for db in dbs:
70+
for index, db in enumerate(dbs):
7171
for table_name in await db.tables():
7272
display_data = await db.all(table_name)
7373
if table_name == commons_enums.DBTables.INPUTS.value:
@@ -78,7 +78,10 @@ async def fill_from_database(self, trading_mode, database_manager, exchange_name
7878
cached_values += display_data
7979
else:
8080
try:
81-
filtered_data = self._filter_and_adapt_displayed_elements(display_data, symbol, time_frame, table_name)
81+
filter_symbol = index != len(dbs) - 1 # don't filter symbol for symbol db
82+
filtered_data = self._filter_and_adapt_displayed_elements(
83+
display_data, symbol, time_frame, table_name, filter_symbol
84+
)
8285
chart = display_data[0][commons_enums.DisplayedElementTypes.CHART.value]
8386
if chart is None:
8487
continue
@@ -258,16 +261,17 @@ def _adapt_for_display(self, table_name, filtered_elements):
258261
commons_enums.PlotCharts.MAIN_CHART.value
259262
return filtered_elements
260263

261-
def _filter_and_adapt_displayed_elements(self, elements, symbol, time_frame, table_name):
264+
def _filter_and_adapt_displayed_elements(self, elements, symbol, time_frame, table_name, filter_symbol):
265+
default_symbol = None if filter_symbol else symbol
262266
filtered_elements = [
263267
display_element
264268
for display_element in elements
265269
if (
266-
display_element.get(commons_enums.DBRows.SYMBOL.value) == symbol
270+
display_element.get(commons_enums.DBRows.SYMBOL.value, default_symbol) == symbol
267271
and display_element.get(commons_enums.DBRows.TIME_FRAME.value) == time_frame
268272
) or (
269273
display_element.get(trading_constants.STORAGE_ORIGIN_VALUE, {})
270-
.get(trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value, None) == symbol
274+
.get(trading_enums.ExchangeConstantsOrderColumns.SYMBOL.value, default_symbol) == symbol
271275
)
272276
]
273277
return self._adapt_for_display(table_name, filtered_elements)

Services/Interfaces/web_interface/static/css/style.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,4 +719,11 @@ table.dataTable tfoot th {
719719

720720
.introjs-tooltip-title {
721721
color: #fff; /* avoid using h1 color */
722+
}
723+
724+
/* markdown fixes */
725+
pre code {
726+
font-size: inherit;
727+
color: var(--mdb-code-color); /* 'inherit' overridden for themes compatibility */
728+
word-break: normal;
722729
}

Services/Interfaces/web_interface/static/js/common/resources_rendering.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ const mardownConverter = new showdown.Converter();
2020
const currentURL = `${window.location.protocol}//${window.location.host}`;
2121

2222
function markdown_to_html(text) {
23-
return mardownConverter.makeHtml(text?.trim().replaceAll("<br><br>", "\n\n"))
23+
return mardownConverter.makeHtml(
24+
text?.trim().replaceAll("<br><br>", "\n\n")
25+
)
2426
}
2527

2628
function fetch_images() {

Services/Services_bases/gpt_service/gpt.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import octobot_commons.time_frame_manager as time_frame_manager
3030
import octobot_commons.authentication as authentication
3131
import octobot_commons.tree as tree
32+
import octobot_commons.configuration.fields_utils as fields_utils
3233

3334
import octobot.constants as constants
3435
import octobot.community as community
@@ -46,13 +47,15 @@ def get_fields_description(self):
4647
if self._env_secret_key is None:
4748
return {
4849
services_constants.CONIG_OPENAI_SECRET_KEY: "Your openai API secret key",
50+
services_constants.CONIG_LLM_CUSTOM_BASE_URL: "Custom LLM base url to use. Leave empty to use openai.com",
4951
}
5052
return {}
5153

5254
def get_default_value(self):
5355
if self._env_secret_key is None:
5456
return {
5557
services_constants.CONIG_OPENAI_SECRET_KEY: "",
58+
services_constants.CONIG_LLM_CUSTOM_BASE_URL: "",
5659
}
5760
return {}
5861

@@ -104,7 +107,10 @@ async def get_chat_completion(
104107
return await self._get_signal_from_gpt(messages, model, max_tokens, n, stop, temperature)
105108

106109
def _get_client(self) -> openai.AsyncOpenAI:
107-
return openai.AsyncOpenAI(api_key=self._get_api_key())
110+
return openai.AsyncOpenAI(
111+
api_key=self._get_api_key(),
112+
base_url=self._get_base_url(),
113+
)
108114

109115
async def _get_signal_from_gpt(
110116
self,
@@ -128,7 +134,10 @@ async def _get_signal_from_gpt(
128134
)
129135
self._update_token_usage(completions.usage.total_tokens)
130136
return completions.choices[0].message.content
131-
except openai.BadRequestError as err:
137+
except (
138+
openai.BadRequestError, # error in request
139+
openai.UnprocessableEntityError # error in model (ex: model not found)
140+
)as err:
132141
raise errors.InvalidRequestError(
133142
f"Error when running request with model {model} (invalid request): {err}"
134143
) from err
@@ -315,6 +324,14 @@ def _get_api_key(self):
315324
services_constants.CONIG_OPENAI_SECRET_KEY
316325
]
317326

327+
def _get_base_url(self):
328+
value = self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT].get(
329+
services_constants.CONIG_LLM_CUSTOM_BASE_URL
330+
)
331+
if fields_utils.has_invalid_default_config_value(value):
332+
return None
333+
return value or None
334+
318335
async def prepare(self) -> None:
319336
try:
320337
if self.use_stored_signals_only():
@@ -323,8 +340,12 @@ async def prepare(self) -> None:
323340
fetched_models = await self._get_client().models.list()
324341
self.models = [d.id for d in fetched_models.data]
325342
if self.model not in self.models:
326-
self.logger.warning(f"Warning: selected '{self.model}' model is not in GPT available models. "
327-
f"Available models are: {self.models}")
343+
self.logger.warning(
344+
f"Warning: the default '{self.model}' model is not in available LLM models from the "
345+
f"selected LLM provider. "
346+
f"Available models are: {self.models}. Please select an available model when configuring your "
347+
f"evaluators."
348+
)
328349
except openai.AuthenticationError as err:
329350
self.logger.error(f"Invalid OpenAI api key: {err}")
330351
self.creation_error_message = err

Trading/Exchange/bitmart/bitmart_exchange.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ class BitMartConnector(exchanges.CCXTConnector):
2727
def _client_factory(self, force_unauth, keys_adapter=None) -> tuple:
2828
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)
2929

30-
def _keys_adapter(self, key, secret, password, uid):
30+
def _keys_adapter(self, key, secret, password, uid, auth_token):
3131
# use password as uid
32-
return key, secret, "", password
32+
return key, secret, "", password, None, None
3333

3434

3535
class BitMart(exchanges.RestExchange):

Trading/Exchange/coinbase/coinbase_exchange.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,15 @@ class CoinbaseConnector(ccxt_connector.CCXTConnector):
6565
def _client_factory(self, force_unauth, keys_adapter=None) -> tuple:
6666
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)
6767

68-
def _keys_adapter(self, key, secret, password, uid):
68+
def _keys_adapter(self, key, secret, password, uid, auth_token):
69+
if auth_token:
70+
# when auth token is provided, force invalid keys
71+
return "ANY_KEY", "ANY_SECRET", password, uid, auth_token, "Bearer "
6972
# CCXT pem key reader is not expecting users to under keys pasted as text from the coinbase UI
7073
# convert \\n to \n to make this format compatible as well
7174
if secret and "\\n" in secret:
7275
secret = secret.replace("\\n", "\n")
73-
return key, secret, password, uid
76+
return key, secret, password, uid, None, None
7477

7578
@_coinbase_retrier
7679
async def _load_markets(self, client, reload: bool):

Trading/Mode/daily_trading_mode/daily_trading.py

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import asyncio
1717
import decimal
1818
import math
19+
import dataclasses
20+
import typing
1921

2022
import octobot_commons.constants as commons_constants
2123
import octobot_commons.enums as commons_enums
@@ -35,6 +37,12 @@
3537
import octobot_trading.api as trading_api
3638

3739

40+
@dataclasses.dataclass
41+
class OrderDetails:
42+
price: decimal.Decimal
43+
quantity: typing.Optional[decimal.Decimal]
44+
45+
3846
class DailyTradingMode(trading_modes.AbstractTradingMode):
3947

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

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+
471492
async def _create_order(
472493
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],
475496
symbol_market, tag
476497
):
477498
params = {}
478499
chained_orders = []
479500
is_long = current_order.side is trading_enums.TradeOrderSide.BUY
480501
exit_side = trading_enums.TradeOrderSide.SELL if is_long else trading_enums.TradeOrderSide.BUY
481502
if use_stop_loss_orders:
503+
if len(stop_loss_details) > 1:
504+
self.logger.error(f"Multiple stop loss orders is not supported.")
482505
stop_price = trading_personal_data.decimal_adapt_price(
483506
symbol_market,
484507
current_order.origin_price * (
485508
trading_constants.ONE + (self.TARGET_PROFIT_STOP_LOSS * (-1 if is_long else 1))
486509
)
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
488511
param_update, chained_order = await self.register_chained_order(
489512
current_order, stop_price, trading_enums.TraderOrderType.STOP_LOSS, exit_side, tag=tag
490513
)
491514
params.update(param_update)
492515
chained_orders.append(chained_order)
493516
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
498520
)
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)
510543
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)
513549
for order in chained_orders:
514550
order.add_to_order_group(oco_group)
515551
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):
568604
symbol_market,
569605
data.get(self.TAKE_PROFIT_PRICE_KEY, decimal.Decimal(math.nan))
570606
)
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+
]
571614
user_stop_price = trading_personal_data.decimal_adapt_price(
572615
symbol_market,
573616
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):
599642
use_stop_orders = is_reducing_position and (self.USE_STOP_ORDERS or not user_stop_price.is_nan())
600643
# use stop loss when increasing the position and the user explicitly asks for one
601644
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
603647
)
604648
use_chained_stop_loss_orders = increasing_position and (
605649
not user_stop_price.is_nan() or (self.USE_TARGET_PROFIT_MODE and self.USE_STOP_ORDERS)
606650
)
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+
607663
if state == trading_enums.EvaluatorStates.VERY_SHORT.value and not self.DISABLE_SELL_ORDERS:
608664
quantity = user_volume \
609665
or await self._get_market_quantity_from_risk(
@@ -631,8 +687,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
631687
)
632688
if current_order := await self._create_order(
633689
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,
636692
symbol_market, tag
637693
):
638694
created_orders.append(current_order)
@@ -667,8 +723,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
667723
updated_limit = None
668724
if create_stop_only or (updated_limit := await self._create_order(
669725
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,
672728
symbol_market, tag
673729
)):
674730
if updated_limit:
@@ -735,8 +791,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
735791
updated_limit = None
736792
if create_stop_only or (updated_limit := await self._create_order(
737793
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,
740796
symbol_market, tag
741797
)):
742798
if updated_limit:
@@ -797,8 +853,8 @@ async def create_new_orders(self, symbol, final_note, state, **kwargs):
797853
)
798854
if current_order := await self._create_order(
799855
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,
802858
symbol_market, tag
803859
):
804860
created_orders.append(current_order)

0 commit comments

Comments
 (0)