# source: https://raw.githubusercontent.com/RoboticAutomations/freqtradestrategies/52c4bd40414645f079a980cf4486dc7af4061efb/AIRA%20-%20open%20code%20v2/strategies/smart-money-concepts/Chronos2LLMTankAIHybrid.py
# directory_url: https://github.com/RoboticAutomations/freqtradestrategies/blob/main/AIRA - open code v2/strategies/smart-money-concepts/
# User: RoboticAutomations
# Repository: freqtradestrategies
# --------------------# pragma pylint: disable=missing-module-docstring, invalid-name

import json
import logging
import time
import requests
import numpy as np
import pandas as pd
import talib.abstract as ta

from pandas import DataFrame
from freqtrade.strategy import IStrategy
from freqtrade.persistence import Trade

from autogluon.timeseries import TimeSeriesPredictor, TimeSeriesDataFrame

logger = logging.getLogger(__name__)

# =========================
# USER CONFIG
# =========================
OLLAMA_URL = "http://localhost:11434"
OLLAMA_MODEL = "phi4-reasoning:14b"
OLLAMA_TIMEOUT = 30

ENSEMBLE_THRESHOLD = 0.65
LLM_CONF_MIN = 0.60
CHRONOS_CONF_MIN = 0.55

CHRONOS_CONTEXT = 120
CHRONOS_PRED_STEPS = 3
CHRONOS_REFIT_SECONDS = 1800
MIN_CHRONOS_EDGE = 0.006


class Github_RoboticAutomations_freqtradestrategies__Chronos2LLMTankAIHybrid__20260915_233254(IStrategy):
    """
    Chronos-2 + LLM Ensemble Strategy (FINAL FIXED VERSION)
    """

    timeframe = "15m"
    startup_candle_count = 200
    process_only_new_candles = True

    stoploss = -0.12
    exit_profit_only = True
    ignore_roi_if_entry_signal = True

    minimal_roi = {"0": 0.08}

    def __init__(self, config: dict) -> None:
        super().__init__(config)
        self.chronos_models = {}
        self.chronos_last_fit = {}

    # =========================
    # INDICATORS
    # =========================
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["atr"] = ta.ATR(
            dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14
        )
        dataframe["rsi"] = ta.RSI(dataframe["close"], timeperiod=14)

        dataframe["ob_imbalance"] = 0.0
        if self.dp and self.dp.runmode.value in ("live", "dry_run"):
            try:
                ob = self.dp.orderbook(metadata["pair"], 5)
                bids = np.array([b[0] * b[1] for b in ob["bids"]])
                asks = np.array([a[0] * a[1] for a in ob["asks"]])
                dataframe.loc[dataframe.index[-1], "ob_imbalance"] = (
                    (bids.sum() - asks.sum()) / max(bids.sum() + asks.sum(), 1)
                )
            except Exception:
                pass

        return dataframe

    # =========================
    # CHRONOS-2 (CORRECT PRESET)
    # =========================
    def _chronos_predict(self, pair: str, df: DataFrame):

        try:
            hist = df[["date", "close"]].tail(CHRONOS_CONTEXT).copy()

            # 🚨 REQUIRED: strip timezone
            hist["timestamp"] = (
                pd.to_datetime(hist["date"], utc=True)
                .dt.tz_localize(None)
            )

            hist["item_id"] = pair
            hist = hist[["item_id", "timestamp", "close"]]

            ts_df = TimeSeriesDataFrame.from_data_frame(
                hist,
                id_column="item_id",
                timestamp_column="timestamp"
            )

            predictor = self.chronos_models.get(pair)
            if predictor is None or (time.time() - self.chronos_last_fit.get(pair, 0)) > CHRONOS_REFIT_SECONDS:
                logger.info(f"[CHRONOS INIT] {pair}")
                predictor = TimeSeriesPredictor(
                    prediction_length=CHRONOS_PRED_STEPS,
                    target="close",
                    verbosity=0
                )
                predictor.fit(
                    ts_df,
                    presets="chronos2"   # ✅ FIXED
                )
                self.chronos_models[pair] = predictor
                self.chronos_last_fit[pair] = time.time()

            forecast = predictor.predict(ts_df)

            # robust extraction
            if "0.5" in forecast.columns:
                future = forecast["0.5"].loc[pair].mean()
            else:
                future = forecast.mean(axis=1).loc[pair].mean()

            last_price = df["close"].iloc[-1]
            edge = (future - last_price) / last_price

            if edge > MIN_CHRONOS_EDGE:
                direction = "UP"
            elif edge < -MIN_CHRONOS_EDGE:
                direction = "DOWN"
            else:
                direction = "FLAT"

            confidence = min(abs(edge) * 10, 1.0)

            logger.info(
                f"[CHRONOS] {pair} dir={direction} edge={edge:.2%} conf={confidence:.2f}"
            )

            return direction, confidence

        except Exception as e:
            logger.warning(f"[CHRONOS FAIL] {pair}: {e}")
            return "FLAT", 0.0

    # =========================
    # LLM
    # =========================
    def _llm(self, row: pd.Series, trade: Trade | None, pair: str):

        prompt = f"""
Market snapshot:
Price: {row.close}
ATR: {row.atr}
RSI: {row.rsi}
Orderbook imbalance: {row.ob_imbalance}

Position:
{ "NONE" if trade is None else f"entry={trade.open_rate}, pnl={trade.calc_profit_ratio(row.close)}" }

Return JSON only:
{{"decision":"BUY|SELL|HOLD","confidence":0-1,"reason":"short"}}
"""

        try:
            r = requests.post(
                f"{OLLAMA_URL}/api/generate",
                json={
                    "model": OLLAMA_MODEL,
                    "prompt": prompt,
                    "stream": False,
                    "format": "json"
                },
                timeout=OLLAMA_TIMEOUT
            )
            data = json.loads(r.json()["response"])

            logger.info(
                f"[LLM] {pair} → {data['decision']} conf={data['confidence']} | {data.get('reason','')}"
            )

            return data["decision"], float(data["confidence"])

        except Exception as e:
            logger.warning(f"[LLM FAIL] {pair}: {e}")
            return "HOLD", 0.0

    # =========================
    # ENTRY
    # =========================
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["enter_long"] = 0
        pair = metadata["pair"]
        row = dataframe.iloc[-1]

        chronos_dir, chronos_conf = self._chronos_predict(pair, dataframe)
        llm_dec, llm_conf = self._llm(row, None, pair)

        ensemble = 0.55 * llm_conf + 0.45 * chronos_conf

        logger.info(
            f"[ENSEMBLE ENTRY] {pair} LLM={llm_dec}({llm_conf:.2f}) "
            f"Chronos={chronos_dir}({chronos_conf:.2f}) → {ensemble:.2f}"
        )

        if (
            chronos_dir == "UP"
            and llm_dec == "BUY"
            and llm_conf >= LLM_CONF_MIN
            and chronos_conf >= CHRONOS_CONF_MIN
            and ensemble >= ENSEMBLE_THRESHOLD
        ):
            dataframe.loc[dataframe.index[-1], "enter_long"] = 1
            logger.info(f"[ENTRY OK] {pair}")
        else:
            logger.info(f"[ENTRY BLOCKED] {pair}")

        return dataframe

    # =========================
    # EXIT
    # =========================
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["exit_long"] = 0
        pair = metadata["pair"]
        row = dataframe.iloc[-1]

        trade = Trade.get_trades([Trade.pair == pair, Trade.is_open.is_(True)]).first()
        if not trade:
            return dataframe

        profit = trade.calc_profit_ratio(row.close)
        if profit <= -0.10:
            dataframe.loc[dataframe.index[-1], "exit_long"] = 1
            logger.warning(f"[EMERGENCY EXIT] {pair} drawdown={profit:.2%}")
            return dataframe

        chronos_dir, chronos_conf = self._chronos_predict(pair, dataframe)
        llm_dec, llm_conf = self._llm(row, trade, pair)

        ensemble = 0.55 * llm_conf + 0.45 * chronos_conf

        logger.info(
            f"[ENSEMBLE EXIT] {pair} LLM={llm_dec}({llm_conf:.2f}) "
            f"Chronos={chronos_dir}({chronos_conf:.2f}) → {ensemble:.2f}"
        )

        if profit > 0 and chronos_dir == "DOWN" and llm_dec == "SELL" and ensemble >= ENSEMBLE_THRESHOLD:
            dataframe.loc[dataframe.index[-1], "exit_long"] = 1
            logger.info(f"[EXIT OK] {pair}")

        return dataframe
