Skip to content

Commit

Permalink
Merge pull request #1445 from Drakkar-Software/dev
Browse files Browse the repository at this point in the history
Dev merge
  • Loading branch information
GuillaumeDSM authored Feb 18, 2025
2 parents 863095e + bbe5c1f commit 8f4baa7
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import typing

import octobot_commons.constants as commons_constants
import octobot_commons.enums as commons_enums
import octobot_commons.evaluators_util as evaluators_util
Expand Down Expand Up @@ -48,18 +50,21 @@ def init_user_inputs(self, inputs: dict) -> None:
those are defined somewhere else.
"""
super().init_user_inputs(inputs)
default_config = self.get_default_config()
self.UI.user_input(commons_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT, commons_enums.UserInputTypes.INT,
500, inputs, min_val=1,
default_config[commons_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT],
inputs, min_val=1,
title="Initialization candles count: the number of historical candles to fetch from "
"exchanges when OctoBot is starting.")
self.social_evaluators_default_timeout = \
self.UI.user_input(self.SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY, commons_enums.UserInputTypes.INT,
1 * commons_constants.HOURS_TO_SECONDS, inputs, min_val=0,
title="Number of seconds to consider a social evaluation valid from the moment it "
default_config[self.SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY],
inputs, min_val=0,
title="Number of seconds to consider a social evaluation valid from the moment it "
"appears on OctoBot. Example: a tweet evaluation.")
self.re_evaluate_TA_when_social_or_realtime_notification = \
self.UI.user_input(self.RE_EVAL_TA_ON_RT_OR_SOCIAL, commons_enums.UserInputTypes.BOOLEAN,
True, inputs,
default_config[self.RE_EVAL_TA_ON_RT_OR_SOCIAL], inputs,
title="Recompute technical evaluators on real-time evaluator signal: "
"When activated, technical evaluators will be asked to recompute their evaluation "
"based on the current in-construction candle "
Expand All @@ -70,13 +75,26 @@ def init_user_inputs(self, inputs: dict) -> None:
"alongside technical analysis results of the last closed candle.")
self.background_social_evaluators = \
self.UI.user_input(self.BACKGROUND_SOCIAL_EVALUATORS, commons_enums.UserInputTypes.MULTIPLE_OPTIONS,
[], inputs, other_schema_values={"minItems": 0, "uniqueItems": True},
default_config[self.BACKGROUND_SOCIAL_EVALUATORS],
inputs, other_schema_values={"minItems": 0, "uniqueItems": True},
options=["RedditForumEvaluator", "TwitterNewsEvaluator",
"TelegramSignalEvaluator", "GoogleTrendsEvaluator"],
title="Social evaluator to consider as background evaluators: they won't trigger technical "
"evaluators re-evaluation when updated. Avoiding unnecessary updates increases "
"performances.")

@classmethod
def get_default_config(cls, time_frames: typing.Optional[list[str]] = None) -> dict:
return {
evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME: (
time_frames or [commons_enums.TimeFrames.ONE_HOUR.value]
),
commons_constants.CONFIG_TENTACLES_REQUIRED_CANDLES_COUNT: 500,
cls.SOCIAL_EVALUATORS_NOTIFICATION_TIMEOUT_KEY: 1 * commons_constants.HOURS_TO_SECONDS,
cls.RE_EVAL_TA_ON_RT_OR_SOCIAL: True,
cls.BACKGROUND_SOCIAL_EVALUATORS: [],
}

async def matrix_callback(self,
matrix_id,
evaluator_name,
Expand Down
62 changes: 54 additions & 8 deletions Evaluator/TA/momentum_evaluator/momentum.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@


class RSIMomentumEvaluator(evaluators.TAEvaluator):
PERIOD_LENGTH = "period_length"
TREND_CHANGE_IDENTIFIER = "trend_change_identifier"
LONG_THRESHOLD = "long_threshold"
SHORT_THRESHOLD = "short_threshold"

def __init__(self, tentacles_setup_config):
super().__init__(tentacles_setup_config)
Expand All @@ -43,16 +47,19 @@ def init_user_inputs(self, inputs: dict) -> None:
"""
Called right before starting the evaluator, should define all the evaluator's user inputs
"""
default_config = self.get_default_config()
self.period_length = self.UI.user_input(
"period_length", enums.UserInputTypes.INT, self.period_length, inputs, min_val=0, title="RSI period length"
self.PERIOD_LENGTH, enums.UserInputTypes.INT, default_config["period_length"],
inputs, min_val=0, title="RSI period length"
)

self.is_trend_change_identifier = self.UI.user_input(
"trend_change_identifier", enums.UserInputTypes.BOOLEAN, self.is_trend_change_identifier, inputs,
self.TREND_CHANGE_IDENTIFIER, enums.UserInputTypes.BOOLEAN,
default_config["trend_change_identifier"], inputs,
title="Trend identifier: Identify RSI trend changes and evaluate the trend changes strength",
)
self.short_threshold = self.UI.user_input(
"short_threshold", enums.UserInputTypes.FLOAT, self.short_threshold, inputs,
self.SHORT_THRESHOLD, enums.UserInputTypes.FLOAT, default_config["short_threshold"], inputs,
min_val=0,
title="Short threshold: RSI value from with to send a short (sell) signal. "
"Evaluates as 1 when the current RSI value is equal or higher.",
Expand All @@ -63,7 +70,7 @@ def init_user_inputs(self, inputs: dict) -> None:
}
)
self.long_threshold = self.UI.user_input(
"long_threshold", enums.UserInputTypes.FLOAT, self.long_threshold, inputs,
self.LONG_THRESHOLD, enums.UserInputTypes.FLOAT, default_config["long_threshold"], inputs,
min_val=0,
title="Long threshold: RSI value from with to send a long (buy) signal. "
"Evaluates as -1 when the current RSI value is equal or lower.",
Expand All @@ -74,6 +81,18 @@ def init_user_inputs(self, inputs: dict) -> None:
}
)

@classmethod
def get_default_config(
cls, period_length: typing.Optional[float] = None, trend_change_identifier: typing.Optional[bool] = None,
short_threshold: typing.Optional[float] = None, long_threshold: typing.Optional[float] = None
):
return {
cls.PERIOD_LENGTH: period_length or 14,
cls.TREND_CHANGE_IDENTIFIER: True if trend_change_identifier is None else trend_change_identifier,
cls.SHORT_THRESHOLD: short_threshold or 70,
cls.LONG_THRESHOLD: long_threshold or 30,
}

async def ohlcv_callback(self, exchange: str, exchange_id: str,
cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):
candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),
Expand Down Expand Up @@ -365,28 +384,51 @@ async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle

# EMA
class EMAMomentumEvaluator(evaluators.TAEvaluator):
PERIOD_LENGTH = "period_length"
PRICE_THRESHOLD_PERCENT = "price_threshold_percent"
REVERSE_SIGNAL = "reverse_signal"

def __init__(self, tentacles_setup_config):
super().__init__(tentacles_setup_config)
self.period_length = 21
self.price_threshold_percent = 2
self.price_threshold_multiplier = self.price_threshold_percent / 100
self.reverse_signal = False

def init_user_inputs(self, inputs: dict) -> None:
default_config = self.get_default_config()
self.period_length = self.UI.user_input(
"period_length", enums.UserInputTypes.INT, self.period_length, inputs,
self.PERIOD_LENGTH, enums.UserInputTypes.INT, default_config["period_length"], inputs,
min_val=1, title="Period: Moving Average period length."
)
self.price_threshold_percent = self.UI.user_input(
"price_threshold_percent", enums.UserInputTypes.FLOAT, self.price_threshold_multiplier, inputs,
self.PRICE_THRESHOLD_PERCENT, enums.UserInputTypes.FLOAT,
default_config["price_threshold_percent"], inputs,
min_val=0,
title="Price threshold: Percent difference between the current price and current EMA value from "
"which to trigger a long or short signal. "
"Example with EMA value=200, Price threshold=5: a short signal will fire when price is above or "
"equal to 210 and a long signal will when price is bellow or equal to 190",
)
self.reverse_signal = self.UI.user_input(
self.REVERSE_SIGNAL, enums.UserInputTypes.BOOLEAN, default_config["reverse_signal"], inputs,
title="Reverse signal: when enabled, emits a short signal when the current price is bellow the EMA "
"value and long signal when the current price is above the EMA value.",
)
self.price_threshold_multiplier = self.price_threshold_percent / 100

@classmethod
def get_default_config(
cls,
period_length: typing.Optional[int] = None, price_threshold_percent: typing.Optional[float] = None,
reverse_signal: typing.Optional[bool] = False,
) -> dict:
return {
cls.PERIOD_LENGTH: period_length or 21,
cls.PRICE_THRESHOLD_PERCENT: 2 if price_threshold_percent is None else price_threshold_percent,
cls.REVERSE_SIGNAL: reverse_signal or False,
}

async def ohlcv_callback(self, exchange: str, exchange_id: str,
cryptocurrency: str, symbol: str, time_frame, candle, inc_in_construction_data):
candle_data = trading_api.get_symbol_close_candles(self.get_exchange_symbol_data(exchange, exchange_id, symbol),
Expand All @@ -400,10 +442,14 @@ async def evaluate(self, cryptocurrency, symbol, time_frame, candle_data, candle
if len(candle_data) >= self.period_length:
# compute ema
ema_values = tulipy.ema(candle_data, self.period_length)
if candle_data[-1] >= (ema_values[-1] * (1 + self.price_threshold_multiplier)):
is_price_above_ema_threshold = candle_data[-1] >= (ema_values[-1] * (1 + self.price_threshold_multiplier))
is_price_bellow_ema_threshold = candle_data[-1] <= (ema_values[-1] * (1 - self.price_threshold_multiplier))
if is_price_above_ema_threshold:
self.eval_note = 1
elif candle_data[-1] <= (ema_values[-1] * (1 - self.price_threshold_multiplier)):
elif is_price_bellow_ema_threshold:
self.eval_note = -1
if self.reverse_signal:
self.eval_note = -1 * self.eval_note
await self.evaluation_completed(cryptocurrency, symbol, time_frame,
eval_time=evaluators_util.get_eval_time(full_candle=candle,
time_frame=time_frame))
Expand Down
7 changes: 7 additions & 0 deletions Trading/Exchange/bingx/bingx_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ class Bingx(exchanges.RestExchange):
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
('the order is filled or cancelled', )
]
# text content of errors due to unhandled IP white list issues
EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [
# "PermissionDenied("bingx {"code":100419,"msg":"your current request IP is xx.xx.xx.xxx does not match IP
# whitelist , please go to https://bingx.com/en/account/api/ to verify the ip you have set",
# "timestamp":1739291708037}")"
("not match ip whitelist",),
]

# Set True when get_open_order() can return outdated orders (cancelled or not yet created)
CAN_HAVE_DELAYED_CANCELLED_ORDERS = True
Expand Down
3 changes: 2 additions & 1 deletion Trading/Exchange/kucoin/kucoin_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ class Kucoin(exchanges.RestExchange):
# text content of errors due to unhandled IP white list issues
EXCHANGE_IP_WHITELIST_ERRORS: typing.List[typing.Iterable[str]] = [
# "kucoinfutures Invalid request ip, the current clientIp is:e3b:e3b:e3b:e3b:e3b:e3b:e3b:e3b"
("invalid request ip",),]
("invalid request ip",),
]

DEFAULT_BALANCE_CURRENCIES_TO_FETCH = ["USDT"]

Expand Down
2 changes: 1 addition & 1 deletion Trading/Exchange/mexc/mexc_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def get_additional_connector_config(self):

async def get_account_id(self, **kwargs: dict) -> str:
# current impossible to get account UID (10/01/25)
return constants.DEFAULT_SUBACCOUNT_ID
return constants.DEFAULT_ACCOUNT_ID

async def get_all_tradable_symbols(self, active_only=True) -> set[str]:
"""
Expand Down
Loading

0 comments on commit 8f4baa7

Please sign in to comment.