# Zeus Strategy: First Generation of GodStra Strategy with maximum
# AVG/MID profit in USDT
# Author: @Mablue (Masoud Azizi)
# github: https://github.com/mablue/
# IMPORTANT: INSTALL TA BEFOUR RUN(pip install ta)
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --spaces buy sell roi --strategy Zeus
# --- Do not remove these libs ---
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 typing import List

import logging
import configparser
from technical import pivots_points
# --------------------------------

# Add your lib to import here test git
import ta
import talib.abstract as talib
import freqtrade.vendor.qtpylib.indicators as qtpylib
import requests
from datetime import timezone, timedelta
from scipy.signal import savgol_filter
from ta.trend import SMAIndicator, EMAIndicator, MACD, ADXIndicator
from collections import Counter

logger = logging.getLogger(__name__)

from tabulate import tabulate

# 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"


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

"""
SMA z-score derivative strategy with trailing exit (large).
- 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
"""
class Simple(IStrategy):
    levels = [1, 2, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
    startup_candle_count = 12 * 24 * 2

    # ROI table:
    minimal_roi = {
        "0": 10
    }
    stakes = 40

    # Stoploss:
    stoploss = -1  # 0.256
    # Custom stoploss
    use_custom_stoploss = True

    # Buy hypers
    timeframe = '1h'

    max_open_trades = 5
    max_amount = 40

    # DCA config
    position_adjustment_enable = True

    # Parameters (tweakable)
    z_window = 50  # window for rolling mean/std to compute zscore
    entry_z1 = 0.1  # threshold on z-score of first derivative
    entry_z2 = 0.1  # 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 requested
    trailing_stop_pct = 0.05  # 5% retracement from highest since entry

    # Smoothing for z-scores
    ewm_span = 5

    # Plot config: price + sma5 + markers + slope/accel subplots
    plot_config = {
        "main_plot": {
            "close": {"color": "blue"},
            "sma5": {"color": "orange"},
        },
        "subplots": {
            "slope_and_accel": {
                "z_d1": {"color": "green"},
                "z_d2": {"color": "red"},
            }
        },
        # Markers (Freqtrade charting supports markers via these keys)
        "markers": [
            # buy marker: '^' green when enter_long==1
            {"type": "buy", "column": "enter_long", "marker": "^", "color": "green", "markersize": 10},
            # sell marker: 'v' red when exit_long==1
            {"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_d1'] = df['sma5'].diff().rolling(5).mean()
        df['sma5_d2'] = df['sma5_d1'].diff().rolling(5).mean()

        # Relative derivatives (percentage-like)
        eps = 1e-9
        df['sma5_d1_rel'] = df['sma5_d1'] / (df['sma5'].shift(1).replace(0, np.nan) + eps)
        df['sma5_d2_rel'] = df['sma5_d2'] / (df['sma5'].shift(2).replace(0, np.nan) + eps)

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

        # Z-scores on relative derivatives
        df['z_d1'] = self._zscore(df['sma5_d1_rel'], self.z_window)
        df['z_d2'] = self._zscore(df['sma5_d2_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). They will be filled in populate_entry_trend/populate_exit_trend
        df['enter_long'] = 0
        df['exit_long'] = 0

        return df

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

        # Use last closed candle for signals -> shift(1)
        cond_entry = (
                (df['z_d1'] > self.entry_z1) &
                (df['z_d2'] > self.entry_z2) &
                (df['volume'].shift(1) > self.min_volume)
        )

        df.loc[cond_entry, 'enter_long'] = 1
        # Ensure others are explicitly zero (for clean plotting)
        df.loc[~cond_entry, 'enter_long'] = 0

        return df

    def custom_exit(self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs) -> \
    Optional[str]:
        """
        Exit policy (mode C - trailing large):
        - Must detect inversion: z_d1_smooth < 0 on last closed candle
        - Compute highest close since trade entry (inclusive)
        - If price has retraced >= trailing_stop_pct from that highest point and we're in inversion -> exit
        """

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

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

        # Only consider exits when inversion detected (z1 < 0)
        inversion = (z1 < 0)

        if not inversion:
            return None

        # If we don't have profit info, be conservative
        if current_profit is None:
            return None

        # Determine highest close since entry
        highest_since_entry = None
        try:
            # trade.open_date_utc is available: find rows after that timestamp
            # df.index is expected to be pd.DatetimeIndex in UTC or local; convert safely
            if hasattr(trade, 'open_date_utc') and trade.open_date_utc:
                # pandas comparison: ensure same tz awareness
                entry_time = pd.to_datetime(trade.open_date_utc)
                # select rows with index >= entry_time
                mask = df.index >= entry_time
                if mask.any():
                    highest_since_entry = df.loc[mask, 'close'].max()
            # fallback: use trade.open_rate or the max over full df
        except Exception as e:
            logger.debug(f"Couldn't compute highest_since_entry from open_date_utc: {e}")

        if highest_since_entry is None:
            # fallback: use the maximum close in the entire provided dataframe slice
            highest_since_entry = df['close'].max() if not df['close'].empty else current_rate

        # Calculate retracement ratio from the highest
        if highest_since_entry and highest_since_entry > 0:
            retrace = 1.0 - (current_rate / highest_since_entry)
        else:
            retrace = 0.0

        # Exit if:
        # - currently in profit AND
        # - retracement >= trailing_stop_pct (i.e. price has fallen enough from top since entry) AND
        # - inversion detected (z1 < 0)
        if (current_profit > 0) and (retrace >= self.trailing_stop_pct):
            # Mark the dataframe for plotting (if possible)
            # Note: freqtrade expects strategies to set exit flags in populate_exit_trend,
            # but we set exit via custom_exit return reason; plotting will read exit_long if populated.
            return "zscore"

        # Otherwise, do not exit yet
        return None

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        For plotting only: mark exit points where our logic would trigger.
        This is approximate: we mark an exit when z_d1_smooth < 0 and the price has retraced >= trailing_stop_pct
        based on the available dataframe window (best-effort).
        """
        df = dataframe
        # df['exit_long'] = 0
        #
        # # compute highest close since each possible entry (best-effort: use rolling max up to current index)
        # rolling_max = df['close'].cummax()
        #
        # # retracement relative to rolling max
        # retrace = 1.0 - (df['close'] / rolling_max.replace(0, np.nan))
        #
        # # mark exits where inversion and retrace >= threshold
        # cond_exit = (df['z_d1_smooth'] < 0) & (retrace >= self.trailing_stop_pct)
        # # shift by 0: we mark the candle where the exit condition appears
        # df.loc[cond_exit, 'exit_long'] = 1

        return df
