fix: 全面系统修复 — delivery目标修正 + 脚本同步 + refresh_mtf_cache import修复 + clean_watchlist硬编码修复
修复清单: - cron delivery修正:10个job从deliver=origin/all改为local,消除推送错误 - refresh_mtf_cache.py:替换strategy_lifecycle.PORTFOLIO_PATH导入为mofin_db直接读DB - clean_watchlist.py:修复mo_data导入名错误和JSON硬编码路径 - MoFin/scripts/与profile scripts互相补全,双向sync到一致 - price_monitor.py:现金源从portfolio_summary表改为cash_log(Dad确认的现金权威) - 更新watchlist.json加入000850华茂股份
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pre-flight-check.py — 策略上线前检查清单的自动化部分
|
||||
|
||||
检查三项可自动验证的关卡,输出结构化结果供知微在报告中引用。
|
||||
只检查,不拦报告。不过的项目标记 @preflight:fail。
|
||||
|
||||
用法:
|
||||
python3 pre-flight-check.py [code]
|
||||
不传 code → 校验版本一致性 + 数据时效 + 全量持仓成本检查
|
||||
传 code → 只检查单只股票(止损技术位 + R/R + 成本)
|
||||
|
||||
依赖:
|
||||
- /home/hmo/projects/MoFin/data/prompts/registry.json(版本一致性)
|
||||
- /home/hmo/web-dashboard/data/decisions.json(策略+成本)
|
||||
- /home/hmo/web-dashboard/data/portfolio.json(持仓数据)
|
||||
|
||||
输出格式:
|
||||
✅ 项目名 — 通过
|
||||
❌ 项目名 — 失败原因
|
||||
⚠️ 项目名 — 警告
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# === 路径 ===
|
||||
REGISTRY = Path("/home/hmo/projects/MoFin/data/prompts/registry.json")
|
||||
DECISIONS = Path("/home/hmo/web-dashboard/data/decisions.json")
|
||||
PORTFOLIO = Path("/home/hmo/web-dashboard/data/portfolio.json")
|
||||
|
||||
# === 检查 7:版本一致性 ===
|
||||
def check_version_consistency():
|
||||
"""analysis-rules.depends_on.version == strategy-generation.current_version"""
|
||||
try:
|
||||
reg = json.loads(REGISTRY.read_text())
|
||||
except Exception as e:
|
||||
return ("⚠️ 版本一致性", f"无法读取 registry.json: {e}")
|
||||
|
||||
sg = None
|
||||
ar = None
|
||||
for p in reg["prompts"]:
|
||||
if p["id"] == "strategy-generation":
|
||||
sg = p
|
||||
if p["id"] == "analysis-rules":
|
||||
ar = p
|
||||
|
||||
if not sg or not ar:
|
||||
return ("⚠️ 版本一致性", "registry.json 中缺少 strategy-generation 或 analysis-rules")
|
||||
|
||||
sg_ver = sg["current_version"]
|
||||
ar_dep_ver = ar.get("depends_on", {}).get("version", "none")
|
||||
|
||||
if sg_ver == ar_dep_ver:
|
||||
return ("✅ 版本一致性", f"strategy-generation@{sg_ver} == analysis-rules.depends_on@{ar_dep_ver}")
|
||||
else:
|
||||
return ("❌ 版本一致性",
|
||||
f"strategy-generation@{sg_ver} != analysis-rules.depends_on@{ar_dep_ver}。"
|
||||
f"运行 check-prompt-deps.py 修复")
|
||||
|
||||
|
||||
# === 检查 1:数据时效 ===
|
||||
def check_data_freshness():
|
||||
"""检查 decisions.json 的更新时间是否在合理范围内"""
|
||||
try:
|
||||
mtime = DECISIONS.stat().st_mtime
|
||||
mtime_dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
age_minutes = (now - mtime_dt).total_seconds() / 60
|
||||
except Exception as e:
|
||||
return ("⚠️ 数据时效", f"无法检查 decisions.json: {e}")
|
||||
|
||||
if age_minutes < 60:
|
||||
return ("✅ 数据时效",
|
||||
f"decisions.json 更新于 {age_minutes:.0f} 分钟前 ({mtime_dt.strftime('%H:%M')})")
|
||||
elif age_minutes < 240:
|
||||
return ("⚠️ 数据时效",
|
||||
f"decisions.json 已 {age_minutes:.0f} 分钟未更新(可能已收盘),数据视为陈旧")
|
||||
else:
|
||||
return ("❌ 数据时效",
|
||||
f"decisions.json 已 {age_minutes:.0f} 分钟未更新,数据过期,请检查数据管线")
|
||||
|
||||
|
||||
# === 检查 5:成本有效 ===
|
||||
def check_cost_validity():
|
||||
"""检查 decisions.json 中所有持仓的成本是否有效"""
|
||||
try:
|
||||
dec = json.loads(DECISIONS.read_text())
|
||||
except Exception as e:
|
||||
return ("⚠️ 成本有效性", f"无法读取 decisions.json: {e}")
|
||||
|
||||
stocks = dec.get("stocks", dec.get("holdings", dec.get("strategies", [])))
|
||||
if not stocks:
|
||||
stocks = dec.get("portfolio", [])
|
||||
|
||||
issues = []
|
||||
ok = 0
|
||||
total = 0
|
||||
|
||||
for s in stocks:
|
||||
# 支持 stocks[] 和 strategies[] 两种结构
|
||||
cost = s.get("cost") or s.get("actual", {}).get("cost", None)
|
||||
code = s.get("code") or s.get("symbol", "?")
|
||||
name = s.get("name", "")
|
||||
|
||||
if cost is None or cost == 0:
|
||||
issues.append(f"{code} {name}: cost={'null' if cost is None else cost}")
|
||||
else:
|
||||
ok += 1
|
||||
total += 1
|
||||
|
||||
if not issues:
|
||||
return ("✅ 成本有效性", f"全部 {total} 条持仓成本有效")
|
||||
else:
|
||||
return ("⚠️ 成本有效性",
|
||||
f"{len(issues)}/{total} 条持仓成本缺失:{','.join(issues[:5])}"
|
||||
f"{'...' if len(issues) > 5 else ''}")
|
||||
|
||||
|
||||
# === 检查 3(单股):止损技术依据 ===
|
||||
def check_stop_technical(code):
|
||||
"""检查单只股票的止损是否基于技术位(仅对单股模式生效)"""
|
||||
try:
|
||||
dec = json.loads(DECISIONS.read_text())
|
||||
except Exception as e:
|
||||
return ("⚠️ 止损技术位", f"无法读取 decisions.json: {e}")
|
||||
|
||||
stocks = dec.get("stocks", dec.get("strategies", []))
|
||||
for s in stocks:
|
||||
c = s.get("code", "")
|
||||
if c.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", "") == code.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", ""):
|
||||
stop = s.get("stop_loss", s.get("actual", {}).get("stop_loss"))
|
||||
buy_zone = s.get("entry_low", s.get("buy_zone", [None, None]))
|
||||
if isinstance(buy_zone, list):
|
||||
entry_low = buy_zone[0]
|
||||
else:
|
||||
entry_low = buy_zone
|
||||
|
||||
if stop and entry_low and stop < entry_low * 0.95:
|
||||
return ("⚠️ 止损技术位",
|
||||
f"{code} 止损{stop} < 买入区下沿{entry_low}×0.95,"
|
||||
f"检查是否用了固定百分比而非技术位")
|
||||
return ("✅ 止损技术位", f"{code} 止损{stop},需要人工确认是否基于支撑位")
|
||||
|
||||
return ("⚠️ 止损技术位", f"代码 {code} 未在 decisions.json 中找到")
|
||||
|
||||
|
||||
# === 检查 4(单股):R/R 达标 ===
|
||||
def check_rr(code, price=None):
|
||||
"""检查单只股票的 R/R 是否达标"""
|
||||
try:
|
||||
dec = json.loads(DECISIONS.read_text())
|
||||
except Exception as e:
|
||||
return ("⚠️ R/R 达标", f"无法读取 decisions.json: {e}")
|
||||
|
||||
stocks = dec.get("stocks", dec.get("strategies", []))
|
||||
for s in stocks:
|
||||
c = s.get("code", "")
|
||||
if c.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", "") == code.replace(".", "").replace("SZ", "").replace("SH", "").replace("HK", ""):
|
||||
stop = s.get("stop_loss", s.get("actual", {}).get("stop_loss"))
|
||||
target = s.get("take_profit", s.get("actual", {}).get("take_profit"))
|
||||
p = price or s.get("price", s.get("current_price"))
|
||||
|
||||
if not all([stop, target, p]):
|
||||
return ("⚠️ R/R 达标", f"{code} 缺少止损/止盈/现价之一")
|
||||
|
||||
risk = p - stop
|
||||
reward = target - p
|
||||
if risk <= 0:
|
||||
return ("❌ R/R 达标", f"{code} 现价{p}已低于止损{stop},无R/R可言")
|
||||
|
||||
rr = reward / risk
|
||||
cost = s.get("cost", s.get("actual", {}).get("cost"))
|
||||
is_new = (cost is None or cost == 0)
|
||||
|
||||
min_rr = 2.5 if is_new else 2.0 # 简化处理,弱阻距检测需额外输入
|
||||
if rr >= min_rr:
|
||||
return ("✅ R/R 达标", f"{code} R/R={rr:.2f} >= {min_rr}")
|
||||
else:
|
||||
return ("❌ R/R 达标", f"{code} R/R={rr:.2f} < {min_rr}")
|
||||
|
||||
return ("⚠️ R/R 达标", f"代码 {code} 未在 decisions.json 中找到")
|
||||
|
||||
|
||||
def main():
|
||||
print("「策略上线前检查」自动化部分")
|
||||
print(f"检查时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
print()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# 单股模式
|
||||
code = sys.argv[1]
|
||||
price = float(sys.argv[2]) if len(sys.argv) > 2 else None
|
||||
results = [
|
||||
check_stop_technical(code),
|
||||
check_rr(code, price),
|
||||
]
|
||||
else:
|
||||
# 全量模式
|
||||
results = [
|
||||
check_version_consistency(),
|
||||
check_data_freshness(),
|
||||
check_cost_validity(),
|
||||
]
|
||||
|
||||
for tag, msg in results:
|
||||
print(f" {tag} — {msg}")
|
||||
|
||||
# 汇总
|
||||
fails = sum(1 for tag, _ in results if tag.startswith("❌"))
|
||||
warns = sum(1 for tag, _ in results if tag.startswith("⚠️"))
|
||||
total = len(results)
|
||||
|
||||
print()
|
||||
if fails == 0 and warns == 0:
|
||||
print(f" ✅ {total}/{total} 项通过")
|
||||
elif fails == 0:
|
||||
print(f" ⚠️ {warns} 项警告,{total - warns} 项通过")
|
||||
else:
|
||||
print(f" ❌ {fails} 项失败,{warns} 项警告,{total - fails - warns} 项通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user