Files

184 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
generate_report.py — 模板报告生成器 v2
流程: 代码采集数据 → 构建所有数据段 → 填入模板 → 输出预填模板+参数表
用法: python3 generate_report.py <report_type>
LLM只写分析文本,所有数字由代码保障。
"""
import sys, json, subprocess, re
from pathlib import Path
SCRIPTS_DIR = Path(__file__).parent
TEMPLATES_DIR = SCRIPTS_DIR.parent / "templates"
def get_report_data():
r = subprocess.run(["python3", str(SCRIPTS_DIR / "prepare_report_data.py")], capture_output=True, text=True, timeout=30)
if r.returncode != 0:
print(f"ERROR: {r.stderr}", file=sys.stderr)
sys.exit(1)
return json.loads(r.stdout)
def fill_template(text: str, data: dict) -> str:
def replacer(m):
full_key = m.group(1)
# 分离key和format spec
if ":" in full_key:
key, fmt = full_key.split(":", 1)
else:
key, fmt = full_key, ""
parts = key.split(".")
val = data
try:
for p in parts:
val = val[p]
if val is None:
return "N/A"
if fmt:
try:
return format(val, fmt)
except:
return str(val)
if isinstance(val, float):
return f"{val:.2f}" if abs(val) < 10000 else f"{val:.0f}"
return str(val)
except (KeyError, TypeError):
return f"【缺失:{full_key}】"
return re.sub(r'\{([^}]+)\}', replacer, text)
def build_holdings_table(data) -> str:
"""生成持仓明细表"""
h = data["portfolio"]["holdings"]
if not h:
return "(空仓)"
lines = []
for stk in h:
lines.append(f" {stk['code']} {stk['name']} {stk['shares']}{stk['price_display']} {stk['mv_display']} {stk['pnl_pct']:+.2f}%")
return "\n".join(lines)
def build_risk_holdings(data) -> str:
"""浮亏>20%的持仓"""
h = data["portfolio"]["holdings"]
risk = [s for s in h if s["pnl_pct"] < -20]
if not risk:
return "无"
lines = []
for s in risk:
lines.append(f"🔴 {s['code']} {s['name']} {s['pnl_pct']:+.2f}% {s['price_display']}")
return "\n".join(lines)
def build_cash_source(data) -> str:
"""现金来源追溯"""
logs = data.get("cash_history", [])
if not logs:
return "无记录"
latest = logs[0]
return f"{latest['note']} (验证:{'✅' if latest['verified'] else '❌'})"
def build_sections(data) -> dict:
"""构建所有动态段"""
return {
"HOLDINGS_TABLE": build_holdings_table(data),
"HOLDINGS_RISK": build_risk_holdings(data),
"CASH_SOURCE": build_cash_source(data),
"CASH_AMOUNT": f"{data['portfolio']['cash']:.0f}",
"TOTAL_ASSETS": f"{data['portfolio']['total_assets']:.0f}",
"POSITION_PCT": f"{data['portfolio']['position_pct']}",
"STOCK_VALUE": f"{data['portfolio']['stock_value_cny']:.0f}",
"HOLDINGS_COUNT": str(data['portfolio']['holdings_count']),
"GENERATED_AT": data['_meta']['generated_at'],
"HK_RATE": str(data['portfolio']['hk_rate']),
}
def main():
report_type = sys.argv[1] if len(sys.argv) > 1 else "intraday_monitor"
# ★ 前置重评:对 holding_strategies 中信号为买入/卖出的股票,先触发重评
try:
import sqlite3
_conn = sqlite3.connect("/home/hmo/web-dashboard/data/mofin.db")
_actionable = _conn.execute(
"SELECT hs.code, lp.price, hs.entry_low, hs.entry_high FROM holding_strategies hs "
"LEFT JOIN live_prices lp ON hs.code = lp.code "
"WHERE hs.status LIKE '%active%' "
"AND hs.timing_signal IN ('买入','可买入','可加仓','卖出','止盈')"
).fetchall()
_conn.close()
MAX_PRE_REASSESS = 5
for _code, _price, _el, _eh in _actionable[:MAX_PRE_REASSESS]:
# 价格必须在买入区内或附近(不高于上沿20%),否则不触发重评
if _price and _el and _eh and _price > 0 and _el > 0 and _eh > 0:
if _price > _eh * 1.20:
print(f" ⏭️ {_code}: 价{_price}超买入区上沿+{((_price/_eh)-1)*100:.0f}%,跳过重评")
continue
try:
subprocess.run(
["python3", str(SCRIPTS_DIR / "per_stock_reassess.py"), _code],
capture_output=True, timeout=15
)
except:
pass
except Exception:
pass
data = get_report_data()
sections = build_sections(data)
# 读模板
template_path = TEMPLATES_DIR / f"{report_type}.txt"
if not template_path.exists():
print(f"ERROR: 模板 {template_path} 不存在", file=sys.stderr)
sys.exit(1)
template = template_path.read_text(encoding="utf-8")
# 先填大段占位符 {SECTION_NAME}(全大写),再填简单占位符 {xxx.yyy}
filled = template
for key, val in sections.items():
filled = filled.replace(f"{{{key}}}", val)
filled = fill_template(filled, data)
# 输出
print(filled)
print()
# 参数表附在最后供LLM参考
print("【参 | 代码采集 | LLM不得修改】")
param_table = {
"portfolio": {k: data["portfolio"][k] for k in ["total_assets","cash","frozen_cash","stock_value_cny","position_pct","holdings_count","hk_rate"]},
"market": {k: data["market"][k] for k in ["sh_index","sz_index","sh_change","sz_change","advance_decline_ratio","mood"]},
"cash_source": sections["CASH_SOURCE"],
"data_integrity": data["_meta"]["data_integrity"],
}
print(json.dumps(param_table, ensure_ascii=False, indent=2))
# 2026-08-14 通道统一:简报内容记录到统一日志(daily_brief.log),方便溯源
try:
from datetime import datetime as _dt
_log_dir = Path("/home/hmo/MoFin/gateway/logs")
_log_dir.mkdir(parents=True, exist_ok=True)
_brief_log = _log_dir / "daily_brief.log"
with open(_brief_log, "a", encoding="utf-8") as _f:
_f.write(f"\n=== {_dt.now().strftime('%Y-%m-%d %H:%M:%S')} {report_type} ===\n")
_f.write(filled)
_f.write("\n")
except Exception:
pass
# 2026-08-14 通道统一:简报推送到 XMPP bridge5805),记录到 xmpp_messages.jsonl
try:
import urllib.request as _ur
_body = filled.strip()
if _body:
_req = _ur.Request(
"http://127.0.0.1:5805/",
data=_body.encode("utf-8"),
headers={"Content-Type": "text/plain"},
method="POST",
)
_ur.urlopen(_req, timeout=5).read()
except Exception:
pass
if __name__ == "__main__":
main()