7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
3.2 KiB
Python
122 lines
3.2 KiB
Python
"""
|
||
报告持久化模块。
|
||
|
||
将 CLI 脚本输出的 Markdown 报告存入 DB(mac_report 表)。
|
||
同一日期+研究对象的新报告入库时,旧报告自动标记为失效(is_active=0)。
|
||
"""
|
||
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import text
|
||
|
||
from database.connection import get_engine
|
||
from database.models import Report, create_all_tables
|
||
|
||
|
||
def save_report(
|
||
content: str,
|
||
title: str,
|
||
report_date: str | None = None,
|
||
subject_type: str = "daily",
|
||
subject_code: str = "",
|
||
) -> int:
|
||
"""
|
||
保存报告到 DB。
|
||
|
||
如果同一天、同一研究对象已有报告,先将其标记为 is_active=0,
|
||
然后插入新报告。
|
||
|
||
参数:
|
||
content: Markdown 报告内容
|
||
title: 报告标题
|
||
report_date: 报告日期 YYYYMMDD,默认今天
|
||
subject_type: 研究对象类型 (stock/index/sector/portfolio/daily)
|
||
subject_code: 研究对象代码 (如 000001.SZ 或 000300.SH)
|
||
|
||
返回:
|
||
新报告的 id
|
||
"""
|
||
report_date = report_date or datetime.now().strftime("%Y%m%d")
|
||
|
||
engine = get_engine()
|
||
|
||
# 确保表存在
|
||
create_all_tables()
|
||
|
||
# 将同日期+同对象的旧报告标记失效
|
||
with engine.connect() as conn:
|
||
conn.execute(
|
||
text(
|
||
"UPDATE {} SET is_active = 0 "
|
||
"WHERE report_date = :d AND subject_type = :st AND subject_code = :sc AND is_active = 1"
|
||
.format(Report.__tablename__)
|
||
),
|
||
{"d": report_date, "st": subject_type, "sc": subject_code},
|
||
)
|
||
conn.commit()
|
||
|
||
# 插入新报告
|
||
report = Report(
|
||
report_date=report_date,
|
||
title=title,
|
||
subject_type=subject_type,
|
||
subject_code=subject_code,
|
||
content=content,
|
||
created_at=datetime.now(),
|
||
is_active=1.0,
|
||
)
|
||
|
||
from sqlalchemy.orm import Session
|
||
with Session(engine) as session:
|
||
session.add(report)
|
||
session.commit()
|
||
report_id = report.id
|
||
session.expunge_all()
|
||
|
||
return report_id
|
||
|
||
|
||
def query_reports(
|
||
report_date: str | None = None,
|
||
subject_type: str | None = None,
|
||
subject_code: str | None = None,
|
||
active_only: bool = True,
|
||
limit: int = 20,
|
||
) -> list[dict]:
|
||
"""查询报告列表。"""
|
||
engine = get_engine()
|
||
table = Report.__tablename__
|
||
|
||
sql = "SELECT * FROM {} WHERE 1=1".format(table)
|
||
params = {}
|
||
|
||
if report_date:
|
||
sql += " AND report_date = :d"
|
||
params["d"] = report_date
|
||
if subject_type:
|
||
sql += " AND subject_type = :st"
|
||
params["st"] = subject_type
|
||
if subject_code:
|
||
sql += " AND subject_code = :sc"
|
||
params["sc"] = subject_code
|
||
if active_only:
|
||
sql += " AND is_active = 1"
|
||
|
||
sql += " ORDER BY id DESC LIMIT :lim"
|
||
params["lim"] = limit
|
||
|
||
with engine.connect() as conn:
|
||
rows = conn.execute(text(sql), params).fetchall()
|
||
|
||
result = []
|
||
for r in rows:
|
||
d = dict(r._mapping)
|
||
# DATE/DATETIME 列 → 字符串
|
||
for key in ("report_date", "created_at"):
|
||
val = d.get(key)
|
||
if hasattr(val, "strftime"):
|
||
fmt = "%Y%m%d" if key == "report_date" else "%Y-%m-%d %H:%M:%S"
|
||
d[key] = val.strftime(fmt)
|
||
result.append(d)
|
||
return result
|