diff --git a/DecisionTreeStrategy.json b/DecisionTreeStrategy.json new file mode 100644 index 0000000..75f1850 --- /dev/null +++ b/DecisionTreeStrategy.json @@ -0,0 +1,43 @@ +{ + "strategy_name": "DecisionTreeStrategy", + "params": { + "roi": { + "0": 10 + }, + "stoploss": { + "stoploss": -1.0 + }, + "trailing": { + "trailing_stop": false, + "trailing_stop_positive": 0.01, + "trailing_stop_positive_offset": 0.05, + "trailing_only_offset_is_reached": true + }, + "max_open_trades": { + "max_open_trades": 8 + }, + "buy": { + "percent48": 0.039, + "di_pente_5m_buy": 194 + }, + "sell": { + "percent_1d_loss_sell": 0.03, + "sma20_factor": 0.5, + "di_pente_5m_sell": -80 + }, + "protection": { + "di_pente_1d_start": 61, + "di_pente_1d_stop": -54, + "di_pente_1h_start": 79, + "di_pente_1h_stop": -42, + "di_pente_5m_start": 75, + "di_pente_5m_stop": -58, + "max200_diff": 0.042, + "protection_stake_amount": 100, + "sma5_pct_1h_gain_buy": -0.01, + "sma5_pct_1h_loss_buy": -0.004 + } + }, + "ft_stratparam_v": 1, + "export_time": "2025-02-10 19:08:28.239507+00:00" +} \ No newline at end of file diff --git a/DecisionTreeStrategy.py b/DecisionTreeStrategy.py new file mode 100644 index 0000000..d815bee --- /dev/null +++ b/DecisionTreeStrategy.py @@ -0,0 +1,1343 @@ +from datetime import timedelta, datetime +from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, stoploss_from_open, + IntParameter, IStrategy, merge_informative_pair, informative, stoploss_from_absolute) +from pandas import DataFrame +from freqtrade.persistence import Trade +from sklearn.tree import DecisionTreeClassifier +from sklearn.preprocessing import StandardScaler +import numpy as np +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +from typing import Optional, Union, Tuple +from scipy.signal import find_peaks +from typing import Any, Callable, Dict, List + +import logging +import configparser +# +from sklearn.tree import export_text +from sklearn.tree import export_graphviz +from graphviz import Source +import matplotlib.pyplot as plt + +logger = logging.getLogger(__name__) + +class DecisionTreeStrategy(IStrategy): + plot_config = { + "main_plot": { + "enter_tag": { + "color": "#197260" + }, + "max200": { + "color": "#3dde2c" + }, + "min200": { + "color": "#3dde2c" + }, + "sma5_1h": { + "color": "#ffb009" + }, + 'bb_upperband_1d': { + 'color': 'cyan' + }, + 'bb_lowerband_1d': { + 'color': 'cyan' + }, + 'bb_middleband_1d': { + 'color': 'red' + } + }, + "subplots": { + # "Volume": { + # "volume_change": { + # "color": "#d174dd" + # }, + # 'volume_spike': { + # "color": "blue" + # }, + # 'volume_mean': { + # "color": 'green' + # } + # }, + "Rsi": { + "rsi": { + "color": "blue" + }, + "rsi_1h": { + "color": "#c1b255" + }, + "rsi_sma_1h": { + "color": "#9f0e43" + }, + "rsi_change": { + "color": "green" + }, + "max200_diff": { + "color": "red" + } + + }, + "Pct": { + "percent12": { + "color": "blue" + }, + "sma20_pct": { + "color": "green" + }, + "sma5_pct_1h": { + "color": "#6b09f4" + }, + # 'sma5_down_count_1h': { + # 'color': 'blue' + # }, + # 'sma5_up_count_1h': { + # 'color': 'red' + # }, + # 'sma5_down_count_1d': { + # 'color': 'blue' + # }, + # 'sma5_up_count_1d': { + # 'color': 'red' + # } + }, + "Inversion": { + # 'inversion': { + # "color": "green" + # }, + # 'inversion_1h': { + # "color": "blue" + # }, + 'trend_is_positive': { + "color": "red" + }, + 'trend_pente_1h': { + "color": "blue" + } + }, + "DI": { + "DI+_1h": { + 'color': "red" + }, + 'DI_diff': { + 'color': 'blue' + }, + 'DI_diff_1h': { + 'color': 'green' + } + } + } + } + protections = [ + # { + # "method": "StoplossGuard", + # "lookback_period_candles": 12, + # "trade_limit": 1, + # "stop_duration_candles": 6, + # "only_per_pair": True + # }, + # { + # "method": "StoplossGuard", + # "lookback_period_candles": 12, + # "trade_limit": 2, + # "stop_duration_candles": 6, + # "only_per_pair": False + # }, + # { + # "method": "LowProfitPairs", + # "lookback_period_candles": 60, + # "trade_limit": 1, + # "stop_duration": 60, + # "required_profit": -0.05 + # }, + { + "method": "CooldownPeriod", + "stop_duration_candles": 24 + } + ] + + # 20 20 40 60 100 160 260 420 + # 50 50 100 300 500 + # fibo = [1, 1, 2, 3, 5, 8, 13, 21] + # my fibo + # 50 50 50 100 100 150 200 250 350 450 600 1050 + fibo = [1, 1, 1, 2, 2, 3, 4, 5, 7, 9, 12, 16, 21, 28, 37, 49] + baisse = [1, 2, 3, 5, 7, 10, 14, 19, 26, 35, 47, 63, 84] + # Ma suite 1 1 1 2 2 3 4 5 7 9 12 16 21 + # Mise 50 50 50 100 100 150 200 250 350 450 600 800 1050 + # Somme Mises 50 100 150 250 350 500 700 950 1300 1750 2350 3150 4200 + # baisse 1 2 3 5 7 10 14 19 26 35 47 63 84 + + # Configuration de base + minimal_roi = {"0": 10} # ROI minimum + stoploss = -1 # Stop-loss à 5% + timeframe = '5m' # Timeframe utilisée + features = ['rsi', 'max200_diff', 'adx', 'atr', 'atr_1h', 'volatility_1h', 'sma5', 'rsi_change', 'volume_change'] + target = 'future_direction' + features_to_scale = ['volume_change'] # Excluez le RSI + # Regrouper toutes les informations dans un seul dictionnaire + pairs = { + pair: { + "last_max": 0, + "trade_info": {}, + "max_touch": 0.0, + "last_sell": 0.0, + "last_buy": 0.0 + } + for pair in ["BTC/USDT", "ETH/USDT", "DOGE/USDT", "DASH/USDT", "XRP/USDT", "SOL/USDT"] + } + + close = 'close' + open = 'open' + startup_candle_count = 288 + # threshold = DecimalParameter(-10, 10, decimals=1, default=0.5, space='buy') + percent48 = DecimalParameter(-0.05, 0.05, decimals=3, default=-0.025, space='buy', optimize=False) + # bb_width = DecimalParameter(0.00, 0.05, decimals=3, default=0.02, space='buy') + # sma_pct_min = DecimalParameter(-0.05, 0.05, decimals=2, default=0.0, space='buy') + # sma_pct_max = DecimalParameter(-0.05, 0.05, decimals=2, default=0.0, space='buy') + # volume_change_buy = IntParameter(1, 20, default=2, space='buy') + # volume_change_sell = IntParameter(1, 20, default=2, space='sell') + sma20_factor = DecimalParameter(0, 1, decimals=1, default=0.25, space='sell', optimize=False) + # percent_threshold = DecimalParameter(-0.1, 0.1, decimals=2, default=0, space='buy') + sma5_pct_1h_sell = DecimalParameter(-0.01, 0.01, decimals=3, default=0.01, space='sell', optimize=False) + # percent_1d_sell = DecimalParameter(0, 0.1, decimals=2, default=0.015, space='sell', optimize=False) + # percent_1d_buy = DecimalParameter(0, 0.1, decimals=2, default=0.02, space='buy', optimize=False) + percent_1d_loss_sell = DecimalParameter(0, 0.2, decimals=2, default=0.03, space='sell', optimize=False) + # inversion_diff_sell = DecimalParameter(-1, 0, decimals=1, default=-0.8, space='sell') + di_pente_5m_sell = IntParameter(-200, 0, default=-50, space='sell') + + # first_stack_factor = IntParameter(1, 20, default=20, space='buy') + # bb_mid_pct_1d_stop = IntParameter(-100, 0, default=-50, space='buy', optimize=True) + # bb_mid_pct_1d_start = IntParameter(0, 100, default=50, space='buy', optimize=True) + # rsi_pct_1h_stop = IntParameter(-30, 0, default=-5, space='buy', optimize=True) + # rsi_pct_1h_start = IntParameter(0, 30, default=5, space='buy', optimize=True) + # percent24_1h_stop = IntParameter(-30, 0, default=-5, space='buy', optimize=True) + # percent24_1h_start = IntParameter(0, 30, default=5, space='buy', optimize=True) + # inversion_diff_buy = DecimalParameter(0.5, 1, decimals=2, default=0.8, space='buy', optimize=False) + di_pente_5m_buy = IntParameter(0, 200, default=50, space='buy') + + protection_stake_amount = IntParameter(0, 1000, default=100, space='protection', optimize=False) + sma5_pct_1h_gain_buy = DecimalParameter(-0.01, 0.01, decimals=3, default=0.1, space='protection', optimize=False) + sma5_pct_1h_loss_buy = DecimalParameter(-0.01, 0.01, decimals=3, default=0.1, space='protection', optimize=False) + max200_diff = DecimalParameter(0.00, 0.05, decimals=3, default=0.025, space='protection', optimize=False) + + di_pente_5m_stop = IntParameter(-100, 0, default=-50, space='protection') + di_pente_1h_stop = IntParameter(-100, 0, default=-50, space='protection') + di_pente_1d_stop = IntParameter(-100, 0, default=-50, space='protection') + di_pente_5m_start = IntParameter(0, 100, default=50, space='protection') + di_pente_1h_start = IntParameter(0, 100, default=50, space='protection') + di_pente_1d_start = IntParameter(0, 100, default=50, space='protection') + + stop_buying = 0 + # trailing stoploss hyperopt parameters + use_custom_stoploss = True + # Trailing stoploss + trailing_stop = True + trailing_stop_positive = 0.01 + trailing_stop_positive_offset = 0.05 + trailing_only_offset_is_reached = True + + columns_logged = False + # hard stoploss profit + pHSL = DecimalParameter(-0.15, -0.040, default=-1, decimals=3, space='sell', load=True) + # # profit threshold 1, trigger point, SL_1 is used + # pPF_1 = DecimalParameter(0.008, 0.020, default=0.016, decimals=3, space='sell', load=True) + # pSL_1 = DecimalParameter(0.008, 0.020, default=0.011, decimals=3, space='sell', load=True) + # + # # profit threshold 2, SL_2 is used + # pPF_2 = DecimalParameter(0.040, 0.100, default=0.080, decimals=3, space='sell', load=True) + # pSL_2 = DecimalParameter(0.020, 0.070, default=0.040, decimals=3, space='sell', load=True) + + position_adjustment_enable = True + # Example specific variables + max_entry_position_adjustment = 15 + # This number is explained a bit further down + max_dca_multiplier = 5.5 + + # # This is called when placing the initial order (opening trade) + # def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + # proposed_stake: float, min_stake: float | None, max_stake: float, + # leverage: float, entry_tag: str | None, side: str, + # **kwargs) -> float: + # + # # We need to leave most of the funds for possible further DCA orders + # # This also applies to fixed stakes + # return proposed_stake / self.max_dca_multiplier + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, + current_time: datetime, entry_tag: Optional[str], **kwargs) -> bool: + # print('entry_tag' + str(entry_tag)) + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + dispo = round(self.wallets.get_available_stake_amount()) + stake_amount = self.calculate_stake(pair, rate, last_candle) + self.testStopBuying(last_candle) + + if self.stop_buying: + # self.stop_buying = max(self.stop_buying, last_candle['close']) + self.log_trade( + last_candle=last_candle, + date=current_time, + action="STOP BUY", + pair=pair, + trade_type=f"{100 * last_candle['percent24_1h']:.0f} / {10000 * last_candle['bb_mid_pct_1d']:.0f} / {int(last_candle['rsi_pct_1h']):.0f}", + rate=rate, + dispo=dispo, + profit=0, + stake=round(stake_amount, 2) + ) + else: + # if (self.stop_buying > 0) & ((last_candle['close'] - self.stop_buying) / last_candle['close'] > - 0): + # self.stop_buying = 0 + self.log_trade( + last_candle=last_candle, + date=current_time, + action="START BUY", + pair=pair, + trade_type=f"{100 * last_candle['percent24_1h']:.0f} / {10000 * last_candle['bb_mid_pct_1d']:.0f} / {int(last_candle['rsi_pct_1h']):.0f}", + rate=rate, + dispo=dispo, + profit=0, + stake=round(stake_amount, 2) + ) + allow_to_buy = not self.stop_buying + if allow_to_buy: + self.pairs[pair]['last_max'] = rate + self.pairs[pair]['max_touch'] = rate + self.pairs[pair]['last_buy'] = rate + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Buy", + pair=pair, + trade_type=entry_tag, + rate=rate, + dispo=dispo, + profit=0, + stake=round(stake_amount, 2) + ) + # logger.info(f"Allow_to_buy {allow_to_buy} {pair} {current_time} Buy={entry_tag} rate={rate} dispo={dispo} rsi_1h={rsi_1h}") + + return allow_to_buy + + def testStopBuying(self, last_candle): + + # trade_type = f"{100 * last_candle['percent24_1h']:.0f} / {10000 * last_candle['bb_mid_pct_1d']:.0f} / {int(last_candle['rsi_pct_1h']):.0f}", + + # if self.stop_buying == 0: + # test_buying = (100 * last_candle['percent24_1h'] < self.percent24_1h_stop.value) | \ + # ((int(10000 * last_candle['bb_mid_pct_1d']) < self.bb_mid_pct_1d_stop.value) & (int(last_candle['rsi_pct_1h']) < self.rsi_pct_1h_stop.value)) + # if test_buying: + # self.stop_buying = max(self.stop_buying, last_candle['close']) + # else: + # test_buying = (100 * last_candle['percent24_1h'] > self.percent24_1h_start.value) & \ + # ((int(10000 * last_candle['bb_mid_pct_1d']) > self.bb_mid_pct_1d_start.value) & (int(last_candle['rsi_pct_1h']) > self.rsi_pct_1h_start.value)) + # if test_buying: + # self.stop_buying = 0 + if self.stop_buying == 0: + test_buying = (last_candle['DI+_pente'] < self.di_pente_5m_stop.value) \ + & (last_candle['DI+_pente_1h'] < self.di_pente_1h_stop.value) \ + & (last_candle['DI+_pente_1d'] < self.di_pente_1d_stop.value) + if test_buying: + self.stop_buying = max(self.stop_buying, last_candle['close']) + else: + test_buying = (last_candle['DI+_pente'] > self.di_pente_5m_start.value) \ + & (last_candle['DI+_pente_1h'] > self.di_pente_1h_start.value) \ + & (last_candle['DI+_pente_1d'] > self.di_pente_1d_start.value) + if test_buying: + self.stop_buying = 0 + + return + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, + exit_reason: str, current_time, **kwargs) -> bool: + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + allow_to_sell = (last_candle['percent'] < -0.00) + dispo = round(self.wallets.get_available_stake_amount()) + + if allow_to_sell: + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Sell", + pair=pair, + trade_type=exit_reason, + rate=last_candle['close'], + dispo=dispo, + profit=round(trade.calc_profit(rate, amount), 2) + ) + self.pairs[pair]['last_max'] = 0 + self.pairs[pair]['max_touch'] = 0 + self.pairs[pair]['last_sell'] = rate + else: + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Cancel", + pair=pair, + trade_type=exit_reason, + rate=last_candle['close'], + dispo=dispo + # profit=round(trade.calc_profit(rate, amount), 2), + ) + ok = (allow_to_sell) | (exit_reason == 'force_exit') + return ok + + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: float | None, max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs + ) -> float | None | tuple[float | None, str | None]: + + # Obtain pair dataframe (just to show how to access it) + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + + # Only buy when not actively falling price. + last_candle = dataframe.iloc[-1].squeeze() + previous_candle = dataframe.iloc[-2].squeeze() + previous_candle_12 = dataframe.iloc[-13].squeeze() + previous_candle_288 = dataframe.iloc[-289].squeeze() + # self.testStopBuying(last_candle) + # return None + + if (len(dataframe) < 1) | (self.wallets.get_available_stake_amount() < 50) | (self.stop_buying > 0): + return None + + count_of_buys, hours, days, first_price, last_price = self.getTradeInfos(current_time, trade) + dispo = round(self.wallets.get_available_stake_amount()) + first_stack = self.calculate_stake(trade.pair, current_rate, last_candle) + + # if last_candle['enter_tag'] == 'buy_upperclose_1d' and (hours > 1): + # if (count_of_buys <= 3): + # stake_amount = min(self.wallets.get_available_stake_amount(), self.calculate_stake(trade.pair, 0, last_candle)) + # else: + # stake_amount = min(self.wallets.get_available_stake_amount(), + # count_of_buys / 3 * self.calculate_stake(trade.pair, 0, last_candle)) + # # Take half of the profit at +5% + # self.log_trade( + # last_candle=last_candle, + # date=current_time, + # action="Loss BB -", + # dispo=dispo, + # pair=trade.pair, + # rate=current_rate, + # trade_type='Decrease', + # profit=round(current_profit, 4), # round(current_profit * trade.stake_amount, 2), + # buys=trade.nr_of_successful_entries, + # stake=round(stake_amount, 2) + # ) + # self.pairs[trade.pair]['max_touch'] = current_rate + # + # return stake_amount, "upperclose_1d" + + + # self.last_max_by_pair[trade.pair] * (1 - self.percent_1d_loss_sell.value)) \ + # & (last_candle['sma5_up_count_1h'] >= 1) + last_pct = (last_candle['close'] - last_price) / last_price + if ((last_pct <= -0.06) \ + & (count_of_buys > 3) \ + & (last_candle['DI_diff_1h'] >= -5) + & (hours > 1) + & (last_candle['close'] < last_candle['bb_middleband_1d']) + & (previous_candle_288['bb_middleband_1d'] <= last_candle['bb_middleband_1d']) + # & (last_candle['sma5_up_count_1h'] >= 1) + ) | ( + (last_pct <= -0.01 * count_of_buys) \ + & (count_of_buys <= 3) + & (last_candle['DI_diff_1h'] >= -5) + & (hours > 1) + & (previous_candle_288['bb_middleband_1d'] <= last_candle['bb_middleband_1d']) + # & (last_candle['close'] < last_candle['bb_middleband_1d']) + # & (last_candle['sma5_up_count_1h'] >= 1) + ): + # & (last_candle['enter_tag'] != ''): + if (count_of_buys <= 3): + stake_amount = min(self.wallets.get_available_stake_amount(), self.calculate_stake(trade.pair, 0, last_candle)) + else: + stake_amount = min(self.wallets.get_available_stake_amount(), + count_of_buys / 3 * self.calculate_stake(trade.pair, 0, last_candle)) + # Take half of the profit at +5% + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Loss -", + dispo=dispo, + pair=trade.pair, + rate=current_rate, + trade_type='Decrease', + profit=round(current_profit, 4), # round(current_profit * trade.stake_amount, 2), + buys=trade.nr_of_successful_entries, + stake=round(stake_amount, 2) + ) + self.pairs[trade.pair]['max_touch'] = current_rate + + return stake_amount, "half_loss%" + + # if current_profit < (-0.05 - (0.05 * trade.nr_of_successful_entries)) \ + # and trade.nr_of_successful_entries >= 1 \ + # and (last_candle['rsi_1h'] > previous_candle_12['rsi_1h']) \ + # and (last_candle['rsi_1h'] > 40): + if (False) & (current_profit < - 0.01) & (count_of_buys <= 3) \ + & (days >= 1) & (last_candle['DI+_1h'] > 10) \ + & (last_candle['enter_tag'] != ''): + # & (last_candle['sma5_pct_1h'] * 100 > self.sma5_pct_1h_loss_buy.value): + # & (last_candle['max200_diff'] >= self.max200_diff.value): + # & (last_candle['rsi_1h'] > 50) \ + # & (last_candle['sma5_pct_1h'] * 100 > self.sma5_pct_1h_loss_buy.value): + stake_amount = min(self.wallets.get_available_stake_amount(), + max(first_stack, first_stack * (- current_profit / 0.05))) # (trade.stake_amount / 2) + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Loss +", + dispo=dispo, + pair=trade.pair, + rate=current_rate, + trade_type='Increase', + profit=round(current_profit, 4), # round(current_profit * trade.stake_amount, 2), + buys=trade.nr_of_successful_entries, + stake=round(stake_amount, 2) + ) + self.pairs[trade.pair]['last_max'] = max(self.pairs[trade.pair]['last_max'], current_rate) + self.pairs[trade.pair]['max_touch'] = max(self.pairs[trade.pair]['max_touch'], current_rate) + return stake_amount, "increase" + + # if (current_profit > -0.05) | (last_candle['rsi_1h'] < previous_candle_12['rsi_1h']) | ( + # last_candle['rsi_1h'] < 40): + # return None + + filled_entries = trade.select_filled_orders(trade.entry_side) + count_of_entries = trade.nr_of_successful_entries + # Allow up to 3 additional increasingly larger buys (4 in total) + # Initial buy is 1x + # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% + # If that falls down to -5% again, we buy 1.5x more + # If that falls once again down to -5%, we buy 1.75x more + # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. + # That is why max_dca_multiplier is 5.5 + # Hope you have a deep wallet! + try: + if (current_profit > 0.01) \ + & (last_candle['close'] > first_price) \ + & (last_candle['close'] >= last_price * 1.01) \ + & ((hours >= 2) | (last_candle['rsi_change'] > 0.2)) \ + & (last_candle['rsi_1h'] <= 55) \ + & (count_of_buys <= self.max_entry_position_adjustment) \ + & (last_candle['sma5_1h'] > previous_candle_12['sma5_1h']): + # This returns first order stake size + stake_amount = filled_entries[0].stake_amount_filled + # print(f"1 - {stake_amount}") + # This then calculates current safety order size + # stake_amount = min(self.wallets.get_available_stake_amount(), stake_amount * ( + # 1 + (count_of_entries * (100 + 10 * count_of_buys) * last_candle['sma5_pct_1h']))) + stake_amount = min(self.wallets.get_available_stake_amount(), + self.calculate_stake(trade.pair, 0, last_candle)) + + # print(f"2 - {stake_amount}") + self.log_trade( + last_candle=last_candle, + date=current_time, + dispo=dispo, + action="Gain +", + rate=current_rate, + pair=trade.pair, + trade_type='Increase', + profit=round(current_profit, 2), + buys=count_of_entries, + stake=round(stake_amount, 2) + ) + self.pairs[trade.pair]['last_max'] = max(self.pairs[trade.pair]['last_max'], current_rate) + self.pairs[trade.pair]['max_touch'] = max(self.pairs[trade.pair]['max_touch'], current_rate) + + return stake_amount, "inc_volume" + except Exception as exception: + print(exception) + return None + + return None + + def getTradeInfos(self, current_time, trade): + filled_buys = trade.select_filled_orders('buy') + count_of_buys = len(filled_buys) + first_price = filled_buys[0].price + days = 0 + minutes = 0 + hours = 0 + last_price = first_price + mises=0 + for buy in filled_buys: + minutes = (current_time - buy.order_date_utc).seconds / 60 + hours = round(minutes / 60, 0) + days = (current_time - buy.order_date_utc).days + last_price = buy.price + mises += buy.amount * buy.price + self.pairs[trade.pair]['trade_info'] = { + "count_of_buys": count_of_buys, + "hours": hours, + "days": days, + "minutes": minutes, + "first_price": first_price, + "last_price": last_price, + "mises": mises + } + return count_of_buys, hours, days, first_price, last_price + + def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': + """ + Custom exit function for dynamic trade exits. + """ + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + + # Obtenir les données actuelles pour cette paire + last_candle = dataframe.iloc[-1].squeeze() + previous_last_candle = dataframe.iloc[-2].squeeze() + + count_of_buys, hours, days, first_price, last_price = self.getTradeInfos(current_time, trade) + days = (current_time - trade.open_date_utc).days + + expected_profit = 2 * last_candle['atr_1h'] + # current_profit = current_profit / count_of_buys + self.pairs[pair]['max_touch'] = max(current_rate, self.pairs[pair]['max_touch']) + + max_percent = expected_profit * 0.75 # self.min_max_buys[pair]['profit'] / 5 # last_candle['bb_width'] / 3.5 # 0.005 + max_profit = 0.015 # last_candle['bb_width'] * 3 / 4 # 0.015 + + # if (current_profit > 0.05) & (last_candle['volume_change_1h'] < - self.volume_change_sell.value * 100): + # return 'volume' + limit_sell = (last_candle['close'] - self.pairs[trade.pair]['max_touch']) / self.pairs[trade.pair]['max_touch'] + + # if (days > 1) \ + # & (last_candle['percent'] < 0) \ + # & (current_profit > 0.00) \ + # & (previous_last_candle['rsi'] >= 65) \ + # & (last_candle['volume_change'] < previous_last_candle['volume_change']): + # return f"rsi {last_candle['rsi_1h']:.0f}" + + if (current_profit > 0.01) & (hours > 3) & (count_of_buys > 1) & (last_candle['percent12'] < 0) & \ + (previous_last_candle['sma20'] > last_candle['sma20']) & \ + (previous_last_candle['sma5_1h'] > last_candle['sma5_1h']) & \ + (previous_last_candle['trend_is_positive'] > last_candle['trend_is_positive']): + return 'sell_tout_baisse' + + # ((last_candle['sma5_up_count_1h'] > 5) & (limit_sell > -0.03)) + if (last_candle['percent'] > 0) | (last_candle['percent3'] > 0.0) | (last_candle['percent5'] > 0.0)\ + | (last_candle['close'] * 1.006 < last_candle['bb_upperband_1d']): + return None + self.testStopBuying(last_candle) + + if (self.stop_buying > 0) & (limit_sell < -0.03) & (last_candle['DI+_1h'] < 10): + return f"stop_buying {limit_sell * 100:.0f}" + + # if (current_profit > 0.000) & ((current_time - trade.open_date_utc).days >= 2): + # return f"too_old" + # + if (current_profit > 0.005) & (limit_sell < -current_profit * 0.75) & (last_candle['DI+_1h'] < 10): + return f"limit_1 {limit_sell:.3f}" + + # if (current_profit > 0.005) & (limit_sell < - current_profit / 4) & (last_candle['percent12'] < 0) \ + # & (last_candle['close'] > last_candle['bb_upperband_1d']): + # return f"limit_2 {limit_sell:.3f}" + + if (current_profit >= expected_profit) \ + & (last_candle['percent3'] < -max_percent) \ + & (last_candle[self.close] > last_candle['sma5_1h']) \ + & (last_candle['DI_diff'] < -20): + return f"quick_lost {last_candle['percent3'] * 100:.1f}" + + if (False) & (current_profit > 0) & \ + (limit_sell < -0.01) \ + & ((current_time - trade.open_date_utc).seconds >= 3600) \ + & (last_candle['DI_diff'] < 0) \ + & (last_candle['sma5_pct_1h'] < -0): + return f"DI_diff {last_candle['DI_diff']:.0f} {limit_sell:.2f}" + + # ((self.pairs[trade.pair]['last_max'] - last_candle['close']) / self.pairs[trade.pair]['last_max'] > 0.03): + # if (last_candle['DI+_pente_1d'] < 0) & (last_candle['DI+_pente_1h'] < 0) \ + # & ((self.pairs[trade.pair]['last_max'] - last_candle['close']) / self.pairs[trade.pair]['last_max'] > 0.03): + # return f"DI {last_candle['DI+_pente_1d']:.1f} / {last_candle['DI+_pente_1h']:.1f}" + + # if (last_candle['percent48'] <= -0.04) | (last_candle['percent24'] <= -0.04) | (last_candle['percent12'] <= -0.04) \ + # & ((current_time - trade.open_date_utc).seconds >= 3600): + # return "quick_lost" + # if self.profit_b_sma20.value: + if (current_profit > expected_profit) \ + & (previous_last_candle['sma10'] > last_candle['sma10']) \ + & ((current_time - trade.open_date_utc).seconds >= 3600) \ + & ((previous_last_candle['sma20'] > last_candle['sma20']) & + ((last_candle['percent5'] < - current_profit * self.sma20_factor.value) + # | (last_candle['percent12'] < - current_profit * self.sma20_factor.value) + # | (last_candle['percent24'] < - current_profit * self.sma20_factor.value) + )) \ + & (last_candle['sma5_pct_1h'] * 100 < self.sma5_pct_1h_sell.value): + # print("over_bb_band_sma10_desc", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_sma20' + + # Par défaut, ne pas sortir + return None + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + + # Obtenir les données actuelles pour cette paire + last_candle = dataframe.iloc[-1].squeeze() + # self.getTradeInfos(current_time, trade) + # print(f"current_profit={current_profit} mises=" + str(round(self.pairs[pair]['trade_info']['mises'], 4))) + limit_sell = (last_candle['close'] - self.pairs[trade.pair]['max_touch']) / self.pairs[trade.pair]['max_touch'] + + if (current_profit > 0.08) & (limit_sell < -0.01) & (last_candle['DI+_1h'] < 10): + sl_profit = 0.85 * current_profit # n% du profit en cours + else: + sl_profit = self.pHSL.value # Hard stop-loss + stoploss = stoploss_from_open(sl_profit, current_profit) + return stoploss + + # def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', + # current_rate: float, current_profit: float, **kwargs) -> float: + # dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + # + # # Obtenir les données actuelles pour cette paire + # last_candle = dataframe.iloc[-1].squeeze() + # # hard stoploss profit + # HSL = self.pHSL.value + # PF_1 = self.pPF_1.value + # SL_1 = self.pSL_1.value + # PF_2 = self.pPF_2.value + # SL_2 = self.pSL_2.value + # + # # For profits between PF_1 and PF_2 the stoploss (sl_profit) used is linearly interpolated + # # between the values of SL_1 and SL_2. For all profits above PL_2 the sl_profit value + # # rises linearly with current profit, for profits below PF_1 the hard stoploss profit is used. + # if current_profit > PF_2: + # sl_profit = SL_2 + (current_profit - PF_2) + # elif current_profit > PF_1: + # sl_profit = SL_1 + ((current_profit - PF_1) * (SL_2 - SL_1) / (PF_2 - PF_1)) + # else: + # sl_profit = HSL + # + # stoploss = stoploss_from_open(sl_profit, current_profit) + # #logger.info(f"stoploss={stoploss}") + # return stoploss + + # Indicateurs personnalisés pour le DataFrame + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['enter_long'] = "" + dataframe['enter_tag'] = "" + dataframe['mid'] = (dataframe['close'] + dataframe['open']) / 2 + + dataframe['haopen'] = heikinashi['open'] + dataframe['haclose'] = heikinashi['close'] + dataframe['hapercent'] = dataframe['haclose'].pct_change() + + # Ajout d'indicateurs techniques + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + dataframe['rsi_change'] = dataframe['rsi'].pct_change(3) + # dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=9) + # dataframe['ema_long'] = ta.EMA(dataframe, timeperiod=21) + # dataframe['ema_50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema_200'] = ta.EMA(dataframe, timeperiod=200) + dataframe['bb_upperband'], dataframe['bb_middleband'], dataframe['bb_lowerband'] = ta.BBANDS( + dataframe['close'], timeperiod=20 + ) + dataframe["bb_width"] = ( + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] + ) + dataframe['min200'] = ta.MIN(dataframe['close'], timeperiod=200) + dataframe['max200'] = ta.MAX(dataframe['close'], timeperiod=200) + dataframe['max50'] = ta.MAX(dataframe['close'], timeperiod=50) + dataframe['min50'] = ta.MIN(dataframe['close'], timeperiod=50) + dataframe['max200_diff'] = (dataframe['max200'] - dataframe['close']) / dataframe['close'] + dataframe['max50_diff'] = (dataframe['max50'] - dataframe['close']) / dataframe['close'] + + # ADX pour la force de tendance + dataframe['adx'] = ta.ADX(dataframe) + dataframe['atr'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe[self.close], timeperiod=144) / \ + dataframe[self.close] + + # Calcul de nouvelles colonnes pour l'entraînement du modèle + dataframe['percent'] = dataframe['close'].pct_change() + dataframe['futur_pct'] = dataframe['percent'] # dataframe['close'] - dataframe['close'].shift(1) + dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) + dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) + dataframe['sma20'] = ta.SMA(dataframe, timeperiod=20) + dataframe['Interpolated'] = dataframe['sma5'].ewm(span=3, adjust=False).mean() + + dataframe["percent3"] = dataframe[self.close].pct_change(3) + dataframe["percent5"] = dataframe[self.close].pct_change(5) + dataframe["percent12"] = dataframe[self.close].pct_change(12) + dataframe["percent24"] = dataframe[self.close].pct_change(24) + dataframe["percent48"] = dataframe[self.close].pct_change(48) + dataframe["sma20_pct"] = dataframe['sma20'].pct_change() + + dataframe['volume2'] = dataframe['volume'] + dataframe.loc[dataframe['percent'] < 0, 'volume2'] *= -1 + dataframe['volume_change'] = dataframe['volume2'].rolling(5).sum() + + # # Charger les données (par exemple, dataframe avec une colonne "close") + # dataframe['SMA5'] = dataframe['mid'].rolling(window=5).mean() + # + # # Calcul de la variation de pente + # dataframe['SMA5_diff'] = dataframe['SMA5'].diff() + # + # # Calcul de la variation accélérée (différence de pente) + # dataframe['SMA5_diff2'] = dataframe['SMA5_diff'].diff() + # + # # Identifier les inversions brusques (exemple : seuil = 0.5) + # dataframe['inversion'] = ((dataframe['SMA5_diff'] * dataframe['SMA5_diff'].shift(-1) < 0) & + # (dataframe['SMA5_diff2'] > self.threshold.value)) + + # Ajoutez la colonne en utilisant .loc pour éviter l'avertissement + # dataframe.loc[:, 'future_direction'] = dataframe['close'].shift(-1) - dataframe['close'] + # dataframe.loc[:, 'future_direction'] = dataframe['future_direction'].apply(lambda x: 1 if x > 0 else 0) + # dataframe['future_direction'] = dataframe['percent48'] > 0.02 + # dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0 else 0) + + # Supprime les lignes avec des valeurs manquantes + # dataframe = dataframe.dropna() + + dataframe['DI+'] = ta.PLUS_DI(dataframe, window=12) + dataframe['DI-'] = ta.MINUS_DI(dataframe, window=12) + dataframe['DI_diff'] = dataframe['DI+'] - dataframe['DI-'] + + # Vérification de la tendance + dataframe['trend_is_positive'] = (dataframe['DI+'] - dataframe['DI-']) * (dataframe['adx']) + dataframe['trend_pente'] = dataframe['trend_is_positive'] - dataframe['trend_is_positive'].shift(1) + + # Calcul de la pente de la SMA + dataframe['SMA20_slope'] = 100 * dataframe['sma20'].diff() / dataframe['sma20'] + dataframe['trend_is_positive_2'] = dataframe['SMA20_slope'] > 0 + dataframe['trend_is_negative_2'] = dataframe['SMA20_slope'] < 0 + dataframe["volume_mean"] = dataframe["volume"].rolling(window=14).mean() + + dataframe["volume_change_mean"] = dataframe["volume_change"].rolling(window=14).mean() + + # Sortie si le volume est 3x supérieur à la moyenne + dataframe["volume_spike"] = dataframe["volume"] > (dataframe["volume_mean"] * 3) + + # Déterminer si la bougie est haussière ou baissière + dataframe["bullish_candle"] = dataframe["close"] > dataframe["open"] + dataframe["bearish_candle"] = dataframe["close"] < dataframe["open"] + + # Volume spike haussier (fort volume + bougie haussière) + dataframe["bullish_volume_spike"] = (dataframe["volume_change"] > dataframe["volume_change_mean"] * 3) & dataframe["volume_change"] > 0 + + # Volume spike baissier (fort volume + bougie baissière) + dataframe["bearish_volume_spike"] = (dataframe["volume_change"] < dataframe["volume_change_mean"] * 3) & dataframe["volume_change"] < 0 + + # ====================================================================================== + ################### INFORMATIVE 1h + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe="1h") + informative['sma5'] = ta.SMA(informative, timeperiod=5) + informative['sma5_pct'] = informative['sma5'].pct_change() + informative["percent"] = informative[self.close].pct_change() + informative['volume2'] = informative['volume'] + informative.loc[dataframe['percent'] < 0, 'volume2'] *= -1 + informative['volume_change'] = informative['volume2'].rolling(5).sum() + informative['volatility'] = ta.STDDEV(informative['close'], timeperiod=14) / informative['close'] + informative['atr'] = (ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)) / \ + informative['close'] + informative['rsi'] = ta.RSI(informative['close'], timeperiod=12) + informative['rsi_sma'] = ta.SMA(informative['rsi'], timeperiod=5) + informative['rsi_pct'] = 100 * informative['rsi'].pct_change() + informative['percent24'] = informative['close'].pct_change(24) + + # Détecter les baisses de SMA5 (True si SMA5 baisse, False sinon) + informative['sma5_down'] = informative['sma5'].diff() <= 0 + informative['sma5_up'] = informative['sma5'].diff() >= 0 + + # Compter les baisses consécutives + informative['sma5_down_count'] = informative['sma5_down'].astype(int) * ( + informative['sma5_down'].groupby((informative['sma5_down'] != informative['sma5_down'].shift()).cumsum()).cumcount() + 1) + informative['sma5_up_count'] = informative['sma5_up'].astype(int) * ( + informative['sma5_up'].groupby((informative['sma5_up'] != informative['sma5_up'].shift()).cumsum()).cumcount() + 1) + + informative['Interpolated'] = informative['sma5'].ewm(span=3, adjust=False).mean() + informative['inversion'] = ( + (informative['Interpolated'].shift(2) > informative['Interpolated'].shift( + 1)) # La pente devient positive (actuel) + & (informative['Interpolated'].shift(1) < informative['Interpolated']) + # & (data[f'SMA{sma_period}_diff2'] < data['threshold']) # Le changement est brusque + ) + # Calcul de l'ADX + informative['adx'] = ta.ADX(informative) + + informative['DI+'] = ta.PLUS_DI(informative, window=5) + informative['DI+_pente'] = 100 * informative['DI+'].diff(3) / informative['DI+'] + informative['DI-'] = ta.MINUS_DI(informative, window=5) + informative['DI-_pente'] = 100 * informative['DI-'].diff(3) / informative['DI-'] + informative['DI_diff'] = informative['DI+'] - informative['DI-'] + # Vérification de la tendance + informative['trend_is_positive'] = (informative['DI+'] - informative['DI-']) * (informative['adx']) + informative['trend_pente'] = informative['trend_is_positive'] - informative['trend_is_positive'].shift(1) + + # Calcul de la pente de la SMA + informative['SMA5_slope'] = 100 * informative['sma5'].diff() / informative['sma5'] + informative['trend_is_positive_2'] = informative['SMA5_slope'] > 0 + informative['trend_is_negative_2'] = informative['SMA5_slope'] < 0 + informative['inversion_slope'] = ( + (informative['SMA5_slope'].shift(2) > informative['SMA5_slope'].shift( + 1)) # La pente devient positive (actuel) + & (informative['SMA5_slope'].shift(1) < informative['SMA5_slope']) + # & (data[f'SMA{sma_period}_diff2'] < data['threshold']) # Le changement est brusque + ) + # heikinashi = qtpylib.heikinashi(informative) + # informative['ha_open'] = heikinashi['open'] + # informative['ha_close'] = heikinashi['close'] + # informative['ha_high'] = heikinashi['high'] + # informative['ha_low'] = heikinashi['low'] + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, "1h", ffill=True) + + # ====================================================================================== + ################### INFORMATIVE 1d + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe="1d") + informative['rsi'] = ta.RSI(informative['close'], timeperiod=12) + informative['sma5'] = ta.SMA(informative, timeperiod=3) + informative['sma5_pct'] = informative['sma5'].pct_change() + informative['rsi_pct'] = 100 * informative['rsi'].pct_change() + informative['bb_upperband'], informative['bb_middleband'], informative['bb_lowerband'] = ta.BBANDS( + informative['close'], timeperiod=20 + ) + informative["bb_mid_pct"] = informative['bb_middleband'].pct_change() + # Calcul de l'ADX + informative['adx'] = ta.ADX(informative) + + informative['DI+'] = ta.PLUS_DI(informative, window=5) + informative['DI+_pente'] = 100 * informative['DI+'].diff(3) / informative['DI+'] + informative['DI-'] = ta.MINUS_DI(informative, window=5) + informative['DI-_pente'] = 100 * informative['DI-'].diff(3) / informative['DI-'] + + # Vérification de la tendance + informative['trend_is_positive'] = (informative['DI+'] - informative['DI-']) * (informative['adx']) + + # Calcul de la pente de la SMA + informative['SMA5_slope'] = 100 * informative['sma5'].diff() / informative['sma5'] + informative['trend_is_positive_2'] = informative['SMA5_slope'] > 0 + informative['trend_is_negative_2'] = informative['SMA5_slope'] < 0 + + informative['atr'] = (ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)) / \ + informative['close'] + informative['sma5_down'] = informative['sma5'].diff() <= 0 + informative['sma5_up'] = informative['sma5'].diff() >= 0 + informative['sma5_down_count'] = informative['sma5_down'].astype(int) * ( + informative['sma5_down'].groupby((informative['sma5_down'] != informative['sma5_down'].shift()).cumsum()).cumcount() + 1) + informative['sma5_up_count'] = informative['sma5_up'].astype(int) * ( + informative['sma5_up'].groupby((informative['sma5_up'] != informative['sma5_up'].shift()).cumsum()).cumcount() + 1) + + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, "1d", ffill=True) + + # dataframe = self.detect_sma_inversions(dataframe, column='mid', sma_period=5, threshold_factor=self.threshold.value) + dataframe['DI+'] = ta.PLUS_DI(dataframe, window=14) + dataframe['DI+_pente'] = 100 * dataframe['DI+'].diff(3) / dataframe['DI+'] + dataframe['DI-'] = ta.MINUS_DI(dataframe, window=14) + dataframe['DI-_pente'] = 100 * dataframe['DI-'].diff(3) / dataframe['DI-'] + + dataframe['sma20_5_diff'] = 100 * (dataframe['sma5'].shift(7) - dataframe['sma5'].shift(2)) / dataframe[ + 'sma5'].shift(2) + dataframe['inversion'] = ( + (dataframe['Interpolated'].shift(2) >= dataframe['Interpolated'].shift(1)) # La pente devient positive (actuel) + & (dataframe['Interpolated'].shift(1) <= dataframe['Interpolated']) + # Le changement est brusque + ) + dataframe['inversion_s'] = ( + (dataframe['Interpolated'].shift(2) <= dataframe['Interpolated'].shift(1)) # La pente devient positive (actuel) + & (dataframe['Interpolated'].shift(1) >= dataframe['Interpolated']) + ) + + dataframe = self.populate_future_direction(dataframe) + + return dataframe + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Initialisation du modèle d'arbre de décision + self.model = DecisionTreeClassifier(max_depth=5, random_state=42) + self.scaler = StandardScaler() # Pour normaliser les données + + def train_model(self, dataframe: DataFrame) -> None: + + # Supprimez les valeurs infinies et NaN + dataframe.replace([np.inf, -np.inf], np.nan, inplace=True) + + # Supprimez les lignes avec des NaN dans les colonnes spécifiques + train_data = dataframe.dropna(subset=self.features + [self.target]) + + if train_data.empty: + raise ValueError("Aucune donnée valide après suppression des NaN et infinis. Vérifiez vos calculs.") + + X = train_data[self.features] + y = train_data[self.target] + + # Ajustez le scaler et le modèle + self.scaler.fit(X) + # dataframe[features_to_scale] = self.scaler.fit_transform(dataframe[self.features_to_scale]) + self.model.fit(self.scaler.transform(X), y) + + def validate_data(self, X): + if np.any(np.isnan(X)): + raise ValueError("Les données contiennent des valeurs NaN.") + if np.any(np.isinf(X)): + raise ValueError("Les données contiennent des valeurs infinies.") + + def predict(self, dataframe: DataFrame) -> np.ndarray: + predict_data = dataframe.dropna(subset=self.features) + + if predict_data.empty: + return np.array([]) + + X = predict_data[self.features] + + # Vérifiez que le scaler est ajusté + if not hasattr(self.scaler, 'mean_'): + raise ValueError("Scaler not fitted. Call `train_model` before predicting.") + + X_scaled = self.scaler.transform(X) + return self.model.predict(X_scaled) + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # dataframe = self.populate_future_direction(dataframe) + # # Appeler train_model pour ajuster scaler et modèle + # + # #if self.model is None: + # self.train_model(dataframe) + # + # predictions = self.predict(dataframe) + # dataframe['buy_signal'] = 0 + # dataframe.loc[dataframe.index[:len(predictions)], 'buy_signal'] = predictions + + # dataframe.loc[(dataframe['buy_signal'] == 1), + # ['enter_long', 'enter_tag'] + # ] = [1 , 'buy_predict'] + + # dataframe.loc[ + # (dataframe['inversion'] == 1) + # # & (dataframe['close'] < dataframe['min50'] * 1.002) + # & (dataframe['DI+'] > 0) + # # & (dataframe['DI+'] - dataframe['DI+'].shift(1) > 0) + # & (dataframe["sma20"] > dataframe["sma20"].shift(1)) + # , + # ['enter_long', 'enter_tag']] = [1, 'buy_inversion'] + + dataframe.loc[ + (dataframe['percent3'] > 0) & + ((dataframe['bb_upperband_1d'].shift(10) - dataframe['close'].shift(10)) / dataframe['close'].shift(10) > 0.05) & + (dataframe['close'].shift(10) < dataframe['bb_lowerband_1d'].shift(10) * 1.007) & + (dataframe['min50'].shift(10) == dataframe['min50']), + ['enter_long', 'enter_tag']] = [1, 'buy_upperclose_1d'] + + dataframe.loc[ + (dataframe['percent3'] > 0) & + (dataframe['close'] < dataframe['bb_upperband_1d']) & + (dataframe['sma20'].shift(1) < dataframe['sma20']) & + (dataframe['sma5_1h'].shift(1) < dataframe['sma5_1h']) & + (dataframe['trend_is_positive'].shift(1) < dataframe['trend_is_positive']), + ['enter_long', 'enter_tag']] = [1, 'buy_tout_monte'] + # dataframe.loc[ + # (dataframe['inversion'] == 1) + # & (dataframe['max50_diff'].shift(1) >= 0.035) + # & (dataframe['DI_diff'] > - 30), + # ['enter_long', 'enter_tag']] = [1, 'buy_inversion_2'] + + # dataframe.loc[ + # # (dataframe['inversion'] == 1) + # # & (dataframe['atr'] >= 0.001) + # # (dataframe['volume_change_1h'] > self.volume_change_buy.value * 100) + # (dataframe["close"] == dataframe['min200']) + # & ( + # (dataframe['percent12'] <= self.percent48.value) + # | (dataframe['percent24'] <= self.percent48.value) + # | (dataframe['percent12'] <= self.percent48.value) + # ), + # #& (self.sma_pct_min.value < dataframe["sma20_pct"] * 100) + # #& (dataframe["sma20_pct"] * 100 > self.sma_pct_max.value) + # # & (dataframe['bb_width'] >= self.bb_width.value) + # #& (dataframe['max200_diff'] >= self.max200_diff.value), + # ['enter_long', 'enter_tag'] + # ] = [1, 'buy_inversion'] + + # Supposons que `model` est votre modèle DecisionTreeClassifier ou DecisionTreeRegressor + + # tree_rules = export_text(self.model, feature_names=self.features) # `feature_names` est la liste des noms de vos colonnes + # print(tree_rules) + + # # Exporter l'arbre au format .dot + # export_graphviz( + # self.model, + # out_file="tree.dot", + # feature_names=self.features, + # #class_names=self.target, # Facultatif + # filled=True, + # rounded=True + # ) + # + # # Charger et visualiser le fichier .dot + # with open("tree.dot") as f: + # dot_graph = f.read() + # graph = Source(dot_graph) + # graph.render("decision_tree " + metadata['pair']) # Génère un fichier PNG ou PDF + # graph.view() + + # plt.figure(figsize=(12, 6)) + # plt.plot(dataframe['close'], label='Close Price', alpha=0.5) + # plt.plot(dataframe['SMA5'], label='SMA5', linewidth=2) + # inversions = dataframe[dataframe['inversion']] + # # Marquer les inversions + # plt.scatter(inversions.index, inversions['SMA5'], color='red', label='Inversions brusques' + metadata['pair']) + # plt.legend() + # plt.show() + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Définit les signaux de sortie. + """ + # Sort lorsque le prix ferme sous la bande inférieure des Bollinger + # dataframe.loc[ + # (dataframe['close'] > dataframe['bb_upperband']), + # 'sell' + # ] = 1 + # dataframe.loc[ + # (dataframe['inversion_s'] == 1) + # & (dataframe['sma20_5_diff'] < self.inversion_diff_sell.value), + # ['exit_long', 'exit_tag']] = [1 , 'sell_inversion'] + + return dataframe + + def populate_future_direction(self, dataframe: DataFrame) -> DataFrame: + """ + Ajoute une colonne `future_direction` pour indiquer si le prix augmente (1) ou diminue (0). + """ + dataframe['future_change'] = dataframe['percent12'] + dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0.005 else 0) + return dataframe + + def backtest(self, dataframe: DataFrame) -> None: + """ + Exemple de test du modèle sur des données historiques. + """ + dataframe = self.populate_future_direction(dataframe) + self.train_model(dataframe) + + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + return self.calculate_stake(pair, None, None) + + def calculate_stake(self, pair, value, last_candle): + amount = self.config['stake_amount'] #1000 / self.first_stack_factor.value self.protection_stake_amount.value # + return amount + + # def add_future_direction(self, dataframe: DataFrame) -> DataFrame: + # dataframe['future_change'] = dataframe['percent48'] + # dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0 else 0) + # return dataframe + def informative_pairs(self): + # get access to all pairs available in whitelist. + pairs = self.dp.current_whitelist() + informative_pairs = [(pair, '1d') for pair in pairs] + informative_pairs += [(pair, '1h') for pair in pairs] + + return informative_pairs + + # # Fonction générique pour détecter les inversions brusques d'une SMA + # def detect_sma_inversions(self, data, column='close', sma_period=20, threshold_factor=2): + # """ + # Détecte les inversions brusques pour une SMA donnée. + # + # :param data: DataFrame contenant les données de prix. + # :param column: Nom de la colonne des prix (ex. 'close'). + # :param sma_period: Période de la SMA (ex. 20 pour SMA20). + # :param threshold_factor: Facteur pour calculer le seuil basé sur l'écart-type. + # :return: DataFrame avec une colonne 'inversion' indiquant les points d'inversion. + # """ + # # Calcul de la SMA + # data[f'SMA{sma_period}'] = data[column].rolling(window=sma_period).mean() + # + # # Calcul de la variation de la pente (différences successives) + # data[f'SMA{sma_period}_diff'] = 100 * data[f'SMA{sma_period}'].diff() / data['close'] + # + # # Calcul de la variation accélérée (différence des différences) + # data[f'SMA{sma_period}_diff2'] = \ + # (data[f'SMA{sma_period}_diff'].shift(3) + data[f'SMA{sma_period}_diff'].shift(2) + data[ + # f'SMA{sma_period}_diff'].shift(1)) / 3 - (data[f'SMA{sma_period}_diff'].shift(2) + data[f'SMA{sma_period}_diff'].shift(1) + data[f'SMA{sma_period}_diff']) / 3 + # + # # Calcul de l'écart-type pour normaliser le seuil + # # std_diff2 = data[f'SMA{sma_period}_diff2'].std() + # + # # Définir un seuil basé sur le facteur et l'écart-type + # #threshold = threshold_factor * std_diff2 + # + # data['dir_change'] = (data[f'SMA{sma_period}_diff'].shift(1) * data[f'SMA{sma_period}_diff'] < 0) + # data['threshold_diff'] = data[f'SMA{sma_period}_diff2'].shift(1) - threshold_factor + # # Identifier les inversions brusques + # # data['inversion'] = ( + # # (data[f'SMA{sma_period}_diff'] * data[f'SMA{sma_period}_diff'].shift(-1) < 0) & # Changement de direction + # # (data[f'SMA{sma_period}_diff2'] < threshold) # Changement brusque + # # ) + # data['inversion'] = ( + # (data[f'SMA{sma_period}_diff'].shift(1) < 0) & # La pente était négative + # (data[f'SMA{sma_period}_diff'] > 0) & # La pente devient positive + # # (data['percent5'] <= self.percent_threshold.value) & + # (data['close'] <= data['close_1h'].shift(12)) & + # (data[f'SMA{sma_period}_diff2'].shift(1) > threshold_factor) # Le changement est brusque + # ) + # + # return data + + def detect_sma_inversions(self, data, column='close', sma_period=20, threshold_factor=2, rolling_window=50): + """ + Détecte les inversions de tendance SMA vers le haut sans regarder dans le futur. + + :param data: DataFrame contenant les données de prix. + :param column: Nom de la colonne des prix (ex. 'close'). + :param sma_period: Période de la SMA. + :param threshold_factor: Facteur pour calculer le seuil basé sur l'écart-type. + :param rolling_window: Fenêtre pour calculer l'écart-type sur les données passées. + :return: DataFrame avec une colonne 'inversion_up' indiquant les points d'inversion. + """ + # Calcul de la SMA + data[f'SMA{sma_period}'] = data[column].rolling(window=sma_period).mean() + + # Calcul de la variation de la pente (différences successives) + data[f'SMA{sma_period}_diff'] = data[f'SMA{sma_period}'].shift(1) - data[f'SMA{sma_period}'] + + # Calcul de la variation accélérée (différence des différences) + data[f'SMA{sma_period}_diff2'] = data[f'SMA{sma_period}_diff'].shift(1) - data[f'SMA{sma_period}_diff'] + + # Calcul de l'écart-type basé uniquement sur les données historiques + data['rolling_std_diff2'] = ( + data[f'SMA{sma_period}_diff2'].rolling(window=rolling_window, min_periods=1).std() + ) + + # Définir un seuil basé sur le facteur et l'écart-type historique + data['threshold'] = threshold_factor * data['rolling_std_diff2'] + + # Identifier les inversions vers le haut sans regarder dans le futur + data['inversion'] = ( + (data[f'SMA{sma_period}_diff'] > 0) # La pente était négative (historique) + & (data[f'SMA{sma_period}_diff'].shift(2) < data[f'SMA{sma_period}_diff'].shift( + 1)) # La pente devient positive (actuel) + & (data[f'SMA{sma_period}_diff'].shift(1) >= data[f'SMA{sma_period}_diff']) + # & (data[f'SMA{sma_period}_diff2'] < data['threshold']) # Le changement est brusque + ) + print(data['inversion']) + return data + + def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, buys=None, stake=None, last_candle=None): + # Afficher les colonnes une seule fois + if self.config.get('runmode') == 'hyperopt': + return + if not self.columns_logged: + print( + f"| {'Date':<16} | {'Action':<10} | {'Pair':<10} | {'Trade Type':<18} | {'Rate':>12} | {'Dispo':>6} | {'Profit':>8} | {'RSI':>5} | {'sma_pct_1d':>11} | {'last_max':>12} | {'rsi_pct':>12} | {'Buys':>5} | {'Stake':>10} |" + ) + print( + f"|{'-' * 18}|{'-' * 12}|{'-' * 12}|{'-' * 20}|{'-' * 14}|{'-' * 8}|{'-' * 10}|{'-' * 7}|{'-' * 13}|{'-' * 14}|{'-' * 14}|{'-' * 7}|{'-' * 12}|" + ) + self.columns_logged = True + date = str(date)[:16] if date else "-" + limit = None + # if buys is not None: + # limit = round(last_rate * (1 - self.fibo[buys] / 100), 4) + + rsi = '' + rsi_pct = '' + if last_candle is not None: + if (not np.isnan(last_candle['rsi_1d'])) and (not np.isnan(last_candle['rsi_1h'])): + rsi = str(int(last_candle['rsi_1d'])) + " " + str(int(last_candle['rsi_1h'])) + if (not np.isnan(last_candle['rsi_pct_1d'])) and (not np.isnan(last_candle['rsi_pct_1h'])): + rsi_pct = str(int(10000 * last_candle['bb_mid_pct_1d'])) + " " + str( + int(last_candle['rsi_pct_1d'])) + " " + str(int(last_candle['rsi_pct_1h'])) + + # first_rate = self.percent_threshold.value + # last_rate = self.threshold.value + # action = self.color_line(action, action) + sma5_1d = '' + sma5_1h = '' + if last_candle['sma5_pct_1d'] is not None: + sma5_1d = round(last_candle['sma5_pct_1d'] * 100, 2) + if last_candle['sma5_pct_1h'] is not None: + sma5_1h = round(last_candle['sma5_pct_1h'] * 100, 2) + sma5 = str(sma5_1d) + ' ' + str(sma5_1h) + first_rate = self.pairs[pair]['last_max'] + if action != 'Sell': + profit = round((last_candle['close'] - self.pairs[pair]['last_max']) / self.pairs[pair]['last_max'], 2) + limit_sell = rsi_pct # round((last_candle['close'] - self.pairs[pair]['last_max']) / self.pairs[pair]['last_max'], 4) + print( + f"| {date:<16} | {action:<10} | {pair:<10} | {trade_type or '-':<18} | {rate or '-':>12} | {dispo or '-':>6} | {profit or '-':>8} | {rsi or '-':>5} | {sma5 or '-':>11} | {first_rate or '-':>12} | {limit_sell or '-':>12} | {buys or '-':>5} | {stake or '-':>10} |" + ) + + @property + def protections(self): + return [ + { + "method": "CooldownPeriod", + "stop_duration_candles": 12 + } + # { + # "method": "MaxDrawdown", + # "lookback_period_candles": self.lookback.value, + # "trade_limit": self.trade_limit.value, + # "stop_duration_candles": self.protection_stop.value, + # "max_allowed_drawdown": self.protection_max_allowed_dd.value, + # "only_per_pair": False + # }, + # { + # "method": "StoplossGuard", + # "lookback_period_candles": 24, + # "trade_limit": 4, + # "stop_duration_candles": self.protection_stoploss_stop.value, + # "only_per_pair": False + # }, + # { + # "method": "StoplossGuard", + # "lookback_period_candles": 24, + # "trade_limit": 4, + # "stop_duration_candles": 2, + # "only_per_pair": False + # }, + # { + # "method": "LowProfitPairs", + # "lookback_period_candles": 6, + # "trade_limit": 2, + # "stop_duration_candles": 60, + # "required_profit": 0.02 + # }, + # { + # "method": "LowProfitPairs", + # "lookback_period_candles": 24, + # "trade_limit": 4, + # "stop_duration_candles": 2, + # "required_profit": 0.01 + # } + ] + + # def custom_backtest_summary(self, results: dict): + # # Récupérer les stats existantes + # total_trades = results.get("total_trades", 0) + # profit_total = results.get("profit_total", 0) + # wins = results.get("wins", 0) + # losses = results.get("losses", 0) + # days = results.get("backtest_days", 1) + # + # # Calculer de nouvelles métriques + # win_loss_ratio = wins / losses if losses > 0 else float('inf') + # avg_daily_profit = profit_total / days + # trades_per_day = total_trades / days + # + # # Ajouter les nouvelles stats aux logs + # logger.info("━━━━━━ METRIQUES PERSO ━━━━━━") + # logger.info(f"Win/Loss Ratio: {win_loss_ratio:.2f}") + # logger.info(f"Avg Daily Profit USDT: {avg_daily_profit:.2f}") + # logger.info(f"Trades Per Day: {trades_per_day:.2f}") + # logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + + diff --git a/DecisionTreeStrategy2020.json b/DecisionTreeStrategy2020.json new file mode 100644 index 0000000..7b061b3 --- /dev/null +++ b/DecisionTreeStrategy2020.json @@ -0,0 +1,41 @@ +{ + "strategy_name": "DecisionTreeStrategy2020", + "params": { + "roi": { + "0": 10 + }, + "stoploss": { + "stoploss": -1.0 + }, + "trailing": { + "trailing_stop": false, + "trailing_stop_positive": 0.01, + "trailing_stop_positive_offset": 0.05, + "trailing_only_offset_is_reached": true + }, + "max_open_trades": { + "max_open_trades": 8 + }, + "buy": { + "bb_width": 0.032, + "percent48": 0.043, + "percent_threshold": 0.08, + "sma_pct_max": 0.0002, + "sma_pct_min": -0.0004, + "threshold": -4.7, + "volume_change_buy": 1 + }, + "sell": { + "pHSL": -0.13, + "pPF_1": 0.009, + "pPF_2": 0.042, + "pSL_1": 0.01, + "pSL_2": 0.06, + "sma20_factor": 0.8, + "volume_change_sell": 8 + }, + "protection": {} + }, + "ft_stratparam_v": 1, + "export_time": "2025-01-30 07:44:10.962788+00:00" +} \ No newline at end of file diff --git a/DecisionTreeStrategyLong.json b/DecisionTreeStrategyLong.json new file mode 100644 index 0000000..f66e6df --- /dev/null +++ b/DecisionTreeStrategyLong.json @@ -0,0 +1,41 @@ +{ + "strategy_name": "DecisionTreeStrategyLong", + "params": { + "roi": { + "0": 10 + }, + "stoploss": { + "stoploss": -1.0 + }, + "trailing": { + "trailing_stop": false, + "trailing_stop_positive": 0.01, + "trailing_stop_positive_offset": 0.05, + "trailing_only_offset_is_reached": true + }, + "max_open_trades": { + "max_open_trades": 8 + }, + "buy": { + "bb_width": 0.0, + "percent48": 0.047, + "percent_threshold": 0.0, + "sma_pct_max": 0.0002, + "sma_pct_min": 0.0, + "threshold": -4.4, + "volume_change_buy": 2 + }, + "sell": { + "pHSL": -0.175, + "pPF_1": 0.008, + "pPF_2": 0.06, + "pSL_1": 0.014, + "pSL_2": 0.066, + "sma20_factor": 0.8, + "volume_change_sell": 8 + }, + "protection": {} + }, + "ft_stratparam_v": 1, + "export_time": "2025-01-29 22:17:25.987035+00:00" +} \ No newline at end of file diff --git a/Detecteur.py b/Detecteur.py new file mode 100644 index 0000000..b427c9c --- /dev/null +++ b/Detecteur.py @@ -0,0 +1,339 @@ +from datetime import timedelta, datetime +from freqtrade.persistence import Trade +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, stoploss_from_open, + IntParameter, IStrategy, merge_informative_pair, informative, stoploss_from_absolute) +import pandas as pd +import numpy as np +from pandas import DataFrame +from typing import Optional, Union, Tuple +from scipy.signal import find_peaks + +import logging +import configparser +from technical import pivots_points +# -------------------------------- +#pip install scikit-learn + +# Add your lib to import here +import ta +import talib.abstract as talib +import freqtrade.vendor.qtpylib.indicators as qtpylib +import requests + +logger = logging.getLogger(__name__) +# Configuration du logger +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) + +from technical.indicators import RMI +from ta.momentum import RSIIndicator, StochasticOscillator +from ta.trend import SMAIndicator, EMAIndicator, MACD, ADXIndicator +from ta.volatility import BollingerBands +from ta.volume import ChaikinMoneyFlowIndicator +from sklearn.model_selection import train_test_split +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import accuracy_score + + +class Detecteur(IStrategy): + minimal_roi = { + "0": 10, # Take profit when reaching +5% + } + stoploss = -0.05 # Stop loss at -5% + timeframe = '1h' + use_custom_stoploss = False + trailing_stop = False + model = None + + plot_config = { + "main_plot": { + "close": {"color": "black", "type": "line", "label": "Close Price"}, + "ema50": {"color": "blue", "type": "line", "label": "EMA 50"}, + "ema200": {"color": "red", "type": "line", "label": "EMA 200"}, + "bb_lower": {"color": "green", "type": "line", "label": "Bollinger Lower Band"}, + }, + "subplots": { + "RSI": { + "rsi": {"color": "purple", "type": "line", "label": "RSI (14)"}, + }, + "MACD": { + "macd": {"color": "orange", "type": "line", "label": "MACD"}, + "macd_signal": {"color": "pink", "type": "line", "label": "MACD Signal"}, + }, + "ADX": { + "adx": {"color": "brown", "type": "line", "label": "ADX"}, + }, + "Stochastic": { + "stoch_k": {"color": "cyan", "type": "line", "label": "Stoch %K"}, + "stoch_d": {"color": "magenta", "type": "line", "label": "Stoch %D"}, + }, + "Chaikin Money Flow": { + "cmf": {"color": "teal", "type": "line", "label": "Chaikin Money Flow"}, + }, + "ATR": { + "atr": {"color": "darkblue", "type": "line", "label": "Average True Range"}, + }, + "Volume": { + "volume": {"color": "gray", "type": "bar", "label": "Volume"}, + }, + "Heikin Ashi": { + "ha_close": {"color": "lime", "type": "line", "label": "HA Close"}, + "ha_open": {"color": "gold", "type": "line", "label": "HA Open"}, + }, + } + } + + # Optimal timeframe for indicators + # process_only_new_candles = True + + features = ['rsi', 'rsi_percent', 'rsi_percent3', 'rsi_percent5', 'bb_upperband', 'bb_lowerband', 'close'] + + # Charger les modèles + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model = None + + def train_model(self, dataframe: DataFrame): + # Sélection des colonnes de caractéristiques (features) + features = self.features + target = 'future_direction' + + # Supprimer les lignes avec des valeurs NaN + dataframe = dataframe.dropna() + + # Diviser les données en train (80%) et test (20%) + X = dataframe[features] + y = dataframe[target] + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + + # Entraîner le modèle de régression logistique + model = LogisticRegression(max_iter=500) + model.fit(X_train, y_train) + + # Évaluer le modèle + predictions = model.predict(X_test) + accuracy = accuracy_score(y_test, predictions) + print(f"Accuracy du modèle : {accuracy * 100:.2f}%") + + return model + + def add_future_direction(self, dataframe: DataFrame) -> DataFrame: + dataframe['future_change'] = dataframe['close'].shift(-5) - dataframe['close'] + dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0 else 0) + return dataframe + + def predict_with_model(self, dataframe: DataFrame, model): + features = self.features + + # Supprimer les lignes avec des NaN + + # Prédire les probabilités + #dataframe['prediction_prob'] = model.predict_proba(dataframe[features])[:, 1] + #dataframe['predicted_direction'] = dataframe['prediction_prob'].apply(lambda x: 1 if x > 0.5 else 0) + + # Assurez-vous que dataframe est une copie complète + dataframe = dataframe.copy() + dataframe = dataframe.dropna() + + # Prédire les probabilités + dataframe.loc[:, 'prediction_prob'] = model.predict_proba(dataframe[features])[:, 1] + + # Appliquer la direction prédite + dataframe.loc[:, 'predicted_direction'] = (dataframe['prediction_prob'] > 0.5).astype(int) + + return dataframe + + # Define indicators to avoid recalculating them + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['enter_long'] = "" + dataframe['enter_tag'] = "" + dataframe['percent'] = (dataframe['close'] - dataframe['open']) / dataframe['open'] + + """ + Populate indicators used in the strategy. + """ + # Moving Averages + dataframe['ema50'] = EMAIndicator(dataframe['close'], window=50).ema_indicator() + dataframe['ema200'] = EMAIndicator(dataframe['close'], window=200).ema_indicator() + dataframe['ma_downtrend'] = (dataframe['close'] < dataframe['ema50']) & (dataframe['ema50'] < dataframe['ema200']) + + # RSI + dataframe['rsi'] = RSIIndicator(dataframe['close'], window=14).rsi() + dataframe['rsi_downtrend'] = dataframe['rsi'] < 50 + + # MACD + macd = MACD(dataframe['close'], window_slow=26, window_fast=12, window_sign=9) + dataframe['macd'] = macd.macd() + dataframe['macd_signal'] = macd.macd_signal() + dataframe['macd_downtrend'] = (dataframe['macd'] < dataframe['macd_signal']) & (dataframe['macd'] < 0) + + # ADX + adx = ADXIndicator(dataframe['high'], dataframe['low'], dataframe['close'], window=14) + dataframe['adx'] = adx.adx() + dataframe['adx_downtrend'] = (adx.adx_neg() > adx.adx_pos()) & (dataframe['adx'] > 25) + + # Bollinger Bands + bb = BollingerBands(dataframe['close'], window=20, window_dev=2) + dataframe['bb_lower'] = bb.bollinger_lband() + dataframe['bb_downtrend'] = dataframe['close'] < dataframe['bb_lower'] + + # Stochastic Oscillator + stoch = StochasticOscillator(dataframe['high'], dataframe['low'], dataframe['close'], window=14, smooth_window=3) + dataframe['stoch_k'] = stoch.stoch() + dataframe['stoch_d'] = stoch.stoch_signal() + dataframe['stoch_downtrend'] = (dataframe['stoch_k'] < 20) & (dataframe['stoch_d'] < 20) + + # CMF (Chaikin Money Flow) + cmf = ChaikinMoneyFlowIndicator(dataframe['high'], dataframe['low'], dataframe['close'], dataframe['volume'], window=20) + dataframe['cmf'] = cmf.chaikin_money_flow() + dataframe['cmf_downtrend'] = dataframe['cmf'] < -0.2 + + # Heikin Ashi (simplified) + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_mid'] = (dataframe['ha_close'] + dataframe['ha_open']) / 2 + dataframe['ha_percent'] = (dataframe['ha_close'] - dataframe['ha_open']) / dataframe['ha_open'] + dataframe['ha_percent12'] = (dataframe['ha_close'] - dataframe['ha_open'].shift(12)) / dataframe['ha_open'] + dataframe['ha_percent24'] = (dataframe['ha_close'] - dataframe['ha_open'].shift(24)) / dataframe['ha_open'] + + dataframe['volume2'] = dataframe['volume'] + dataframe.loc[dataframe['ha_percent'] < 0, 'volume2'] *= -1 + + # Volume confirmation + dataframe['volume_spike'] = abs(dataframe['volume2']) > abs(dataframe['volume2'].rolling(window=20).mean() * 1.5) + + # dataframe['ha_close'] = (dataframe['open'] + dataframe['high'] + dataframe['low'] + dataframe['close']) / 4 + # dataframe['ha_open'] = dataframe['ha_close'].shift(1).combine_first(dataframe['open']) + dataframe['ha_red'] = dataframe['ha_close'] < dataframe['ha_open'] + + # ATR (Average True Range) + dataframe['atr'] = (dataframe['high'] - dataframe['low']).rolling(window=14).mean() + dataframe['atr_spike'] = dataframe['atr'] > dataframe['atr'].rolling(window=20).mean() + + dataframe['min30'] = talib.MIN(dataframe['close'], timeperiod=30) + dataframe['max30'] = talib.MAX(dataframe['close'], timeperiod=30) + + # Calcul des indicateurs + dataframe['rsi'] = talib.RSI(dataframe, timeperiod=14) + #dataframe['macd'], dataframe['macd_signal'], _ = talib.MACD(dataframe) + + # Bollinger Bands + bollinger = talib.BBANDS(dataframe, timeperiod=20) + dataframe['bb_upperband'] = bollinger['upperband'] + dataframe['bb_middleband'] = bollinger['middleband'] + dataframe['bb_lowerband'] = bollinger['lowerband'] + + # Calcul des changements futurs + dataframe['future_change'] = dataframe['close'].shift(-5) - dataframe['close'] + dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0 else 0) + + # Segmenter RSI + dataframe['rsi_range'] = pd.cut(dataframe['rsi'], bins=[0, 30, 70, 100], labels=['oversold', 'neutral', 'overbought']) + dataframe['rsi_percent'] = 100 * (dataframe['rsi'] - dataframe['rsi'].shift(1)) / dataframe['rsi'] + dataframe['rsi_percent3'] = 100 * (dataframe['rsi'] - dataframe['rsi'].shift(3)) / dataframe['rsi'] + dataframe['rsi_percent5'] = 100 * (dataframe['rsi'] - dataframe['rsi'].shift(5)) / dataframe['rsi'] + dataframe['rsi_pct_range'] = pd.cut(dataframe['rsi_percent3'], bins=[-80, -15, -10, -5, 0, 5, 10, 15, 80], labels=['-20', '-15', '-10', '-5', '0', '5', '10', '15']) + + #dataframe = self.calculate_negative(dataframe) + # Probabilités par RSI + #for group in self.features: + # rsi_probabilities = dataframe.groupby(group)['future_direction'].mean() + # print(f"Probabilités de hausse selon {group} :\n", rsi_probabilities) + + rsi_probabilities = dataframe.groupby('rsi_pct_range')['future_direction'].mean() + print(f"Probabilités de hausse selon rsi_pct_range :\n", rsi_probabilities) + + # Probabilités par MACD (positif/négatif) + #dataframe['macd_signal'] = (dataframe['macd'] > dataframe['macd_signal']).astype(int) + #macd_probabilities = dataframe.groupby('macd_signal')['future_direction'].mean() + #print("Probabilités de hausse selon MACD :\n", macd_probabilities) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Define buy signal logic. + """ + dataframe.loc[ + ( + # Combine indicators to detect a strong downtrend + (dataframe['ha_red'].shift(1) == 1) + & (dataframe['ha_red'] == 0) + # dataframe['ma_downtrend'] & + # dataframe['rsi_downtrend'] & + # dataframe['macd_downtrend'] & + # dataframe['adx_downtrend'] & + # dataframe['bb_downtrend'] & + # dataframe['volume_spike'] & + # dataframe['stoch_downtrend'] & + # dataframe['cmf_downtrend'] & + # dataframe['ha_red'] & + # dataframe['atr_spike'] + ), + 'enter_long'] = 1 + nan_columns = dataframe.columns[dataframe.isna().any()].tolist() + print("Colonnes contenant des NaN :", nan_columns) + + # Compter les colonnes avec uniquement des NaN + num_only_nan_columns = dataframe.isna().all().sum() + print(f"Nombre de colonnes contenant uniquement des NaN : {num_only_nan_columns}") + + # Compter les NaN par colonne + nan_count_per_column = dataframe.isna().sum() + print("Nombre de NaN par colonne :") + print(nan_count_per_column) + columns_with_nan = nan_count_per_column[nan_count_per_column > 1000] + print("Colonnes avec au moins 1000 NaN :") + print(columns_with_nan) + + + # Former le modèle si ce n'est pas déjà fait + if self.model is None: + dataframe = self.add_future_direction(dataframe) + self.model = self.train_model(dataframe) + + # Faire des prédictions + dataframe = self.predict_with_model(dataframe, self.model) + + # Entrée sur un signal de hausse + #dataframe.loc[ + # (dataframe['predicted_direction'] == 1), + # 'enter_long'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Define sell signal logic. + """ + # dataframe.loc[ + # ( + # # Exit condition: close price crosses above EMA50 + # dataframe['close'] > dataframe['ema50'] + # ), + # 'sell'] = 1 + + return dataframe + + def calculate_negative(self, df: DataFrame) -> DataFrame: + negative_counts = [] + + # Boucle optimisée pour calculer les positions fermées en perte avant la position actuelle + for i in range(len(df)): + # Subset jusqu'à la ligne actuelle + previous_trades = df['percent'].iloc[:i] + + # Trouver toutes les positions négatives avant le premier profit positif + if not previous_trades.empty: + mask = (previous_trades > 0).idxmax() # Trouver l'index du premier profit positif + count = (previous_trades.iloc[:mask] < 0).sum() if mask > 0 else len(previous_trades) + else: + count = 0 # Si aucune position précédente, pas de perte + + negative_counts.append(count) + + df['negative_positions_before'] = negative_counts + print(df) diff --git a/HammerReversalStrategy.py b/HammerReversalStrategy.py new file mode 100644 index 0000000..8db1f11 --- /dev/null +++ b/HammerReversalStrategy.py @@ -0,0 +1,601 @@ +from datetime import timedelta, datetime +from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, stoploss_from_open, + IntParameter, IStrategy, merge_informative_pair, informative, stoploss_from_absolute) +from pandas import DataFrame +from freqtrade.persistence import Trade +from sklearn.tree import DecisionTreeClassifier +from sklearn.preprocessing import StandardScaler +import numpy as np +import talib.abstract as ta +import pandas_ta as pdta +import freqtrade.vendor.qtpylib.indicators as qtpylib +from typing import Optional, Union, Tuple + + +class HammerReversalStrategy(IStrategy): + plot_config = { + "main_plot": { + "enter_tag": { + "color": "#197260" + } + }, + "subplots": { + "Hammer": { + "hammer": { + "color": "blue" + }, + "inv_hammer": { + "color": "#c1b255" + } + } + } + } + minimal_roi = { + "0": 5 + } + # Regrouper toutes les informations dans un seul dictionnaire + pairs = { + pair: { + "last_max": 0, + "trade_info": {}, + "max_touch": 0.0, + "last_sell": 0.0, + "last_buy": 0.0 + } + for pair in ["BTC/USDT", "ETH/USDT", "DOGE/USDT", "DASH/USDT", "XRP/USDT", "SOL/USDT"] + } + + stoploss = -1 + timeframe = '1h' + position_adjustment_enable = True + columns_logged = False + max_entry_position_adjustment = 20 + + def new_adjust_trade_position(self, trade, current_time, current_rate, current_profit, min_stake, max_stake, + **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + count_of_buys, hours, days, first_price, last_price = self.getTradeInfos(current_time, trade) + + # Initialisation des user_data (backtest compatible) + if 'dynamic_stoploss' not in trade.user_data: + trade.user_data['dynamic_stoploss'] = first_price * 0.98 # Stoploss initial à -2% + + if hours < 1 or trade.stake_amount >= max_stake: + return 0 + + # Ajustement en cas de perte : renfort à la baisse + if (last_candle['close'] < first_price) and (last_candle['touch_support']) and (current_profit < -0.015 * count_of_buys): + additional_stake = self.config['stake_amount'] + print(f"Adjust Loss - {current_time} rate={current_rate:.3f} stake={additional_stake} count={count_of_buys}") + return max(additional_stake, 0) + + # Ajustement en cas de gain : renfort à la hausse + if (last_candle['close'] > first_price) and (current_profit > 0.01 * count_of_buys): + additional_stake = self.config['stake_amount'] + + # Mise à jour du stoploss dynamique (on lock un profit partiel par exemple) + new_stoploss = current_rate * 0.99 # Stoploss dynamique à -1% sous le prix actuel + trade.user_data['dynamic_stoploss'] = max(trade.user_data['dynamic_stoploss'], new_stoploss) + + print(f"Adjust Gain + {current_time} rate={current_rate:.3f} stake={additional_stake} count={count_of_buys}") + return max(additional_stake, 0) + + return 0 + + + def adjust_trade_position(self, trade, current_time, current_rate, current_profit, min_stake, max_stake, + **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + """ + Ajuste la position suite à un signal de sortie partielle. + """ + count_of_buys, hours, days, first_price, last_price = self.getTradeInfos(current_time, trade) + if hours < 1 or trade.stake_amount >= max_stake: + return 0 + + dispo = round(self.wallets.get_available_stake_amount()) + + if (count_of_buys > 1) \ + and (current_profit > 0.01) \ + and (last_candle['close'] < self.pairs[trade.pair]['max_touch'] * 0.99) \ + and (last_candle['percent5'] < 0): + # print(f"Adjust Sell all {current_time} rate={current_rate:.3f} stake={trade.stake_amount} count={count_of_buys} profit={profit:.1f}") + + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Sell All", + dispo=dispo, + pair=trade.pair, + rate=current_rate, + trade_type='Sell', + profit=round(trade.calc_profit(current_rate, trade.amount), 2), # round(current_profit * trade.stake_amount, 2), + buys=trade.nr_of_successful_entries, + stake=round(- trade.stake_amount, 2) + ) + return - trade.stake_amount + + if (last_candle['close'] < first_price) and (last_candle['touch_support']) and (current_profit < -0.015 * count_of_buys): + additional_stake = self.calculate_stake(trade.pair, last_candle) + + # print(f"Adjust Loss - {current_time} rate={current_rate:.3f} stake={additional_stake} count={count_of_buys} profit={profit:.1f}") + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Loss -", + dispo=dispo, + pair=trade.pair, + rate=current_rate, + trade_type='Decrease', + profit=round(current_profit, 4), # round(current_profit * trade.stake_amount, 2), + buys=trade.nr_of_successful_entries, + stake=round(additional_stake, 2) + ) + + return max(additional_stake, 0) + + if (last_candle['close'] > first_price) and (current_profit > 0.01 * count_of_buys): + additional_stake = self.calculate_stake(trade.pair, last_candle) + self.log_trade( + last_candle=last_candle, + date=current_time, + dispo=dispo, + action="Gain +", + rate=current_rate, + pair=trade.pair, + trade_type='Increase', + profit=round(current_profit, 2), + buys=count_of_buys, + stake=round(additional_stake, 2) + ) + + # print(f"Adjust Gain + {current_time} rate={current_rate:.3f} stake={additional_stake} count={count_of_buys} profit={profit:.1f}") + return max(additional_stake, 0) + + return 0 + + use_custom_stoploss = True + + def new_custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> float: + + if 'dynamic_stoploss' in trade.user_data: + stoploss_price = trade.user_data['dynamic_stoploss'] + if current_rate < stoploss_price: + print(f"Stoploss touché ! Vente forcée {pair} à {current_rate}") + return 0.001 # on force une sortie immédiate (stop très proche) + + # Sinon on reste sur le stoploss standard de la stratégie + return -1 # Exemple: 5% de perte max + + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + + # Obtenir les données actuelles pour cette paire + last_candle = dataframe.iloc[-1].squeeze() + return self.calculate_stake(pair, last_candle) + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + + # Obtenir les données actuelles pour cette paire + last_candle = dataframe.iloc[-1].squeeze() + # self.getTradeInfos(current_time, trade) + # print(f"current_profit={current_profit} mises=" + str(round(self.pairs[pair]['trade_info']['mises'], 4))) + limit_sell = (last_candle['close'] - self.pairs[trade.pair]['max_touch']) / self.pairs[trade.pair]['max_touch'] + + if (current_profit > 0.01) & (limit_sell < -0.01) & (last_candle['percent12'] < 0): # & (limit_sell < -0.01) & (last_candle['DI+_1h'] < 10): + sl_profit = 0.85 * current_profit # n% du profit en cours + else: + sl_profit = -1 # Hard stop-loss + stoploss = stoploss_from_open(sl_profit, current_profit) + return stoploss + + def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': + """ + Custom exit function for dynamic trade exits. + """ + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + self.pairs[pair]['max_touch'] = max(current_rate, self.pairs[pair]['max_touch']) + + # # Obtenir les données actuelles pour cette paire + # last_candle = dataframe.iloc[-1].squeeze() + # previous_last_candle = dataframe.iloc[-2].squeeze() + # if (last_candle['percent'] > 0) | (last_candle['percent3'] > 0.0) | (last_candle['percent5'] > 0.0): + # return None + # + # if current_profit > 0 and last_candle['inv_hammer'] > 0: + # return 'Sell_Hammer' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, + current_time: datetime, entry_tag: Optional[str], **kwargs) -> bool: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + dispo = round(self.wallets.get_available_stake_amount()) + stake_amount = self.calculate_stake(pair, last_candle) + + self.pairs[pair]['last_max'] = max(rate, self.pairs[pair]['last_max']) + self.pairs[pair]['max_touch'] = rate + self.pairs[pair]['last_buy'] = rate + + #print(f"Buy {current_time} {entry_tag} rate={rate:.3f} amount={amount}") + self.log_trade( + last_candle=last_candle, + date=current_time, + action="START BUY", + pair=pair, + rate=rate, + dispo=dispo, + profit=0, + stake=round(stake_amount, 2) + ) + return True + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, + exit_reason: str, current_time, **kwargs) -> bool: + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + dispo = round(self.wallets.get_available_stake_amount()) + + allow_to_sell = (last_candle['percent5'] < -0.00) + ok = (allow_to_sell) | (exit_reason == 'force_exit') + if ok: + # self.pairs[pair]['last_max'] = 0 + # self.pairs[pair]['max_touch'] = 0 + self.pairs[pair]['last_buy'] = 0 + self.log_trade( + last_candle=last_candle, + date=current_time, + action="Sell", + pair=pair, + trade_type=exit_reason, + rate=last_candle['close'], + dispo=dispo, + profit=round(trade.calc_profit(rate, amount), 2) + ) + #print(f"Sell {current_time} {exit_reason} rate={rate:.3f} amount={amount} profit={amount * rate:.3f}") + + return ok + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['hammer'] = ta.CDLHAMMER(dataframe['open'], dataframe['high'], dataframe['low'], dataframe['close']) + dataframe['inv_hammer'] = ta.CDLINVERTEDHAMMER(dataframe['open'], dataframe['high'], dataframe['low'], + dataframe['close']) + + # Volume + dataframe['volume_mean'] = ta.SMA(dataframe['volume'], timeperiod=20) + dataframe['volume_above_avg'] = dataframe['volume'] > 1.2 * dataframe['volume_mean'] + + # RSI + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['rsi_low'] = dataframe['rsi'] < 30 + dataframe['rsi_high'] = dataframe['rsi'] > 70 + + # Support / Résistance + dataframe['lowest_20'] = dataframe['low'].rolling(window=20).min() + dataframe['highest_20'] = dataframe['high'].rolling(window=20).max() + dataframe['touch_support'] = dataframe['low'] <= dataframe['lowest_20'] + dataframe['touch_resistance'] = dataframe['high'] >= dataframe['highest_20'] + + # MACD + macd = pdta.macd(dataframe['close']) + # dataframe['macd'] = macd['macd'] + # dataframe['macdsignal'] = macd['macdsignal'] + # dataframe['macdhist'] = macd['macdhist'] + dataframe['macd'] = macd['MACD_12_26_9'] + dataframe['macdsignal'] = macd['MACDs_12_26_9'] + dataframe['macdhist'] = macd['MACDh_12_26_9'] + + # Bollinger Bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + dataframe['touch_bb_lower'] = dataframe['low'] <= dataframe['bb_lowerband'] + + # ADX (Trend Force) + dataframe['adx'] = ta.ADX(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14) + # ATR + dataframe['atr'] = ta.ATR(dataframe['high'], dataframe['low'], dataframe['close'], timeperiod=14) + + # Ratio mèche/corps (manche) + dataframe['candle_length'] = dataframe['high'] - dataframe['low'] + dataframe['candle_body'] = abs(dataframe['close'] - dataframe['open']) + dataframe['wick_ratio'] = dataframe['candle_length'] / dataframe['candle_body'] + + dataframe["percent"] = dataframe['close'].pct_change() + dataframe["percent3"] = dataframe['close'].pct_change(3) + dataframe["percent5"] = dataframe['close'].pct_change(5) + dataframe["percent12"] = dataframe['close'].pct_change(12) + + dataframe = self.pattern_hammer(dataframe) + dataframe = self.detect_hammer_with_context(dataframe) + dataframe = self.detect_loose_hammer(dataframe) + #dataframe = self.detect_squeeze_pump(dataframe) + + # ====================================================================================== + ################### INFORMATIVE 1d + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe="1d") + # informative['hammer'] = ta.CDLHAMMER(informative['open'], informative['high'], informative['low'], informative['close']) + informative = self.detect_loose_hammer(informative) + informative['max7'] = ta.MAX(informative['close'], timeperiod=7) + + informative['bb_upperband'], informative['bb_middleband'], informative['bb_lowerband'] = ta.BBANDS( + informative['close'], timeperiod=20 + ) + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, "1d", ffill=True) + + dataframe['hammer_marker'] = np.where(dataframe['hammer_signal'], dataframe['low'] * 0.99, np.nan) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + (dataframe['hammer'] > 0) & False + # & (dataframe['close'] < dataframe['bb_middleband']) + # & (dataframe['volume_above_avg']) + # & (dataframe['rsi_low']) + # & (dataframe['touch_support'] | dataframe['touch_bb_lower']) # Support ou BB inférieure + # & (dataframe['wick_ratio'] > 2) # Manche >= 2x corps + # (dataframe['adx'] < 30) & # Éviter les tendances trop fortes + # (dataframe['macd'] > dataframe['macdsignal']) + , # Divergence possible + ['enter_long', 'enter_tag']] = [1, 'buy_hammer'] + + # dataframe.loc[ + # (dataframe['hammer2'] > 0) + # # & (dataframe['close'] < dataframe['bb_middleband']) + # # (dataframe['volume_above_avg']) & + # # (dataframe['rsi_low']) & + # # & (dataframe['touch_support'] | dataframe['touch_bb_lower']) # Support ou BB inférieure + # # (dataframe['wick_ratio'] > 2) & # Manche >= 2x corps + # # (dataframe['adx'] < 30) & # Éviter les tendances trop fortes + # # (dataframe['macd'] > dataframe['macdsignal']) + # , # Divergence possible + # ['enter_long', 'enter_tag']] = [1, 'buy_hammer2'] + dataframe.loc[ + (dataframe['loose_hammer'] > 0) + , # Divergence possible + ['enter_long', 'enter_tag']] = [1, 'buy_loose_hammer'] + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # dataframe.loc[ + # (dataframe['inv_hammer'] > 0) + # # (dataframe['volume_above_avg']) & + # # (dataframe['rsi_high']) & + # # (dataframe['touch_resistance'] | (dataframe['high'] >= dataframe['bb_upperband'])) & + # # (dataframe['wick_ratio'] > 2) & + # # (dataframe['adx'] < 30) & + # # (dataframe['macd'] < dataframe['macdsignal']) + # , + # ['exit_long', 'exit_tag']] = [1, 'sell_hammer'] + return dataframe + + def getTradeInfos(self, current_time, trade): + filled_buys = trade.select_filled_orders('buy') + count_of_buys = len(filled_buys) + first_price = filled_buys[0].price + days = 0 + minutes = 0 + hours = 0 + last_price = first_price + mises=0 + for buy in filled_buys: + minutes = (current_time - buy.order_date_utc).seconds / 60 + hours = round(minutes / 60, 0) + days = (current_time - buy.order_date_utc).days + last_price = buy.price + mises += buy.amount * buy.price + # self.pairs[trade.pair]['trade_info'] = { + # "count_of_buys": count_of_buys, + # "hours": hours, + # "days": days, + # "minutes": minutes, + # "first_price": first_price, + # "last_price": last_price, + # "mises": mises + # } + return count_of_buys, hours, days, first_price, last_price + + def informative_pairs(self): + # get access to all pairs available in whitelist. + pairs = self.dp.current_whitelist() + informative_pairs = [(pair, '1d') for pair in pairs] + informative_pairs += [(pair, '1h') for pair in pairs] + + return informative_pairs + + # def pattern_hammer(self, df: DataFrame) -> DataFrame: + # """ + # Expected df contains Open, High, Low, Close, + # """ + # # Compute percentile + # for level in [50, 90]: + # df[f'{level}_percentile'] = df[['high', 'low']].apply(lambda x: np.percentile(x, q=level), + # axis=1) + # + # condition = ((df['open'].values >= df[ + # '50_percentile'].values) # open larger then 50 percentile, i.e. at the upper half + # & (df['close'].values >= df['90_percentile'].values) # close larger then 90 percentile, i.e. at the top of candlestick + # & (df['close'].values >= df['open'].values) # bullish candlestick + # ) + # + # df['hammer2'] = np.where(condition, 1, 0) + # return df + + def pattern_hammer(self, df: DataFrame) -> DataFrame: + lower_shadow = np.minimum(df['open'], df['close']) - df['low'] + upper_shadow = df['high'] - np.maximum(df['open'], df['close']) + body = abs(df['close'] - df['open']) + + df['hammer2'] = ( + (lower_shadow > 2 * body) & # Longue mèche basse + (upper_shadow < body) & # Faible mèche haute + (df['close'] > df['open']) & # Bougie verte + ((df['close'] - df['low']) / (df['high'] - df['low']) > 0.75) # Clôture dans le quart supérieur + ).astype(int) + + return df + + def detect_hammer_with_context(self, df: DataFrame) -> DataFrame: + """ + Détection d'un marteau validé par : + - Structure de la bougie (marteau classique) + - Volume anormalement haut (signale l'intérêt du marché) + - Divergence RSI (momentum qui se retourne) + + """ + + # === Détection du marteau === + lower_shadow = np.minimum(df['open'], df['close']) - df['low'] + upper_shadow = df['high'] - np.maximum(df['open'], df['close']) + body = abs(df['close'] - df['open']) + + df['hammer'] = ( + (lower_shadow > 2 * body) & # Longue mèche basse + (upper_shadow < body) & # Faible mèche haute + (df['close'] > df['open']) & # Bougie verte + ((df['close'] - df['low']) / (df['high'] - df['low']) > 0.75) # Clôture en haut de la bougie + ).astype(int) + + # === Filtre sur le volume === + df['volume_mean'] = df['volume'].rolling(window=20).mean() + df['high_volume'] = df['volume'] > 1.5 * df['volume_mean'] + + # === RSI pour la divergence === + df['rsi'] = ta.RSI(df['close'], timeperiod=14) + + df['rsi_lowest'] = df['rsi'].rolling(window=5).min() # Cherche un creux récent de RSI + df['price_lowest'] = df['close'].rolling(window=5).min() + + # Divergence haussière = prix fait un nouveau plus bas, mais RSI remonte + df['bullish_divergence'] = ( + (df['low'] < df['low'].shift(1)) & + (df['rsi'] > df['rsi'].shift(1)) & + (df['rsi'] < 30) # Survendu + ) + + # === Condition finale : marteau + contexte favorable === + df['hammer_signal'] = ( + df['hammer'] & + df['high_volume'] & + df['bullish_divergence'] + ).astype(int) + + return df + + def detect_loose_hammer(self, df: DataFrame) -> DataFrame: + """ + Détection large de marteaux : accepte des corps plus gros, ne vérifie pas le volume, + ne demande pas de divergence, juste un pattern visuel simple. + """ + + body = abs(df['close'] - df['open']) + upper_shadow = abs(df['high'] - np.maximum(df['close'], df['open'])) + lower_shadow = abs(np.minimum(df['close'], df['open']) - df['low']) + + # Critères simplifiés : + df['loose_hammer'] = ( + (lower_shadow > body * 2.5) & # mèche basse > 1.5x corps + (upper_shadow < body) # petite mèche haute + # (df['close'] > df['open']) # bougie verte (optionnel, on peut prendre aussi les rouges) + ).astype(int) + df['won_hammer'] = ( + (upper_shadow > body * 2.5) & # mèche basse > 1.5x corps + (lower_shadow < body) # petite mèche haute + # (df['close'] > df['open']) # bougie verte (optionnel, on peut prendre aussi les rouges) + ).astype(int) + + return df + + def detect_squeeze_pump(self, dataframe: DataFrame) -> DataFrame: + """ + Détecte un pump vertical violent, pour éviter d'acheter dans une phase de distribution ultra risquée. + """ + # Ratio volume par rapport à la moyenne mobile + dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume'].rolling(20).mean() + + dataframe['bb_upper_dist'] = (dataframe['close'] - dataframe['bb_upperband']) / dataframe['bb_upperband'] + + # Bougie ultra verticale + dataframe['candle_pct_change'] = (dataframe['close'] - dataframe['open']) / dataframe['open'] + + # ATR pour détecter la volatilité excessive + dataframe['atr_ratio'] = dataframe['atr'] / dataframe['close'] + + # Condition de détection (à ajuster selon la pair et le marché) + dataframe['squeeze_alert'] = ( + (dataframe['volume_ratio'] > 5) & # volume X5 ou plus + (dataframe['candle_pct_change'] > 0.05) & # bougie verte de +5% ou plus + (dataframe['bb_upper_dist'] > 0.03) # ferme largement au-dessus de la BB supérieure + ) + + return dataframe + + def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, buys=None, stake=None, last_candle=None): + # Afficher les colonnes une seule fois + if self.config.get('runmode') == 'hyperopt': + return + if not self.columns_logged: + print( + f"| {'Date':<16} | {'Action':<10} | {'Pair':<10} | {'Trade Type':<18} | {'Rate':>12} | {'Dispo':>6} | {'Profit':>8} | {'Pct':>5} | {'max7_1d':>11} | {'max_touch':>12} | {'last_max':>12} | {'Buys':>5} | {'Stake':>10} |" + ) + print( + f"|{'-' * 18}|{'-' * 12}|{'-' * 12}|{'-' * 20}|{'-' * 14}|{'-' * 8}|{'-' * 10}|{'-' * 7}|{'-' * 13}|{'-' * 14}|{'-' * 14}|{'-' * 7}|{'-' * 12}|" + ) + self.columns_logged = True + date = str(date)[:16] if date else "-" + limit = None + # if buys is not None: + # limit = round(last_rate * (1 - self.fibo[buys] / 100), 4) + + rsi = '' + rsi_pct = '' + # if last_candle is not None: + # if (not np.isnan(last_candle['rsi_1d'])) and (not np.isnan(last_candle['rsi_1h'])): + # rsi = str(int(last_candle['rsi_1d'])) + " " + str(int(last_candle['rsi_1h'])) + # if (not np.isnan(last_candle['rsi_pct_1d'])) and (not np.isnan(last_candle['rsi_pct_1h'])): + # rsi_pct = str(int(10000 * last_candle['bb_mid_pct_1d'])) + " " + str( + # int(last_candle['rsi_pct_1d'])) + " " + str(int(last_candle['rsi_pct_1h'])) + + # first_rate = self.percent_threshold.value + # last_rate = self.threshold.value + # action = self.color_line(action, action) + sma5_1d = '' + sma5_1h = '' + # if last_candle['sma5_pct_1d'] is not None: + # sma5_1d = round(last_candle['sma5_pct_1d'] * 100, 2) + # if last_candle['sma5_pct_1h'] is not None: + # sma5_1h = round(last_candle['sma5_pct_1h'] * 100, 2) + sma5 = str(sma5_1d) + ' ' + str(sma5_1h) + first_rate = self.pairs[pair]['last_max'] + + if action != 'Sell': + profit = round((last_candle['close'] - self.pairs[pair]['last_max']) / self.pairs[pair]['last_max'], 2) + limit_sell = rsi_pct # round((last_candle['close'] - self.pairs[pair]['last_max']) / self.pairs[pair]['last_max'], 4) + max7_1d = last_candle['max7_1d'] #round(100 * (last_candle['close'] - self.pairs[pair]['last_max']) / self.pairs[pair]['last_max'], 1) + pct_max = round(100 * (last_candle['close'] - max7_1d) / max7_1d, 1) + print( + f"| {date:<16} | {action:<10} | {pair:<10} | {trade_type or '-':<18} | {rate or '-':>12} | {dispo or '-':>6} | {profit or '-':>8} | {pct_max or '-':>5} | {max7_1d or '-':>11} | {self.pairs[pair]['max_touch'] or '-':>12} | {self.pairs[pair]['last_max'] or '-':>12} | {buys or '-':>5} | {stake or '-':>10} |" + ) + + def calculate_stake(self, pair, last_candle): + factor = 1 - 2 * (last_candle['close'] - last_candle['max7_1d']) / last_candle['max7_1d'] + + amount = self.config['stake_amount'] * factor #1000 / self.first_stack_factor.value self.protection_stake_amount.value # + return amount diff --git a/Thor_001.json b/Thor_001.json new file mode 100644 index 0000000..72443ad --- /dev/null +++ b/Thor_001.json @@ -0,0 +1,61 @@ +{ + "strategy_name": "Thor_001", + "params": { + "roi": { + "0": 10 + }, + "stoploss": { + "stoploss": -0.10 + }, + "trailing": { + "trailing_stop": false, + "trailing_stop_positive": 0.254, + "trailing_stop_positive_offset": 0.323, + "trailing_only_offset_is_reached": false + }, + "max_open_trades": { + "max_open_trades": 3 + }, + "buy": { + "buy_rsi_1d": 45, + "buy_rsi_1h": 49, + "buy_sum_rsi_1d": 17.9, + "buy_sum_rsi_1h": 11.5 + }, + "sell": { + "pHSL": -0.99, + "pPF_1": 0.022, + "pSL_1": 0.015, + "pPF_2": 0.05, + "pSL_2": 0.03, + "profit_b_no_change": false, + "profit_b_old_sma10": false, + "profit_b_over_rsi": true, + "profit_b_quick_gain": false, + "profit_b_quick_gain_3": true, + "profit_b_quick_lost": true, + "profit_b_short_loss": false, + "profit_b_sma10": false, + "profit_b_sma20": false, + "profit_b_sma5": false, + "profit_b_very_old_sma10": false, + "sell_b_RSI": 87, + "sell_b_RSI2": 82, + "sell_b_RSI2_percent": 0.007, + "sell_b_RSI3": 75, + "sell_b_candels": 23, + "sell_b_percent": 0.014, + "sell_b_percent3": 0.018, + "sell_b_profit_no_change": 0.003, + "sell_b_profit_percent12": 0.0011, + "sell_b_too_old_day": 10, + "sell_b_too_old_percent": 0.013 + }, + "protection": { + "protection_fibo": 9, + "protection_percent_buy_lost": 3 + } + }, + "ft_stratparam_v": 1, + "export_time": "2023-02-18 16:52:23.048460+00:00" +} diff --git a/Thor_001.py b/Thor_001.py new file mode 100644 index 0000000..5dde09c --- /dev/null +++ b/Thor_001.py @@ -0,0 +1,1561 @@ +from datetime import timedelta, datetime +from freqtrade.persistence import Trade +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, stoploss_from_open, + IntParameter, IStrategy, merge_informative_pair, informative, stoploss_from_absolute) +import pandas as pd +import numpy as np +from pandas import DataFrame +from typing import Optional, Union, Tuple +from scipy.signal import find_peaks + +import logging +import configparser +from technical import pivots_points +# -------------------------------- + +# Add your lib to import here +import ta +import talib.abstract as talib +import freqtrade.vendor.qtpylib.indicators as qtpylib +import requests + +import joblib +from sklearn.model_selection import train_test_split +from sklearn.linear_model import LogisticRegression +from sklearn.ensemble import HistGradientBoostingClassifier +from sklearn.ensemble import HistGradientBoostingRegressor +from sklearn.metrics import accuracy_score +from sklearn.metrics import mean_squared_error +from sklearn.metrics import mean_absolute_error + +logger = logging.getLogger(__name__) +# Configuration du logger +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) + +from tabulate import tabulate + + +def pprint_df(dframe): + print(tabulate(dframe, headers='keys', tablefmt='psql', showindex=False)) + + +def normalize(df): + df = (df - df.min()) / (df.max() - df.min()) + return df + + +def get_limit_from_config(section, pair): + file_path = '/HOME/home/souti/freqtrade2/user_data/strategies/Thor_001.txt' + # Créez un objet ConfigParser + config = configparser.ConfigParser() + + try: + # Lisez le fichier avec les valeurs + config.read(file_path) + + # Vérifiez si la section existe + if config.has_section(section): + # Obtenez les valeurs à partir de la section et de la clé (pair) + limit = config.get(section, pair) + return limit + # else: + # raise ValueError(f"La section '{section}' n'existe pas dans le fichier de configuration.") + except Exception as e: + print(f"Erreur lors de la lecture du fichier de configuration : {e}") + return None + + +# Order( +# id=123456, +# pair='BTC/USDT', +# order_type='limit', +# side='buy', +# price=45000.0, +# amount=0.1, +# filled=0.1, +# status='closed', +# order_date=datetime(2023, 5, 12, 14, 30, 0), # Date de création de l'ordre +# exchange_order_id='abcdef123456', +# fee=0.0005, +# fee_currency='BTC' +# ) + +class Thor_001(IStrategy): + levels = [1, 2, 2, 4, 6, 8] + + # ROI table: + minimal_roi = { + "0": 10, + # "567": 0.273, + # "2814": 0.12, + # "7675": 0.05 + } + + min_max_buys = { + "BTC/USDT": {"min": 1000000, "max": 0, "profit": 0.01, 'last_rsi_1d': 0, 'last_sell_price': 1000000}, + "ETH/USDT": {"min": 1000000, "max": 0, "profit": 0.01, 'last_rsi_1d': 0, 'last_sell_price': 1000000}, + "DOGE/USDT": {"min": 1000000, "max": 0, "profit": 0.02, 'last_rsi_1d': 0, 'last_sell_price': 1000000}, + "XRP/USDT": {"min": 1000000, "max": 0, "profit": 0.02, 'last_rsi_1d': 0, 'last_sell_price': 1000000}, + "SOL/USDT": {"min": 1000000, "max": 0, "profit": 0.02, 'last_rsi_1d': 0, 'last_sell_price': 1000000}, + "DASH/USDT": {"min": 1000000, "max": 0, "profit": 0.02, 'last_rsi_1d': 0, 'last_sell_price': 1000000} + } + + last_sell = { + "BTC/USDT": 0, + "ETH/USDT": 0, + "DOGE/USDT": 0, + "DASH/USDT": 0, + "XRP/USDT": 0, + "SOL/USDT": 0 + } + # Hyperparameters + sell_max_threshold = 5.0 # % sous le max où vendre + sell_min_threshold = 1.0 + + close = 'close' + open = 'open' + + # Stoploss: + stoploss = -0.10 # 0.256 + # Custom stoploss + use_custom_stoploss = False + + # Buy hypers + timeframe = '5m' + + max_open_trades = 5 + max_amount = 40 + + startup_candle_count = 26 + + # DCA config + position_adjustment_enable = True + + plot_config = { + "main_plot": { + "min200": { + "color": "#86c932" + }, + "max50": { + "color": "white" + }, + "max200": { + "color": "yellow" + }, + "max_previous_1h": { + "color": "#da59a6"}, + "min_previous_1h": { + "color": "#da59a6", + }, + "sma5_1h": { + "color": "red", + }, + "close_1d": { + "color": "yellow" + } + }, + "subplots": { + "Rsi": { + "rsi": { + "color": "pink" + }, + "rsi_1h": { + "color": "yellow" + }, + "rsi_sma_1h": { + "color": "yellow" + } + }, + "Pct": { + "percent": { + "color": "blue" + }, + "percent3": { + "color": "green" + }, + "percent12": { + "color": "yellow" + }, + "percent24": { + "color": "pink" + }, + "sma5_pct_1h": { + "color": "red", + } + } + } + } + + # 20 20 40 60 100 160 260 420 + # 50 50 100 300 500 + # fibo = [1, 1, 2, 3, 5, 8, 13, 21] + # my fibo + # 50 50 50 100 100 150 200 250 350 450 600 1050 + fibo = [1, 1, 1, 2, 2, 3, 4, 5, 7, 9, 12, 16, 21] + baisse = [1, 2, 3, 5, 7, 10, 14, 19, 26, 35, 47, 63, 84] + # Ma suite 1 1 1 2 2 3 4 5 7 9 12 16 21 + # Mise 50 50 50 100 100 150 200 250 350 450 600 800 1050 + # Somme Mises 50 100 150 250 350 500 700 950 1300 1750 2350 3150 4200 + # baisse 1 2 3 5 7 10 14 19 26 35 47 63 84 + + trades = list() + max_profit_pairs = {} + + profit_b_no_change = BooleanParameter(default=True, space="sell") + profit_b_quick_lost = BooleanParameter(default=True, space="sell") + profit_b_sma5 = BooleanParameter(default=True, space="sell") + profit_b_sma10 = BooleanParameter(default=True, space="sell") + profit_b_sma20 = BooleanParameter(default=True, space="sell") + profit_b_quick_gain = BooleanParameter(default=True, space="sell") + profit_b_quick_gain_3 = BooleanParameter(default=True, space="sell") + profit_b_old_sma10 = BooleanParameter(default=True, space="sell") + profit_b_very_old_sma10 = BooleanParameter(default=True, space="sell") + profit_b_over_rsi = BooleanParameter(default=True, space="sell") + profit_b_short_loss = BooleanParameter(default=True, space="sell") + + sell_b_percent = DecimalParameter(0, 0.02, decimals=3, default=0.01, space='sell') + sell_b_percent3 = DecimalParameter(0, 0.02, decimals=3, default=0.01, space='sell') + sell_b_candels = IntParameter(0, 48, default=12, space='sell') + + sell_b_too_old_day = IntParameter(0, 10, default=300, space='sell') + sell_b_too_old_percent = DecimalParameter(0, 0.02, decimals=3, default=0.01, space='sell') + + sell_b_profit_no_change = DecimalParameter(0, 0.02, decimals=3, default=0.005, space='sell') + sell_b_profit_percent12 = DecimalParameter(0, 0.002, decimals=4, default=0.001, space='sell') + + sell_b_RSI = IntParameter(70, 98, default=88, space='sell') + sell_b_RSI2 = IntParameter(70, 98, default=88, space='sell') + sell_b_RSI3 = IntParameter(70, 98, default=80, space='sell') + + sell_b_RSI2_percent = DecimalParameter(0, 0.02, decimals=3, default=0.01, space='sell') + # sell_b_expected_profit = DecimalParameter(0, 0.01, decimals=3, default=0.01, space='sell') + + protection_percent_buy_lost = IntParameter(1, 10, default=5, space='protection') + # protection_nb_buy_lost = IntParameter(1, 2, default=2, space='protection') + + protection_fibo = IntParameter(1, 10, default=2, space='protection') + + # trailing stoploss hyperopt parameters + # hard stoploss profit + pHSL = DecimalParameter(-0.200, -0.040, default=-0.08, decimals=3, space='sell', optimize=False, load=True) + # profit threshold 1, trigger point, SL_1 is used + pPF_1 = DecimalParameter(0.008, 0.020, default=0.016, decimals=3, space='sell', optimize=True, load=True) + pSL_1 = DecimalParameter(0.008, 0.020, default=0.011, decimals=3, space='sell', optimize=True, load=True) + + # profit threshold 2, SL_2 is used + pPF_2 = DecimalParameter(0.040, 0.100, default=0.080, decimals=3, space='sell', optimize=True, load=True) + pSL_2 = DecimalParameter(0.020, 0.070, default=0.040, decimals=3, space='sell', optimize=True, load=True) + + columns_logged = False + + features = ['rsi_percent', 'rsi_percent3', 'rsi_percent5', 'bb_upperband', 'bb_lowerband', 'close', + close, 'percent12', 'percent24', 'tag_min', 'tag_percent24', 'tag_stable', 'tag_buy'] + + # Charger les modèles + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model = None + + def train_model(self, dataframe: DataFrame): + # Sélection des colonnes de caractéristiques (features) + features = self.features + target = 'future_direction' + + # Supprimer les lignes avec des valeurs NaN + # dataframe = dataframe.dropna() + # dataframe = dataframe.fillna("Unknown") # Remplace NaN par "Unknown" + + # Création d'une nouvelle DataFrame contenant uniquement les colonnes spécifiées + # dataframe = old_dataframe[self.features] + # dataframe['future_direction'] = old_dataframe['future_direction'] + # dataframe['future_change'] = old_dataframe['future_change'] + # dataframe['rsi_pct_range'] = old_dataframe['rsi_pct_range'] + print(f"Shape of dataset: {dataframe.shape}") + print(dataframe.head()) + + dataframe = dataframe.dropna() + #dataframe = dataframe.fillna(0) # Remplace NaN par 0 + + # Diviser les données en train (80%) et test (20%) + X = dataframe[features] + y = dataframe[target] + print(f"Shape of dataset: {dataframe.shape}") + print(dataframe.head()) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + + # Entraîner le modèle de régression logistique + model = LogisticRegression(max_iter=5000) + model.fit(X_train, y_train) + # Évaluer le modèle + predictions = model.predict(X_test) + accuracy = accuracy_score(y_test, predictions) + print(f"Accuracy du modèle : {accuracy * 100:.2f}%") + + # # HistGradientBoostingClassifier est conçu pour les problèmes de classification, pas pour la régression. + # # Si votre problème est une classification (par exemple, prédire une catégorie ou une classe), + # # vous pouvez utiliser ce modèle. + # # Remplacer LinearRegression par HistGradientBoostingClassifier + # # Diviser les données + # X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + # + # model = HistGradientBoostingClassifier() + # model.fit(X_train, y_train) + # + # # Prédictions + # y_pred = model.predict(X_test) + # accuracy = accuracy_score(y_test, y_pred) + # print(f"Accuracy: {accuracy:.2f}") + + return model + + def add_future_direction(self, dataframe: DataFrame) -> DataFrame: + dataframe['future_change'] = dataframe['close'].shift(-12) - dataframe['close'] + dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0 else 0) + return dataframe + + def predict_with_model(self, dataframe: DataFrame, model): + features = self.features + + # Supprimer les lignes avec des NaN + + # Prédire les probabilités + #dataframe['prediction_prob'] = model.predict_proba(dataframe[features])[:, 1] + #dataframe['predicted_direction'] = dataframe['prediction_prob'].apply(lambda x: 1 if x > 0.5 else 0) + + # Assurez-vous que dataframe est une copie complète + dataframe = dataframe.copy() + dataframe = dataframe.dropna() + + # Prédire les probabilités + dataframe.loc[:, 'prediction_prob'] = model.predict_proba(dataframe[features])[:, 1] + + # Appliquer la direction prédite + dataframe.loc[:, 'predicted_direction'] = (dataframe['prediction_prob'] > 0.5).astype(int) + + return dataframe + + def color_line(self, action, line): + if "INC" in action: # Exemple pour un prix croissant + return f"\033[92m{line}\033[0m" # Vert + elif "DEC" in action: # Exemple pour un prix décroissant + return f"\033[91m{line}\033[0m" # Rouge + elif "Allow Sell" in action: # Exemple pour une vente + return f"\033[93m{line}\033[0m" # Jaune + elif "Allow To buy" in action: # Exemple pour un achat + return f"\033[94m{line}\033[0m" # Bleu + else: + return line # Sans modification + + def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, rsi=None, minutes=None, + first_rate=None, last_rate=None, buys=None, stake=None): + # Afficher les colonnes une seule fois + if not self.columns_logged: + print( + f"| {'Date':<16} | {'Action':<20} | {'Pair':<10} | {'Trade Type':<18} | {'Rate':>12} | {'Dispo':>6} | {'Profit':>8} | {'RSI':>5} | {'Limit':>10} | {'First Rate':>12} | {'Last Rate':>12} | {'Buys':>5} | {'Stake':>10} |" + ) + print( + f"|{'-' * 18}|{'-' * 22}|{'-' * 12}|{'-' * 20}|{'-' * 14}|{'-' * 8}|{'-' * 10}|{'-' * 7}|{'-' * 12}|{'-' * 14}|{'-' * 14}|{'-' * 7}|{'-' * 12}|" + ) + self.columns_logged = True + date = str(date)[:16] if date else "-" + limit = None + if buys is not None: + limit = round(last_rate * (1 - self.fibo[buys] / 100), 4) + + rsi = (str(rsi) if rsi else '') + " " + str(rsi - self.min_max_buys[pair]['last_rsi_1d']) + + #action = self.color_line(action, action) + print( + f"| {date:<16} | {action:<20} | {pair:<10} | {trade_type or '-':<18} | {rate or '-':>12} | {dispo or '-':>6} | {profit or '-':>8} | {rsi or '-':>5} | {limit or '-':>10} | {first_rate or '-':>12} | {last_rate or '-':>12} | {buys or '-':>5} | {stake or '-':>10} |" + ) + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, + current_time: datetime, entry_tag: Optional[str], **kwargs) -> bool: + # count_buys = 0 + # trade = self.getTrade(pair) + # if trade: + # filled_buys = trade.select_filled_orders('buy') + # count_buys = len(filled_buys) + + # print('entry_tag' + str(entry_tag)) + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + # last_candle_288 = dataframe.iloc[-289].squeeze() + # limit = get_limit_from_config('Achats', pair) + + # allow_to_buy = True #(not self.stop_all) #& (not self.all_down) + + #allow_to_buy = (last_candle['rsi_1d'] > 50) # | (last_candle['rsi_1d'] > last_candle_288['rsi_1d']) # (rate <= float(limit)) | (entry_tag == 'force_entry') + allow_to_buy = \ + ( + ((last_candle['ha_red_1d'] == 0) | (entry_tag == "buy_crash")) # & (last_candle['close'] < self.min_max_buys[pair]['last_sell_price'] * 0.99) + #& (last_candle['rsi_1d'] >= 45) + ) + self.trades = list() + dispo = round(self.wallets.get_available_stake_amount()) + stake_amount = self.calculate_stake(pair, rate, last_candle) + if allow_to_buy: + self.log_trade( + date=current_time, + action="Allow To buy", + pair=pair, + trade_type=entry_tag, + rate=rate, + dispo=dispo, + profit=0, + stake=round(stake_amount, 2), + rsi=round(last_candle['rsi_1d']) + ) + self.min_max_buys[pair]['last_rsi_1d'] = round(last_candle['rsi_1d']) + # logger.info(f"Allow_to_buy {allow_to_buy} {pair} {current_time} Buy={entry_tag} rate={rate} dispo={dispo} rsi_1d={rsi_1d}") + + return allow_to_buy + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, + exit_reason: str, current_time, **kwargs) -> bool: + # allow_to_sell = (minutes > 30) + # limit = get_limit_from_config('Ventes', pair) + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + # & (last_candle['rsi_1d'] <= 50)# rate > float(limit) + allow_to_sell = ( + last_candle['percent'] < -0.00) | (last_candle['percent3'] < - 0.00) | (40 >= last_candle['rsi'] >= 80 + ) #& (last_candle['rsi_1d'] < self.min_max_buys[pair]['last_rsi_1d']) + string = "" + # allow_to_sell = (last_candle['rsi_1d'] >= self.min_max_buys[pair]['last_rsi_1d']) + + dispo = round(self.wallets.get_available_stake_amount()) + + if allow_to_sell: + self.trades = list() + self.log_trade( + date=current_time, + action="Allow Sell trade", + pair=pair, + trade_type=exit_reason, + first_rate=trade.open_rate, + rate=last_candle['close'], + dispo=dispo, + profit=round(trade.calc_profit(rate, amount), 2), + rsi=round(last_candle['rsi_1d']) + ) + self.min_max_buys[pair]['last_rsi_1d'] = round(last_candle['rsi_1d']) + + else: + self.log_trade( + date=current_time, + action="Cancel Sell trade", + pair=pair, + trade_type=exit_reason, + first_rate=trade.open_rate, + rate=last_candle['close'], + dispo=dispo, + #profit=round(trade.calc_profit(rate, amount), 2), + rsi=round(last_candle['rsi_1d']) + ) + ok = (allow_to_sell) | (exit_reason == 'force_exit') + if ok: + self.min_max_buys[pair]['last_sell_price'] = last_candle['close'] + self.last_sell[pair] = rate + return ok + + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + + dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + # adjusted_stake_amount = self.adjust_stake_amount(pair, current_candle) + + # adjusted_stake_amount = self.calculate_stake(pair, current_rate) + # print(f"Pour {pair}, avec une valeur actuelle de {current_rate}, la mise est de {adjusted_stake_amount:.2f}%") + adjusted_stake_amount = self.calculate_stake(pair, current_rate, last_candle) # self.config['stake_amount'] + + # logger.info(f"{pair} adjusted_stake_amount{adjusted_stake_amount}") + + # Use default stake amount. + return adjusted_stake_amount + + # def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, + # current_profit: float, **kwargs) -> float: + # # Récupérer les données pour le trade en cours + # dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + # last_candle = dataframe.iloc[-1] + # + # # Heikin-Ashi values + # ha_close = last_candle['ha_close_1h'] + # ha_open = last_candle['ha_open_1h'] + # ha_low = last_candle['ha_low_1h'] + # ha_high = last_candle['ha_high_1h'] + # + # # Calcul de la volatilité et du buffer dynamique + # ha_range = abs(ha_close - ha_open) + # ha_relative_diff = ha_range / ha_close if ha_close > 0 else 0 + # stoploss_buffer = ha_relative_diff * 0.5 # Coefficient ajustable + # + # # Calcul du stoploss dynamique basé sur Heikin-Ashi + # if ha_close > ha_open: # Tendance haussière + # stoploss = ha_open * (1 - stoploss_buffer) + # else: # Tendance baissière + # stoploss = ha_low * (1 - stoploss_buffer) + # + # # Contrainte de la fourchette [95%, 98%] + # stoploss_min = current_rate * 0.97 + # stoploss_max = current_rate * 0.985 + # stoploss_final = max(stoploss_min, min(stoploss, stoploss_max)) + # + # return stoploss_final + + # def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + # current_rate: float, current_profit: float, **kwargs) -> float: + # # dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + # # last_candle = dataframe.iloc[-1].squeeze() + # + # # # hard stoploss profit + # HSL = self.pHSL.value + # PF_1 = self.pPF_1.value + # SL_1 = self.pSL_1.value + # PF_2 = self.pPF_2.value + # SL_2 = self.pSL_2.value + # + # # For profits between PF_1 and PF_2 the stoploss (sl_profit) used is linearly interpolated + # # between the values of SL_1 and SL_2. For all profits above PL_2 the sl_profit value + # # rises linearly with current profit, for profits below PF_1 the hard stoploss profit is used. + # + # # if current_profit > PF_2: + # # sl_profit = SL_2 + (current_profit - PF_2) + # # elif current_profit > PF_1: + # # sl_profit = SL_1 + ((current_profit - PF_1) * (SL_2 - SL_1) / (PF_2 - PF_1)) + # # else: + # # sl_profit = HSL + # + # filled_buys = trade.select_filled_orders('buy') + # count_of_buys = len(filled_buys) + # # first_price = filled_buys[0].price + # + # days = (current_time - trade.open_date_utc).days + # + # #print(f"entry_tag={trade.entry_tag} max={trade.max_rate} min={trade.min_rate} ") + # if current_profit > (count_of_buys / 0.6) / 100: #0.0125: + # sl_profit = 0.6 * current_profit # 75% du profit en cours + # else: + # sl_profit = self.pHSL.value # Hard stop-loss + # + # stoploss = stoploss_from_open(sl_profit, current_profit) + # + # # logger.info(f"{pair} {current_time} stoploss={stoploss:.4f} sl_profit={sl_profit:.4f} current_profit={current_profit:.4f} count_of_buys={count_of_buys}") + # return stoploss + + def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + previous_last_candle = dataframe.iloc[-2].squeeze() + fifth_candle = dataframe.iloc[-6].squeeze() + if (last_candle['percent'] > 0) | (last_candle['percent3'] > 0.0): # | (last_candle['max200_diff'] > 0.02): + return None + + # if not (pair in self.max_profit_pairs): + # self.max_profit_pairs[pair] = current_profit + # self.max_profit_pairs[pair] = max(self.max_profit_pairs[pair], current_profit) + # + # if (self.max_profit_pairs[pair] > 0.01) & (current_profit > 0): + # allow_decrease = 0.2 * self.max_profit_pairs[pair] + # if self.max_profit_pairs[pair] - current_profit > allow_decrease: + # print("Max profit=" + str(self.max_profit_pairs[pair]) + ' ' + str(current_profit) + ' ' + str( + # allow_decrease) + ' ' + str(self.max_profit_pairs[pair] - current_profit)) + # return 'max_lost_profit' + # hours = (current_time - trade.open_date_utc).seconds / 3600 + # + # if (hours >= 2) & (self.max_profit_pairs[pair] < 0.002) & (self.max_profit_pairs[pair] > - 0.002): + # return "no_change" + + # if (current_profit < - 0.03 ): + # print("Stop loss max profit=" + str(self.max_profit_pairs[pair]) + ' current=' + str(current_profit)) + # return 'stop_loss' + + filled_buys = trade.select_filled_orders('buy') + count_of_buys = len(filled_buys) + + expected_profit = max(self.min_max_buys[pair]['profit'], 0.01 * count_of_buys) + # print(last_candle['buy_tag']) + + days = (current_time - trade.open_date_utc).days + hours = (current_time - trade.open_date_utc).seconds / 3600 + + # fibo_fact = fibo[hours] + ###### + # if last_candle['percent5'] < -0.05: + # return "stop_loss_005" + + max_percent = 0.005 # self.min_max_buys[pair]['profit'] / 5 # last_candle['bb_width'] / 3.5 # 0.005 + max_profit = self.min_max_buys[pair]['profit'] # last_candle['bb_width'] * 3 / 4 # 0.015 + + # if (current_profit > expected_profit) & \ + # (last_candle['open'] > last_candle['max_previous_1h']) & \ + # (last_candle['percent'] < 0) \ + # & (last_candle[self.close] > last_candle['sma5_1h']): + # return 'b_max_previous' + + # if (current_profit < - 0.03) & \ + # (last_candle['rsi_1d'] < 45) & \ + # (last_candle['percent'] < 0): + # return 'b_rsi_1d' + + if (current_profit > expected_profit) & \ + (last_candle['percent12'] < -max_percent) \ + & ((current_time - trade.open_date_utc).seconds >= 3600) \ + & (last_candle[self.close] > last_candle['sma5_1h']) \ + & (80 > last_candle['rsi_1h'] < 50): + return 'b_percent12' + # if (current_profit > max_profit) & \ + # ((last_candle['percent'] < - max_percent) | (last_candle['percent3'] < -max_percent) | ( + # last_candle['percent5'] < -max_percent)): + # return 'b_percent_quick' + + if self.profit_b_quick_lost.value and (current_profit >= max_profit) \ + & (last_candle['percent3'] < -max_percent) \ + & (last_candle[self.close] > last_candle['sma5_1h']) \ + & (80 > last_candle['rsi_1h'] < 50): + return "b_quick_lost" + + # if self.profit_b_no_change.value and (current_profit > self.sell_b_profit_no_change.value) \ + # & (last_candle['percent12'] < self.sell_b_profit_percent12.value) & (last_candle['percent5'] < 0) \ + # & ((current_time - trade.open_date_utc).seconds >= 3600): + # return "b_no_change" + + if (current_profit > self.sell_b_percent.value) & (last_candle['percent3'] < - self.sell_b_percent3.value) \ + & ((current_time - trade.open_date_utc).seconds <= 300 * self.sell_b_candels.value): + return "b_quick_gain_param" + + if self.profit_b_sma5.value: + if (current_profit > expected_profit) \ + & ((fifth_candle['sma5'] > last_candle['sma5']) \ + | (last_candle['percent3'] < -expected_profit) | ( + last_candle['percent5'] < -expected_profit)) \ + & ((last_candle['percent'] < 0) & (last_candle['percent3'] < 0)): + # print("over_bb_band_sma10_desc", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_sma5' + + if self.profit_b_sma10.value: + if (current_profit > expected_profit) \ + & ((fifth_candle['sma10'] > last_candle['sma10']) \ + | (last_candle['percent3'] < -expected_profit) | ( + last_candle['percent5'] < -expected_profit)) \ + & ((last_candle['percent'] < 0) & (last_candle['percent3'] < 0)): + # print("over_bb_band_sma10_desc", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_sma10' + + if self.profit_b_sma20.value: + if (current_profit > expected_profit) \ + & (previous_last_candle['sma10'] > last_candle['sma10']) \ + & ((current_time - trade.open_date_utc).seconds >= 3600) \ + & ((previous_last_candle['sma20'] > last_candle['sma20']) & + ((last_candle['percent5'] < 0) | (last_candle['percent12'] < 0) | ( + last_candle['percent24'] < 0))): + # print("over_bb_band_sma10_desc", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_sma20' + + if self.profit_b_over_rsi.value: + if (current_profit > 0) & (previous_last_candle['rsi'] > self.sell_b_RSI.value): + # & (last_candle['percent'] < 0): #| (previous_last_candle['rsi'] > 75 & last_candle['rsi'] < 70)): + # print("over_rsi", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_over_rsi' + + if (current_profit > 0) & (previous_last_candle['rsi'] > self.sell_b_RSI2.value) & \ + (last_candle[ + 'percent'] < - self.sell_b_RSI2_percent.value): # | (previous_last_candle['rsi'] > 75 & last_candle['rsi'] < 70)): + # print("over_rsi", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_over_rsi_2' + + if (current_profit > 0) & (previous_last_candle['rsi'] > self.sell_b_RSI3.value) & \ + (last_candle[self.close] >= last_candle['max200']) \ + & (last_candle[ + 'percent'] < - self.sell_b_RSI2_percent.value): # | (previous_last_candle['rsi2'] > 75 & last_candle['rsi'] < 70)): + # print("over_rsi", pair, trade, " profit=", current_profit, " rate=", current_rate) + return 'b_over_rsi_max' + + def informative_pairs(self): + # get access to all pairs available in whitelist. + pairs = self.dp.current_whitelist() + informative_pairs = [(pair, '1d') for pair in pairs] + informative_pairs += [(pair, '1h') for pair in pairs] + + return informative_pairs + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Add all ta features + pair = metadata['pair'] + + # dataframe['achats'] = get_limit_from_config('Achats', pair) + # dataframe['ventes'] = get_limit_from_config('Ventes', pair) + + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['enter_long'] = "" + dataframe['enter_tag'] = "" + dataframe['haopen'] = heikinashi['open'] + dataframe['haclose'] = heikinashi['close'] + dataframe['hapercent'] = (dataframe['haclose'] - dataframe['haopen']) / dataframe['haclose'] + dataframe['mid'] = (dataframe['close'] + dataframe['open']) / 2 + + dataframe['pct_change'] = dataframe[self.close].pct_change(5) + dataframe['min'] = talib.MIN(dataframe[self.close], timeperiod=200) + dataframe['min12'] = talib.MIN(dataframe[self.close], timeperiod=12) + + dataframe['min50'] = talib.MIN(dataframe[self.close], timeperiod=50) + dataframe['min200'] = talib.MIN(dataframe[self.close], timeperiod=200) + + dataframe['max50'] = talib.MAX(dataframe[self.close], timeperiod=50) + dataframe['max144'] = talib.MAX(dataframe[self.close], timeperiod=144) + dataframe['min_max50'] = (dataframe['max50'] - dataframe['min50']) / dataframe['min50'] + + dataframe['max200'] = talib.MAX(dataframe[self.close], timeperiod=200) + dataframe['min_max200'] = (dataframe['max200'] - dataframe['min200']) / dataframe['min200'] + dataframe['max200_diff'] = (dataframe['max200'] - dataframe[self.close]) / dataframe[self.close] + dataframe['max50_diff'] = (dataframe['max50'] - dataframe[self.close]) / dataframe[self.close] + + dataframe['sma5'] = talib.SMA(dataframe, timeperiod=5) + dataframe['sma10'] = talib.SMA(dataframe, timeperiod=10) + dataframe['sma20'] = talib.SMA(dataframe, timeperiod=20) + dataframe["percent"] = (dataframe[self.close] - dataframe[self.open]) / dataframe[self.open] + dataframe["percent3"] = (dataframe[self.close] - dataframe[self.open].shift(3)) / dataframe[self.open].shift(3) + dataframe["percent5"] = (dataframe[self.close] - dataframe[self.open].shift(5)) / dataframe[self.open].shift(5) + dataframe["percent12"] = (dataframe[self.close] - dataframe[self.open].shift(12)) / dataframe[self.open].shift( + 12) + dataframe["percent24"] = (dataframe[self.close] - dataframe[self.open].shift(24)) / dataframe[self.open].shift( + 24) + dataframe["percent48"] = (dataframe[self.close] - dataframe[self.open].shift(48)) / dataframe[self.open].shift( + 48) + dataframe["percent_max_144"] = (dataframe[self.close] - dataframe["max144"]) / dataframe[self.close] + # print(metadata['pair']) + dataframe['rsi'] = talib.RSI(dataframe[self.close], timeperiod=12) + dataframe['rsi24'] = talib.RSI(dataframe[self.close], timeperiod=24) + dataframe['rsi48'] = talib.RSI(dataframe[self.close], timeperiod=48) + + dataframe['volume2'] = dataframe['volume'] + dataframe.loc[dataframe['percent'] < 0, 'volume2'] *= -1 + + # ====================================================================================== + # MACD pour identifier les croisements + macd = talib.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + + # Moyennes mobiles pour les tendances + dataframe['ema_50'] = talib.EMA(dataframe, timeperiod=50) + dataframe['ema_200'] = talib.EMA(dataframe, timeperiod=200) + + # ADX pour la force de tendance + dataframe['adx'] = talib.ADX(dataframe) + + # Volume moyen pour filtrer les opportunités à faible volume + dataframe['volume_mean_sma'] = talib.SMA(dataframe['volume'], timeperiod=20) + # ====================================================================================== + + # ====================================================================================== + # Bollinger Bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe["bb_percent"] = ( + (dataframe[self.close] - dataframe["bb_lowerband"]) / + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) + ) + # ====================================================================================== + # Normalization + + dataframe['average_line'] = dataframe[self.close].mean() + dataframe['average_line_50'] = talib.MIDPOINT(dataframe[self.close], timeperiod=50) + + dataframe['average_line_288'] = talib.MIDPOINT(dataframe[self.close], timeperiod=288) + dataframe['average_line_288_098'] = dataframe['average_line_288'] * 0.98 + dataframe['average_line_288_099'] = dataframe['average_line_288'] * 0.99 + # Sort the close prices to find the 4 lowest values + sorted_close_prices = dataframe[self.close].tail(576).sort_values() + lowest_4 = sorted_close_prices.head(20) + + dataframe['lowest_4_average'] = dataframe['min200'] # lowest_4.mean() + # Propagate this mean value across the entire dataframe + # dataframe['lowest_4_average'] = dataframe['lowest_4_average'].iloc[0] + + # # Sort the close prices to find the 4 highest values + sorted_close_prices = dataframe[self.close].tail(288).sort_values(ascending=False) + highest_4 = sorted_close_prices.head(20) + + # # Calculate the mean of the 4 highest values + dataframe['highest_4_average'] = dataframe['max200'] # highest_4.mean() + + # # Propagate this mean value across the entire dataframe + # dataframe['highest_4_average'] = dataframe['highest_4_average'].iloc[0] + + dataframe['volatility'] = talib.STDDEV(dataframe[self.close], timeperiod=144) / dataframe[self.close] + dataframe['atr'] = talib.ATR(dataframe['high'], dataframe['low'], dataframe[self.close], timeperiod=144) / \ + dataframe[self.close] + + # ====================================================================================== + ################### INFORMATIVE 1h + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe="1h") + informative['sma5'] = talib.SMA(informative, timeperiod=10) + informative['sma5_pct'] = (informative["sma5"] - informative["sma5"].shift(6)) / informative["sma5"] + informative["max_previous"] = talib.MAX(informative['close'], timeperiod=48) + informative["min_previous"] = talib.MIN(informative['close'], timeperiod=48) + + informative['volatility'] = talib.STDDEV(informative['close'], timeperiod=14) / informative['close'] + informative['atr'] = (talib.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)) / \ + informative['close'] + informative['rsi'] = talib.RSI(informative['close'], timeperiod=12) + informative['rsi_sma'] = talib.SMA(informative['rsi'], timeperiod=10) + heikinashi = qtpylib.heikinashi(informative) + informative['ha_open'] = heikinashi['open'] + informative['ha_close'] = heikinashi['close'] + informative['ha_high'] = heikinashi['high'] + informative['ha_low'] = heikinashi['low'] + + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, "1h", ffill=True) + + # Calcul des seuils dynamiques + dataframe['sell_threshold_max'] = dataframe['max_previous_1h'] - ( + dataframe['max_previous_1h'] * self.sell_max_threshold / 100) + dataframe['sell_threshold_min'] = dataframe['min_previous_1h'] + ( + dataframe['min_previous_1h'] * self.sell_min_threshold / 100) + + # ====================================================================================== + ################### INFORMATIVE 1d + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe="1d") + sorted_close_prices = informative['close'].tail(365).sort_values() + lowest_4 = sorted_close_prices.head(4) + informative['lowest_4'] = lowest_4.mean() + + sorted_close_prices = informative['close'].tail(365).sort_values(ascending=False) + highest_4 = sorted_close_prices.head(4) + informative['highest_4'] = highest_4.mean() + + last_14_days = informative.tail(14) + + # Récupérer le minimum et le maximum de la colonne 'close' des 14 derniers jours + min_14_days = last_14_days['close'].min() + max_14_days = last_14_days['close'].max() + informative['lowest'] = min_14_days + informative['highest'] = max_14_days + informative['pct_min_max'] = (max_14_days - min_14_days) / min_14_days + informative['mid_min_max'] = min_14_days + (max_14_days - min_14_days) / 2 + informative['middle'] = informative['lowest_4'] + (informative['highest_4'] - informative['lowest_4']) / 2 + informative['mid_min_max_0.98'] = informative['mid_min_max'] * 0.98 + informative['rsi'] = talib.RSI(informative['close'], timeperiod=12) + informative['rsi_sma'] = talib.SMA(informative['rsi'], timeperiod=12) + informative["percent"] = (informative[self.close] - informative[self.open]) / informative[self.open] + + # Heikin Ashi (simplified) + heikinashi = qtpylib.heikinashi(informative) + informative['ha_open'] = heikinashi['open'] + informative['ha_close'] = heikinashi['close'] + informative['ha_high'] = heikinashi['high'] + informative['ha_low'] = heikinashi['low'] + # informative['ha_close'] = (informative['open'] + informative['high'] + informative['low'] + informative['close']) / 4 + # informative['ha_open'] = informative['ha_close'].shift(1).combine_first(informative['open']) + informative['ha_red'] = informative['ha_close'] < informative['ha_open'] + + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, "1d", ffill=True) + + dataframe['count_buys'] = 0 + + dataframe['last_price'] = dataframe[self.close] + dataframe['first_price'] = dataframe[self.close] + dataframe['mid_price'] = (dataframe['last_price'] + dataframe['first_price']) / 2 + dataframe['close01'] = dataframe.iloc[-1][self.close] * 1.01 + dataframe['amount'] = 0 + dataframe['limit'] = dataframe[self.close] + count_buys = 0 + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + self.getOpenTrades() + + for trade in self.trades: + if trade.pair != pair: + continue + filled_buys = trade.select_filled_orders('buy') + dataframe['count_buys'] = len(filled_buys) + count = 0 + amount = 0 + for buy in filled_buys: + if count == 0: + dataframe['first_price'] = buy.price + dataframe['close01'] = buy.price * 1.01 + + # Order(id=2396, trade=1019, order_id=29870026652, side=buy, filled=0.00078, price=63921.01, + # status=closed, date=2024-08-26 02:20:11) + dataframe['last_price'] = buy.price + print(buy) + count = count + 1 + amount += buy.price * buy.filled + dataframe['mid_price'] = (dataframe['last_price'] + dataframe['first_price']) / 2 + count_buys = count + dataframe['limit'] = dataframe['last_price'] * (1 - self.baisse[count] / 100) + dataframe['amount'] = amount + print(f"amount= {amount}") + # trades = Trade.get_trades([Trade.is_open is False]).all() + trades = Trade.get_trades_proxy(is_open=False, pair=metadata['pair']) + if trades: + trade = trades[-1] + print('closed trade pair is : ') + dataframe['expected_profit'] = (1 + self.expectedProfit(pair, dataframe.iloc[-1])) * dataframe[ + 'last_price'] + + dataframe['buy_level'] = dataframe['lowest_4_average'] * (1 - self.levels[count_buys] / 100) + + # ====================================================================================================== + + dataframe['tag_min'] = (dataframe[self.close] <= dataframe['min200'] * 1.002) + dataframe['tag_percent24'] = (dataframe['percent24'] <= -self.min_max_buys[pair]['profit']) + dataframe['tag_stable'] = (dataframe['min50'].shift(6) == dataframe['min50']) + dataframe['tag_buy'] = dataframe['tag_min'].shift(6) & dataframe['tag_percent24'].shift(6) & dataframe[ + 'tag_stable'] + + self.getBinanceOrderBook(pair, dataframe) + + # ========================================================================= + dataframe['rsi_range'] = pd.cut(dataframe['rsi'], bins=[0,10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + labels=['00', '10', '20', '30', '40', '50', '60', '70', '80', '90']) + dataframe['rsi_percent'] = 100 * (dataframe['rsi'] - dataframe['rsi'].shift(1)) / dataframe['rsi'] + dataframe['rsi_percent3'] = 100 * (dataframe['rsi'] - dataframe['rsi'].shift(3)) / dataframe['rsi'] + dataframe['rsi_percent5'] = 100 * (dataframe['rsi'] - dataframe['rsi'].shift(5)) / dataframe['rsi'] + dataframe['rsi_pct_range'] = pd.cut(dataframe['rsi_percent3'], + bins=[-1000, -80, -60, -40, -20, 0, 20, 40, 60, 80, 1000], + labels=['-100', '-80', '-60', '-40', '-20', '0', '20', '40', '60', '80']) + + # Calcul des changements futurs + dataframe['future_change'] = dataframe['close'].shift(-12) - dataframe['close'] + dataframe['future_direction'] = dataframe['future_change'].apply(lambda x: 1 if x > 0 else 0) + + rsi_probabilities = dataframe.groupby('rsi_pct_range')['future_direction'].mean() + print(f"Probabilités de hausse selon rsi_pct_range :\n", rsi_probabilities) + + return dataframe + + def getOpenTrades(self): + # if len(self.trades) == 0: + print('search open trades') + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + self.trades = Trade.get_open_trades() + return self.trades + + def getTrade(self, pair): + trades = self.getOpenTrades() + trade_for_pair = None + for trade in trades: + if trade.pair == pair: + trade_for_pair = trade + break + return trade_for_pair + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + pair = metadata['pair'] + + self.getOpenTrades() + # expected_profit = self.expectedProfit(pair, dataframe.iloc[-1]) + # self.getBinanceOrderBook(pair, dataframe) + last_candle = dataframe.iloc[-1].squeeze() + # limit = last_candle['first_price'] * (1 - self.baisse[last_candle['count_buys']] / 100) + + # self.updateLastValue(dataframe, 'expected_profit', expected_profit) + # print("---------------" + pair + "----------------") + # print('adjust stake amount ' + str(self.adjust_stake_amount(pair, dataframe.iloc[-1]))) + # print('adjust exit price ' + str(self.adjust_exit_price(dataframe.iloc[-1]))) + # print('calcul expected_profit ' + str(expected_profit)) + + buy_level = dataframe[self.open] # dataframe['buy_level'] # self.get_buy_level(pair, dataframe) + + dataframe.loc[ + ( + (dataframe['max200_diff'] >= 0.018) + & (dataframe[self.open] < dataframe['average_line_288']) + & (dataframe[self.close] < dataframe['min12'] * 1.002) + & (dataframe['min12'].shift(2) == dataframe['min12']) + ), ['enter_long', 'enter_tag']] = (1, 'buy_min_max200') + + dataframe.loc[ + ( + ( + (dataframe['percent12'].shift(3) < -0.015) | + (dataframe['percent24'].shift(3) < -0.022) | + (dataframe['percent48'].shift(3) < -0.030) + ) + & (dataframe[self.close].shift(3) <= dataframe['min50'].shift(3) * 1.002) + & (dataframe[self.open].shift(3) < dataframe['average_line_50'].shift(3)) + & ( + (dataframe['min200'].shift(3) == dataframe['min200']) + ) + ), ['enter_long', 'enter_tag']] = (1, 'buy_0_percent12') + dataframe.loc[ + ( + (dataframe['count_buys'] == 0) + & + ( + (dataframe[self.close] <= dataframe['sma5_1h'] * (1 - self.min_max_buys[pair]['profit'])) | + (dataframe['percent24'] <= -self.min_max_buys[pair]['profit']) + ) + & (dataframe[self.close] <= dataframe['sma5_1h']) + & ((last_candle['close'] - last_candle['min_previous_1h']) / (last_candle['max_previous_1h'] - last_candle['close']) < 0.2) + ), ['enter_long', 'enter_tag']] = (1, 'buy_percent12') + + # dataframe.loc[ + # ( + # (dataframe['percent_1d'] <= -0.1) + # & (dataframe['min12'].shift(12) == dataframe['min12']) + # ), ['enter_long', 'enter_tag']] = (1, 'buy_crash') + + nan_columns = dataframe.columns[dataframe.isna().any()].tolist() + print("Colonnes contenant des NaN :", nan_columns) + + # Compter les colonnes avec uniquement des NaN + num_only_nan_columns = dataframe.isna().all().sum() + print(f"Nombre de colonnes contenant uniquement des NaN : {num_only_nan_columns}") + + # Compter les NaN par colonne + nan_count_per_column = dataframe.isna().sum() + print("Nombre de NaN par colonne :") + print(nan_count_per_column) + columns_with_nan = nan_count_per_column[nan_count_per_column > 1000] + print("Colonnes avec au moins 1000 NaN :") + print(columns_with_nan) + + #self.model = joblib.load('regression_model.pkl') + # Former le modèle si ce n'est pas déjà fait + if self.model is None: + print("Calculate model") + dataframe = self.add_future_direction(dataframe) + self.model = self.train_model(dataframe) + else: + print("Load model") + + # Coefficients du modèle + print("Coefficients :", self.model.coef_) + + # Ordonnée à l'origine (intercept) + print("Ordonnée à l'origine :", self.model.intercept_) + # Hyperparamètres du modèle + print("Hyperparamètres du modèle :", self.model.get_params()) + + + # Faire des prédictions + dataframe = self.predict_with_model(dataframe, self.model) + # Sauvegarde du modèle + # joblib.dump(self.model, 'regression_model.pkl') + + # Entrée sur un signal de hausse + # dataframe.loc[ + # (dataframe['predicted_direction'] == 1), + # ['enter_long', 'enter_tag']] = (1, 'buy_predict') + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # dataframe.loc[ + # ( + # (dataframe['sma5_pct_1h'].shift(24) < dataframe['sma5_pct_1h'].shift(12)) + # & (dataframe['sma5_pct_1h'].shift(12) > dataframe['sma5_pct_1h']) + # & (dataframe['sma5_pct_1h'] < 0.035) + # ), ['sell', 'exit_long']] = (1, 'sell_sma5_pct_1h') + return dataframe + + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, min_stake: float, + max_stake: float, **kwargs): + pair = trade.pair + # self.min_max_buys[pair][min] = min(self.min_max_buys[pair][min], current_rate) + # self.min_max_buys[pair][max] = max(self.min_max_buys[pair][max], current_rate) + + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + # print(dataframe) + last_candle = dataframe.iloc[-1].squeeze() + last_candle_1 = dataframe.iloc[-2].squeeze() + last_candle_3 = dataframe.iloc[-4].squeeze() + last_candle_12 = dataframe.iloc[-13].squeeze() + last_candle_24 = dataframe.iloc[-25].squeeze() + return None + + # "& (last_candle['sma5_pct_1h'] < -0.035) + if (len(dataframe) < 1) | (self.wallets.get_available_stake_amount() < 10): + return None + if pair not in ('BTC/USDT', 'DOGE/USDT', 'ETH/USDT'): + return None + if last_candle['rsi_1d'] is None: + return None + if (last_candle['ha_red_1d'] is None) | (last_candle['ha_red_1d'] == 1): + return None + max_buys = 10 + + filled_buys = trade.select_filled_orders('buy') + count_of_buys = len(filled_buys) + if count_of_buys >= max_buys: + return None + first_price = filled_buys[0].price + days = 0 + minutes = 0 + last_price = first_price + for buy in filled_buys: + minutes = (current_time - buy.order_date_utc).seconds / 60 + days = (current_time - buy.order_date_utc).days + last_price = buy.price + condition = (last_candle['sma5_pct_1h'] >= 0) + dispo = round(self.wallets.get_available_stake_amount()) + profit = ((current_rate - last_price) / current_rate) + rsi_1d = last_candle['rsi_1d'] + + if ( + # (last_candle_3['volume'] * 5 < last_candle['volume']) + (last_candle['rsi_1h'] > 50) + #(last_candle['rsi_1d'] >= self.min_max_buys[pair]['last_rsi_1d']) + & (minutes > 180) + & (current_rate > max(first_price, last_price) * (1 + count_of_buys / 100)) + ): + stake_amount = self.calculate_stake(pair, current_rate, last_candle) + self.log_trade( + date=current_time, + action="INC price", + pair=trade.pair, + rate=current_rate, + minutes=minutes, + first_rate=first_price, + last_rate=last_price, + buys=count_of_buys, + stake=round(stake_amount, 2), + profit=round(profit * 100, 2), + dispo=dispo, + rsi=round(last_candle['rsi_1d']) + + ) + self.min_max_buys[pair]['last_rsi_1d'] = round(last_candle['rsi_1d']) + + # logger.info( + # f"INC price={trade.pair} {current_time} minutes={minutes} first={first_price:.4f} last={last_price:.4f} rate={current_rate:.4f} buys={count_of_buys} stake={stake_amount:.4f}") + return stake_amount + + # self.protection_nb_buy_lost.value + # limit = last_candle[limits[count_of_buys]] + limit = last_price * (1 - self.fibo[count_of_buys] / 100) + # limit = last_price * (0.99) + # stake_amount = min(200, self.adjust_stake_amount(pair, last_candle) * self.fibo[count_of_buys]) + # stake_amount = self.config.get('stake_amount', 50) * self.fibo[count_of_buys] + stake_amount = self.calculate_stake(pair, current_rate, last_candle) * self.fibo[count_of_buys] + percent_max = (last_candle['max200'] - last_candle['close']) / last_candle['close'] + if ( + False + & (last_candle['rsi_1d'] >= self.min_max_buys[pair]['last_rsi_1d']) + #(last_candle_1['rsi'] < 12) + #(last_price <= first_price) + & (current_rate <= limit) + & (minutes > 180)): + fact = - profit / 0.05 + stake_amount = 100 #min(self.wallets.get_available_stake_amount(), max(100, min(666, stake_amount * fact))) + self.min_max_buys[pair]['last_rsi_1d'] = round(last_candle['rsi_1d']) + + self.log_trade( + date=current_time, + action="DEC price", + pair=trade.pair, + rate=current_rate, + minutes=minutes, + first_rate=first_price, + last_rate=last_price, + buys=count_of_buys, + stake=round(stake_amount, 2), + profit=round(profit * 100, 2), + dispo=dispo, + rsi=round(last_candle['rsi_1d']) + ) + + # logger.info( + # f"DEC price={trade.pair} {current_time} minutes={minutes} first={first_price:.4f} last={last_price:.4f} rate={current_rate:.4f} buys={count_of_buys} limit={limit:.4f} stake={stake_amount:.4f} rsi={rsi_1d:.4f}") + + return stake_amount + + # if days > 1: + # self.log_trade( + # date=current_time, + # action="NO BUY", + # pair=trade.pair, + # rate=current_rate, + # minutes=minutes, + # first_rate=first_price, + # last_rate=last_price, + # buys=count_of_buys, + # profit=round(profit * 100, 2), + # dispo=dispo, + # rsi=round(last_candle['rsi_1d']) + # ) + return None + + # print("Adjust " + trade.pair + " time=" + str(current_time) + ' rate=' + str(current_rate) + " buys=" + str(count_of_buys) + " limit=" + str(limit) + " stake=" + str(stake_amount)) + # logger.info( + # f"Adjust price={trade.pair} buy={condition} rate={current_rate:.4f} buys={count_of_buys} limit={limit:.4f} stake={stake_amount:.4f}") + + if (0 < count_of_buys <= max_buys) & (current_rate <= limit) & (condition) & (minutes > 180): + try: + + # This then calculates current safety order size + # stake_amount = stake_amount * pow(1.5, count_of_buys) + # print("Effective Adjust " + trade.pair + " time=" + str(current_time) + ' rate=' + str(current_rate) + " buys=" + str(count_of_buys) + " limit=" + str(limit) + " stake=" + str(stake_amount)) + logger.info( + f"Adjust price={trade.pair} {current_time} first={first_price:.4f} rate={current_rate:.4f} buys={count_of_buys} limit={limit:.4f} stake={stake_amount:.4f}") + + return min(self.wallets.get_available_stake_amount(), stake_amount) + except Exception as exception: + print(exception) + return None + return None + + # def adjust_stake_amount(self, pair: str, dataframe: DataFrame): + # # Calculer le minimum des 14 derniers jours + # current_price = dataframe[self.close] + # + # v_max = max(current_price, self.min_max_buys[pair]['max']) + # v_min = min(current_price, self.min_max_buys[pair]['min']) + # + # adjusted_stake_amount = self.config['stake_amount'] * v_min / v_max + # + # #print( + # # f"Stack amount ajusté price={current_price} max_min={max_min_4:.4f} min_14={min_14_days_4:.4f} max_14={max_14_days_4:.4f} factor={factor_4:.4f} percent={percent_4:.4f} amount={adjusted_stake_amount:.4f}") + # # print(f"Stack amount ajusté price={current_price} max_min={max_min:.4f} min_14={min_14_days:.4f} max_14={max_14_days:.4f} factor={factor:.4f} percent={percent:.4f} amount={adjusted_stake_amount_2:.4f}") + # + # return adjusted_stake_amount + + def adjust_exit_price(self, dataframe: DataFrame): + # Calculer le max des 14 derniers jours + min_14_days = dataframe['lowest_1d'] + max_14_days = dataframe['highest_1d'] + # entry_price = dataframe['fbp'] + current_price = dataframe[self.close] + percent = 0.5 * (max_14_days - min_14_days) / min_14_days + exit_price = (1 + percent) * entry_price + + print(f"Exit price ajusté price={current_price:.4f} max_14={max_14_days:.4f} exit_price={exit_price:.4f}") + + return exit_price + + def expectedProfit(self, pair: str, dataframe: DataFrame): + + current_price = dataframe['last_price'] # dataframe[self.close] + + # trade = self.getTrade(pair) + # if trade: + # current_price = trade.open_rate + + # Calculer le max des 14 derniers jours + min_14_days = dataframe['lowest_1d'] + max_14_days = dataframe['highest_1d'] + percent = (max_14_days - current_price) / (min_14_days) + + min_max = dataframe['pct_min_max_1d'] # (max_14_days - min_14_days) / min_14_days + expected_profit = min(0.1, max(0.01, dataframe['min_max200'] * 0.5)) + + print( + f"Expected profit price={current_price:.4f} min_max={min_max:.4f} min_14={min_14_days:.4f} max_14={max_14_days:.4f} percent={percent:.4f} expected_profit={expected_profit:.4f}") + + # self.analyze_conditions(pair, dataframe) + return expected_profit + + def getBinanceOrderBook(self, pair, dataframe: DataFrame): + """Fetch the order book (depth) from Binance.""" + # print(dataframe) + last_candle = dataframe.iloc[-1].squeeze() + symbol = pair.replace('/', '') + + try: + url = f"https://api.binance.com/api/v3/depth?symbol={symbol}&limit=5000" + response = requests.get(url) + data = response.json() + + # Extract bids and asks from the order book + asks, bids = self.calculateSMA(20, data['asks'], data['bids']) # Ventes List of [price, quantity] + # bids = data['bids'] + # asks = data['asks'] # Achats List of [price, quantity] + + # Process the depth data as you need it + # bid_volume = sum([float(bid[1]) for bid in bids]) # Sum of all bid volumes + # $ * nb / $ => nb + bid_volume = sum([float(bid[0]) * float(bid[1]) / float(last_candle[self.close]) for bid in bids[:10]]) + # ask_volume = sum([float(ask[1]) for ask in asks]) # Sum of all ask volumes + ask_volume = sum([float(ask[0]) * float(ask[1]) / float(last_candle[self.close]) for ask in asks[:10]]) + + # Example: add the difference in volumes as an indicator + if bid_volume and ask_volume: + self.updateLastValue(dataframe, 'depth_bid_ask_diff', round(bid_volume - ask_volume, 2)) + else: + self.updateLastValue(dataframe, 'depth_bid_ask_diff', 0) + + # probabilité baisse hausse sur les n premiers élements + for start in [0, 50, 100, 150]: + self.updateLastValue(dataframe, 'prob_hausse_' + str(start + 50), + self.calculateProbaNb(asks, bids, start, start + 50)) + # dataframe['prob_hausse_' + str(nb)] = self.calculateProbaNb(asks, bids, nb) + # Analyse des prix moyens pondérés par les volumes (VWAP) : + # + # Le VWAP (Volume Weighted Average Price) peut être utilisé pour comprendre la pression directionnelle. + # Si le VWAP basé sur les ordres d'achat est plus élevé que celui des ordres de vente, + # cela peut indiquer une probabilité de hausse. + nb = 50 + + bid_vwap = sum([float(bid[0]) * float(bid[1]) for bid in bids[:nb]]) / sum( + [float(bid[1]) for bid in bids[:nb]]) + ask_vwap = sum([float(ask[0]) * float(ask[1]) for ask in asks[:nb]]) / sum( + [float(ask[1]) for ask in asks[:nb]]) + + if bid_vwap > ask_vwap: + self.updateLastValue(dataframe, 'vwap_hausse', + round(100 * (bid_vwap - ask_vwap) / (bid_vwap + ask_vwap), 2)) + else: + self.updateLastValue(dataframe, 'vwap_hausse', + - round(100 * (ask_vwap - bid_vwap) / (bid_vwap + ask_vwap), 2)) + + current_price = last_candle[self.close] # le prix actuel du marché + + # Calcul du seuil de variation de 1% + lower_threshold = current_price * 0.99 + upper_threshold = current_price * 1.01 + + # Volumes d'achat (bids) sous 1% du prix actuel + bid_volume_1percent = sum( + [float(bid[1]) for bid in bids if current_price >= float(bid[0]) >= lower_threshold]) + + # Volumes de vente (asks) au-dessus de 1% du prix actuel + ask_volume_1percent = sum( + [float(ask[1]) for ask in asks if current_price <= float(ask[0]) <= upper_threshold]) + + # Estimation de la probabilité basée sur le déséquilibre des volumes + total_volume = bid_volume_1percent + ask_volume_1percent + if total_volume > 0: + prob_hausse = bid_volume_1percent / total_volume + prob_baisse = ask_volume_1percent / total_volume + else: + prob_hausse = prob_baisse = 0 + + self.updateLastValue(dataframe, 'proba_hausse_1%', round(prob_hausse * 100, 2)) + self.updateLastValue(dataframe, 'proba_baisse_1%', round(prob_baisse * 100, 2)) + print(f"Probabilité de hausse de 1%: {prob_hausse * 100:.2f}%") + print(f"Probabilité de baisse de 1%: {prob_baisse * 100:.2f}%") + + self.calculateResistance(pair, asks, dataframe) + self.calculateSupport(pair, bids, dataframe) + + dataframe['r_s'] = 100 * (dataframe['r_min'] - dataframe['s_min']) / dataframe['s_min'] + + except Exception as e: + logger.error(f"Error fetching order book: {e}") + return None, None + + def calculateProbaNb(self, asks, bids, start, nb): + top_bids = sum([float(bid[1]) for bid in bids[start:nb]]) + top_asks = sum([float(ask[1]) for ask in asks[start:nb]]) + if top_bids > top_asks: + proba = round(100 * (top_bids - top_asks) / (top_bids + top_asks), 2) + else: + proba = - round(100 * (top_asks - top_bids) / (top_bids + top_asks), 2) + return proba + + def calculateResistance(self, pair, asks, dataframe: DataFrame): + last_candle = dataframe.iloc[-1].squeeze() + + # Filtrage +-5% + current_price = float(last_candle[self.close]) + lower_bound = current_price * 0.95 + upper_bound = current_price * 1.05 + ask_prices = [float(ask[0]) for ask in asks] + ask_volumes = [float(ask[1]) for ask in asks] + ask_df = pd.DataFrame({'price': ask_prices, 'volume': ask_volumes}) + filtered_ask_df = ask_df[(ask_df['price'] >= lower_bound) & (ask_df['price'] <= upper_bound)] + # Trier le DataFrame sur la colonne 'volume' en ordre décroissant + sorted_ask_df = filtered_ask_df.sort_values(by='volume', ascending=False) + + # Ne garder que les 3 premières lignes (les 3 plus gros volumes) + top_3_asks = sorted_ask_df.head(3) + print(top_3_asks) + + # Convertir les ordres de vente en numpy array pour faciliter le traitement + asks_array = np.array(filtered_ask_df, dtype=float) + + # Détecter les résistances : on peut définir qu'une résistance est un niveau de prix où la quantité est élevée + # Ex: seuil de résistance à partir des 10% des plus grosses quantités + resistance_threshold = np.percentile(asks_array[:, 1], 90) + resistances = asks_array[asks_array[:, 1] >= resistance_threshold] + + # Afficher les résistances trouvées + # print(f"{pair} Niveaux de résistance détectés:") + # for resistance in resistances: + # print(f"{pair} Prix: {resistance[0]}, Quantité: {resistance[1]}") + + # Exemple : somme des quantités sur des plages de prix + # Intervalles de 10 USDT + step = last_candle[self.close] / 100 + price_intervals = np.arange(asks_array[:, 0].min(), asks_array[:, 0].max(), step=step) + + for start_price in price_intervals: + end_price = start_price + step + mask = (asks_array[:, 0] >= start_price) & (asks_array[:, 0] < end_price) + volume_in_range = asks_array[mask, 1].sum() + amount = volume_in_range * end_price + print( + f"Prix entre {start_price:.6f} et {end_price:.6f}: Volume total = {volume_in_range:.2f} amount={amount:.2f}") + + # Trier les asks par quantité en ordre décroissant + asks_sorted = asks_array[asks_array[:, 1].argsort()][::-1] + + # Sélectionner les trois plus gros resistances + top_3_resistances = asks_sorted[:3] + + # Afficher les trois plus gros resistances + print("Les trois plus grosses resistances détectées : ") + self.updateLastValue(dataframe, 'r3', top_3_resistances[0][0]) + self.updateLastValue(dataframe, 'r2', top_3_resistances[1][0]) + self.updateLastValue(dataframe, 'r1', top_3_resistances[2][0]) + self.updateLastValue(dataframe, 'r_min', + min(top_3_resistances[0][0], top_3_resistances[1][0], top_3_resistances[2][0])) + for resistance in top_3_resistances: + print(f"{pair} Prix: {resistance[0]}, Quantité: {resistance[1]}") + + def calculateSupport(self, pair, bids, dataframe: DataFrame): + last_candle = dataframe.iloc[-1].squeeze() + + # Convert to pandas DataFrame to apply moving average + current_price = float(last_candle[self.close]) + lower_bound = current_price * 0.95 + upper_bound = current_price * 1.05 + bid_prices = [float(bid[0]) for bid in bids] + bid_volumes = [float(bid[1]) for bid in bids] + bid_df = pd.DataFrame({'price': bid_prices, 'volume': bid_volumes}) + filtered_bid_df = bid_df[(bid_df['price'] >= lower_bound) & (bid_df['price'] <= upper_bound)] + # Trier le DataFrame sur la colonne 'volume' en ordre décroissant + sorted_bid_df = filtered_bid_df.sort_values(by='volume', ascending=False) + + # Ne garder que les 3 premières lignes (les 3 plus gros volumes) + top_3_bids = sorted_bid_df.head(3) + print(top_3_bids) + + # Convertir les ordres d'achat en numpy array pour faciliter le traitement + bids_array = np.array(filtered_bid_df, dtype=float) + + # Détecter les supports : on peut définir qu'un support est un niveau de prix où la quantité est élevée + # Ex: seuil de support à partir des 10% des plus grosses quantités + support_threshold = np.percentile(bids_array[:, 1], 90) + supports = bids_array[bids_array[:, 1] >= support_threshold] + + # Afficher les supports trouvés + # print(f"{pair} Niveaux de support détectés:") + # for support in supports: + # print(f"{pair} Prix: {support[0]}, Quantité: {support[1]}") + + step = last_candle[self.close] / 100 + # Exemple : somme des quantités sur des plages de prix pour les bids + price_intervals = np.arange(bids_array[:, 0].min(), bids_array[:, 0].max(), step=step) # Intervalles de 10 USDT + + for start_price in price_intervals: + end_price = start_price + step + mask = (bids_array[:, 0] >= start_price) & (bids_array[:, 0] < end_price) + volume_in_range = bids_array[mask, 1].sum() + amount = volume_in_range * end_price + print( + f"Prix entre {start_price:.6f} et {end_price:.6f}: Volume total = {volume_in_range:.2f} amount={amount:.2f}") + + # Trier les bids par quantité en ordre décroissant + bids_sorted = bids_array[bids_array[:, 1].argsort()][::-1] + + # Sélectionner les trois plus gros supports + top_3_supports = bids_sorted[:3] + + # Afficher les trois plus gros supports + print("Les trois plus gros supports détectés:") + + self.updateLastValue(dataframe, 's1', top_3_supports[0][0]) + self.updateLastValue(dataframe, 's2', top_3_supports[1][0]) + self.updateLastValue(dataframe, 's3', top_3_supports[2][0]) + self.updateLastValue(dataframe, 's_min', max(top_3_supports[0][0], top_3_supports[1][0], top_3_supports[2][0])) + + for support in top_3_supports: + print(f"{pair} Prix: {support[0]}, Quantité: {support[1]}") + + def updateLastValue(self, df: DataFrame, col, value): + if col in df.columns: + print(f"update last col {col} {value}") + df.iloc[-1, df.columns.get_loc(col)] = value + else: + print(f"update all col {col} {value}") + df[col] = value + + # def update_last_record(self, dataframe: DataFrame, new_data): + # # Vérifiez si de nouvelles données ont été reçues + # if new_data is not None: + # # Ne mettez à jour que la dernière ligne du dataframe + # last_index = dataframe.index[-1] # Sélectionne le dernier enregistrement + # dataframe.loc[last_index] = new_data # Met à jour le dernier enregistrement avec les nouvelles données + # return dataframe + + def calculateSMA(self, nb, asks, bids): + # Prepare data for plotting + bid_prices = [float(bid[0]) for bid in bids] + bid_volumes = [float(bid[1]) for bid in bids] + + ask_prices = [float(ask[0]) for ask in asks] + ask_volumes = [float(ask[1]) for ask in asks] + + # Convert to pandas DataFrame to apply moving average + bid_df = pd.DataFrame({'price': bid_prices, 'volume': bid_volumes}) + ask_df = pd.DataFrame({'price': ask_prices, 'volume': ask_volumes}) + + # Apply a rolling window to calculate a 10-value simple moving average (SMA) + bid_df['volume_sma'] = bid_df['volume'].rolling(window=nb).mean() + ask_df['volume_sma'] = ask_df['volume'].rolling(window=nb).mean() + + # Pour bid_df + bid_df = bid_df.dropna(subset=['volume_sma']) + bids_with_sma = list(zip(bid_df['price'], bid_df['volume_sma'])) + + # Pour ask_df + ask_df = ask_df.dropna(subset=['volume_sma']) + asks_with_sma = list(zip(ask_df['price'], ask_df['volume_sma'])) + + # print(bids_with_sma) + # print(asks_with_sma) + + return asks_with_sma, bids_with_sma + + def calculate_stake(self, pair, value, last_candle): + """ + Calcule la mise en pourcentage (entre 20% et 100%) pour une paire donnée + en fonction de la valeur actuelle et de la plage min/max de la paire. + + Parameters: + pair (str): La paire (ex: "BTC/USDT"). + value (float): La valeur actuelle de la paire. + + Returns: + float: Le pourcentage de mise, entre 20% et 100%. + """ + # if pair not in self.min_max_buys: + # raise ValueError(f"La paire {pair} n'est pas définie dans min_max_buys") + + min_val = last_candle['min_previous_1h'] # self.min_max_buys[pair]["min"] + max_val = last_candle['max_previous_1h'] # self.min_max_buys[pair]["max"] + + amount = self.config['stake_amount'] + return amount + if (max_val == min_val) | (np.isnan(max_val)): + return amount + if value <= min_val: + return amount # Mise minimale de 20% + elif value >= max_val: + return amount / 5 # Mise maximale de 100% + else: + # Calcul de la position relative entre min et max + position = (value - min_val) / (max_val - min_val) + # Interpolation linéaire entre 20% et 100% + stake = amount - (position * (amount * 4 / 5)) + return stake diff --git a/Thor_001.txt b/Thor_001.txt new file mode 100644 index 0000000..5df2352 --- /dev/null +++ b/Thor_001.txt @@ -0,0 +1,15 @@ +[Achats] +BTC/USDT=63400 +ETH/USDT=2570 +ETC/USDT=10 +DOGE/USDT=0.106 +SOL/USDT=150 +XRP/USDT=0.584 + +[Ventes] +BTC/USDT=63979 +ETH/USDT=2542 +ETC/USDT=70 +DOGE/USDT=0.122 +SOL/USDT=150.24 +XRP/USDT=0.6