# source: https://raw.githubusercontent.com/owenops61-byte/claude_project/40774debaa0d74569db3dfa5a194c626c93d81c9/freqtrade/strategies/AICommitteeStrategy.py
"""
AI 委员会驱动的合约交易策略
- 15m 周期判定，每根 K 线触发一次分析
- 通过 HTTP 调用 trigger_server (host.docker.internal:8765/crypto-signal)
- 委员会通过技术/宏观/资金/结构/风控 5 位分析师投票，首席策略师汇总
- 只有 signal == long/short 且 consensus_score >= 70 才开单
- 20x 杠杆，强止损
"""
from datetime import datetime, timezone
from typing import Optional
import json
import logging

import pandas as pd
import talib.abstract as ta
import requests

from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative

logger = logging.getLogger(__name__)


# Trigger server 在宿主机 8765 端口，docker 容器通过 host.docker.internal 访问
SIGNAL_API = "http://host.docker.internal:8765/crypto-signal"
SIGNAL_TIMEOUT = 8  # trigger_server 非阻塞，8 秒足够

# 信号缓存（进程内，时间内避免重复请求）
_signal_cache: dict = {}
_cache_ttl_sec = 300  # 5 分钟


def fetch_committee_signal(contract: str, force: bool = False) -> dict:
    """调用 AI 委员会获取信号"""
    import time

    now = time.time()
    cached = _signal_cache.get(contract)
    if not force and cached and (now - cached["ts"]) < _cache_ttl_sec:
        return cached["data"]

    try:
        params = {"pair": contract}
        if force:
            params["force"] = "1"
        r = requests.get(SIGNAL_API, params=params, timeout=SIGNAL_TIMEOUT)
        r.raise_for_status()
        data = r.json()
        _signal_cache[contract] = {"ts": now, "data": data}
        return data
    except Exception as e:
        logger.warning(f"Failed to fetch committee signal for {contract}: {e}")
        return {"ok": False, "error": str(e)}


def freqtrade_pair_to_contract(pair: str) -> str:
    """'BTC/USDT:USDT' -> 'BTC_USDT'"""
    base = pair.split(":")[0]
    return base.replace("/", "_")


class Github_owenops61_byte_claude_project__AICommitteeStrategy__20260413_062419(IStrategy):
    INTERFACE_VERSION = 3

    # ============ 基础配置 ============
    timeframe = "15m"
    can_short = True
    process_only_new_candles = True

    # ============ 风控（分批止盈 + 动态止损） ============

    # ROI 禁用（由 adjust_trade_position 分批管理）
    # 仅保留 48 小时兜底，防止仓位永远不平
    minimal_roi = {
        "0": 999,       # 永不触发（分批止盈接管）
        "2880": 0.02,   # 48 小时兜底：剩余仓位盈利 2% 全平
    }

    # 初始硬止损：-8% 敞口 = -16% 本金
    stoploss = -0.08

    # 关闭全局追踪止损（由 custom_stoploss 动态管理）
    trailing_stop = False

    # 启用自定义止损 + 仓位调整
    use_custom_stoploss = True
    position_adjustment_enable = True

    order_types = {
        "entry": "market",
        "exit": "market",
        "stoploss": "market",
        "stoploss_on_exchange": True,
        "stoploss_on_exchange_interval": 60,
    }

    protections = [
        {
            "method": "StoplossGuard",
            "lookback_period_candles": 48,  # 过去 12 小时
            "trade_limit": 3,              # 3 次止损才触发（放宽）
            "stop_duration_candles": 8,    # 停 2 小时（缩短）
            "only_per_pair": False,
        },
        {
            "method": "CooldownPeriod",
            "stop_duration_candles": 2,    # 每笔后冷却 30 分钟（缩短）
        },
        {
            "method": "MaxDrawdown",
            "lookback_period_candles": 192,  # 48 小时
            "trade_limit": 8,
            "stop_duration_candles": 48,     # 停 12 小时
            "max_allowed_drawdown": 0.20,    # 本金 20% 回撤才停机（放宽）
        },
    ]

    startup_candle_count: int = 60

    # ============ 分批止盈计划 ============
    #
    #  敞口盈利    动作              剩余仓位
    #  ─────────  ────────────────  ────────
    #   +10%      卖出 10%（回手续费）   90%
    #   +30%      卖出 30%（锁利润）     60%
    #   +50%      卖出 30%（大利润）     30%
    #   剩余 30%  追踪止损管理           0%
    #
    PROFIT_TIERS = [
        (0.10, 0.10),   # 盈利 10% → 卖 10%
        (0.30, 0.333),  # 盈利 30% → 卖当前仓位的 1/3（≈原始 30%）
        (0.50, 0.50),   # 盈利 50% → 卖当前仓位的 1/2（≈原始 30%）
    ]

    def adjust_trade_position(
        self,
        trade,
        current_time,
        current_rate,
        current_profit,
        min_stake,
        max_stake,
        current_entry_rate,
        current_exit_rate,
        current_entry_profit,
        current_exit_profit,
        **kwargs,
    ):
        """
        分批止盈：根据当前盈利水平，阶梯式部分平仓。
        返回负数 = 减仓（部分平仓），None = 不操作。
        """
        if current_profit <= 0:
            return None

        exits_done = trade.nr_of_successful_exits

        for tier_idx, (profit_threshold, sell_ratio) in enumerate(self.PROFIT_TIERS):
            if exits_done == tier_idx and current_profit >= profit_threshold:
                sell_amount = trade.stake_amount * sell_ratio
                if sell_amount < min_stake:
                    return None  # 太小无法卖出
                tier_names = ["回本手续费", "锁定利润", "大额止盈"]
                logger.info(
                    f"{trade.pair}: 分批止盈 第{tier_idx+1}阶 "
                    f"({tier_names[tier_idx]}) | "
                    f"盈利 {current_profit*100:.1f}% >= {profit_threshold*100:.0f}% | "
                    f"卖出 {sell_ratio*100:.0f}% 仓位 ({sell_amount:.2f} USDT)"
                )
                return -sell_amount

        return None

    def custom_stoploss(
        self,
        pair,
        trade,
        current_time,
        current_rate,
        current_profit,
        after_fill,
        **kwargs,
    ):
        """
        动态止损：随着分批止盈推进，止损逐步上移保护利润。

        阶段       止损位置            逻辑
        ────────  ────────────────    ─────────────
        未止盈     -8%（初始）          正常止损
        第1阶止盈  保本（-0.5%）        已回本手续费，不亏
        第2阶止盈  +5%               已锁利润，保护底仓
        第3阶止盈  追踪：回撤 8% 平     剩余仓位跑趋势
        """
        exits_done = trade.nr_of_successful_exits

        if exits_done == 0:
            # 还没有任何止盈执行，保持初始 -8%
            return -0.08

        elif exits_done == 1:
            # 已执行第一阶梯（盈利 10% 卖了 10%），止损上移到保本
            return -0.005

        elif exits_done == 2:
            # 已执行第二阶梯（盈利 30% 卖了 30%），止损上移到 +5%
            if current_profit > 0.05:
                return -(current_profit - 0.05)  # 锁住 5% 利润
            return -0.005  # 至少保本

        else:
            # 第三阶梯后（盈利 50% 卖了 30%），剩余 30% 用追踪止损
            if current_profit > 0.15:
                # 高利润区：回撤 8% 平仓（让利润跑）
                return -(current_profit - 0.08)
            elif current_profit > 0.05:
                # 中等利润：回撤到 +5% 平仓
                return -(current_profit - 0.05)
            else:
                # 利润缩水到 5% 以下：保本出
                return -0.005

    # ============ 杠杆 ============
    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: Optional[str],
        side: str,
        **kwargs,
    ) -> float:
        return 20.0

    # ============ 技术指标（作为兜底信号，避免只依赖 AI） ============
    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["ema21"] = ta.EMA(dataframe, timeperiod=21)
        dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        return dataframe

    # ============ 入场信号 ============
    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # 默认不入场
        dataframe["enter_long"] = 0
        dataframe["enter_short"] = 0
        dataframe["enter_tag"] = ""

        pair = metadata["pair"]
        contract = freqtrade_pair_to_contract(pair)

        # 只在最新一根 K 线判断（避免历史回测每根都调 AI）
        last_idx = len(dataframe) - 1
        if last_idx < self.startup_candle_count:
            return dataframe

        # 调用 AI 委员会
        signal_result = fetch_committee_signal(contract)
        if not signal_result.get("ok"):
            logger.info(f"{contract}: committee signal not ok, skip ({signal_result.get('error', '')})")
            return dataframe

        chief = signal_result.get("chief") or {}
        signal = chief.get("signal", "hold")
        score = chief.get("consensus_score", 0)
        try:
            score = int(score)
        except Exception:
            score = 0

        # 门槛：共识评分 >= 70 才开单
        if score < 70:
            logger.info(f"{contract}: score={score} < 70, hold")
            return dataframe

        tag = f"{signal}_score{score}"
        logger.info(
            f"{contract}: AI signal={signal} score={score} | "
            f"entry={chief.get('entry_price')} sl={chief.get('stop_loss_price')} tp={chief.get('take_profit_price')}"
        )

        if signal == "long":
            dataframe.loc[dataframe.index[last_idx], "enter_long"] = 1
            dataframe.loc[dataframe.index[last_idx], "enter_tag"] = tag
        elif signal == "short":
            dataframe.loc[dataframe.index[last_idx], "enter_short"] = 1
            dataframe.loc[dataframe.index[last_idx], "enter_tag"] = tag

        return dataframe

    # ============ 出场信号 ============
    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # 出场主要靠 ROI / stoploss / trailing_stop
        # 这里加一个兜底：RSI 进入极端反向区域
        dataframe["exit_long"] = 0
        dataframe["exit_short"] = 0

        dataframe.loc[dataframe["rsi"] > 88, "exit_long"] = 1
        dataframe.loc[dataframe["rsi"] < 12, "exit_short"] = 1

        return dataframe

    # ============ 自定义入场价（从 AI 信号读取） ============
    def custom_entry_price(
        self,
        pair: str,
        trade: Optional[Trade],
        current_time: datetime,
        proposed_rate: float,
        entry_tag: Optional[str],
        side: str,
        **kwargs,
    ) -> float:
        """从 AI 信号读取建议入场价；失败则用盘口价"""
        contract = freqtrade_pair_to_contract(pair)
        cached = _signal_cache.get(contract)
        if cached:
            chief = (cached["data"].get("chief") or {})
            ep = chief.get("entry_price")
            if ep:
                try:
                    # 限价单偏离不超过 0.3%，避免无法成交
                    ep_f = float(ep)
                    if abs(ep_f - proposed_rate) / proposed_rate < 0.003:
                        return ep_f
                except Exception:
                    pass
        return proposed_rate
