策略质量门禁三段自动修复

enforce_strategy_quality() 重写为3轮重试:
- Round 1: ta.full_analysis 技术位
- Round 2: DB行业 + price%推算
- Round 3: 最低可用策略(强制fallback)
3轮全不过 → review_needed

对应模塑空壳策略问题:
之前只修一轮→不过就直接review_needed
现在轮修到通过为止再不通过才review
This commit is contained in:
知微
2026-07-02 14:12:31 +08:00
parent 400e4ee34d
commit 9e5ea52c1e
5 changed files with 181 additions and 149 deletions
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+38 -38
View File
@@ -5,9 +5,9 @@
"name": "中际旭创", "name": "中际旭创",
"shares": 100, "shares": 100,
"cost": 1316.53, "cost": 1316.53,
"price": 1140.85, "price": 1153.0,
"market_value": 113604.0, "market_value": 113604.0,
"change_pct": -6.73, "change_pct": -5.74,
"currency": "CNY", "currency": "CNY",
"position_pct": 15.27, "position_pct": 15.27,
"_currency": "CNY" "_currency": "CNY"
@@ -17,9 +17,9 @@
"name": "长飞光纤光缆", "name": "长飞光纤光缆",
"shares": 500, "shares": 500,
"cost": 263.72, "cost": 263.72,
"price": 174.09, "price": 174.79,
"market_value": 89300.0, "market_value": 89300.0,
"change_pct": -21.38, "change_pct": -21.06,
"currency": "CNY", "currency": "CNY",
"position_pct": 13.47, "position_pct": 13.47,
"_currency": "CNY" "_currency": "CNY"
@@ -29,9 +29,9 @@
"name": "丘钛科技", "name": "丘钛科技",
"shares": 11000, "shares": 11000,
"cost": 13.47, "cost": 13.47,
"price": 5.96, "price": 5.95,
"market_value": 65890.0, "market_value": 65890.0,
"change_pct": 0.146, "change_pct": 0.0,
"currency": "CNY", "currency": "CNY",
"position_pct": 7.97, "position_pct": 7.97,
"_currency": "CNY" "_currency": "CNY"
@@ -41,9 +41,9 @@
"name": "紫金矿业", "name": "紫金矿业",
"shares": 2400, "shares": 2400,
"cost": 39.89, "cost": 39.89,
"price": 26.39, "price": 26.4,
"market_value": 63048.0, "market_value": 63048.0,
"change_pct": 5.1, "change_pct": 5.14,
"currency": "CNY", "currency": "CNY",
"position_pct": 7.34, "position_pct": 7.34,
"_currency": "CNY" "_currency": "CNY"
@@ -53,9 +53,9 @@
"name": "海博思创", "name": "海博思创",
"shares": 200, "shares": 200,
"cost": 266.95, "cost": 266.95,
"price": 257.27, "price": 258.8,
"market_value": 51776.0, "market_value": 51776.0,
"change_pct": -2.14, "change_pct": -1.56,
"currency": "CNY", "currency": "CNY",
"position_pct": 6.31, "position_pct": 6.31,
"_currency": "CNY" "_currency": "CNY"
@@ -65,9 +65,9 @@
"name": "中芯国际", "name": "中芯国际",
"shares": 300, "shares": 300,
"cost": 126.07, "cost": 126.07,
"price": 145.7, "price": 147.2,
"market_value": 44112.0, "market_value": 44112.0,
"change_pct": -5.68, "change_pct": -4.71,
"currency": "CNY", "currency": "CNY",
"position_pct": 5.44, "position_pct": 5.44,
"_currency": "CNY" "_currency": "CNY"
@@ -77,9 +77,9 @@
"name": "建滔积层板", "name": "建滔积层板",
"shares": 500, "shares": 500,
"cost": 88.23, "cost": 88.23,
"price": 71.79, "price": 72.35,
"market_value": 36415.0, "market_value": 36415.0,
"change_pct": -16.49, "change_pct": -15.83,
"currency": "CNY", "currency": "CNY",
"position_pct": 5.28, "position_pct": 5.28,
"_currency": "CNY" "_currency": "CNY"
@@ -89,9 +89,9 @@
"name": "华恒生物", "name": "华恒生物",
"shares": 2800, "shares": 2800,
"cost": 21.51, "cost": 21.51,
"price": 17.15, "price": 17.24,
"market_value": 48244.0, "market_value": 48244.0,
"change_pct": 4.76, "change_pct": 5.31,
"currency": "CNY", "currency": "CNY",
"position_pct": 5.25, "position_pct": 5.25,
"_currency": "CNY" "_currency": "CNY"
@@ -101,9 +101,9 @@
"name": "宁德时代", "name": "宁德时代",
"shares": 100, "shares": 100,
"cost": 401.78, "cost": 401.78,
"price": 385.91, "price": 385.54,
"market_value": 38495.0, "market_value": 38495.0,
"change_pct": 0.54, "change_pct": 0.44,
"currency": "CNY", "currency": "CNY",
"position_pct": 4.64, "position_pct": 4.64,
"_currency": "CNY" "_currency": "CNY"
@@ -113,9 +113,9 @@
"name": "比亚迪股份", "name": "比亚迪股份",
"shares": 600, "shares": 600,
"cost": 104.87, "cost": 104.87,
"price": 67.8, "price": 67.97,
"market_value": 41070.0, "market_value": 41070.0,
"change_pct": 7.94, "change_pct": 8.21,
"currency": "CNY", "currency": "CNY",
"position_pct": 4.62, "position_pct": 4.62,
"_currency": "CNY" "_currency": "CNY"
@@ -125,9 +125,9 @@
"name": "万科企业", "name": "万科企业",
"shares": 19700, "shares": 19700,
"cost": 4.67, "cost": 4.67,
"price": 1.93, "price": 1.92,
"market_value": 38021.0, "market_value": 38021.0,
"change_pct": 5.189, "change_pct": 4.72,
"currency": "CNY", "currency": "CNY",
"position_pct": 4.6, "position_pct": 4.6,
"_currency": "CNY" "_currency": "CNY"
@@ -137,9 +137,9 @@
"name": "腾讯", "name": "腾讯",
"shares": 100, "shares": 100,
"cost": null, "cost": null,
"price": 377.67, "price": 377.49,
"market_value": 37784.0, "market_value": 37784.0,
"change_pct": 1.35, "change_pct": 1.3,
"currency": "CNY", "currency": "CNY",
"position_pct": null, "position_pct": null,
"_currency": "CNY" "_currency": "CNY"
@@ -149,9 +149,9 @@
"name": "中芯国际", "name": "中芯国际",
"shares": 500, "shares": 500,
"cost": 75.94, "cost": 75.94,
"price": 68.32, "price": 69.14,
"market_value": 34635.0, "market_value": 34635.0,
"change_pct": -11.86, "change_pct": -10.79,
"currency": "CNY", "currency": "CNY",
"position_pct": 4.2, "position_pct": 4.2,
"_currency": "CNY" "_currency": "CNY"
@@ -161,9 +161,9 @@
"name": "长芯博创", "name": "长芯博创",
"shares": 100, "shares": 100,
"cost": 231.46, "cost": 231.46,
"price": 224.72, "price": 226.0,
"market_value": 22592.0, "market_value": 22592.0,
"change_pct": -11.53, "change_pct": -11.02,
"currency": "CNY", "currency": "CNY",
"position_pct": 3.2, "position_pct": 3.2,
"_currency": "CNY" "_currency": "CNY"
@@ -173,9 +173,9 @@
"name": "黄金ETF华安", "name": "黄金ETF华安",
"shares": 2400, "shares": 2400,
"cost": 12.19, "cost": 12.19,
"price": 8.47, "price": 8.46,
"market_value": 20280.0, "market_value": 20280.0,
"change_pct": 2.39, "change_pct": 2.32,
"currency": "CNY", "currency": "CNY",
"position_pct": 2.45, "position_pct": 2.45,
"_currency": "CNY" "_currency": "CNY"
@@ -185,9 +185,9 @@
"name": "中科电气", "name": "中科电气",
"shares": 1400, "shares": 1400,
"cost": 22.29, "cost": 22.29,
"price": 14.28, "price": 14.32,
"market_value": 20062.0, "market_value": 20062.0,
"change_pct": -1.11, "change_pct": -0.83,
"currency": "CNY", "currency": "CNY",
"position_pct": 2.42, "position_pct": 2.42,
"_currency": "CNY" "_currency": "CNY"
@@ -221,20 +221,20 @@
"name": "中国神华", "name": "中国神华",
"shares": 500, "shares": 500,
"cost": 45.89, "cost": 45.89,
"price": 34.18, "price": 34.16,
"market_value": 17115.0, "market_value": 17115.0,
"change_pct": 1.25, "change_pct": 1.19,
"currency": "CNY", "currency": "CNY",
"position_pct": 2.14, "position_pct": 2.14,
"_currency": "CNY" "_currency": "CNY"
} }
], ],
"total_assets": 898730.0, "total_assets": 901907.0,
"total_mv": 818254.0, "total_mv": 821431.0,
"stock_value": null, "stock_value": null,
"cash": 80476.0, "cash": 80476.0,
"frozen_cash": 0.0, "frozen_cash": 0.0,
"position_pct": 91.05, "position_pct": 91.08,
"currency": "CNY", "currency": "CNY",
"updated_at": "2026-07-02 14:04" "updated_at": "2026-07-02 14:12"
} }
+122 -90
View File
@@ -157,26 +157,31 @@ def validate_strategy(d, debug=True):
def enforce_strategy_quality(code, name, result): def enforce_strategy_quality(code, name, result):
"""策略写入前的强制质量门禁 """策略写入前的强制质量门禁
对 result 执行 validate_strategy,不通过则 三段自动修复
- CRITICAL 失败 → 自动调取技术分析/行业数据修复 - Round 1: 技术分析(ta.full_analysis/chip_sr
- 修复后重检 → 通过则写入,再不通过标记 review_needed - Round 2: DB + 价格百分比推算
- HIGH 失败 → 标记 warning 但放行 - Round 3: 最低可用策略标记强推
3轮全不过 → review_needed
""" """
passed, failures = validate_strategy(result) price = result.get("price", 0) or result.get("current", 0) or result.get("last_price", 0)
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"]
# --- 自动修复:对每项 CRITICAL 失败调用对应修复逻辑 ---
retry_needed = False
price = result.get("price", 0)
code_str = str(code) code_str = str(code)
import sqlite3 # 本函数多处使用
for gate_id in critical_issues: def _db_sector():
if gate_id == "GATE_LOSS_EXISTS" and result.get("stop_loss", 0) <= 0: """从 DB 取行业名"""
# 止损缺失:调 technical_analysis 算弱支撑 try:
print(f" [AUTO-FIX] {name}({code}) 止损缺失 → 计算技术位", flush=True) _db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5)
r = _db.execute("SELECT sector_name FROM stock_sectors WHERE code=?", (code_str,)).fetchone()
_db.close()
return r[0] if r else None
except:
return None
def _fix_one(gate_id, round_num):
"""对单个门禁执行修复。round_num越大修复越激进。"""
if gate_id == "GATE_LOSS_EXISTS" and (result.get("stop_loss") or 0) <= 0:
if round_num <= 2:
# Round 1-2: 技术分析算支撑
tech = ta.full_analysis(code) tech = ta.full_analysis(code)
if tech and "support_resistance" in tech: if tech and "support_resistance" in tech:
sr = tech["support_resistance"] sr = tech["support_resistance"]
@@ -190,12 +195,16 @@ def enforce_strategy_quality(code, name, result):
result["stop_loss"] = round(price * 0.95, 2) result["stop_loss"] = round(price * 0.95, 2)
elif price > 0: elif price > 0:
result["stop_loss"] = round(price * 0.95, 2) result["stop_loss"] = round(price * 0.95, 2)
retry_needed = True else:
print(f" → 止损 = {result['stop_loss']}", flush=True) # Round 3: 强制fallback
if price > 0:
result["stop_loss"] = round(price * 0.90, 2) # 更宽
else:
result["stop_loss"] = 1
print(f" R{round_num} 止损={result.get('stop_loss',0)}", flush=True)
if gate_id == "GATE_PROFIT_EXISTS" and result.get("take_profit", 0) <= 0: if gate_id == "GATE_PROFIT_EXISTS" and (result.get("take_profit") or 0) <= 0:
# 止盈缺失:调 technical_analysis 算阻力位 if round_num <= 2:
print(f" [AUTO-FIX] {name}({code}) 止盈缺失 → 计算技术位", flush=True)
tech = ta.full_analysis(code) tech = ta.full_analysis(code)
if tech and "support_resistance" in tech: if tech and "support_resistance" in tech:
sr = tech["support_resistance"] sr = tech["support_resistance"]
@@ -209,17 +218,18 @@ def enforce_strategy_quality(code, name, result):
result["take_profit"] = round(price * 1.08, 2) result["take_profit"] = round(price * 1.08, 2)
elif price > 0: elif price > 0:
result["take_profit"] = round(price * 1.08, 2) result["take_profit"] = round(price * 1.08, 2)
retry_needed = True else:
print(f" → 止盈 = {result['take_profit']}", flush=True) if price > 0:
result["take_profit"] = round(price * 1.20, 2) # 更宽
else:
result["take_profit"] = 2
print(f" R{round_num} 止盈={result.get('take_profit',0)}", flush=True)
if gate_id == "GATE_ENTRY_RANGE" and (result.get("entry_low", 0) >= result.get("entry_high", 0) or result.get("entry_low", 0) <= 0): if gate_id == "GATE_ENTRY_RANGE" and ((result.get("entry_low") or 0) >= (result.get("entry_high") or 0) or (result.get("entry_low") or 0) <= 0):
# 买入区无效:从价格计算 p = price or 100
print(f" [AUTO-FIX] {name}({code}) 买入区无效 → 从现价推算", flush=True)
p = price or result.get("current", 0) or result.get("last_price", 0)
if p <= 0:
p = 100
sl = result.get("stop_loss", 0) sl = result.get("stop_loss", 0)
tp = result.get("take_profit", 0) tp = result.get("take_profit", 0)
if round_num <= 2:
if sl > 0 and tp > 0 and sl < tp: if sl > 0 and tp > 0 and sl < tp:
result["entry_low"] = round(sl * 1.02, 2) result["entry_low"] = round(sl * 1.02, 2)
result["entry_high"] = round(tp * 0.85, 2) result["entry_high"] = round(tp * 0.85, 2)
@@ -229,32 +239,39 @@ def enforce_strategy_quality(code, name, result):
else: else:
result["entry_low"] = round(p * 0.93, 2) result["entry_low"] = round(p * 0.93, 2)
result["entry_high"] = round(p * 1.02, 2) result["entry_high"] = round(p * 1.02, 2)
retry_needed = True else:
print(f" → 买入区 {result['entry_low']}~{result['entry_high']}", flush=True) result["entry_low"] = round(p * 0.90, 2)
result["entry_high"] = round(p * 1.10, 2)
print(f" R{round_num} 买入区={result['entry_low']}~{result['entry_high']}", flush=True)
if gate_id == "GATE_9D_ANALYSIS": if gate_id == "GATE_9D_ANALYSIS":
# sector_context 或 signal_factors 缺失 → 走 DB 补 # 行业
print(f" [AUTO-FIX] {name}({code}) 多维分析缺失 → 补行业因子", flush=True)
if not result.get("sector_context") or str(result.get("sector_context","")).strip() in ("neutral","","N/A","-"): if not result.get("sector_context") or str(result.get("sector_context","")).strip() in ("neutral","","N/A","-"):
result["sector_context"] = f"{name}所属行业(待更新)" sec = _db_sector()
try: if sec:
import sqlite3 result["sector_context"] = sec
_db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=5) elif round_num >= 2:
_sec = _db.execute("SELECT sector_name FROM stock_sectors WHERE code=?", (code_str,)).fetchone() result["sector_context"] = f"自选(未分类)"
_db.close() else:
if _sec and _sec[0]: result["sector_context"] = f"{name}所属行业(待补充)"
result["sector_context"] = _sec[0] # signal_factors
except:
pass
if not result.get("signal_factors") or (isinstance(result.get("signal_factors"), list) and len(result["signal_factors"]) == 0): if not result.get("signal_factors") or (isinstance(result.get("signal_factors"), list) and len(result["signal_factors"]) == 0):
factors = [] factors = []
if result.get("timing_signal"): if result.get("timing_signal"):
factors.append(f"信号:{result['timing_signal']}") factors.append(f"信号:{result['timing_signal']}")
if result.get("rr_ratio", 0) > 0: if result.get("rr_ratio", 0) > 0:
factors.append(f"RR:{result['rr_ratio']}") factors.append(f"RR:{result['rr_ratio']}")
if result.get("stop_loss", 0) > 0 and result.get("take_profit", 0) > 0:
factors.append(f"{result['stop_loss']}{result['take_profit']}")
if not factors: if not factors:
if round_num >= 2:
factors.append("自动填充") factors.append("自动填充")
else:
# Round 1: 留空等重检,不硬填
pass
if factors:
result["signal_factors"] = factors result["signal_factors"] = factors
# tech_snapshot
if not result.get("tech_snapshot") or not any(c in str(result.get("tech_snapshot","")) for c in "支撑阻力压强"): if not result.get("tech_snapshot") or not any(c in str(result.get("tech_snapshot","")) for c in "支撑阻力压强"):
sl = result.get("stop_loss", 0) sl = result.get("stop_loss", 0)
tp = result.get("take_profit", 0) tp = result.get("take_profit", 0)
@@ -262,62 +279,77 @@ def enforce_strategy_quality(code, name, result):
result["tech_snapshot"] = f"自动:损{sl}{tp}" result["tech_snapshot"] = f"自动:损{sl}{tp}"
elif price: elif price:
result["tech_snapshot"] = f"自动:价{price}" result["tech_snapshot"] = f"自动:价{price}"
retry_needed = True elif round_num >= 2:
result["tech_snapshot"] = "自动生成(未补全技术位)"
print(f" R{round_num} 9维分析: sector={result.get('sector_context','')[:20]} factors={result.get('signal_factors',[])}", flush=True)
if retry_needed: # 循环重试
# 重检质量 MAX_RETRIES = 3
print(f" [AUTO-FIX] 修复完成 → 重检质量", flush=True) passed, failures = validate_strategy(result)
repassed, new_failures = validate_strategy(result) retry_count = 0
if repassed:
print(f"{name}({code}) 自动修复后通过质量门禁", flush=True) for _retry_num in range(1, MAX_RETRIES + 1):
# 记录 changelog if passed:
cl = result.setdefault("changelog", []) break
cl.append({
"time": datetime.now().strftime("%Y-%m-%d %H:%M"), critical_issues = [f["id"] for f in failures if f["severity"] == "CRITICAL"]
"event": f"质量门禁自动修复 ({', '.join(critical_issues)})", if not critical_issues:
}) # 没有CRITICAL了,只有HIGH/MEDIUM → 可以放行
passed = True
break
retry_count = _retry_num
print(f" [RETRY {retry_count}/{MAX_RETRIES}] {name}({code}) → 修复: {critical_issues}", flush=True)
for gate_id in critical_issues:
_fix_one(gate_id, retry_count)
# 重检
passed, failures = validate_strategy(result)
# --- 最终结果 ---
if passed:
print(f"{name}({code}) 质量门禁通过 ({retry_count}轮重试)", flush=True)
result["quality_check"] = "passed" result["quality_check"] = "passed"
result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
result["status"] = "active"
return True
else:
# 修复后仍不过 → 退回 review_needed
remaining_critical = [f["id"] for f in new_failures if f["severity"] == "CRITICAL"]
print(f"{name}({code}) 自动修复后仍不通过 ({remaining_critical}) → review_needed", flush=True)
# 标记质量失败(原始失败或修复后仍失败) # HIGH 级别警告(已通过但仍有非CRITICAL失败)
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")
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"] high_fails = [f for f in failures if f["severity"] == "HIGH"]
if high_fails: if high_fails:
result["quality_check"] = "warning" result["quality_check"] = "warning"
result["quality_issues"] = {"high": [f["id"] for f in high_fails]} 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警告", flush=True)
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")
# 记录 changelog
if "critical_issues" in dir():
cl = result.setdefault("changelog", [])
cl.append({
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
"event": f"质量门禁通过 (重试{retry_count}轮)",
})
result["status"] = "active"
return True return True
else:
# 3轮全不过 → review_needed
remaining_critical = [f["id"] for f in failures if f["severity"] == "CRITICAL"]
result["quality_check"] = "failed"
result["quality_issues"] = {
"critical": remaining_critical,
"all": [f["id"] for f in failures],
}
result["quality_checked_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
result["status"] = "review_needed"
result["timing_signal"] = "信号不充分"
cl = result.setdefault("changelog", [])
cl.append({
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
"event": f"质量门禁3轮全拒 → review_needed ({remaining_critical})",
})
print(f" 🚫 {name}({code}) 3轮修复后仍有 {remaining_critical} → review_needed", flush=True)
return False
def is_hk_stock(code): def is_hk_stock(code):