Files
MoFin/deploy/profile-scripts/strategy_review.py
T

304 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""strategy_review.py — 三层策略复盘 (no_agent)
每层独立评估:
1. 信号层 — 买入/卖出/持有的timing对不对?
2. 执行层 — 止损/止盈设得合理吗?
3. 综合层 — 这波操作整体赚钱了吗?
用法:
python3 scripts/strategy_review.py
"""
import json, sqlite3, sys, time
from pathlib import Path
from datetime import datetime
from collections import Counter
from mo_data import read_portfolio, read_decisions, read_watchlist, get_price, get_prices_batch
# ── 消息通道统一路由(broadcast/xmpp by delivery) ──
try:
from messenger import install_stdio_hook as _msh
_msh()
except Exception:
pass
BASE = Path("/home/hmo/MoFin")
DATA = BASE / "data"
DB_PATH = DATA / "mofin.db"
# 失败模式定义(执行层)
EXEC_FAILURES = {
"stop_too_tight": {"label": "止损过紧", "fix": "放宽止损到强支撑×0.95,给价格波动留空间"},
"tp_too_close": {"label": "止盈过近", "fix": "止盈放到更高阻力位,让利润奔跑"},
"stop_too_loose": {"label": "止损过宽", "fix": "收紧止损,少亏当赢"},
"tp_too_far": {"label": "止盈过远", "fix": "止盈靠近合理阻力位,提高兑现概率"},
}
# 失败模式定义(信号层)
SIGNAL_FAILURES = {
"wrong_direction": {"label": "方向看反", "fix": "检查多周期趋势判断逻辑"},
"entry_too_early": {"label": "入场过早", "fix": "等缩量确认支撑再入,不追回调"},
"bad_signal": {"label": "信号误判", "fix": "修正timing_signal合成权重"},
"regime_mismatch": {"label": "情景错配", "fix": "加入市场情景过滤条件"},
}
def fetch_price(code):
# DB 优先
try: from mofin_db import get_price_from_db; p, _ = get_price_from_db(code); return p if p else 0
except: pass
# Fallback: mo_data.get_price
try:
p, _ = get_price(code)
return p if p else 0
except:
return 0
def evaluate_strategy(s, price):
"""三层评估单条策略,返回 (signal_verdict, exec_verdict, overall_verdict, detail)"""
code = s.get("code", "")
name = s.get("name", "")
sl = s.get("stop_loss", 0) or 0
tp = s.get("take_profit", 0) or 0
entry_low = s.get("entry_low", 0) or 0
entry_high = s.get("entry_high", 0) or 0
cost = s.get("cost", 0) or s.get("avg_price", 0) or 0
signal = (s.get("timing_signal", "") or s.get("current", "") or "").lower()
created = s.get("created_at", "") or s.get("timestamp", "")
s_type = s.get("type", "") # 持仓策略/自选策略
if not created or not price:
return "skip", "skip", "skip", "数据不足", ""
# 计算运行天数
try:
days = (datetime.now() - datetime.fromisoformat(created)).days
except:
days = 0
# ─── 综合层:赚钱了吗? ───
if cost > 0 and s_type == "持仓策略":
profit_pct = (price - cost) / cost * 100
if profit_pct > 5:
overall = "盈利"
elif profit_pct > -5:
overall = "持平"
else:
overall = f"亏损{profit_pct:.0f}%"
elif tp > 0 and price >= tp:
overall = "触止盈"
elif sl > 0 and price <= sl:
overall = "触止损"
else:
overall = "持有中"
# ─── 信号层:timing对不对? ───
is_buy_signal = any(kw in signal for kw in ["买入", "加仓", "追涨", "可买"])
is_sell_signal = any(kw in signal for kw in ["卖出", "减仓", "止损", "离场"])
is_hold_signal = any(kw in signal for kw in ["持有", "观望", "等待", "持股"])
signal_verdict = "待定"
signal_fail = None
if is_buy_signal or is_hold_signal:
if sl > 0 and price <= sl:
# 买入/持有信号下触发止损 → 信号方向可能错了
signal_verdict = "存疑"
signal_fail = "wrong_direction"
elif tp > 0 and price >= tp * 0.95:
signal_verdict = "正确"
elif entry_low > 0 and price < entry_low * 0.85:
signal_verdict = "存疑"
signal_fail = "entry_too_early"
elif days > 30 and tp > 0 and price < entry_low:
signal_verdict = "存疑"
signal_fail = "wrong_direction"
else:
signal_verdict = "待定"
elif is_sell_signal:
if sl > 0 and price <= sl:
signal_verdict = "正确"
elif price > (cost or entry_low or 0) * 1.05:
signal_verdict = "存疑"
signal_fail = "bad_signal"
else:
signal_verdict = "待定"
else:
# 无明确信号
if price > (entry_high or 0):
signal_verdict = "待定(价涨)"
elif sl > 0 and price <= sl * 1.05:
signal_verdict = "待定(近止损)"
else:
signal_verdict = "待定"
# ─── 执行层:止损/止盈设得好不好? ───
exec_verdict = "待定"
exec_fail = None
# 取近期最高/最低价(判断卖飞/洗盘)
recent_high = 0
recent_low = 0
sl_recovery = False
if tp > 0 or sl > 0:
try:
# 读 stock_daily 近60根日K2026-08-26 分层铁律:消费层不直连腾讯API)
raw_code = str(code).split("_")[0]
if raw_code.lower().startswith("hk"):
raw_code = raw_code[2:]
_conn = sqlite3.connect(str(DB_PATH), timeout=5)
_bars = _conn.execute(
"SELECT date, high, low FROM stock_daily WHERE code=? "
"ORDER BY date DESC LIMIT 60", (raw_code,)
).fetchall()
_conn.close()
if _bars:
# 转为升序(旧→新),与 qfqday 时序一致
prices = [(float(b[1]), float(b[2]), b[0]) for b in reversed(_bars) if b[1] and b[2]] # (high, low, date)
recent_high = max(p[0] for p in prices)
recent_low = min(p[1] for p in prices)
# 检查止损触发后的走势:是否后来反弹了?
if sl > 0:
# 找出价格低于SL的K线
below_sl = [p for p in prices if p[1] <= sl]
above_sl_later = [p for p in prices if p[1] > sl * 1.03]
if below_sl and above_sl_later:
# 曾跌破SL,但后来涨回去了 → 洗盘
first_below = min(below_sl, key=lambda x: x[2])
last_above = max(above_sl_later, key=lambda x: x[2])
if last_above[2] > first_below[2]:
sl_recovery = True
except:
pass
if sl > 0 and price <= sl:
if sl_recovery:
exec_verdict = "洗盘(触发后反弹)"
exec_fail = "stop_too_tight"
elif price >= sl * 0.95:
exec_verdict = "临界(差一点触发)"
exec_fail = "stop_too_tight"
else:
exec_verdict = "已触发"
elif tp > 0 and (price >= tp or recent_high >= tp):
# 止盈触发或曾触发过
max_price = max(price, recent_high)
if max_price <= tp * 1.05:
exec_verdict = "已触发"
else:
overshoot = (max_price - tp) / tp * 100
exec_verdict = f"卖飞({overshoot:.0f}%)"
exec_fail = "tp_too_close"
elif days > 45 and tp > 0 and price < entry_low:
exec_verdict = "存疑(久未达标)"
exec_fail = "tp_too_far"
elif sl > 0 and price >= entry_low and price <= entry_high:
exec_verdict = "持有中"
else:
exec_verdict = "待定"
return signal_verdict, exec_verdict, overall, signal_fail, exec_fail
def review():
start = time.time()
decisions = read_decisions()
strategies = decisions.get("decisions", [])
conn = sqlite3.connect(str(DB_PATH))
stats = {"correct": 0, "wrong": 0, "mixed": 0, "pending": 0, "total": 0}
signal_fails = Counter()
exec_fails = Counter()
detail_lines = []
for s in strategies:
if s.get("status") == "closed":
continue
stats["total"] += 1
code = s.get("code", "")
name = s.get("name", "")
price = fetch_price(code)
if not price:
detail_lines.append(f" ⏭️ {name}({code}): 无行情")
stats["pending"] += 1
continue
sv, ev, overall, sf, ef = evaluate_strategy(s, price)
# 综合评级
if overall in ("盈利", "触止盈"):
if sv == "正确" or "存疑" not in sv:
stats["correct"] += 1
else:
stats["mixed"] += 1
elif overall in ("触止损",) and "存疑" in sv:
stats["wrong"] += 1
elif "存疑" in sv or "存疑" in ev:
stats["wrong"] += 1
elif overall in ("持有中", "持平"):
stats["mixed"] += 1
else:
stats["pending"] += 1
# 记录失败模式
if sf:
signal_fails[sf] += 1
if ef:
exec_fails[ef] += 1
# 逐条摘要
tags = []
if overall in ("盈利", "触止盈"):
tags.append("✅")
elif overall == "触止损":
tags.append("❌")
else:
tags.append("⏳")
tags.append(f"信号:{sv}")
tags.append(f"执行:{ev}")
tags.append(f"整体:{overall}")
detail_lines.append(f" {' | '.join(tags)} {name}({code})")
# 写入accuracy_stats
conn.execute(
"INSERT OR REPLACE INTO accuracy_stats (id, total_advice, correct, wrong, partial, pending, "
"accuracy_pct, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?)",
(stats["total"], stats["correct"], stats["wrong"],
stats["mixed"], stats["pending"],
round(stats["correct"] / max(stats["total"] - stats["pending"], 1) * 100, 1),
datetime.now().isoformat()))
conn.commit()
conn.close()
# 输出
total_eval = stats["total"] - stats["pending"]
accuracy = stats["correct"] / max(total_eval, 1) * 100
print(f"策略复盘 | {datetime.now().strftime('%Y-%m-%d')} | {stats['total']}条 | ({time.time()-start:.0f}s)")
print(f" ✅正确 {stats['correct']} | ❌错误 {stats['wrong']} | ⚠️部分 {stats['mixed']} | ⏳待定 {stats['pending']}")
print(f" 综合准确率: {accuracy:.1f}%")
if signal_fails:
print(f"\n📡 信号层失败模式:")
for mode, cnt in signal_fails.most_common():
info = SIGNAL_FAILURES.get(mode, {})
print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}")
if exec_fails:
print(f"\n🎯 执行层失败模式:")
for mode, cnt in exec_fails.most_common():
info = EXEC_FAILURES.get(mode, {})
print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}")
if detail_lines:
print(f"\n逐条复盘:")
for line in detail_lines:
print(line)
if __name__ == "__main__":
review()