# sma_zscore_trailing_dca.py
from freqtrade.strategy.interface import IStrategy
from freqtrade.persistence import Trade
from pandas import DataFrame
import pandas as pd
import numpy as np
from typing import Optional
import logging
from datetime import timedelta, datetime
import talib.abstract as talib

logger = logging.getLogger(__name__)

class Simple_01(IStrategy):
    """
    SMA z-score derivative strategy with trailing exit and intelligent DCA (large pullback).
    - timeframe: 1h
    - sma5, relative derivatives, z-score normalization (rolling z over z_window)
    - smoothing: ewm(span=5) on z-scores
    - entry: z_d1_smooth > entry_z1 AND z_d2_smooth > entry_z2
    - exit: inversion (z_d1_smooth < 0) AND retrace from highest since entry >= trailing_stop_pct
    - adjust_trade_position: add to position on controlled pullback (large = 4-6%)
    """

    timeframe = "1h"
    startup_candle_count = 24
    # Risk mgmt (we handle exit in custom_exit)
    minimal_roi = {"0": 0.99}
    stoploss = -0.99
    use_custom_exit = True
    position_adjustment_enable = True

    columns_logged = False

    # Parameters
    z_window = 10              # window for rolling mean/std to compute zscore
    entry_z1 = 0.1             # threshold on z-score of first derivative
    entry_z2 = 0               # threshold on z-score of second derivative
    min_volume = 0.0           # minimal volume to accept an entry
    min_relative_d1 = 1e-6     # clip tiny d1 relative values to reduce noise

    # Trailing parameters for "large" trailing (exit)
    trailing_stop_pct = 0.01   # 5% retracement from highest since entry

    # Smoothing for z-scores
    ewm_span = 5

    # DCA intelligent (adjust_trade_position) parameters for "large" pullback
    dca_enabled = True
    # Pullback bounds (large): allow adding when retrace is between 4% and 6%
    dca_pullback_min = 0.01
    dca_pullback_max = 0.02
    # Maximum number of adds per trade (to avoid infinite pyramiding)
    dca_max_adds = 8
    # Percentage of base position to add on each reinforcement (50% of original size by default)
    dca_add_ratio = 0.5
    # Require momentum still positive to add
    dca_require_z1_positive = True
    dca_require_z2_positive = True
    # Do not add if current_profit < min_profit_to_add (avoid averaging down when deep in loss)
    min_profit_to_add = -0.02  # allow small loss but not big drawdown

    pairs = {
        pair: {
            "first_buy": 0,
            "last_buy": 0.0,
            "first_amount": 0.0,
            "last_min": 999999999999999.5,
            "last_max": 0,
            "trade_info": {},
            "max_touch": 0.0,
            "last_sell": 0.0,
            'count_of_buys': 0,
            'current_profit': 0,
            'expected_profit': 0,
            "last_candle": {},
            "last_trade": None,
            "last_count_of_buys": 0,
            'base_stake_amount': 0,
            'stop_buy': False,
            'last_date': 0,
            'stop': False,
            'max_profit': 0,
            'last_palier_index': -1,
            'total_amount': 0,
            'has_gain': 0,
            'force_sell': False,
            'force_buy': False
        }
        for pair in ["BTC/USDC", "ETH/USDC", "DOGE/USDC", "XRP/USDC", "SOL/USDC",
                     "BTC/USDT", "ETH/USDT", "DOGE/USDT", "XRP/USDT", "SOL/USDT"]
    }

    # Plot config
    plot_config = {
        "main_plot": {
            "close": {"color": "blue"},
            "sma5": {"color": "orange"},
        },
        "subplots": {
            "slope_and_accel": {
                "z_d1_smooth": {"color": "green"},
                "z_d2_smooth": {"color": "red"},
            },
            "sma_deriv": {
                "sma5_deriv1_rel": {"color": "green"},
                "sma5_deriv2_rel": {"color": "red"},
            },
            "rsi": {
                "rsi": {"color": "blue"}
            }

        },
        "markers": [
            {"type": "buy", "column": "enter_long", "marker": "^", "color": "green", "markersize": 10},
            {"type": "sell", "column": "exit_long", "marker": "v", "color": "red", "markersize": 10},
        ],
    }

    def informative_pairs(self):
        return []

    def _zscore(self, series: pd.Series, window: int) -> pd.Series:
        mean = series.rolling(window=window, min_periods=3).mean()
        std = series.rolling(window=window, min_periods=3).std(ddof=0)
        z = (series - mean) / std
        z = z.replace([np.inf, -np.inf], np.nan)
        return z

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe

        # SMA(5)
        df['sma5'] = df['close'].rolling(5, min_periods=1).mean()

        # Absolute derivatives
        df['sma5_deriv1'] = df['sma5'].diff()
        df['sma5_deriv2'] = df['sma5_deriv1'].diff()

        # Relative derivatives (percentage-like)
        eps = 1e-9
        df['sma5_deriv1_rel'] = df['sma5_deriv1'] / (df['sma5'].shift(1).replace(0, np.nan) + eps)
        df['sma5_deriv2_rel'] = df['sma5_deriv2'] / (df['sma5'].shift(2).replace(0, np.nan) + eps)

        # Clip micro-noise
        df.loc[df['sma5_deriv1_rel'].abs() < self.min_relative_d1, 'sma5_deriv1_rel'] = 0.0
        df.loc[df['sma5_deriv2_rel'].abs() < self.min_relative_d1, 'sma5_deriv2_rel'] = 0.0

        # Z-scores on relative derivatives
        df['z_d1'] = self._zscore(df['sma5_deriv1_rel'], self.z_window)
        df['z_d2'] = self._zscore(df['sma5_deriv2_rel'], self.z_window)

        # Smoothing z-scores with EWM to reduce jitter
        df['z_d1_smooth'] = df['z_d1'].ewm(span=self.ewm_span, adjust=False).mean()
        df['z_d2_smooth'] = df['z_d2'].ewm(span=self.ewm_span, adjust=False).mean()

        # Prepare marker columns (for plots)
        df['enter_long'] = 0
        df['exit_long'] = 0
        df['rsi'] = talib.RSI(df['close'], timeperiod=14)
        df['max_rsi_12'] = talib.MAX(df['rsi'], timeperiod=12)
        df['min_rsi_12'] = talib.MIN(df['rsi'], timeperiod=12)
        # self.calculeDerivees(df, 'rsi', horizon=12)

        return df

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe

        # Use last closed candle for signals -> shift(1)
        cond_entry = (
            (df['z_d1_smooth'] > self.entry_z1) &
            (df['z_d2_smooth'] > self.entry_z2) &
            (df['max_rsi_12'] < 70) &
            (df['volume'] > self.min_volume)
        )

        df.loc[cond_entry, 'enter_long'] = 1
        df.loc[~cond_entry, 'enter_long'] = 0

        return df

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe
        # df['exit_long'] = 0
        #
        # # compute rolling max (best-effort for plotting)
        # rolling_max = df['close'].cummax()
        # retrace = 1.0 - (df['close'] / rolling_max.replace(0, np.nan))
        #
        # cond_exit = (df['z_d1_smooth'] < 0) & (retrace >= self.trailing_stop_pct)
        # df.loc[cond_exit, 'exit_long'] = 1

        return df

    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)
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        force = self.pairs[pair]['force_sell']
        allow_to_sell = True #(last_candle['percent'] < 0) #or force

        minutes = int(round((current_time - trade.date_last_filled_utc).total_seconds() / 60, 0))

        if allow_to_sell:
            self.trades = list()
            self.pairs[pair]['last_count_of_buys'] = trade.nr_of_successful_entries  # self.pairs[pair]['count_of_buys']
            self.pairs[pair]['last_sell'] = rate
            self.pairs[pair]['last_trade'] = trade
            self.pairs[pair]['last_candle'] = last_candle
            self.trades = list()
            dispo = round(self.wallets.get_available_stake_amount())
            print(f"Sell {pair} {current_time} {exit_reason}  dispo={dispo}  amount={amount} rate={rate} open_rate={trade.open_rate}")
            self.log_trade(
                last_candle=last_candle,
                date=current_time,
                action="🟥Sell " + str(minutes),
                pair=pair,
                trade_type=exit_reason,
                rate=last_candle['close'],
                dispo=dispo,
                profit=round(trade.calc_profit(rate, amount), 2)
            )
            self.pairs[pair]['max_profit'] = 0
            self.pairs[pair]['force_sell'] = False
            self.pairs[pair]['has_gain'] = 0
            self.pairs[pair]['current_profit'] = 0
            self.pairs[pair]['total_amount'] = 0
            self.pairs[pair]['count_of_buys'] = 0
            self.pairs[pair]['max_touch'] = 0
            self.pairs[pair]['last_buy'] = 0
            self.pairs[pair]['last_date'] = current_time
            self.pairs[pair]['last_palier_index'] = -1
            self.pairs[pair]['last_trade'] = trade
            self.pairs[pair]['current_trade'] = None

        return (allow_to_sell) | (exit_reason == 'force_exit')

    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:

        minutes = 0
        if self.pairs[pair]['last_date'] != 0:
            minutes = round(int((current_time - self.pairs[pair]['last_date']).total_seconds() / 60))

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()
        last_candle_2 = dataframe.iloc[-2].squeeze()
        last_candle_3 = dataframe.iloc[-3].squeeze()
        # val = self.getProbaHausse144(last_candle)

        # allow_to_buy = True #(not self.stop_all) #& (not self.all_down)
        allow_to_buy = not self.pairs[pair]['stop']  # and val > self.buy_val.value #not last_candle['tendency'] in ('B-', 'B--')  # (rate <= float(limit)) | (entry_tag == 'force_entry')

        # force = self.pairs[pair]['force_buy']
        # if self.pairs[pair]['force_buy']:
        #     self.pairs[pair]['force_buy'] = False
        #     allow_to_buy = True
        # else:
        #     if not self.should_enter_trade(pair, last_candle, current_time):
        #         allow_to_buy = False

        if allow_to_buy:
            self.trades = list()
            self.pairs[pair]['first_buy'] = rate
            self.pairs[pair]['last_buy'] = rate
            self.pairs[pair]['max_touch'] = last_candle['close']
            self.pairs[pair]['last_candle'] = last_candle
            self.pairs[pair]['count_of_buys'] = 1
            self.pairs[pair]['current_profit'] = 0
            self.pairs[pair]['last_palier_index'] = -1
            self.pairs[pair]['last_max'] = max(last_candle['close'], self.pairs[pair]['last_max'])
            self.pairs[pair]['last_min'] = min(last_candle['close'], self.pairs[pair]['last_min'])

            dispo = round(self.wallets.get_available_stake_amount())
            self.printLineLog()

            stake_amount = self.adjust_stake_amount(pair, last_candle)

            self.pairs[pair]['total_amount'] = stake_amount

            self.log_trade(
                last_candle=last_candle,
                date=current_time,
                action=("🟩Buy" if allow_to_buy else "Canceled") + " " + str(minutes),
                pair=pair,
                rate=rate,
                dispo=dispo,
                profit=0,
                trade_type=entry_tag,
                buys=1,
                stake=round(stake_amount, 2)
            )

        return allow_to_buy

    def custom_exit(self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs) -> Optional[str]:
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = df.iloc[-1].squeeze()

        self.pairs[pair]['last_max'] = max(last_candle['close'], self.pairs[pair]['last_max'])
        self.pairs[pair]['last_min'] = min(last_candle['close'], self.pairs[pair]['last_min'])
        self.pairs[pair]['current_trade'] = trade
        count_of_buys = trade.nr_of_successful_entries

        profit = round(current_profit * trade.stake_amount, 1)
        self.pairs[pair]['max_profit'] = max(self.pairs[pair]['max_profit'], profit)
        max_profit = self.pairs[pair]['max_profit']
        baisse = 0
        if profit > 0:
            baisse = 100 * abs(max_profit - profit) / max_profit
        mx = max_profit / 5
        self.pairs[pair]['count_of_buys'] = count_of_buys
        self.pairs[pair]['current_profit'] = profit

        dispo = round(self.wallets.get_available_stake_amount())
        hours_since_first_buy = (current_time - trade.open_date_utc).seconds / 3600.0
        days_since_first_buy = (current_time - trade.open_date_utc).days
        hours = (current_time - trade.date_last_filled_utc).total_seconds() / 3600.0

        if hours % 4 == 0:
            self.log_trade(
                last_candle=last_candle,
                date=current_time,
                action="🔴 CURRENT" if self.pairs[pair]['stop'] else "🟢 CURRENT",
                dispo=dispo,
                pair=pair,
                rate=last_candle['close'],
                trade_type='',
                profit=profit,
                buys='',
                stake=0
            )

        z1 = last_candle.get('z_d1_smooth', None)
        if z1 is None:
            return None

        if z1 >= 0:
            return None

        if current_profit is None:
            return None

        # highest close since entry
        highest_since_entry = self.pairs[trade.pair]['max_touch']
        # try:
        #     if hasattr(trade, 'open_date_utc') and trade.open_date_utc:
        #         entry_time = pd.to_datetime(trade.open_date_utc)
        #         mask = df.index >= entry_time
        #         if mask.any():
        #             highest_since_entry = df.loc[mask, 'close'].max()
        # except Exception as e:
        #     logger.debug(f"Couldn't compute highest_since_entry: {e}")
        #
        # if highest_since_entry is None:
        #     highest_since_entry = df['close'].max() if not df['close'].empty else current_rate

        if highest_since_entry and highest_since_entry > 0:
            retrace = 1.0 - (current_rate / highest_since_entry)
        else:
            retrace = 0.0
        z1 = last_candle.get('z_d1_smooth', None)
        z2 = last_candle.get('z_d2_smooth', None)

        if (current_profit > 0) and (current_profit >= self.trailing_stop_pct) and last_candle['sma5_deriv1'] < -0.002:
            return str(count_of_buys) + '_' + "zscore"

        self.pairs[pair]['max_touch'] = max(last_candle['close'], self.pairs[pair]['max_touch'])

        return None

    def adjust_stake_amount(self, pair: str, last_candle: DataFrame):
        # Calculer le minimum des 14 derniers jours
        return self.config.get('stake_amount')

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float, min_stake: float,
                              max_stake: float, **kwargs):
        """
        DCA intelligent (mode C - 'large'):
        - Only add if:
            * DCA is enabled
            * number of adds done so far < dca_max_adds
            * retracement since highest_since_entry is between dca_pullback_min and dca_pullback_max
            * momentum still positive (z_d1_smooth, z_d2_smooth) if required
            * current_profit >= min_profit_to_add (avoid averaging down into large loss)
        - Returns a dict describing the desired order for Freqtrade to place (common format).
          Example returned dict: {'type': 'market', 'amount': 0.01}
        """
        if not self.dca_enabled:
            return None
        pair = trade.pair

        # Basic guards
        if current_profit is None:
            current_profit = 0.0

        # Do not add if we're already deeply in loss
        if current_profit < self.min_profit_to_add:
            return None

        df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        last_candle = df.iloc[-1].squeeze()

        # Compute highest close since entry
        highest_since_entry = self.pairs[trade.pair]['last_buy']

        last = df.iloc[-1]
        z1 = last.get('z_d1_smooth', None)
        z2 = last.get('z_d2_smooth', None)

        # Count how many adds have been done for this trade.
        # Trade.extra might contain meta; otherwise try trade.tags or use trade.open_rate/amount heuristics.
        adds_done = trade.nr_of_successful_entries
        # Calculate retracement since the highest
        retrace = 0.0
        if highest_since_entry and highest_since_entry > 0:
            retrace = (last['close'] - highest_since_entry) / highest_since_entry

        # logger.info(f"{pair} {current_rate} {current_time} {highest_since_entry} add: retrace={retrace:.4f}, adds_done={adds_done} z1={z1} z2={z2}")

        # Enforce momentum requirements if requested
        if self.dca_require_z1_positive and (z1 is None or z1 <= 0):
            return None
        if self.dca_require_z2_positive and (z2 is None or z2 <= 0):
            return None

        #
        # try:
        #     meta = getattr(trade, 'meta', None) or {}
        #     adds_done = int(meta.get('adds_done', 0))
        # except Exception:
        #     adds_done = 0

        if adds_done >= self.dca_max_adds:
            return None

        # try:
        #     if hasattr(trade, 'open_date_utc') and trade.open_date_utc:
        #         entry_time = pd.to_datetime(trade.open_date_utc)
        #         mask = df.index >= entry_time
        #         if mask.any():
        #             highest_since_entry = df.loc[mask, 'close'].max()
        # except Exception as e:
        #     logger.debug(f"adjust_trade_position: couldn't compute highest_since_entry: {e}")
        #
        # if highest_since_entry is None:
        #     highest_since_entry = df['close'].max() if not df['close'].empty else current_rate

        # Check if retrace is inside the allowed DCA window (large)
        if ((retrace >= self.dca_pullback_min) and (retrace <= self.dca_pullback_max)):
            # Determine amount to add: a fraction of the original trade amount
            # Try to get trade.amount (base asset amount). If not available, fall back to stake percentage
            add_amount = None
            try:
                base_amount = self.config.get('stake_amount')
                if base_amount:
                    add_amount = base_amount * self.dca_add_ratio
                else:
                    # Fallback: attempt to compute amount from trade.open_rate and desired quote stake
                    # We'll propose to use a fraction of current rate worth (this is best-effort)
                    add_amount = None
            except Exception:
                add_amount = None

            # If we couldn't compute an absolute amount, propose a relative size via a suggested stake (user must map)
            if add_amount is None:
                # Return a suggested instruction; adapt according to your freqtrade version.
                suggested = {
                    'type': 'market',
                    'amount': None,  # caller should compute actual amount from stake management
                    'note': f'suggest_add_ratio={self.dca_add_ratio}'
                }
                # logger.info(f"{pair} {current_rate} DCA suggestion (no absolute amount): retrace={retrace:.4f}, adds_done={adds_done}")
                return None

            dispo = round(self.wallets.get_available_stake_amount())
            trade_type = last_candle['enter_tag'] if last_candle['enter_long'] == 1 else 'pct48'
            self.pairs[trade.pair]['count_of_buys'] += 1
            self.pairs[pair]['total_amount'] += add_amount
            self.log_trade(
                last_candle=last_candle,
                date=current_time,
                action="🟧 Loss -",
                dispo=dispo,
                pair=trade.pair,
                rate=current_rate,
                trade_type=trade_type,
                profit=round(current_profit * trade.stake_amount, 1),
                buys=trade.nr_of_successful_entries + 1,
                stake=round(add_amount, 2)
            )

            self.pairs[trade.pair]['last_buy'] = current_rate
            self.pairs[trade.pair]['max_touch'] = last_candle['close']
            self.pairs[trade.pair]['last_candle'] = last_candle

            # All checks passed -> create market order instruction
            # logger.info(f"{pair} {current_rate} {current_time} {highest_since_entry} add: retrace={retrace:.4f}, adds_done={adds_done}, add_amount={add_amount:.8f}")
            return add_amount

        # Not in allowed retrace window -> no action
        return None

    def getPctFirstBuy(self, pair, last_candle):
        return round((last_candle['close'] - self.pairs[pair]['first_buy']) / self.pairs[pair]['first_buy'], 3)

    def getPctLastBuy(self, pair, last_candle):
        return round((last_candle['close'] - self.pairs[pair]['last_buy']) / self.pairs[pair]['last_buy'], 4)

    def getPct60D(self, pair, last_candle):
        return round((last_candle['max60'] - last_candle['min60']) / last_candle['max60'], 4)

    def getPctClose60D(self, pair, last_candle):
        if last_candle['close'] > last_candle['max12']:
            return 1
        if last_candle['close'] < last_candle['min12']:
            return 0
        return round(
            (last_candle['close'] - last_candle['min12']) / (last_candle['max12'] - last_candle['min12']), 4)


    def printLineLog(self):
        self.printLog(
            f"+{'-' * 18}+{'-' * 12}+{'-' * 5}+{'-' * 20}+{'-' * 9}+{'-' * 8}+{'-' * 12}+{'-' * 8}+{'-' * 13}+{'-' * 14}+{'-' * 9}{'-' * 9}+{'-' * 5}+{'-' * 7}+"
            f"{'-' * 3}"
            # "+{'-' * 3}+{'-' * 3}
            f"+{'-' * 6}+{'-' * 7}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+{'-' * 5}+"
        )

    def printLog(self, str):
        if self.config.get('runmode') == 'hyperopt' or self.dp.runmode.value in ('hyperopt'):
            return
        if not self.dp.runmode.value in ('backtest', 'hyperopt', 'lookahead-analysis'):
            logger.info(str)
        else:
            if not self.dp.runmode.value in ('hyperopt'):
                print(str)

    def log_trade(self, action, pair, date, trade_type=None, rate=None, dispo=None, profit=None, buys=None,
                  stake=None,
                  last_candle=None):
        # Couleurs ANSI de base
        RED = "\033[31m"
        GREEN = "\033[32m"
        YELLOW = "\033[33m"
        BLUE = "\033[34m"
        MAGENTA = "\033[35m"
        CYAN = "\033[36m"
        RESET = "\033[0m"

        # Afficher les colonnes une seule fois
        if self.config.get('runmode') == 'hyperopt' or self.dp.runmode.value in ('hyperopt'):
            return
        if self.columns_logged % 10 == 0:
            self.printLog(
                f"| {'Date':<16} | {'Action':<10} |{'Pair':<5}| {'Trade Type':<18} |{'Rate':>8} | {'Dispo':>6} | {'Profit':>8} | {'Pct':>6} | {'max_touch':>11} | {'last_lost':>12} | {'last_max':>7}| {'last_max':>7}|{'Buys':>5}| {'Stake':>5} |"
                f"Tdc|{'val':>6}|  RSI  |s201d|s5_1d|s5_2d|s51h|s52h"
            )
            self.printLineLog()
            df = pd.DataFrame.from_dict(self.pairs, orient='index')
            colonnes_a_exclure = ['last_candle', 'last_trade', 'last_palier_index',
                                  'trade_info', 'last_date', 'expected_profit', 'last_count_of_buys',
                                  'base_stake_amount', 'stop_buy']
            df_filtered = df[df['count_of_buys'] > 0].drop(columns=colonnes_a_exclure)
            # df_filtered = df_filtered["first_buy", "last_max", "max_touch", "last_sell","last_buy", 'count_of_buys', 'current_profit']

            print(df_filtered)

        self.columns_logged += 1
        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 = ''
        sma5_1d = ''
        sma5_1h = ''

        sma5 = str(sma5_1d) + ' ' + str(sma5_1h)

        last_lost = '' #self.getLastLost(last_candle, pair)

        if buys is None:
            buys = ''

        max_touch = ''  # round(last_candle['max12'], 1) #round(self.pairs[pair]['max_touch'], 1)
        pct_max = '' #self.getPctFirstBuy(pair, last_candle)

        total_counts = str(buys) + '/' + str(
            sum(pair_data['count_of_buys'] for pair_data in self.pairs.values()))

        dist_max = '' #self.getDistMax(last_candle, pair)
        val = 0 #self.getProbaHausseSma5d(last_candle)

        pct60 = 0 #round(100 * self.getPct60D(pair, last_candle), 2)

        color = GREEN if profit > 0 else RED
        # color_sma20 = GREEN if last_candle['sma20_deriv1'] > 0 else RED
        # color_sma5 = GREEN if last_candle['mid_smooth_5_deriv1'] > 0 else RED
        # color_sma5_2 = GREEN if last_candle['mid_smooth_5_deriv2'] > 0 else RED
        # color_sma5_1h = GREEN if last_candle['sma5_deriveriv1'] > 0 else RED
        # color_sma5_2h = GREEN if last_candle['sma5_deriveriv2'] > 0 else RED

        last_max = int(self.pairs[pair]['last_max']) if self.pairs[pair]['last_max'] > 1 else round(
            self.pairs[pair]['last_max'], 3)
        last_min = int(self.pairs[pair]['last_min']) if self.pairs[pair]['last_min'] > 1 else round(
            self.pairs[pair]['last_min'], 3)

        profit = str(profit) + '/' + str(round(self.pairs[pair]['max_profit'], 2))

        #     🟢 Dérivée 1 > 0 et dérivée 2 > 0: tendance haussière qui s’accélère.
        #     🟡 Dérivée 1 > 0 et dérivée 2 < 0: tendance haussière qui ralentit → essoufflement potentiel.
        #     🔴 Dérivée 1 < 0 et dérivée 2 < 0: tendance baissière qui s’accélère.
        #     🟠 Dérivée 1 < 0 et dérivée 2 > 0: tendance baissière qui ralentit → possible bottom.

        # tdc last_candle['tendency_12']
        self.printLog(
            f"| {date:<16} |{action:<10} | {pair[0:3]:<3} | {trade_type or '-':<18} |{rate or '-':>9}| {dispo or '-':>6} "
            f"|{color}{profit or '-':>10}{RESET}| {pct_max or '-':>6} | {round(self.pairs[pair]['max_touch'], 2) or '-':>11} | {last_lost or '-':>12} "
            f"| {last_max or '-':>7} | {last_min or '-':>7} |{total_counts or '-':>5}|{stake or '-':>7}"
            f"|{'-':>3}|"
            f"{round(val, 1) or '-' :>6}|"
            # f"{round(last_candle['rsi'], 0):>7}|{color_sma20}{round(last_candle['sma20_deriv1'], 2):>5}{RESET}"
            # f"|{color_sma5}{round(last_candle['mid_smooth_5_deriv1'], 2):>5}{RESET}|{color_sma5_2}{round(last_candle['mid_smooth_5_deriv2'], 2):>5}{RESET}"
            # f"|{color_sma5_1h}{round(last_candle['sma5_deriveriv1'], 2):>5}{RESET}|{color_sma5_2h}{round(last_candle['sma5_deriveriv2'], 2):>5}{RESET}"
            # f"|{last_candle['min60']}|{last_candle['max60']}"
        )