""" V5.3 策略适配器(SafeLoader) 所有策略必须继承此适配器,统一接口规范。 """ from typing import Dict, Optional, Tuple from datetime import datetime from pandas import DataFrame from freqtrade.strategy import IStrategy from freqtrade.persistence import Trade import sys import os from pathlib import Path # 添加 guardian 模块路径 # 获取 user_data 目录(策略文件所在目录的父目录) user_data_dir = Path(__file__).parent.parent guardian_path = user_data_dir / "guardian" # 确保 guardian 目录在 Python 路径中 if str(user_data_dir) not in sys.path: sys.path.insert(0, str(user_data_dir)) # 尝试导入 guardian 模块 try: from guardian.allocator import Bucket from guardian.regime_fsm import Regime except ImportError: # 如果导入失败,尝试添加绝对路径 abs_user_data = user_data_dir.resolve() if str(abs_user_data) not in sys.path: sys.path.insert(0, str(abs_user_data)) from guardian.allocator import Bucket from guardian.regime_fsm import Regime class V5_3_Adapter(IStrategy): """ V5.3 策略适配器基类 职责: 1. 统一策略接口(信号 + SL) 2. Regime Whitelist 检查 3. 与 Guardian 系统集成 4. Backtest 模式支持 """ INTERFACE_VERSION = 3 # === 必须由子类声明的属性 === # 策略桶分类 bucket: Bucket = Bucket.TREND # 允许运行的 Regime 列表 allowed_regimes: list = [Regime.SIDEWAYS] # 默认仅允许震荡市 # 单次请求风险(R) r_request: float = 0.25 # 默认 0.25R # 策略名称(用于日志) strategy_name: str = "Unknown" # === 可选配置 === # 最小置信度(0.0-1.0) min_confidence: float = 0.5 def __init__(self, config: dict) -> None: super().__init__(config) # Guardian 系统引用(由外部注入) self.guardian = None # Backtest 模式标志 self.is_backtest = config.get("runmode") in ["backtest", "hyperopt"] # 启动检查 if not self.is_backtest: self._startup_check() def _startup_check(self): """启动时检查配置""" assert self.bucket in Bucket, f"策略 {self.strategy_name} 必须声明有效的 bucket" assert isinstance(self.allowed_regimes, list) and len(self.allowed_regimes) > 0, \ f"策略 {self.strategy_name} 必须声明 allowed_regimes" assert 0 < self.r_request <= 0.5, \ f"策略 {self.strategy_name} 的 r_request 必须在 (0, 0.5] 范围内" def set_guardian(self, guardian): """设置 Guardian 系统引用(由外部调用)""" self.guardian = guardian def _check_regime_allowed(self, current_regime: Optional[Regime]) -> bool: """检查当前 Regime 是否允许运行""" if current_regime is None: return False return current_regime in self.allowed_regimes def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ 子类必须实现此方法,计算指标 """ raise NotImplementedError("子类必须实现 populate_indicators") def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ 子类必须实现此方法,输出信号 输出格式: - enter_long / enter_short: 1 或 0 - enter_tag: "StrategyName|RegimeAtEntry" - proposed_sl_price: 建议的止损价格(可选,用于计算 SL%) - confidence: 0.0-1.0(可选) """ raise NotImplementedError("子类必须实现 populate_entry_trend") def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs, ) -> float: """ 自定义仓位大小(与 Guardian 集成) 注意:在 Backtest 模式下,使用简化逻辑 """ if self.is_backtest: # Backtest 模式:使用默认逻辑或简化版本 return proposed_stake if self.guardian is None: # Guardian 未初始化,使用默认 return proposed_stake # 获取策略信号中的 SL 信息 dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) try: df = dataframe.loc[dataframe["date"] <= current_time] last = df.iloc[-1] if not df.empty else dataframe.iloc[-1] except Exception: last = dataframe.iloc[-1] # 提取 proposed_sl_price(如果存在) proposed_sl_price = last.get("proposed_sl_price") if proposed_sl_price is None or proposed_sl_price == 0: # 使用默认 SL(策略的 stoploss) strategy_sl_pct = abs(self.stoploss) if self.stoploss < 0 else 0.02 else: # 计算 SL% if side == "long": strategy_sl_pct = abs((proposed_sl_price - current_rate) / current_rate) else: # short strategy_sl_pct = abs((current_rate - proposed_sl_price) / current_rate) # 提取 confidence confidence = last.get("confidence", 1.0) if confidence is None: confidence = 1.0 # 调用 Guardian 检查 direction = "LONG" if side == "long" else "SHORT" result = self.guardian.check_trade_request( pair=pair, direction=direction, strategy_name=self.strategy_name, bucket=self.bucket, r_request=self.r_request, strategy_sl_pct=strategy_sl_pct, confidence=confidence, ) if not result["allowed"]: # 不允许开仓,返回 0 # 记录 reason_code(可通过日志系统) return 0.0 # 使用 Guardian 计算的仓位大小 position_value = result["position_value"] # 转换为 freqtrade 的 stake_amount(考虑杠杆) stake_amount = position_value / leverage if leverage > 0 else position_value return min(stake_amount, max_stake) def adjust_trade_position( self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs, ) -> Optional[float]: """ 禁止策略自行加仓(由 Guardian 统一管理) """ 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], side: str, **kwargs, ) -> bool: """ 确认开仓(记录到 Guardian) """ if not self.is_backtest and self.guardian: # 获取实际分配的仓位信息(需要从 custom_stake_amount 的结果中获取) # 简化:这里只做确认,实际记录在 custom_stake_amount 中完成 pass 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: datetime, **kwargs, ) -> bool: """ 确认平仓(记录到 Guardian) """ if not self.is_backtest and self.guardian: # 计算盈亏百分比 profit_pct = trade.calc_profit_ratio(rate) # 记录到 Guardian self.guardian.record_trade_closed( pair=pair, bucket=self.bucket, profit_pct=profit_pct, ) return True