feat: 策略复盘闭环 Phase1

- 新增 scripts/strategy_review.py: 遍历所有active策略
  - 腾讯API实时价对比止损/止盈/入场点
  - 分类: correct/wrong/partial/pending
  - 失败模式归因: 止损过紧/入场过早/止盈过远等
  - 写入 accuracy_stats 表(首条真实数据)
- 新增 docs/strategy-review-loop.md: 完整闭环设计文档
- 含失败模式→修复方向映射表

Phase1 结果: 38条策略, 94.7%准确率(19条待定), 1条止损过紧
This commit is contained in:
知微
2026-06-25 19:58:00 +08:00
parent 147d6d0fa2
commit b053103377
35 changed files with 56075 additions and 51863 deletions
+55 -51
View File
@@ -151,7 +151,17 @@ def refresh_data_prices():
updated += 1
changed = True
if changed:
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
elif pf.get('updated_at'):
# 即使价格无变化,每10分钟刷新一次updated_at,防健康检查误报
try:
last_ts = datetime.strptime(pf['updated_at'], '%Y-%m-%d %H:%M')
if (datetime.now() - last_ts).total_seconds() > 600:
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
except:
pass
# 更新watchlist(只在价格变化时写入)
changed = False
@@ -282,36 +292,32 @@ def record_event(code, name, event_type, price, trigger_value, event_label=""):
pass # SQLite 写入失败不影响主流程
def get_trigger_zones(trigger):
"""返回该trigger所有可监控的区间列表,跳过已执行的batch"""
def get_trigger_zones(d):
"""返回该decision所有可监控的区间列表,从顶层字段读取"""
zones = []
for key, label in [
("entry_zone", "加仓区间"),
("batch1_price", "试仓区间"),
("batch2_price", "加仓区间"),
("take_profit_zone", "止盈区间"),
("watch_low", "关注区间"),
("watch_high", "减仓区间"),
("watch_break", "止损区间")
]:
status_key = key.replace("_price", "_status")
if status_key in trigger and trigger[status_key] == "executed":
continue
val = trigger.get(key, "")
if val and "~" in val:
try:
parts = val.split("~")
lo, hi = float(parts[0]), float(parts[1])
zones.append((key, label, lo, hi))
except:
pass
sl = trigger.get("stop_loss", "")
if sl:
is_holding = d.get('shares', 0) > 0
# 买入区间(自选和持仓都监控)
el = d.get("entry_low", 0)
eh = d.get("entry_high", 0)
if el and eh and float(el) > 0 and float(eh) > 0:
try:
sl_price = float(sl) if isinstance(sl, (int, float)) else float(sl)
zones.append(("stop_loss", "止损", 0, sl_price))
zones.append(("entry_zone", "买入区间", float(el), float(eh)))
except:
pass
# 止损+止盈(只有持仓才监控,自选无意义)
if is_holding:
sl = d.get("stop_loss", 0)
if sl and float(sl) > 0:
try:
zones.append(("stop_loss", "止损", 0, float(sl)))
except:
pass
tp = d.get("take_profit", 0)
if tp and float(tp) > 0:
try:
zones.append(("take_profit_zone", "止盈区间", 0, float(tp)))
except:
pass
return zones
@@ -344,7 +350,7 @@ def run_once(round_label=""):
print(f"{label} 无法读取decisions.json", file=sys.stderr)
return
active = [d for d in dec.get("decisions", []) if d.get("status") == "active"]
active = [d for d in dec.get("decisions", []) if d.get("status") in ("active", "updated")]
state = load_state()
outputs = []
state_updated = False
@@ -352,8 +358,7 @@ def run_once(round_label=""):
# 收集所有需要检查的代码
check_codes = set()
for d in active:
trig = d.get("trigger", {})
if trig:
if get_trigger_zones(d):
check_codes.add(d["code"])
# 批量拉取这些股票的价格
@@ -361,18 +366,15 @@ def run_once(round_label=""):
for d in active:
code = d["code"]
trig = d.get("trigger", {})
if not trig:
continue
zones = get_trigger_zones(trig)
zones = get_trigger_zones(d)
if not zones:
continue
price_info = prices.get(code)
if not price_info:
continue
price, _ = price_info
price, _, _ = price_info
if price == 0:
continue
@@ -392,12 +394,12 @@ def run_once(round_label=""):
else:
extra = ""
if "_price" in key:
batch_shares = trig.get(key.replace("_price", "_shares"), "")
action = trig.get(key.replace("_price", "_action"), "")
batch_shares = d.get(key.replace("_price", "_shares"), "")
action = d.get(key.replace("_price", "_action"), "")
if batch_shares:
extra = f" {action}{batch_shares}" if action else f" {batch_shares}"
elif key in ("take_profit_zone",):
act = trig.get("take_profit_action", "")
act = d.get("take_profit_action", "")
if act:
extra = f"{act}"
branch_sfx = _branch_alert_suffix(code, price, d.get('shares',0), d.get('cost',0))
@@ -420,7 +422,7 @@ def run_once(round_label=""):
price_info = prices.get(code)
if not price_info:
continue
price, _ = price_info
price, _, _ = price_info
if price == 0:
continue
@@ -510,19 +512,21 @@ def run_once(round_label=""):
# 简短一行一个触发
for o in outputs:
print(o)
# 直接推到老爸私信(免等待 cron_to_xmpp
try:
body = "\n".join([f"{now_str}"] + outputs)
payload = json.dumps({
"to": "hmo@yoin.fun", "body": body, "type": "chat",
}).encode("utf-8")
req = urllib.request.Request(
"http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=5)
except Exception:
pass
# 推送XMPP(只推关键事件:止损跌破+情景切换,不推买入区进出/重评等操作细节
critical = [o for o in outputs if o.startswith(("⚠️", "🌀"))]
if critical:
try:
body = "\n".join([f"{now_str}"] + critical)
payload = json.dumps({
"to": "hmo@yoin.fun", "body": body, "type": "chat",
}).encode("utf-8")
req = urllib.request.Request(
"http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=5)
except Exception:
pass
# else: SILENT — 无触发,无输出,不推
if state_updated: