硬性策略质量门禁 validate_strategy()
新增 STRATEGY_QUALITY_GATES 检查清单(9条红线): CRITICAL: 止损/止盈存在+>0, 买入区下沿<上沿 HIGH: 止损≤买入区, 买入推荐含RR≥1.5, 港股标currency=HKD MEDIUM: signal短词, tech_snapshot含技术位 enforce_strategy_quality() 插在写入链的两处: 1. reassess_with_context() return前 → 单只重评必过 2. regenerate_all() for d in decisions: 写DB前 → 批量重评必过 不过的:status=review_needed, signal降级→信号不充分 不会写进DB/JSON,除非修复了CRITICAL问题
This commit is contained in:
+200
-2
@@ -20,6 +20,174 @@ import multi_timeframe as mtf
|
||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
||||
from strategy_tree import detect_scenario
|
||||
|
||||
# ─── 策略准入门禁 — 硬性质量红线 ───────────────────────────────
|
||||
# 每一条策略写入前必须过此门禁。不过的不得写入DB/JSON,
|
||||
# 必须触发重评修复。代码层面硬拦截,不依赖prompt或文档。
|
||||
#
|
||||
# 规则列表 + 严重程度 + 修复建议
|
||||
STRATEGY_QUALITY_GATES = [
|
||||
{
|
||||
"id": "GATE_LOSS_EXISTS",
|
||||
"desc": "止损必须存在且>0",
|
||||
"check": lambda d: (d.get("stop_loss") or 0) > 0,
|
||||
"severity": "CRITICAL",
|
||||
"fix": "调用 technical_analysis 计算支撑位设置止损"
|
||||
},
|
||||
{
|
||||
"id": "GATE_PROFIT_EXISTS",
|
||||
"desc": "止盈必须存在且>0(纯自选股可放宽)",
|
||||
"check": lambda d: (d.get("take_profit") or 0) > 0,
|
||||
"severity": "CRITICAL",
|
||||
"fix": "调用 technical_analysis 计算阻力位设置止盈目标"
|
||||
},
|
||||
{
|
||||
"id": "GATE_SL_GTE_LOW",
|
||||
"desc": "止损必须 ≤ 买入区下沿",
|
||||
"check": lambda d: (d.get("stop_loss") or 0) <= (d.get("entry_low") or 99999),
|
||||
"severity": "HIGH",
|
||||
"fix": "止损不能高于买入区,调整止损至买入区以下"
|
||||
},
|
||||
{
|
||||
"id": "GATE_ENTRY_RANGE",
|
||||
"desc": "买入区下沿 < 上沿",
|
||||
"check": lambda d: (d.get("entry_low") or 0) < (d.get("entry_high") or 0),
|
||||
"severity": "CRITICAL",
|
||||
"fix": "entry_low=现价×0.95, entry_high=现价×1.05 取近似区间"
|
||||
},
|
||||
{
|
||||
"id": "GATE_RR_COMPUTED",
|
||||
"desc": "买入推荐必须含RR",
|
||||
"check": lambda d: not ("买入" in (d.get("timing_signal") or "") or "加仓" in (d.get("timing_signal") or "")) or (d.get("rr_ratio") or 0) > 0,
|
||||
"severity": "HIGH",
|
||||
"fix": "RR = (止盈-现价)/(现价-止损),数据齐全后自动算"
|
||||
},
|
||||
{
|
||||
"id": "GATE_RR_MINIMUM",
|
||||
"desc": "买入推荐RR≥1.5(非买入信号跳过)",
|
||||
"check": lambda d: not ("买入" in (d.get("timing_signal") or "") or "加仓" in (d.get("timing_signal") or "")) or (d.get("rr_ratio") or 0) >= 1.5,
|
||||
"severity": "HIGH",
|
||||
"fix": "RR不足→signal降级为'信号不充分',不进推荐区"
|
||||
},
|
||||
{
|
||||
"id": "GATE_SIGNAL_SHORT",
|
||||
"desc": "timing_signal 必须是短词(2-4字)",
|
||||
"check": lambda d: len((d.get("timing_signal") or "").strip().split()) <= 4 and (d.get("timing_signal") or "") not in ("neutral", ""),
|
||||
"severity": "MEDIUM",
|
||||
"fix": "使用短词:买入/加仓/观望/持有/关注/信号不充分"
|
||||
},
|
||||
{
|
||||
"id": "GATE_TECH_SNAPSHOT",
|
||||
"desc": "tech_snapshot 必须包含技术位数值",
|
||||
"check": lambda d: bool(d.get("tech_snapshot")) and any(c in d["tech_snapshot"] for c in "支撑阻力压强"),
|
||||
"severity": "MEDIUM",
|
||||
"fix": "tech_snapshot 包含强撑/弱撑/弱压/强压至少3个数值"
|
||||
},
|
||||
{
|
||||
"id": "GATE_CURRENCY_SET",
|
||||
"desc": "港股必须标 currency=HKD",
|
||||
"check": lambda d: not (len(str(d.get("code",""))) == 5 and str(d.get("code",""))[0] in "01") or d.get("currency") == "HKD",
|
||||
"severity": "HIGH",
|
||||
"fix": "设置 d['currency']='HKD'"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _hk_stock(code):
|
||||
return bool(len(str(code)) == 5 and str(code)[0] in ('0','1'))
|
||||
|
||||
def _is_buy_signal_str(signal):
|
||||
"""买入/加仓/建仓类信号"""
|
||||
if not signal:
|
||||
return False
|
||||
return any(kw in signal for kw in ["买入", "加仓", "建仓"])
|
||||
|
||||
# _is_buy_signal is defined later in this file (~line 1240)
|
||||
|
||||
def validate_strategy(d, debug=True):
|
||||
"""策略评审:硬性门禁检查
|
||||
|
||||
返回 (passed: bool, failures: list)
|
||||
任一 CRITICAL 失败 → 拒绝写入,标记 TODO 触发重评
|
||||
任一 HIGH 失败 → 标记 quality_check=failed,写入但不出现在推荐区
|
||||
MEDIUM 失败 → 记录但不拦截
|
||||
"""
|
||||
failures = []
|
||||
for gate in STRATEGY_QUALITY_GATES:
|
||||
try:
|
||||
ok = gate["check"](d)
|
||||
except Exception as e:
|
||||
ok = False
|
||||
if debug:
|
||||
print(f" [VALIDATE] {gate['id']} 检查异常: {e}", flush=True)
|
||||
if not ok:
|
||||
failures.append(gate)
|
||||
if debug:
|
||||
print(f" [VALIDATE] ✗ {gate['id']} ({gate['severity']}): {gate['desc']}", flush=True)
|
||||
|
||||
passed = all(f["severity"] != "CRITICAL" for f in failures)
|
||||
|
||||
if debug:
|
||||
criticals = [f for f in failures if f["severity"] == "CRITICAL"]
|
||||
highs = [f for f in failures if f["severity"] == "HIGH"]
|
||||
if passed:
|
||||
print(f" [VALIDATE] ✅ 通过 ({len(failures)}条警告)" if failures else " [VALIDATE] ✅ 全通过", flush=True)
|
||||
else:
|
||||
print(f" [VALIDATE] ❌ {len(criticals)}条CRITICAL未通过 → 拒绝写入", flush=True)
|
||||
|
||||
return passed, failures
|
||||
|
||||
|
||||
def enforce_strategy_quality(code, name, result):
|
||||
"""策略写入前的强制质量门禁
|
||||
|
||||
对 result 执行 validate_strategy,不通过则:
|
||||
- 设置 quality_check: failed + 失败原因列表
|
||||
- 自动在 changelog 记录
|
||||
- 返回 False,调用方不写入
|
||||
- 返回 True 但 result 被修改(标记了 failed,未被写入)
|
||||
"""
|
||||
passed, failures = validate_strategy(result)
|
||||
|
||||
if not passed:
|
||||
critical_issues = [f["id"] for f in failures if f["severity"] == "CRITICAL"]
|
||||
high_issues = [f["id"] for f in failures if f["severity"] == "HIGH"]
|
||||
|
||||
# 标记质量失败
|
||||
result["quality_check"] = "failed"
|
||||
result["quality_issues"] = {
|
||||
"critical": critical_issues,
|
||||
"high": high_issues,
|
||||
"all": [f["id"] for f in failures],
|
||||
}
|
||||
result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 记录 changelog
|
||||
cl = result.setdefault("changelog", [])
|
||||
cl.append({
|
||||
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
"event": f"质量门禁拒绝 (failed: {critical_issues})",
|
||||
})
|
||||
|
||||
# 信号降级
|
||||
result["status"] = "review_needed"
|
||||
result["timing_signal"] = "信号不充分"
|
||||
|
||||
print(f" 🚫 {name}({code}) 质量门禁未通过 ({critical_issues}) → 已标记 review_needed", flush=True)
|
||||
return False
|
||||
|
||||
# 如果有 HIGH 级别失败 → 标记但不拦截写入
|
||||
high_fails = [f for f in failures if f["severity"] == "HIGH"]
|
||||
if high_fails:
|
||||
result["quality_check"] = "warning"
|
||||
result["quality_issues"] = {"high": [f["id"] for f in high_fails]}
|
||||
result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
print(f" ⚠️ {name}({code}) 有{len(high_fails)}条HIGH警告,标记warning但已写入", flush=True)
|
||||
else:
|
||||
result["quality_check"] = "passed"
|
||||
result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_hk_stock(code):
|
||||
"""判断是否港股(港股代码5位,A股6位带前导零)"""
|
||||
@@ -427,7 +595,13 @@ def batch_fetch_prices(codes):
|
||||
"SELECT price, change_pct FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,)
|
||||
).fetchone()
|
||||
if row and row['price']:
|
||||
all_results[raw_code] = (row['price'], 0, row['change_pct'] or 0)
|
||||
all_results[raw_code] = {
|
||||
"price": row['price'],
|
||||
"close": row['price'], # 用现价近似昨收,仅用于sentiment计算
|
||||
"high": row['price'],
|
||||
"low": row['price'],
|
||||
"code": raw_code,
|
||||
}
|
||||
db.close()
|
||||
if all_results:
|
||||
return all_results
|
||||
@@ -1602,6 +1776,9 @@ def reassess_with_context(code, name, price, cost, shares, current_action,
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 策略质量门禁 ──
|
||||
enforce_strategy_quality(code, name, result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -2156,6 +2333,23 @@ def regenerate_all(stdout=True):
|
||||
if added_to_wl and stdout:
|
||||
print(f" 清仓→自选自动加入: {', '.join(added_to_wl)}")
|
||||
|
||||
# 重新计算 portfolio 汇总(保留已存在的 cash,用最新价格算市值)
|
||||
try:
|
||||
total_mv = 0.0
|
||||
for h in existing_pf.get('holdings', []):
|
||||
p = h.get('price') or 0
|
||||
s = h.get('shares') or 0
|
||||
total_mv += p * s
|
||||
if p and s and total_mv > 0:
|
||||
h['market_value'] = round(p * s, 2)
|
||||
old_cash = existing_pf.get('cash') or 80476 # fallback 6/23 backup
|
||||
existing_pf['cash'] = old_cash
|
||||
existing_pf['total_mv'] = round(total_mv, 2)
|
||||
existing_pf['total_assets'] = round(total_mv + old_cash, 2)
|
||||
existing_pf['position_pct'] = round(total_mv / (total_mv + old_cash) * 100, 2) if (total_mv + old_cash) > 0 else 0
|
||||
except Exception as e:
|
||||
print(f" [汇总计算失败] {e}", flush=True)
|
||||
|
||||
# DB 写入(替代 JSON dump — 强制币种约束)
|
||||
try:
|
||||
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_watchlist_stock, write_holding_strategy
|
||||
@@ -2166,7 +2360,11 @@ def regenerate_all(stdout=True):
|
||||
s.setdefault('currency', 'CNY')
|
||||
write_watchlist_stock(conn, s)
|
||||
for d in decisions:
|
||||
write_holding_strategy(conn, d.get('code', ''), d.get('name', ''), d)
|
||||
# ── 策略质量门禁 ──
|
||||
code = d.get('code', '')
|
||||
name = d.get('name', '')
|
||||
enforce_strategy_quality(code, name, d)
|
||||
write_holding_strategy(conn, code, name, d)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f" [DB写入失败] {e}", flush=True)
|
||||
|
||||
Reference in New Issue
Block a user