feat: 补全 SQLite 表结构 + 查询函数 + 迁移覆盖

mofin_db.py 新增:
- 4 张表: portfolio_summary, advice_timeline, accuracy_stats, strategy_feedback
- 18 个查询函数: query_holdings, query_watchlist, query_strategies,
  query_advice_timeline, query_candidates, query_candidate_scores,
  query_price_events, query_price_events_by_date, query_stock_sectors,
  query_sector_stocks, query_accuracy_stats, query_strategy_feedback,
  query_strategy_evaluations, query_latest_market, query_holding_by_code,
  query_portfolio_summary

migrate_all.py 新增:
- 4 个迁移函数: migrate_portfolio_summary, migrate_advice_timeline,
  migrate_accuracy_stats, migrate_strategy_feedback
- 迁移量: portfolio_summary(1), advice_timeline(2547),
  accuracy_stats(1), strategy_feedback(37)

现在 13 张表全部覆盖,JSON→SQLite 数据完整迁移
This commit is contained in:
hmo
2026-06-20 16:59:24 +08:00
parent 0650673038
commit 1610f184a0
2 changed files with 410 additions and 3 deletions
+124
View File
@@ -387,6 +387,98 @@ def migrate_sectors(conn) -> int:
return count return count
def migrate_portfolio_summary(conn, pf: dict) -> int:
"""portfolio.json 顶层字段 → portfolio_summary"""
try:
conn.execute(
"INSERT OR REPLACE INTO portfolio_summary (id, total_assets, stock_value, cash, position_pct, total_pnl, updated_at) "
"VALUES (1, ?, ?, ?, ?, ?, ?)",
(pf.get("total_assets"), pf.get("stock_value"), pf.get("cash"),
pf.get("position_pct"), pf.get("total_pnl"), pf.get("updated_at", datetime.now().isoformat())))
conn.commit()
return 1
except Exception:
return 0
def migrate_advice_timeline(conn, dec: dict) -> int:
"""decisions.json advice_timeline[] → advice_timeline"""
count = 0
for d in dec.get("decisions", []):
code = _normalize_code(d.get("code", ""))
if not code:
continue
timeline = d.get("advice_timeline", [])
for t in timeline:
try:
conn.execute(
"INSERT INTO advice_timeline (code, date, direction, price, summary, status, evaluated, result, evaluated_at, report_id) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(code, t.get("date"), t.get("direction"), t.get("price"),
t.get("summary"), t.get("status"),
1 if t.get("evaluated") else 0, t.get("result"),
t.get("evaluated_at"), t.get("report_id")))
count += 1
except Exception:
pass
conn.commit()
return count
def migrate_accuracy_stats(conn, acc: dict) -> int:
"""accuracy_stats.json → accuracy_stats"""
try:
conn.execute(
"INSERT OR REPLACE INTO accuracy_stats (id, period_start, period_end, total_advice, correct, wrong, partial, unknown, pending, ignored, evaluated, accuracy_pct, "
"phase1_correct, phase1_wrong, phase1_pending, phase1_accuracy, "
"phase2_correct, phase2_wrong, phase2_pending, phase2_accuracy, total_evaluated, updated_at) "
"VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(acc.get("period_start"), acc.get("period_end"),
acc.get("total_advice"), acc.get("correct"), acc.get("wrong"),
acc.get("partial"), acc.get("unknown"), acc.get("pending"),
acc.get("ignored"), acc.get("evaluated"), acc.get("accuracy_pct"),
acc.get("phase1", {}).get("correct", 0) if isinstance(acc.get("phase1"), dict) else 0,
acc.get("phase1", {}).get("wrong", 0) if isinstance(acc.get("phase1"), dict) else 0,
acc.get("phase1", {}).get("pending", 0) if isinstance(acc.get("phase1"), dict) else 0,
acc.get("phase1", {}).get("accuracy_pct", 0) if isinstance(acc.get("phase1"), dict) else 0,
acc.get("phase2", {}).get("correct", 0) if isinstance(acc.get("phase2"), dict) else 0,
acc.get("phase2", {}).get("wrong", 0) if isinstance(acc.get("phase2"), dict) else 0,
acc.get("phase2", {}).get("pending", 0) if isinstance(acc.get("phase2"), dict) else 0,
acc.get("phase2", {}).get("accuracy_pct", 0) if isinstance(acc.get("phase2"), dict) else 0,
acc.get("total_evaluated"), acc.get("updated_at", datetime.now().isoformat())))
conn.commit()
return 1
except Exception:
return 0
def migrate_strategy_feedback(conn, sf: dict) -> int:
"""strategy_feedback.json → strategy_feedback"""
count = 0
for f in sf.get("feedback", []):
code = _normalize_code(f.get("code", ""))
if not code:
continue
pc = f.get("phase_check", {})
try:
conn.execute(
"INSERT INTO strategy_feedback (code, name, evaluated_at, "
"phase1_completed, phase1_result, phase1_completed_at, phase1_price, "
"phase2_completed, phase2_result, phase2_completed_at, days_in_phase1, adjustments_json) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(code, f.get("name"), f.get("evaluated_at"),
1 if pc.get("phase1_completed") else 0, pc.get("phase1_result"),
pc.get("phase1_completed_at"), pc.get("phase1_price_at_completion"),
1 if pc.get("phase2_completed") else 0, pc.get("phase2_result"),
pc.get("phase2_completed_at"), pc.get("days_in_phase1"),
json.dumps(f.get("adjustments", []), ensure_ascii=False)))
count += 1
except Exception:
pass
conn.commit()
return count
def main(): def main():
print("=" * 60) print("=" * 60)
print(" MoFin 数据迁移 → mofin.db") print(" MoFin 数据迁移 → mofin.db")
@@ -454,6 +546,34 @@ def main():
totals["sector_mappings"] = n totals["sector_mappings"] = n
print(f"[8/8] stock_sectors: {n} 条映射") print(f"[8/8] stock_sectors: {n} 条映射")
# 9. portfolio_summary
pf = load_json("portfolio.json")
if pf:
n = migrate_portfolio_summary(conn, pf)
totals["portfolio_summary"] = n
print(f"[9/12] portfolio_summary: {n}")
# 10. advice_timeline
dec = load_json("decisions.json")
if dec:
n = migrate_advice_timeline(conn, dec)
totals["advice_timeline"] = n
print(f"[10/12] advice_timeline: {n}")
# 11. accuracy_stats
acc = load_json("accuracy_stats.json")
if acc:
n = migrate_accuracy_stats(conn, acc)
totals["accuracy_stats"] = n
print(f"[11/12] accuracy_stats: {n}")
# 12. strategy_feedback
sf = load_json("strategy_feedback.json")
if sf:
n = migrate_strategy_feedback(conn, sf)
totals["strategy_feedback"] = n
print(f"[12/12] strategy_feedback: {n}")
conn.commit() conn.commit()
# ── 验证 ── # ── 验证 ──
@@ -470,6 +590,10 @@ def main():
"price_events": "SELECT COUNT(*) FROM price_events", "price_events": "SELECT COUNT(*) FROM price_events",
"strategy_evaluations": "SELECT COUNT(*) FROM strategy_evaluations", "strategy_evaluations": "SELECT COUNT(*) FROM strategy_evaluations",
"stock_sectors": "SELECT COUNT(*) FROM stock_sectors", "stock_sectors": "SELECT COUNT(*) FROM stock_sectors",
"portfolio_summary": "SELECT COUNT(*) FROM portfolio_summary",
"advice_timeline": "SELECT COUNT(*) FROM advice_timeline",
"accuracy_stats": "SELECT COUNT(*) FROM accuracy_stats",
"strategy_feedback": "SELECT COUNT(*) FROM strategy_feedback",
} }
for name, sql in tables.items(): for name, sql in tables.items():
n = conn.execute(sql).fetchone()[0] n = conn.execute(sql).fetchone()[0]
+286 -3
View File
@@ -218,6 +218,79 @@ def init_all_tables(conn: sqlite3.Connection):
reason TEXT, reason TEXT,
created_at TEXT DEFAULT (datetime('now','localtime')) created_at TEXT DEFAULT (datetime('now','localtime'))
); );
-- 持仓汇总(portfolio.json 顶层字段)
CREATE TABLE IF NOT EXISTS portfolio_summary (
id INTEGER PRIMARY KEY CHECK (id = 1),
total_assets REAL,
stock_value REAL,
cash REAL,
position_pct REAL,
total_pnl REAL,
updated_at TEXT
);
-- 建议时间线(decisions.json advice_timeline[]
CREATE TABLE IF NOT EXISTS advice_timeline (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL REFERENCES stocks(code),
date TEXT,
direction TEXT,
price REAL,
summary TEXT,
status TEXT,
evaluated INTEGER DEFAULT 0,
result TEXT,
evaluated_at TEXT,
report_id TEXT,
created_at TEXT DEFAULT (datetime('now','localtime'))
);
CREATE INDEX IF NOT EXISTS idx_advice_code ON advice_timeline(code);
-- 准确率统计(accuracy_stats.json
CREATE TABLE IF NOT EXISTS accuracy_stats (
id INTEGER PRIMARY KEY CHECK (id = 1),
period_start TEXT,
period_end TEXT,
total_advice INTEGER DEFAULT 0,
correct INTEGER DEFAULT 0,
wrong INTEGER DEFAULT 0,
partial INTEGER DEFAULT 0,
unknown INTEGER DEFAULT 0,
pending INTEGER DEFAULT 0,
ignored INTEGER DEFAULT 0,
evaluated INTEGER DEFAULT 0,
accuracy_pct REAL,
phase1_correct INTEGER DEFAULT 0,
phase1_wrong INTEGER DEFAULT 0,
phase1_pending INTEGER DEFAULT 0,
phase1_accuracy REAL,
phase2_correct INTEGER DEFAULT 0,
phase2_wrong INTEGER DEFAULT 0,
phase2_pending INTEGER DEFAULT 0,
phase2_accuracy REAL,
total_evaluated INTEGER DEFAULT 0,
updated_at TEXT
);
-- 策略反馈(strategy_feedback.json
CREATE TABLE IF NOT EXISTS strategy_feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL REFERENCES stocks(code),
name TEXT,
evaluated_at TEXT,
phase1_completed INTEGER DEFAULT 0,
phase1_result TEXT,
phase1_completed_at TEXT,
phase1_price REAL,
phase2_completed INTEGER DEFAULT 0,
phase2_result TEXT,
phase2_completed_at TEXT,
days_in_phase1 INTEGER,
adjustments_json TEXT,
created_at TEXT DEFAULT (datetime('now','localtime'))
);
CREATE INDEX IF NOT EXISTS idx_feedback_code ON strategy_feedback(code);
""") """)
conn.commit() conn.commit()
@@ -449,14 +522,224 @@ def query_db_stats(conn: sqlite3.Connection) -> dict:
snap_count = conn.execute("SELECT COUNT(*) FROM market_snapshots").fetchone()[0] snap_count = conn.execute("SELECT COUNT(*) FROM market_snapshots").fetchone()[0]
sector_count = conn.execute("SELECT COUNT(*) FROM sector_snapshots").fetchone()[0] sector_count = conn.execute("SELECT COUNT(*) FROM sector_snapshots").fetchone()[0]
stock_count = conn.execute("SELECT COUNT(*) FROM stocks").fetchone()[0] stock_count = conn.execute("SELECT COUNT(*) FROM stocks").fetchone()[0]
kline_count = conn.execute( kline_count = conn.execute("SELECT COUNT(*) FROM stock_daily").fetchone()[0]
"SELECT COUNT(*) FROM stock_daily").fetchone()[0]
event_count = conn.execute("SELECT COUNT(*) FROM price_events").fetchone()[0] event_count = conn.execute("SELECT COUNT(*) FROM price_events").fetchone()[0]
holding_count = conn.execute("SELECT COUNT(*) FROM holdings").fetchone()[0]
candidate_count = conn.execute("SELECT COUNT(*) FROM candidates").fetchone()[0]
latest = conn.execute( latest = conn.execute(
"SELECT timestamp, source FROM market_snapshots ORDER BY id DESC LIMIT 1").fetchone() "SELECT timestamp, source FROM market_snapshots ORDER BY id DESC LIMIT 1").fetchone()
return { return {
"snapshots": snap_count, "sector_rows": sector_count, "snapshots": snap_count, "sector_rows": sector_count,
"stocks": stock_count, "daily_klines": kline_count, "stocks": stock_count, "daily_klines": kline_count,
"price_events": event_count, "price_events": event_count, "holdings": holding_count,
"candidates": candidate_count,
"latest_snapshot": dict(latest) if latest else None, "latest_snapshot": dict(latest) if latest else None,
} }
# ═══════════════════════════════════════════════════════════
# 持仓查询
# ═══════════════════════════════════════════════════════════
def query_holdings(conn: sqlite3.Connection) -> list[dict]:
"""持仓列表(含最新策略)"""
rows = conn.execute("""
SELECT h.code, h.name, h.shares, h.cost, h.position_pct, h.is_active,
hs.stop_loss, hs.take_profit, hs.entry_low, hs.entry_high,
hs.reason as action, hs.created_at as strategy_updated
FROM holdings h
LEFT JOIN holding_strategies hs ON h.code = hs.code
AND hs.id = (SELECT MAX(id) FROM holding_strategies WHERE code = h.code AND strategy_type = 'holding')
WHERE h.is_active = 1
""").fetchall()
return [dict(r) for r in rows]
def query_holding_by_code(conn: sqlite3.Connection, code: str) -> dict | None:
"""单只持仓"""
row = conn.execute("""
SELECT h.*, hs.stop_loss, hs.take_profit, hs.entry_low, hs.entry_high,
hs.reason as action
FROM holdings h
LEFT JOIN holding_strategies hs ON h.code = hs.code
AND hs.id = (SELECT MAX(id) FROM holding_strategies WHERE code = h.code AND strategy_type = 'holding')
WHERE h.code = ?
""", (code,)).fetchone()
return dict(row) if row else None
def query_portfolio_summary(conn: sqlite3.Connection) -> dict:
"""持仓汇总"""
row = conn.execute("SELECT * FROM portfolio_summary WHERE id = 1").fetchone()
return dict(row) if row else {}
# ═══════════════════════════════════════════════════════════
# 自选股查询
# ═══════════════════════════════════════════════════════════
def query_watchlist(conn: sqlite3.Connection) -> list[dict]:
"""自选股列表(含策略)"""
rows = conn.execute("""
SELECT w.code, w.name, w.added_at,
hs.stop_loss, hs.take_profit, hs.entry_low, hs.entry_high,
hs.reason as action
FROM watchlist_stocks w
LEFT JOIN holding_strategies hs ON w.code = hs.code
AND hs.id = (SELECT MAX(id) FROM holding_strategies WHERE code = w.code AND strategy_type = 'watch')
WHERE w.is_active = 1
""").fetchall()
return [dict(r) for r in rows]
# ═══════════════════════════════════════════════════════════
# 决策/策略查询
# ═══════════════════════════════════════════════════════════
def query_strategies(conn: sqlite3.Connection, code: str = None) -> list[dict]:
"""策略列表(按版本倒序)"""
if code:
rows = conn.execute(
"SELECT * FROM holding_strategies WHERE code = ? ORDER BY version DESC", (code,)).fetchall()
else:
rows = conn.execute(
"SELECT * FROM holding_strategies ORDER BY code, version DESC").fetchall()
return [dict(r) for r in rows]
def query_advice_timeline(conn: sqlite3.Connection, code: str = None, limit: int = 50) -> list[dict]:
"""建议时间线"""
if code:
rows = conn.execute(
"SELECT * FROM advice_timeline WHERE code = ? ORDER BY date DESC LIMIT ?",
(code, limit)).fetchall()
else:
rows = conn.execute(
"SELECT * FROM advice_timeline ORDER BY date DESC LIMIT ?", (limit,)).fetchall()
return [dict(r) for r in rows]
# ═══════════════════════════════════════════════════════════
# 候选池查询
# ═══════════════════════════════════════════════════════════
def query_candidates(conn: sqlite3.Connection, active_only: bool = True) -> list[dict]:
"""候选池列表(含最新评分)"""
where = "WHERE c.dropped = 0" if active_only else ""
rows = conn.execute(f"""
SELECT c.*, (SELECT score FROM candidate_score_history
WHERE code = c.code ORDER BY created_at DESC LIMIT 1) as latest_score
FROM candidates c {where}
ORDER BY c.zhiwei_star DESC NULLS LAST
""").fetchall()
return [dict(r) for r in rows]
def query_candidate_scores(conn: sqlite3.Connection, code: str) -> list[dict]:
"""某候选的评分历史"""
rows = conn.execute(
"SELECT * FROM candidate_score_history WHERE code = ? ORDER BY created_at",
(code,)).fetchall()
return [dict(r) for r in rows]
# ═══════════════════════════════════════════════════════════
# 价格事件查询
# ═══════════════════════════════════════════════════════════
def query_price_events(conn: sqlite3.Connection, code: str = None, limit: int = 100) -> list[dict]:
"""价格事件"""
if code:
rows = conn.execute(
"SELECT * FROM price_events WHERE code = ? ORDER BY created_at DESC LIMIT ?",
(code, limit)).fetchall()
else:
rows = conn.execute(
"SELECT * FROM price_events ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()
return [dict(r) for r in rows]
def query_price_events_by_date(conn: sqlite3.Connection, date: str) -> list[dict]:
"""某天的价格事件"""
rows = conn.execute(
"SELECT * FROM price_events WHERE date = ? ORDER BY created_at DESC", (date,)).fetchall()
return [dict(r) for r in rows]
# ═══════════════════════════════════════════════════════════
# 板块成分查询
# ═══════════════════════════════════════════════════════════
def query_stock_sectors(conn: sqlite3.Connection, code: str) -> list[str]:
"""某只股票所属板块"""
rows = conn.execute(
"SELECT sector_name FROM stock_sectors WHERE code = ?", (code,)).fetchall()
return [r[0] for r in rows]
def query_sector_stocks(conn: sqlite3.Connection, sector_name: str) -> list[str]:
"""某板块包含的股票"""
rows = conn.execute(
"SELECT code FROM stock_sectors WHERE sector_name = ?", (sector_name,)).fetchall()
return [r[0] for r in rows]
# ═══════════════════════════════════════════════════════════
# 准确率统计查询
# ═══════════════════════════════════════════════════════════
def query_accuracy_stats(conn: sqlite3.Connection) -> dict:
"""准确率统计"""
row = conn.execute("SELECT * FROM accuracy_stats WHERE id = 1").fetchone()
return dict(row) if row else {}
# ═══════════════════════════════════════════════════════════
# 策略反馈查询
# ═══════════════════════════════════════════════════════════
def query_strategy_feedback(conn: sqlite3.Connection, code: str = None) -> list[dict]:
"""策略反馈"""
if code:
rows = conn.execute(
"SELECT * FROM strategy_feedback WHERE code = ? ORDER BY evaluated_at DESC", (code,)).fetchall()
else:
rows = conn.execute(
"SELECT * FROM strategy_feedback ORDER BY evaluated_at DESC").fetchall()
return [dict(r) for r in rows]
# ═══════════════════════════════════════════════════════════
# 策略评估查询
# ═══════════════════════════════════════════════════════════
def query_strategy_evaluations(conn: sqlite3.Connection, code: str = None) -> list[dict]:
"""策略评估记录"""
if code:
rows = conn.execute(
"SELECT * FROM strategy_evaluations WHERE code = ? ORDER BY created_at DESC", (code,)).fetchall()
else:
rows = conn.execute(
"SELECT * FROM strategy_evaluations ORDER BY created_at DESC").fetchall()
return [dict(r) for r in rows]
# ═══════════════════════════════════════════════════════════
# 市场快照查询(最新)
# ═══════════════════════════════════════════════════════════
def query_latest_market(conn: sqlite3.Connection) -> dict:
"""最新一次市场快照(含板块数据)"""
snap = conn.execute(
"SELECT * FROM market_snapshots ORDER BY id DESC LIMIT 1").fetchone()
if not snap:
return {}
snap = dict(snap)
sectors = conn.execute(
"SELECT * FROM sector_snapshots WHERE snapshot_id = ? ORDER BY change_pct DESC",
(snap["id"],)).fetchall()
snap["sectors"] = [dict(r) for r in sectors]
# 计算 top_gainers / top_losers
snap["top_gainers"] = [dict(r) for r in sectors[:5]]
snap["top_losers"] = [dict(r) for r in sectors[-3:]]
return snap