Skip to content

Commit d5b106c

Browse files
authored
Merge pull request #1420 from Drakkar-Software/dev
Dev merge
2 parents dff5752 + e9fe53f commit d5b106c

File tree

21 files changed

+571
-121
lines changed

21 files changed

+571
-121
lines changed

Evaluator/TA/ai_evaluator/ai.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,13 @@ async def ask_gpt(self, preprompt, inputs, symbol, time_frame, candle_time) -> s
227227
{} if self.is_backtesting else self.services_config
228228
)
229229
service.apply_daily_token_limit_if_possible(self.gpt_tokens_limit)
230+
model = self.gpt_model if self.enable_model_selector else None
230231
resp = await service.get_chat_completion(
231232
[
232-
service.create_message("system", preprompt),
233-
service.create_message("user", inputs),
233+
service.create_message("system", preprompt, model=model),
234+
service.create_message("user", inputs, model=model),
234235
],
235-
model=self.gpt_model if self.enable_model_selector else None,
236+
model=model,
236237
exchange=self.exchange_name,
237238
symbol=symbol,
238239
time_frame=time_frame,

Services/Interfaces/web_interface/advanced_templates/advanced_index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
<br>
55
<div class="card text-center w-50 mx-auto">
66
<h2 class="card-header">Welcome to OctoBot's advanced interface</h2>
7+
{% with messages = get_flashed_messages(with_categories=true) %}
8+
{% if messages %}
9+
{% for category, message in messages %}
10+
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }}">
11+
{{ message }}
12+
</div>
13+
{% endfor %}
14+
{% endif %}
15+
{% endwith %}
716
<div class="card-body py-4">
817
This interface is providing insights on some OctoBot advanced concepts and should be used once OctoBot basic
918
features are understood.

Services/Interfaces/web_interface/controllers/community.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def community():
3131
try:
3232
models.wait_for_login_if_processing()
3333
logged_in_email = authenticator.get_logged_in_email()
34+
all_user_bots = models.get_all_user_bots()
3435
except (authentication.AuthenticationRequired, authentication.UnavailableError):
3536
pass
3637
except Exception as e:
@@ -45,7 +46,7 @@ def community():
4546
is_donor=bool(authenticator.user_account.supports.is_donor()),
4647
strategies=strategies,
4748
current_bots_stats=models.get_current_octobots_stats(),
48-
all_user_bots=models.get_all_user_bots(),
49+
all_user_bots=all_user_bots,
4950
selected_user_bot=models.get_selected_user_bot(),
5051
can_logout=models.can_logout(),
5152
can_select_bot=models.can_select_bot(),

Services/Interfaces/web_interface/login/web_login_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ def active_login_required(func):
105105
def decorated_view(*args, **kwargs):
106106
if is_login_required():
107107
return _login_required_func(func, *args, **kwargs)
108-
flask.flash("For security reasons, please enable password authentication in "
109-
"accounts configuration to use this page.",
108+
flask.flash(f"For security reasons, please enable password authentication in "
109+
f"accounts configuration to use the {flask.request.path} page.",
110110
category=flask_login.LOGIN_MESSAGE_CATEGORY)
111-
return flask.redirect(flask.current_app.login_manager.login_view)
111+
return flask.redirect('home')
112112
return decorated_view
113113

114114

Services/Interfaces/web_interface/websockets/abstract_websocket_namespace_notifier.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ def on_connect(self):
3838
self.clients_count += 1
3939
self.logger.debug(f"Client connected. {self.clients_count} total clients.")
4040

41-
def on_disconnect(self):
41+
def on_disconnect(self, reason):
4242
# will be called after some time (requires timeout)
4343
self.clients_count -= 1
44-
self.logger.debug(f"Client disconnected. {self.clients_count} remaining clients.")
44+
self.logger.debug(f"Client disconnected ({reason}). {self.clients_count} remaining clients.")
4545

4646
def _has_clients(self):
4747
return self.clients_count > 0

Services/Services_bases/gpt_service/gpt.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# License along with this library.
1616
import asyncio
1717
import os
18+
import uuid
19+
1820
import openai
1921
import logging
2022
import datetime
@@ -26,6 +28,7 @@
2628

2729
import octobot_commons.constants as commons_constants
2830
import octobot_commons.enums as commons_enums
31+
import octobot_commons.logging as commons_logging
2932
import octobot_commons.time_frame_manager as time_frame_manager
3033
import octobot_commons.authentication as authentication
3134
import octobot_commons.tree as tree
@@ -38,6 +41,16 @@
3841
octobot_services.util.patch_openai_proxies()
3942

4043

44+
NO_SYSTEM_PROMPT_MODELS = [
45+
"o1-mini",
46+
]
47+
MINIMAL_PARAMS_MODELS = [
48+
"o1-mini",
49+
]
50+
SYSTEM = "system"
51+
USER = "user"
52+
53+
4154
class GPTService(services.AbstractService):
4255
BACKTESTING_ENABLED = True
4356
DEFAULT_MODEL = "gpt-3.5-turbo"
@@ -47,7 +60,10 @@ def get_fields_description(self):
4760
if self._env_secret_key is None:
4861
return {
4962
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",
63+
services_constants.CONIG_LLM_CUSTOM_BASE_URL: (
64+
"Custom LLM base url to use. Leave empty to use openai.com. For Ollama models, "
65+
"add /v1 to the url (such as: http://localhost:11434/v1)"
66+
),
5167
}
5268
return {}
5369

@@ -75,7 +91,12 @@ def __init__(self):
7591
self.last_consumed_token_date = None
7692

7793
@staticmethod
78-
def create_message(role, content):
94+
def create_message(role, content, model: str = None):
95+
if role == SYSTEM and model in NO_SYSTEM_PROMPT_MODELS:
96+
commons_logging.get_logger(GPTService.__name__).debug(
97+
f"Overriding prompt to use {USER} instead of {SYSTEM} for {model}"
98+
)
99+
return {"role": USER, "content": content}
79100
return {"role": role, "content": content}
80101

81102
async def get_chat_completion(
@@ -124,12 +145,18 @@ async def _get_signal_from_gpt(
124145
self._ensure_rate_limit()
125146
try:
126147
model = model or self.model
148+
supports_params = model not in MINIMAL_PARAMS_MODELS
149+
if not supports_params:
150+
self.logger.info(
151+
f"The {model} model does not support every required parameter, results might not be as accurate "
152+
f"as with other models."
153+
)
127154
completions = await self._get_client().chat.completions.create(
128155
model=model,
129-
max_tokens=max_tokens,
156+
max_tokens=max_tokens if supports_params else openai.NOT_GIVEN,
130157
n=n,
131158
stop=stop,
132-
temperature=temperature,
159+
temperature=temperature if supports_params else openai.NOT_GIVEN,
133160
messages=messages
134161
)
135162
self._update_token_usage(completions.usage.total_tokens)
@@ -138,9 +165,15 @@ async def _get_signal_from_gpt(
138165
openai.BadRequestError, # error in request
139166
openai.UnprocessableEntityError # error in model (ex: model not found)
140167
)as err:
141-
raise errors.InvalidRequestError(
142-
f"Error when running request with model {model} (invalid request): {err}"
143-
) from err
168+
if "does not support 'system' with this model" in str(err):
169+
desc = err.body.get("message", str(err))
170+
err_message = (
171+
f"The \"{model}\" model can't be used with {SYSTEM} prompts. "
172+
f"It should be added to NO_SYSTEM_PROMPT_MODELS: {desc}"
173+
)
174+
else:
175+
err_message = f"Error when running request with model {model} (invalid request): {err}"
176+
raise errors.InvalidRequestError(err_message) from err
144177
except openai.AuthenticationError as err:
145178
self.logger.error(f"Invalid OpenAI api key: {err}")
146179
self.creation_error_message = err
@@ -284,7 +317,7 @@ def _update_token_usage(self, consumed_tokens):
284317
self.logger.debug(f"Consumed {consumed_tokens} tokens. {self.consumed_daily_tokens} consumed tokens today.")
285318

286319
def check_required_config(self, config):
287-
if self._env_secret_key is not None or self.use_stored_signals_only():
320+
if self._env_secret_key is not None or self.use_stored_signals_only() or self._get_base_url():
288321
return True
289322
try:
290323
config_key = config[services_constants.CONIG_OPENAI_SECRET_KEY]
@@ -319,10 +352,18 @@ def get_logo(self):
319352
return "https://upload.wikimedia.org/wikipedia/commons/0/04/ChatGPT_logo.svg"
320353

321354
def _get_api_key(self):
322-
return self._env_secret_key or \
323-
self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT][
324-
services_constants.CONIG_OPENAI_SECRET_KEY
325-
]
355+
key = (
356+
self._env_secret_key or
357+
self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT].get(
358+
services_constants.CONIG_OPENAI_SECRET_KEY, None
359+
)
360+
)
361+
if key and not fields_utils.has_invalid_default_config_value(key):
362+
return key
363+
if self._get_base_url():
364+
# no key and custom base url: use random key
365+
return uuid.uuid4().hex
366+
return key
326367

327368
def _get_base_url(self):
328369
value = self.config[services_constants.CONFIG_CATEGORY_SERVICES][services_constants.CONFIG_GPT].get(
@@ -337,20 +378,28 @@ async def prepare(self) -> None:
337378
if self.use_stored_signals_only():
338379
self.logger.info(f"Skipping GPT - OpenAI models fetch as self.use_stored_signals_only() is True")
339380
return
381+
if self._get_base_url():
382+
self.logger.info(f"Using custom LLM url: {self._get_base_url()}")
340383
fetched_models = await self._get_client().models.list()
341384
self.models = [d.id for d in fetched_models.data]
342385
if self.model not in 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-
)
386+
if self._get_base_url():
387+
self.logger.info(
388+
f"Custom LLM available models are: {self.models}. "
389+
f"Please select one of those in your evaluator configuration."
390+
)
391+
else:
392+
self.logger.warning(
393+
f"Warning: the default '{self.model}' model is not in available LLM models from the "
394+
f"selected LLM provider. "
395+
f"Available models are: {self.models}. Please select an available model when configuring your "
396+
f"evaluators."
397+
)
349398
except openai.AuthenticationError as err:
350399
self.logger.error(f"Invalid OpenAI api key: {err}")
351400
self.creation_error_message = err
352401
except Exception as err:
353-
self.logger.error(f"Unexpected error when checking api key: {err}")
402+
self.logger.exception(err, True, f"Unexpected error when initializing GPT service: {err}")
354403

355404
def _is_healthy(self):
356405
return self.use_stored_signals_only() or (self._get_api_key() and self.models)

Trading/Exchange/binance/binance_exchange.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ class Binance(exchanges.RestExchange):
9090
# binance {"code":-2021,"msg":"Order would immediately trigger."}
9191
("order would immediately trigger", )
9292
]
93+
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
94+
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
95+
('Unknown order sent', )
96+
]
9397

9498
BUY_STR = "BUY"
9599
SELL_STR = "SELL"
@@ -274,8 +278,14 @@ async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: di
274278

275279

276280
class BinanceCCXTAdapter(exchanges.CCXTAdapter):
277-
STOP_MARKET = 'stop_market'
278-
STOP_ORDERS = [STOP_MARKET]
281+
STOP_ORDERS = [
282+
"stop_market", "stop", # futures
283+
"stop_loss", "stop_loss_limit" # spot
284+
]
285+
TAKE_PROFITS_ORDERS = [
286+
"take_profit_market", "take_profit_limit", # futures
287+
"take_profit" # spot
288+
]
279289
BINANCE_DEFAULT_FUNDING_TIME = 8 * commons_constants.HOURS_TO_SECONDS
280290

281291
def fix_order(self, raw, symbol=None, **kwargs):
@@ -287,15 +297,34 @@ def fix_order(self, raw, symbol=None, **kwargs):
287297
return fixed
288298

289299
def _adapt_order_type(self, fixed):
290-
if fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value, None) in self.STOP_ORDERS:
291-
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
292-
updated_type = trading_enums.TradeOrderType.UNKNOWN.value
293-
if stop_price is not None:
294-
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
295-
else:
296-
self.logger.error(f"Unknown order type, order: {fixed}")
297-
# stop loss and take profits are not tagged as such by ccxt, force it
298-
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type
300+
if order_type := fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value, None):
301+
is_stop = order_type.lower() in self.STOP_ORDERS
302+
is_tp = order_type.lower() in self.TAKE_PROFITS_ORDERS
303+
if is_stop or is_tp:
304+
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
305+
selling = (
306+
fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.SIDE.value, None)
307+
== trading_enums.TradeOrderSide.SELL.value
308+
)
309+
updated_type = trading_enums.TradeOrderType.UNKNOWN.value
310+
trigger_above = False
311+
if is_stop:
312+
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
313+
trigger_above = not selling # sell stop loss triggers when price is lower than target
314+
elif is_tp:
315+
# updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
316+
# take profits are not yet handled as such: consider them as limit orders
317+
updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
318+
if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:
319+
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling
320+
trigger_above = selling # sell take profit triggers when price is higher than target
321+
else:
322+
self.logger.error(
323+
f"Unknown [{self.connector.exchange_manager.exchange_name}] order type, order: {fixed}"
324+
)
325+
# stop loss and take profits are not tagged as such by ccxt, force it
326+
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type
327+
fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above
299328
return fixed
300329

301330
def fix_trades(self, raw, **kwargs):

Trading/Exchange/bingx/bingx_exchange.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@ class Bingx(exchanges.RestExchange):
3030
# bingx {"code":100404,"msg":"the order you want to cancel is FILLED or CANCELLED already, or is not a valid
3131
# order id ,please verify","debugMsg":""}
3232
("the order you want to cancel is filled or cancelled already", ),
33+
# bingx {"code":100404,"msg":"the order is FILLED or CANCELLED already before, or is not a valid
34+
# order id ,please verify","debugMsg":""}
35+
("the order is filled or cancelled already before", ),
3336
]
3437
# text content of errors due to unhandled authentication issues
3538
EXCHANGE_AUTHENTICATION_ERRORS: typing.List[typing.Iterable[str]] = [
3639
# 'bingx {'code': '100413', 'msg': 'Incorrect apiKey', 'timestamp': '1725195218082'}'
3740
("incorrect apikey",),
3841
]
42+
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
43+
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
44+
('the order is filled or cancelled', )
45+
]
3946

4047
# Set True when get_open_order() can return outdated orders (cancelled or not yet created)
4148
CAN_HAVE_DELAYED_CANCELLED_ORDERS = True
@@ -117,17 +124,36 @@ def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):
117124
trading_enums.ExchangeConstantsOrderColumns.PRICE.value
118125
)
119126
)
127+
is_selling = (
128+
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]
129+
== trading_enums.TradeOrderSide.SELL.value
130+
)
120131
stop_price = float(stop_price)
121132
# use stop price as order price to parse it properly
122133
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price
123134
# type is TAKE_STOP_LIMIT (not unified)
124-
if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) not in (
125-
trading_enums.TradeOrderType.STOP_LOSS.value, trading_enums.TradeOrderType.TAKE_PROFIT.value
126-
):
135+
if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) == "take_stop_limit":
136+
# unsupported: no way to figure out if this order is a stop loss or a take profit
137+
# (trigger above or bellow)
138+
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = (
139+
trading_enums.TradeOrderType.UNSUPPORTED.value)
140+
self.logger.info(f"Unsupported order fetched: {order_or_trade}")
141+
else:
127142
if stop_price <= order_creation_price:
128-
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
143+
trigger_above = False
144+
if is_selling:
145+
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
146+
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] = stop_price
147+
else:
148+
order_type = trading_enums.TradeOrderType.LIMIT.value
129149
else:
130-
order_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
150+
trigger_above = True
151+
if is_selling:
152+
order_type = trading_enums.TradeOrderType.LIMIT.value
153+
else:
154+
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
155+
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] = stop_price
156+
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above
131157
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type
132158

133159
def fix_order(self, raw, **kwargs):

Trading/Exchange/bybit/bybit_exchange.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def _adapt_order_type(self, fixed):
402402
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = \
403403
trading_enums.TradeOrderType.STOP_LOSS.value
404404
else:
405-
self.logger.error(f"Unknown trigger order: {fixed}")
405+
self.logger.error(f"Unknown [{self.connector.exchange_manager.exchange_name}] trigger order: {fixed}")
406406
return fixed
407407

408408
def fix_ticker(self, raw, **kwargs):

0 commit comments

Comments
 (0)