影子模式

This commit is contained in:
薇薇安 2026-03-21 09:23:53 +08:00
parent c6103522be
commit 9a0061c06a
11 changed files with 412 additions and 0 deletions

View File

@ -779,6 +779,42 @@ async def get_global_configs(
"category": "strategy",
"description": "做空:相对位置 pos 低于此值则不开仓(默认约拒绝最下 38% 区域)。越大越严。",
},
"SHADOW_MODE_AUTO_APPLY": {
"value": False,
"type": "boolean",
"category": "strategy",
"description": "影子模式半自动:按 config/current_suggestions.json 跳过黑名单/差时段,并对加仓减仓名单调整杠杆。",
},
"SHADOW_MODE_MIN_CONFIDENCE": {
"value": 0.7,
"type": "number",
"category": "strategy",
"description": "影子跟踪置信度下限(读 shadow_mode_tracking.json低于则不应用建议。",
},
"SHADOW_MODE_SUGGESTIONS_PATH": {
"value": "config/current_suggestions.json",
"type": "string",
"category": "strategy",
"description": "优化建议 JSON 路径(相对项目根)。",
},
"SHADOW_MODE_TRACKING_PATH": {
"value": "config/shadow_mode_tracking.json",
"type": "string",
"category": "strategy",
"description": "影子跟踪准确率 JSON 路径。",
},
"SHADOW_MODE_INCREASE_LEVERAGE_MULT": {
"value": 1.5,
"type": "number",
"category": "strategy",
"description": "increase_position 名单杠杆乘数。",
},
"SHADOW_MODE_DECREASE_LEVERAGE_MULT": {
"value": 0.5,
"type": "number",
"category": "strategy",
"description": "decrease_position 名单杠杆乘数。",
},
}
for k, meta in ADDITIONAL_STRATEGY_DEFAULTS.items():
if k not in result:

View File

@ -974,6 +974,14 @@ class ConfigManager:
'ENTRY_PULLBACK_MAX_LONG_IN_RANGE': eff_get('ENTRY_PULLBACK_MAX_LONG_IN_RANGE', 0.62),
'ENTRY_PULLBACK_MIN_SHORT_IN_RANGE': eff_get('ENTRY_PULLBACK_MIN_SHORT_IN_RANGE', 0.38),
# 影子模式半自动化
'SHADOW_MODE_AUTO_APPLY': eff_get('SHADOW_MODE_AUTO_APPLY', False),
'SHADOW_MODE_MIN_CONFIDENCE': eff_get('SHADOW_MODE_MIN_CONFIDENCE', 0.7),
'SHADOW_MODE_SUGGESTIONS_PATH': eff_get('SHADOW_MODE_SUGGESTIONS_PATH', 'config/current_suggestions.json'),
'SHADOW_MODE_TRACKING_PATH': eff_get('SHADOW_MODE_TRACKING_PATH', 'config/shadow_mode_tracking.json'),
'SHADOW_MODE_INCREASE_LEVERAGE_MULT': eff_get('SHADOW_MODE_INCREASE_LEVERAGE_MULT', 1.5),
'SHADOW_MODE_DECREASE_LEVERAGE_MULT': eff_get('SHADOW_MODE_DECREASE_LEVERAGE_MULT', 0.5),
# 当前交易预设(让 trading_system 能知道是哪种模式)
'TRADING_PROFILE': profile,

View File

@ -79,6 +79,18 @@ DEFAULTS_TO_SYNC = [
"description": "做多区间相对位置上限制0~1默认0.62。"},
{"config_key": "ENTRY_PULLBACK_MIN_SHORT_IN_RANGE", "config_value": "0.38", "config_type": "number", "category": "strategy",
"description": "做空区间相对位置下限制0~1默认0.38。"},
{"config_key": "SHADOW_MODE_AUTO_APPLY", "config_value": "false", "config_type": "boolean", "category": "strategy",
"description": "影子模式:按 current_suggestions.json 自动应用黑名单/差时段/杠杆调整。"},
{"config_key": "SHADOW_MODE_MIN_CONFIDENCE", "config_value": "0.7", "config_type": "number", "category": "strategy",
"description": "影子跟踪置信度下限。"},
{"config_key": "SHADOW_MODE_SUGGESTIONS_PATH", "config_value": "config/current_suggestions.json", "config_type": "string", "category": "strategy",
"description": "优化建议 JSON 路径。"},
{"config_key": "SHADOW_MODE_TRACKING_PATH", "config_value": "config/shadow_mode_tracking.json", "config_type": "string", "category": "strategy",
"description": "影子跟踪 JSON 路径。"},
{"config_key": "SHADOW_MODE_INCREASE_LEVERAGE_MULT", "config_value": "1.5", "config_type": "number", "category": "strategy",
"description": "加仓名单杠杆乘数。"},
{"config_key": "SHADOW_MODE_DECREASE_LEVERAGE_MULT", "config_value": "0.5", "config_type": "number", "category": "strategy",
"description": "减仓名单杠杆乘数。"},
]

View File

@ -0,0 +1,6 @@
{
"blacklist": [],
"increase_position": [],
"decrease_position": [],
"worst_hours": []
}

View File

@ -0,0 +1,5 @@
{
"accuracy": 1.0,
"rolling_accuracy": 1.0,
"notes": "由 shadow_mode_analyzer / 人工更新;低于 SHADOW_MODE_MIN_CONFIDENCE 时不自动应用建议"
}

View File

@ -75,6 +75,12 @@ const KEY_LABELS = {
ENTRY_PULLBACK_MIN_BARS: '回撤过滤最少K线数',
ENTRY_PULLBACK_MAX_LONG_IN_RANGE: '做多区间位置上限(0~1)',
ENTRY_PULLBACK_MIN_SHORT_IN_RANGE: '做空区间位置下限(0~1)',
SHADOW_MODE_AUTO_APPLY: '影子模式自动应用建议',
SHADOW_MODE_MIN_CONFIDENCE: '影子模式最小置信度',
SHADOW_MODE_SUGGESTIONS_PATH: '影子建议JSON路径',
SHADOW_MODE_TRACKING_PATH: '影子跟踪准确率路径',
SHADOW_MODE_INCREASE_LEVERAGE_MULT: '影子加仓名单杠杆乘数',
SHADOW_MODE_DECREASE_LEVERAGE_MULT: '影子减仓名单杠杆乘数',
}
//
@ -684,6 +690,12 @@ const GlobalConfig = () => {
ENTRY_PULLBACK_MIN_BARS: { value: 5, type: 'number', category: 'strategy', description: '至少几根 K 才启用回撤过滤。' },
ENTRY_PULLBACK_MAX_LONG_IN_RANGE: { value: 0.62, type: 'number', category: 'strategy', description: '做多区间相对位置上限制0~1默认 0.62。' },
ENTRY_PULLBACK_MIN_SHORT_IN_RANGE: { value: 0.38, type: 'number', category: 'strategy', description: '做空区间相对位置下限制0~1默认 0.38。' },
SHADOW_MODE_AUTO_APPLY: { value: false, type: 'boolean', category: 'strategy', description: '是否按 current_suggestions.json 自动跳过黑名单/差时段并调整杠杆。' },
SHADOW_MODE_MIN_CONFIDENCE: { value: 0.7, type: 'number', category: 'strategy', description: 'shadow_mode_tracking.json 置信度低于此值则不应用。' },
SHADOW_MODE_SUGGESTIONS_PATH: { value: 'config/current_suggestions.json', type: 'string', category: 'strategy', description: '优化建议 JSON相对项目根。' },
SHADOW_MODE_TRACKING_PATH: { value: 'config/shadow_mode_tracking.json', type: 'string', category: 'strategy', description: '跟踪准确率 JSON。' },
SHADOW_MODE_INCREASE_LEVERAGE_MULT: { value: 1.5, type: 'number', category: 'strategy', description: 'increase_position 名单杠杆乘数。' },
SHADOW_MODE_DECREASE_LEVERAGE_MULT: { value: 0.5, type: 'number', category: 'strategy', description: 'decrease_position 名单杠杆乘数。' },
}
const loadConfigs = async () => {

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""
影子模式分析占位入口
实际流程建议
1. `daily_report.py` 或独立分析脚本根据历史成交生成 `config/current_suggestions.json`
2. 更新 `config/shadow_mode_tracking.json` 中的 accuracy / rolling_accuracy
3. 在全局配置中开启 `SHADOW_MODE_AUTO_APPLY=true` 交易进程将自动读取建议
本仓库提供 JSON schema `config/current_suggestions.json`
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SUG = ROOT / "config" / "current_suggestions.json"
TRACK = ROOT / "config" / "shadow_mode_tracking.json"
def main():
print("影子模式分析占位脚本。")
print(f" 建议文件: {SUG}")
print(f" 跟踪文件: {TRACK}")
if SUG.is_file():
with open(SUG, "r", encoding="utf-8") as f:
data = json.load(f)
print(" 当前建议摘要:", {k: len(v) if isinstance(v, list) else v for k, v in data.items()})
else:
print(" (尚未生成 current_suggestions.json")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -198,6 +198,14 @@ DEFAULT_TRADING_CONFIG = {
'NO_OPEN_HOURS_BJ': '',
'ONE_WAY_POSITION_ONLY': True, # 账号固定为单向持仓模式,不传 positionSide不检测对冲模式避免 -4061
# ===== 影子模式半自动化config/current_suggestions.json由 daily_report / 分析脚本生成)=====
'SHADOW_MODE_AUTO_APPLY': False, # True按建议自动跳过黑名单/差时段,并对加仓/减仓名单调整杠杆倍数
'SHADOW_MODE_MIN_CONFIDENCE': 0.7, # 低于此置信度shadow_mode_tracking.json 中 accuracy 等)则不应用建议
'SHADOW_MODE_SUGGESTIONS_PATH': 'config/current_suggestions.json', # 相对项目根目录
'SHADOW_MODE_TRACKING_PATH': 'config/shadow_mode_tracking.json',
'SHADOW_MODE_INCREASE_LEVERAGE_MULT': 1.5, # increase_position 名单:杠杆 × 此值(上限仍受 MAX_LEVERAGE 约束)
'SHADOW_MODE_DECREASE_LEVERAGE_MULT': 0.5, # decrease_position 名单:杠杆 × 此值
'MAX_POSITION_PERCENT': 0.20, # 单笔仓位上限20%(作为风控熔断,实际仓位由固定风险模型决定)
'MAX_TOTAL_POSITION_PERCENT': 0.80, # 总仓位80%(避免满仓,留有余地)
'MIN_POSITION_PERCENT': 0.01, # 最小仓位1%

View File

@ -343,6 +343,7 @@ class PositionManager:
klines: Optional[List] = None,
bollinger: Optional[Dict] = None,
entry_context: Optional[Dict] = None,
shadow_leverage_multiplier: float = 1.0,
) -> Optional[Dict]:
"""
开仓
@ -393,6 +394,30 @@ class PositionManager:
except Exception as e:
logger.debug(f"{symbol} 开仓前持仓数复核异常(继续): {e}")
# 影子模式:对动态杠杆再乘系数(加仓/减仓名单)
smx = float(shadow_leverage_multiplier or 1.0)
if abs(smx - 1.0) > 1e-9:
old_lev = int(leverage)
adj_lev = int(round(leverage * smx))
max_lev = int(config.TRADING_CONFIG.get('MAX_LEVERAGE', 125) or 125)
min_lev = max(1, int(config.TRADING_CONFIG.get('MIN_LEVERAGE', 1) or 1))
adj_lev = max(min_lev, min(adj_lev, max_lev))
pa = "none"
if entry_context and isinstance(entry_context.get("shadow_mode_applied"), dict):
pa = entry_context["shadow_mode_applied"].get("position_adjustment") or "none"
reason_txt = ""
if entry_context and isinstance(entry_context.get("shadow_mode_applied"), dict):
smd = entry_context["shadow_mode_applied"]
if pa == "increase" and smd.get("increase_detail"):
reason_txt = str(smd["increase_detail"].get("reason") or smd["increase_detail"].get("symbol") or "")
elif pa == "decrease" and smd.get("decrease_detail"):
reason_txt = str(smd["decrease_detail"].get("reason") or "")
logger.info(
f"{symbol} [影子模式] 应用动态仓位调整:{smx}x{old_lev}x→{adj_lev}xposition={pa}"
f"{', 建议: ' + reason_txt if reason_txt else ''}"
)
leverage = adj_lev
# 设置杠杆(确保为 int避免动态杠杆传入 float 导致 API/range 报错)
actual_leverage = await self.client.set_leverage(symbol, int(leverage))

View File

@ -0,0 +1,226 @@
"""
影子模式半自动化读取 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)

View File

@ -14,6 +14,7 @@ try:
from .signal_filters import SignalFilterContext, apply_signal_filters
from . import config
from .symbol_policy import resolve_symbol_trading_policy
from .shadow_mode import evaluate_shadow_mode, invalidate_cache as shadow_invalidate_cache
except ImportError:
from binance_client import BinanceClient
from market_scanner import MarketScanner
@ -22,6 +23,7 @@ except ImportError:
from signal_filters import SignalFilterContext, apply_signal_filters
import config
from symbol_policy import resolve_symbol_trading_policy
from shadow_mode import evaluate_shadow_mode, invalidate_cache as shadow_invalidate_cache
logger = logging.getLogger(__name__)
@ -110,6 +112,10 @@ class TradingStrategy:
config._config_manager.reload_from_redis()
config.TRADING_CONFIG = config._get_trading_config()
logger.debug("配置已从Redis重新加载")
try:
shadow_invalidate_cache()
except Exception:
pass
except Exception as e:
logger.warning(f"从Redis重新加载配置失败: {e}")
@ -241,6 +247,26 @@ class TradingStrategy:
except Exception as e:
logger.warning(f"{symbol} 应用信号过滤插件时出错(忽略,回退为原逻辑): {e}")
# 影子模式半自动化:优化黑名单 / 差时段(跳过自动开仓;推荐已生成)
shadow_mode_result = self._check_shadow_mode_filters(symbol)
if not shadow_mode_result.get("allowed", True):
sm_ap = shadow_mode_result.get("shadow_mode_applied") or {}
if sm_ap.get("blacklist_check") == "blocked":
br = shadow_mode_result.get("skip_reason") or ""
logger.info(f"{symbol} 命中优化黑名单,跳过自动开仓(建议:{br}")
elif sm_ap.get("hour_check") == "blocked":
bj_h = timezone(timedelta(hours=8))
hh = datetime.now(bj_h).hour
avg_txt = shadow_mode_result.get("log_avg_pnl", "")
logger.info(
f"{symbol} 命中差时段 ({hh}:00),跳过自动开仓(平均亏损:{avg_txt} USDT"
)
else:
logger.info(
f"{symbol} 影子模式限制,跳过自动开仓:{shadow_mode_result.get('skip_reason', '')}"
)
continue
# 确定交易方向(基于技术指标)
trade_direction = trade_signal['direction']
# 4H 下跌禁止开多(熊市/保守方案:避免逆势抄底)
@ -328,6 +354,9 @@ class TradingStrategy:
entry_context['macd_histogram'] = macd_hist
if symbol_info.get('atr') is not None:
entry_context['atr'] = symbol_info.get('atr')
# 影子模式执行记录(每笔开仓)
sm_applied = dict(shadow_mode_result.get("shadow_mode_applied") or {})
entry_context["shadow_mode_applied"] = sm_applied
# 开仓(使用改进的仓位管理)
position = await self.position_manager.open_position(
@ -343,6 +372,7 @@ class TradingStrategy:
klines=symbol_info.get('klines'), # 传递K线数据用于动态止损
bollinger=symbol_info.get('bollinger'), # 传递布林带数据用于动态止损
entry_context=entry_context,
shadow_leverage_multiplier=float(shadow_mode_result.get("leverage_multiplier") or 1.0),
)
if position:
@ -583,6 +613,15 @@ class TradingStrategy:
return False
return True
def _check_shadow_mode_filters(self, symbol: str) -> Dict:
"""
检查影子模式过滤条件黑名单差时段杠杆乘子 config/current_suggestions.json
返回 evaluate_shadow_mode 的字典
"""
bj = timezone(timedelta(hours=8))
hour_bj = datetime.now(bj).hour
return evaluate_shadow_mode(symbol, hour_bj, config.TRADING_CONFIG)
async def _analyze_trade_signal(self, symbol_info: Dict) -> Dict:
"""