硬性策略质量门禁 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:
知微
2026-07-02 13:46:53 +08:00
parent 04b8a6d4bc
commit 7c0e85af28
32 changed files with 12496 additions and 75159 deletions
+45 -13
View File
@@ -5,9 +5,12 @@
发现问题→写TODO(消费管道与每日体检共享)。
"""
import json, os, sqlite3, subprocess, urllib.request
import json, os, sqlite3, subprocess, urllib.request, sys, socket
from pathlib import Path
from datetime import datetime, timedelta
# ── MoFin path ─────────────────────────────────────────────────────
sys.path.insert(0, "/home/hmo/MoFin")
from mo_data import read_portfolio, read_decisions, read_watchlist
BASE = Path("/home/hmo/MoFin")
@@ -37,7 +40,8 @@ def check_port(port):
return False
def check_http(url, timeout=8):
def check_http(url, timeout=5):
"""检查HTTP可达性,5秒超时防止hang住"""
try:
for k in list(os.environ.keys()):
if 'proxy' in k.lower():
@@ -68,8 +72,14 @@ def check_xiaoguo():
if scans_today <= 0:
# 可能是小果离线了,不报严重,记录即可
return
# API — 不通时scanner已降级为unknown,不影响
check_http("http://node122:18003/v1/models")
# API — 用socket快速检测可达性(3s超时)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
s.connect(("node122", 18003))
s.close()
except:
pass
PORTFOLIO_PATH = str(DATA / "portfolio.json")
@@ -113,16 +123,28 @@ def check_price_monitor():
return
# 检查portfolio.json数据新鲜度
# 兼容 '2026-07-02 10:43'price_monitor写入,无秒)和 '2026-07-02 10:43:53'DB写入,有秒)
def _parse_updated_at(ts: str) -> datetime | None:
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
try:
return datetime.strptime(ts, fmt)
except ValueError:
continue
return None
try:
pf = mo_data.read_portfolio()
pf = read_portfolio()
pf_updated = pf.get("updated_at", "")
if pf_updated:
pf_dt = datetime.strptime(pf_updated, "%Y-%m-%d %H:%M")
seconds_ago = (datetime.now() - pf_dt).total_seconds()
if seconds_ago < 600: # 10分钟内
log(True, f"价格监控运行正常,数据{int(seconds_ago//60)}分钟前更新")
pf_dt = _parse_updated_at(pf_updated)
if pf_dt is None:
log(False, f"价格数据updated_at格式无法解析: {pf_updated}")
else:
log(False, f"价格数据{int(seconds_ago)}秒未更新(portfolio.json")
seconds_ago = (datetime.now() - pf_dt).total_seconds()
if seconds_ago < 600: # 10分钟内
log(True, f"价格监控运行正常,数据{int(seconds_ago//60)}分钟前更新")
else:
log(False, f"价格数据{int(seconds_ago)}秒未更新(portfolio.json")
else:
log(False, "portfolio.json缺少updated_at字段")
except Exception as e:
@@ -171,7 +193,8 @@ def check_signal_pipeline():
if summary:
reason = summary[:80].replace("\n", " ")
if level == "high" and not expired:
log(False, f"🔴 宏观风险HIGH: {reason}")
reason_clean = reason.replace("【高风险】", "").strip()[:60]
log(False, f"🔴 宏观风险HIGH: {reason_clean}")
elif level == "high" and expired:
log(True, f"⏳ 宏观风险HIGH已过期(无新信号超过15分钟)")
elif level == "medium":
@@ -188,8 +211,17 @@ def write_todos():
for msg in ISSUES:
title = f"[盘中自检] {msg}"
try:
conn = sqlite3.connect(str(DB_PATH))
exist = conn.execute("SELECT id FROM todos WHERE title=? AND status IN ('pending','in_progress')", (title,)).fetchone()
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute("PRAGMA busy_timeout=5000")
# 宏观风险HIGH去重:只要有pending/in_progress的宏观风险TODO,不再新增
if "宏观风险HIGH" in msg:
exist = conn.execute(
"SELECT id FROM todos WHERE title LIKE '%宏观风险HIGH%' AND status IN ('pending','in_progress') LIMIT 1"
).fetchone()
else:
exist = conn.execute(
"SELECT id FROM todos WHERE title=? AND status IN ('pending','in_progress')", (title,)
).fetchone()
if not exist:
conn.execute(
"INSERT INTO todos (title, description, priority, source, status, fix_action) "
+4 -3
View File
@@ -40,8 +40,8 @@ HIGH_PATTERNS = [
r"fed.*(?:emergency|unexpected|surprise|hike|cut)",
# 指数暴跌(需 ≥2% 跌幅或使用更强范围词)
r"指数[^。]*?(?:暴跌|熔断|闪崩|重挫)",
r"指数[^。]*?(?:跌幅|下跌)[^。]*?[2-9]%",
r"(?:暴跌|重挫|熔断).*[5-9]%",
r"指数[^。]*?(?:跌幅[^。]{0,20}(?:扩大至|达|至|超|为|逾)[^。]*?(?<![0-9.])(?:[2-9]|[1-9][0-9])(?:\.\d+)?%|下跌(?!.*?涨)[^。]*?(?<![0-9.])(?:[2-9]|[1-9][0-9])(?:\.\d+)?%)",
r"(?:暴跌|重挫|熔断).*?(?<![0-9.])(?:[5-9]|[1-9][0-9])(?:\.\d+)?%",
r"熔断|闪崩",
# 地缘+贸易
r"关税.*(?:升级|新|报复|制裁)",
@@ -82,7 +82,7 @@ MEDIUM_PATTERNS = [
# 旧版有 银行.*倒闭|挤兑|破产 , 新版有 银行.*(?:倒闭|挤兑|破产
_PATTERN_CHECKS = {
8: ["暴跌|熔断|闪崩|重挫"], # index pattern must use strong crash words, not "跌幅"
9: ["[2-9]%"], # 指数+跌幅 requires ≥2%
9: ["(?<![0-9.])", "(?:扩大至|达|至|超|为|逾)"], # new: negative lookbehind for decimals + measurement words for 跌幅
14: ["核(?:威胁", "核威胁|武器|弹头"], # must NOT have standalone 核
18: ["倒闭|挤兑|破产"], # bank pattern must have crisis keywords
19: ["金融危机(?:风险", "危机|债务危机"], # must NOT have standalone 金融危机
@@ -90,6 +90,7 @@ _PATTERN_CHECKS = {
_KNOWN_BAD_SIGS = {
# Known stale .pyc signature fragments that indicate wrong version
"指数.*跌幅": "旧版用 .* 跨句匹配且无 ≥2% 阈值",
"[2-9]%(?!\")": "旧版 pattern 9 无上下文限制和阈值修复(新版用 (?:[2-9]|[1-9][0-9])(?:\\.\\d+)?% 替代 [2-9]%",
"|核|": "旧版有独立单字核",
"英伟达|nvidia.*跌": "旧版 alternation 分组错误",
"导弹.*发射": "旧版只匹配发射不匹配袭击",
+4 -4
View File
@@ -279,14 +279,14 @@ def refresh_data_prices():
# 收集所有需要拉取的代码
try:
pf = mo_data.read_portfolio()
pf = read_portfolio()
for s in pf.get('holdings', []):
all_codes.add(s['code'])
except:
pf = {"holdings": []}
try:
wl = mo_data.read_watchlist()
wl = read_watchlist()
for s in wl.get('stocks', []):
all_codes.add(s['code'])
except:
@@ -436,7 +436,7 @@ def _branch_alert_suffix(code, price, shares=0, cost=0):
def _record_branch_trigger(code, branch_id, price):
"""记录分支触发事件(自成长:trigger_count+1"""
try:
raw = mo_data.read_decisions()
raw = read_decisions()
for d in raw.get('decisions', []):
if d.get('code') == code and d.get('strategy_tree',{}).get('branches'):
for b in d['strategy_tree']['branches']:
@@ -560,7 +560,7 @@ def run_once(round_label=""):
_SCENARIO_CACHE = detect_scenario() if HAS_TREE else {}
_BRANCH_CACHE = {}
try:
raw = mo_data.read_decisions()
raw = read_decisions()
for d in raw.get('decisions', []):
tree = d.get('strategy_tree', {})
if tree and tree.get('branches'):
+8 -6
View File
@@ -44,9 +44,11 @@ def _http_get(url, headers=None, timeout=10):
def _detect_market(code):
"""自动识别A股/港股"""
"""自动识别A股/港股
A股代码固定6位,港股代码5位(含前缀0如00700)。
"""
code = str(code).strip()
if code.startswith("0") or code.startswith("3") or len(code) == 6:
if len(code) == 6:
return "ashare"
if len(code) <= 5:
return "hk"
@@ -337,10 +339,10 @@ def _sina_hk(code):
return {
"code": code,
"name": name,
"price": price,
"price": round(prev_close + change_amt, 2) if prev_close > 0 and abs(change_amt) > 0 else price,
"change_pct": change_pct,
"high": float(fields[5]) if fields[5] else None,
"low": float(fields[6]) if fields[6] else None,
"high": None, # Sina HK format不含high/low字段
"low": None,
"open": float(fields[4]) if fields[4] else None,
"prev_close": prev_close,
"volume": None,
@@ -366,7 +368,7 @@ def _tencent_hk(code):
prev_close = float(fields[4]) if fields[4] else 0
if price <= 0:
return None
change_pct = float(fields[7]) if len(fields) > 7 and fields[7] else 0
change_pct = round((price - prev_close) / prev_close * 100, 2) if prev_close > 0 else None
return {
"code": code,
"name": name,