""" 影子模式半自动化:读取 config/current_suggestions.json,在开仓前应用黑名单/差时段/杠杆倍数建议。 与 aggregate_trade_stats 的 STATS_* 互补:本模块面向 daily_report / 影子分析产出的 JSON。 """ from __future__ import annotations import json import logging import os import time from pathlib import Path from typing import Any, Dict, List, Optional, Tuple logger = logging.getLogger(__name__) # 项目根目录(trading_system/ 的上一级) _PROJECT_ROOT = Path(__file__).resolve().parent.parent def _project_root() -> Path: return _PROJECT_ROOT def _safe_read_json(path: Path) -> Optional[dict]: try: if not path.is_file(): return None with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: logger.debug(f"读取 JSON 失败 {path}: {e}") return None _suggestions_cache: Tuple[float, Optional[dict]] = (0.0, None) _tracking_cache: Tuple[float, Optional[dict]] = (0.0, None) _CACHE_TTL_SEC = 45.0 def _get_suggestions(trading_config: dict) -> dict: global _suggestions_cache path_str = trading_config.get("SHADOW_MODE_SUGGESTIONS_PATH") or "config/current_suggestions.json" path = Path(path_str) if not path.is_absolute(): path = _project_root() / path now = time.time() if _suggestions_cache[1] is not None and now - _suggestions_cache[0] < _CACHE_TTL_SEC: return _suggestions_cache[1] data = _safe_read_json(path) if data is None: data = { "blacklist": [], "increase_position": [], "decrease_position": [], "worst_hours": [], } _suggestions_cache = (now, data) return data def _get_tracking_confidence(trading_config: dict) -> float: global _tracking_cache path_str = trading_config.get("SHADOW_MODE_TRACKING_PATH") or "config/shadow_mode_tracking.json" path = Path(path_str) if not path.is_absolute(): path = _project_root() / path now = time.time() if _tracking_cache[1] is not None and now - _tracking_cache[0] < _CACHE_TTL_SEC: data = _tracking_cache[1] else: data = _safe_read_json(path) or {} _tracking_cache = (now, data) # 兼容多种字段名 for key in ("accuracy", "rolling_accuracy", "confidence", "min_confidence"): v = data.get(key) if v is not None: try: return float(v) except (TypeError, ValueError): pass return 1.0 def _norm_symbol(s: str) -> str: return (s or "").strip().upper() def _find_blacklist_entry(suggestions: dict, symbol: str) -> Optional[dict]: sym = _norm_symbol(symbol) for item in suggestions.get("blacklist") or []: if not isinstance(item, dict): continue if _norm_symbol(str(item.get("symbol", ""))) == sym: return item return None def _in_worst_hours(suggestions: dict, hour_bj: int) -> Optional[dict]: for item in suggestions.get("worst_hours") or []: if not isinstance(item, dict): continue h = item.get("hour") try: hi = int(h) except (TypeError, ValueError): continue if hi == int(hour_bj): return item return None def _position_list_has(suggestions: dict, key: str, symbol: str) -> Optional[dict]: sym = _norm_symbol(symbol) for item in suggestions.get(key) or []: if not isinstance(item, dict): continue if _norm_symbol(str(item.get("symbol", ""))) == sym: return item return None def evaluate_shadow_mode( symbol: str, hour_bj: int, trading_config: dict, ) -> Dict[str, Any]: """ 评估影子模式:是否允许自动开仓、杠杆乘子、写入 entry_context 的 shadow_mode_applied。 当 SHADOW_MODE_AUTO_APPLY=False 或置信度不足时:不拦截、不调整杠杆,但标记 skipped。 """ auto = bool(trading_config.get("SHADOW_MODE_AUTO_APPLY", False)) min_conf = float(trading_config.get("SHADOW_MODE_MIN_CONFIDENCE", 0.7) or 0.0) min_conf = max(0.0, min(1.0, min_conf)) inc_mul = float(trading_config.get("SHADOW_MODE_INCREASE_LEVERAGE_MULT", 1.5) or 1.5) dec_mul = float(trading_config.get("SHADOW_MODE_DECREASE_LEVERAGE_MULT", 0.5) or 0.5) inc_mul = max(0.1, min(inc_mul, 3.0)) dec_mul = max(0.1, min(dec_mul, 2.0)) applied = { "blacklist_check": "skipped", "hour_check": "skipped", "position_adjustment": "none", "suggestion_source": "current_suggestions.json", } if not auto: return { "allowed": True, "skip_reason": None, "leverage_multiplier": 1.0, "shadow_mode_applied": {**applied, "reason": "SHADOW_MODE_AUTO_APPLY=false"}, } conf = _get_tracking_confidence(trading_config) if conf < min_conf: logger.info( f"影子模式:跟踪置信度 {conf:.2f} < 最小要求 {min_conf:.2f},本笔不自动应用建议" ) return { "allowed": True, "skip_reason": None, "leverage_multiplier": 1.0, "shadow_mode_applied": { **applied, "reason": f"confidence {conf:.2f} < {min_conf:.2f}", }, } suggestions = _get_suggestions(trading_config) # 黑名单 bl = _find_blacklist_entry(suggestions, symbol) if bl is not None: applied["blacklist_check"] = "blocked" reason = bl.get("reason") or "shadow blacklist" return { "allowed": False, "skip_reason": reason, "leverage_multiplier": 1.0, "shadow_mode_applied": {**applied, "blacklist_detail": bl}, } applied["blacklist_check"] = "passed" # 差时段 wh = _in_worst_hours(suggestions, hour_bj) if wh is not None: applied["hour_check"] = "blocked" avg_pnl = wh.get("avg_pnl", wh.get("avgPnl", "")) return { "allowed": False, "skip_reason": f"worst_hour_{hour_bj}", "leverage_multiplier": 1.0, "shadow_mode_applied": {**applied, "worst_hour_detail": wh}, "log_avg_pnl": avg_pnl, } applied["hour_check"] = "passed" # 杠杆:减仓优先于加仓(同时命中时更保守) mult = 1.0 dec_item = _position_list_has(suggestions, "decrease_position", symbol) inc_item = _position_list_has(suggestions, "increase_position", symbol) if dec_item is not None: mult *= dec_mul applied["position_adjustment"] = "decrease" applied["decrease_detail"] = dec_item elif inc_item is not None: mult *= inc_mul applied["position_adjustment"] = "increase" applied["increase_detail"] = inc_item return { "allowed": True, "skip_reason": None, "leverage_multiplier": mult, "shadow_mode_applied": applied, } def invalidate_cache() -> None: """配置热更新时可调用(可选)。""" global _suggestions_cache, _tracking_cache _suggestions_cache = (0.0, None) _tracking_cache = (0.0, None)