176 lines
5.8 KiB
Python
176 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""strategy_executor.py — 策略执行调度器(2026-08-13 温区自适应核心)
|
||
|
||
核心(老莫要求):根据当前温区激活策略,调度对应的选股/买卖/重评任务。
|
||
|
||
温区自适应数据流:
|
||
regime_tracker(K=5 平滑温区) + temp_band(温度) + regime_perf(实测)
|
||
→ strategy_router(当前温区激活策略 strategy_weights.json)
|
||
→ strategy_executor(按激活策略调度对应 scanner/监控/重评)
|
||
|
||
调度逻辑:
|
||
- 读 strategy_weights.json 的 active(当前温区激活策略集合)
|
||
- 对每个激活策略,调度其对应的选股扫描器(scanner)
|
||
- 非激活策略的 scanner 不跑(温区休眠)
|
||
|
||
策略→scanner 映射:
|
||
v_weak → mr_scanner.py (choppy/trend_down 超跌确认)
|
||
v_oversold → predictive_oversold_scanner.py(trend_down 预测超跌反弹)
|
||
s2_panic → s2_scanner.py (trend_down 恐慌买超跌)
|
||
v_mr/v_mr_sel → mr_scanner.py (复用 v_weak 扫描器,同家族)
|
||
v_next4/v8.1 → leader_scanner.py (trend_up 龙头识别)
|
||
|
||
用法:cron 每30分钟盘中跑(替代各 scanner 分散 cron)
|
||
python3 strategy_executor.py
|
||
"""
|
||
import sys
|
||
import json
|
||
import subprocess
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
# ── 消息通道统一路由(broadcast/xmpp by delivery) ──
|
||
try:
|
||
from messenger import install_stdio_hook as _msh
|
||
_msh()
|
||
except Exception:
|
||
pass
|
||
|
||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||
sys.path.insert(0, "/home/hmo/MoFin")
|
||
|
||
WEIGHTS_FILE = "/home/hmo/MoFin/data/strategy_weights.json"
|
||
LOG = "/home/hmo/MoFin/gateway/logs/strategy_executor.log"
|
||
|
||
# 策略 → 选股扫描器映射
|
||
STRATEGY_SCANNER = {
|
||
"v_weak": "mr_scanner.py",
|
||
"v_oversold": "predictive_oversold_scanner.py",
|
||
"p_oversold": "predictive_oversold_scanner.py",
|
||
"s2_panic": "s2_scanner.py",
|
||
"v_mr": "mr_scanner.py",
|
||
"v_mr_sel": "mr_scanner.py",
|
||
"v_mr2": "mr_scanner.py",
|
||
"v_mr3": "mr_scanner.py",
|
||
"v_mr4": "mr_scanner.py",
|
||
"v_lurk_v1": "mr_scanner.py",
|
||
"v_lurk_v2": "mr_scanner.py",
|
||
"v_lurk_v3": "mr_scanner.py",
|
||
"v_next": "leader_scanner.py",
|
||
"v_next4": "leader_scanner.py",
|
||
"v8.1": "leader_scanner.py",
|
||
"v_next5": "leader_scanner.py",
|
||
"v_combo": "leader_scanner.py",
|
||
"b_td1_v3": "b_td1_v3_scanner.py",
|
||
"s2_panic_v2": "s2_panic_v2_scanner.py",
|
||
}
|
||
|
||
|
||
def log(msg):
|
||
line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
|
||
print(line, flush=True)
|
||
try:
|
||
Path(LOG).parent.mkdir(parents=True, exist_ok=True)
|
||
with open(LOG, "a", encoding="utf-8") as f:
|
||
f.write(line + "\n")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def load_active():
|
||
"""读当前温区激活策略"""
|
||
try:
|
||
p = Path(WEIGHTS_FILE)
|
||
if p.exists():
|
||
d = json.loads(p.read_text(encoding="utf-8"))
|
||
return d.get("active") or [], d.get("state", "unknown")
|
||
except Exception as e:
|
||
log(f"加载 weights 失败: {e}")
|
||
return [], "unknown"
|
||
|
||
|
||
def is_trading_time():
|
||
"""盘中时段检查(9:30-11:30 / 13:00-15:00 连续竞价)"""
|
||
now = datetime.now()
|
||
t = now.strftime("%H:%M")
|
||
wd = now.weekday()
|
||
if wd >= 5:
|
||
return False
|
||
return ("09:30" <= t <= "11:30") or ("13:00" <= t <= "15:00")
|
||
|
||
|
||
def run_scanner(scanner_name):
|
||
"""执行一个扫描器(带超时防挂 + returncode 检查 + 完整 stderr 落盘)
|
||
2026-08-17 知微提单 t_b27ad65f 辅修:
|
||
1. returncode 检查——scanner 崩溃(exit!=0)记 ✗,不再误报「✓完成」
|
||
2. 超时 600→480s(每扫描器预算,避免挤占 cron harness 600s 总预算)
|
||
3. 完整 stderr 落盘(截断 200→1000 字符,便于追根因)
|
||
"""
|
||
script = _SCRIPT_DIR / scanner_name
|
||
if not script.exists():
|
||
log(f" ⚠️ {scanner_name} 不存在,跳过")
|
||
return False
|
||
log(f" ▶ 执行 {scanner_name}")
|
||
try:
|
||
r = subprocess.run(
|
||
[sys.executable, str(script)],
|
||
capture_output=True, text=True, timeout=480,
|
||
cwd=str(_SCRIPT_DIR),
|
||
)
|
||
out = (r.stdout or "").strip()[-500:] # 尾部(含完成/命中汇总)
|
||
err = (r.stderr or "").strip()[-1000:] # 完整 stderr(截断1000)
|
||
if r.returncode == 0:
|
||
log(f" ✓ {scanner_name} 完成(rc=0): {out}")
|
||
if err:
|
||
log(f" ⚠️ {scanner_name} stderr(rc=0): {err}")
|
||
return True
|
||
else:
|
||
log(f" ✗ {scanner_name} 崩溃(rc={r.returncode}): {out}")
|
||
log(f" ✗ {scanner_name} stderr: {err}")
|
||
return False
|
||
except subprocess.TimeoutExpired as e:
|
||
# 超时:子进程可能孤儿化,尝试终止进程组
|
||
try:
|
||
if e.stdout or e.stderr:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
log(f" ⏱ {scanner_name} 超时(480s)跳过——检查孤儿进程")
|
||
return False
|
||
except Exception as e:
|
||
log(f" ✗ {scanner_name} 异常: {e}")
|
||
return False
|
||
|
||
|
||
def main():
|
||
if not is_trading_time():
|
||
print(f"[SKIP] {datetime.now().strftime('%H:%M')} 非连续竞价时段", flush=True)
|
||
return 0
|
||
|
||
active, regime = load_active()
|
||
log(f"温区: {regime} | 激活策略: {active}")
|
||
|
||
# 去重:同一 scanner 只跑一次(多策略映射同一扫描器)
|
||
scanners = set()
|
||
for strat in active:
|
||
sc = STRATEGY_SCANNER.get(strat)
|
||
if sc:
|
||
scanners.add(sc)
|
||
log(f"调度扫描器: {sorted(scanners)}")
|
||
|
||
if not scanners:
|
||
log(" 无激活策略的扫描器(当前温区无对应选股任务)")
|
||
return 0
|
||
|
||
for sc in sorted(scanners):
|
||
run_scanner(sc)
|
||
|
||
log("调度完成")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|