refactor: 数据层重构 — 统一 SQLite 访问层 + 多脚本双写

新建 mofin_db.py 共享数据库模块:
- get_conn() 统一连接管理 (WAL + Row factory + 外键)
- init_all_tables() 幂等建表 (12张表: market/sector/stock/kline/fundamentals/sectors/holdings/strategies/watchlist/candidates/score_history/events/evaluations)
- write_market_snapshot() 市场快照双写
- write_klines() K线数据双写 (stocks + daily/weekly/monthly + fundamentals)
- write_price_event() 价格事件双写
- migrate_stock_sectors() 一次性迁移 stock_sector_map.json
- query_*() 通用查询函数 (sector_trend/top_inflow/consecutive_inflow/market_mood/db_stats)

重构现有脚本:
- market_watch.py: 删除内联 DB 代码,改用 mofin_db
- multi_timeframe.py: _save_local_history() 加 SQLite 双写
- price_monitor.py: record_event() 加 SQLite 双写
- mofin_query.py: 改用 mofin_db 查询函数

新增:
- migrate_sectors.py: 一次性迁移脚本

清理:
- get_realtime_prices.py: 死代码 (只读 portfolio.json,不调API)
This commit is contained in:
hmo
2026-06-20 16:25:36 +08:00
parent 8926b11090
commit 0924cf3124
7 changed files with 568 additions and 308 deletions
+35 -117
View File
@@ -6,41 +6,22 @@
python3 mofin_query.py "今天资金净流入最多的5个板块"
python3 mofin_query.py "最近3天连续净流入的板块"
python3 mofin_query.py "市场情绪趋势(最近10次)"
python3 mofin_query.py "数据库概览"
"""
import sqlite3
import sys
from pathlib import Path
DB_PATH = Path(__file__).parent / "data" / "mofin.db"
import re
from mofin_db import (get_conn, query_sector_trend, query_top_inflow,
query_consecutive_inflow, query_market_mood, query_db_stats)
def get_conn():
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
return conn
# ── 预定义查询 ──────────────────────────────────────
def query_sector_trend(name: str, limit: int = 5):
"""查询某板块最近N次采集的涨跌幅趋势"""
def _print_sector_trend(name: str, limit: int = 5):
conn = get_conn()
rows = conn.execute("""
SELECT s.timestamp, ss.change_pct, ss.net_inflow,
ss.up_count, ss.down_count, ss.lead_stock, ss.lead_stock_change
FROM sector_snapshots ss
JOIN market_snapshots s ON ss.snapshot_id = s.id
WHERE ss.name = ?
ORDER BY s.timestamp DESC
LIMIT ?
""", (name, limit)).fetchall()
rows = query_sector_trend(conn, name, limit)
conn.close()
if not rows:
print(f"未找到板块「{name}」的数据")
return
print(f"\n{'='*60}")
print(f" {name} 板块 — 最近 {len(rows)} 次采集")
print(f"{'='*60}")
@@ -51,24 +32,13 @@ def query_sector_trend(name: str, limit: int = 5):
f"{r['up_count'] or '-':>6} {r['down_count'] or '-':>6} {r['lead_stock'] or '-':>10}")
def query_top_inflow(limit: int = 5):
"""最新一次采集中资金净流入最多的板块"""
def _print_top_inflow(limit: int = 5):
conn = get_conn()
rows = conn.execute("""
SELECT ss.name, ss.change_pct, ss.net_inflow, ss.lead_stock, s.timestamp
FROM sector_snapshots ss
JOIN market_snapshots s ON ss.snapshot_id = s.id
WHERE s.id = (SELECT MAX(id) FROM market_snapshots)
AND ss.net_inflow IS NOT NULL
ORDER BY ss.net_inflow DESC
LIMIT ?
""", (limit,)).fetchall()
rows = query_top_inflow(conn, limit)
conn.close()
if not rows:
print("暂无数据")
return
print(f"\n{'='*60}")
print(f" 资金净流入 Top {len(rows)}{rows[0]['timestamp']}")
print(f"{'='*60}")
@@ -78,26 +48,13 @@ def query_top_inflow(limit: int = 5):
print(f"{r['name']:<12} {r['change_pct']:>8.2f} {r['net_inflow']:>10.2f} {r['lead_stock'] or '-':>10}")
def query_consecutive_inflow(days: int = 3):
"""最近N次采集中连续净流入的板块"""
def _print_consecutive_inflow(days: int = 3):
conn = get_conn()
rows = conn.execute("""
SELECT name, COUNT(*) as times, ROUND(AVG(net_inflow), 2) as avg_inflow,
ROUND(AVG(change_pct), 2) as avg_change
FROM sector_snapshots ss
JOIN market_snapshots s ON ss.snapshot_id = s.id
WHERE s.id > (SELECT MAX(id) - ? FROM market_snapshots)
AND net_inflow > 0
GROUP BY name
HAVING COUNT(*) >= ?
ORDER BY avg_inflow DESC
""", (days, days)).fetchall()
rows = query_consecutive_inflow(conn, days)
conn.close()
if not rows:
print(f"没有板块连续 {days} 次净流入")
return
print(f"\n{'='*60}")
print(f" 连续 {days} 次净流入的板块")
print(f"{'='*60}")
@@ -107,21 +64,13 @@ def query_consecutive_inflow(days: int = 3):
print(f"{r['name']:<12} {r['times']:>4} {r['avg_inflow']:>12.2f} {r['avg_change']:>10.2f}")
def query_market_mood(limit: int = 10):
"""市场情绪趋势"""
def _print_market_mood(limit: int = 10):
conn = get_conn()
rows = conn.execute("""
SELECT timestamp, source, up_ratio, mood
FROM market_snapshots
ORDER BY timestamp DESC
LIMIT ?
""", (limit,)).fetchall()
rows = query_market_mood(conn, limit)
conn.close()
if not rows:
print("暂无数据")
return
print(f"\n{'='*60}")
print(f" 市场情绪趋势 — 最近 {len(rows)}")
print(f"{'='*60}")
@@ -132,96 +81,69 @@ def query_market_mood(limit: int = 10):
print(f"{r['timestamp']:<20} {r['source']:>10} {r['up_ratio']:>10.1f} {mood_emoji} {r['mood']:>8}")
def query_stats():
"""数据库概览统计"""
def _print_stats():
conn = get_conn()
snap_count = conn.execute("SELECT COUNT(*) FROM market_snapshots").fetchone()[0]
sector_count = conn.execute("SELECT COUNT(*) FROM sector_snapshots").fetchone()[0]
latest = conn.execute(
"SELECT timestamp, source FROM market_snapshots ORDER BY id DESC LIMIT 1"
).fetchone()
stats = query_db_stats(conn)
conn.close()
print(f"\n{'='*40}")
print(f" MoFin 数据库概览")
print(f"{'='*40}")
print(f" 采集次数: {snap_count}")
print(f" 板块快照: {sector_count}")
if latest:
print(f" 最新采集: {latest['timestamp']} ({latest['source']})")
print(f" 采集次数: {stats['snapshots']}")
print(f" 板块快照: {stats['sector_rows']}")
print(f" 个股数量: {stats['stocks']}")
print(f" 日K线数: {stats['daily_klines']}")
print(f" 价格事件: {stats['price_events']}")
ls = stats.get('latest_snapshot')
if ls:
print(f" 最新采集: {ls['timestamp']} ({ls['source']})")
else:
print(f" 最新采集: 暂无")
# ── 智能路由 ──────────────────────────────────────
def route(query: str):
q = query.strip()
# 板块趋势
if "最近" in q and "" in q and ("涨跌" in q or "趋势" in q or "采集" in q):
# 提取板块名
import re
names = re.findall(r'["「]([^"」]+)["」]', q)
if not names:
# 尝试无引号匹配
for word in ["半导体", "银行", "医药", "新能源", "白酒", "军工", "芯片", "房地产", "汽车"]:
if word in q:
names = [word]
break
names = [word]; break
if names:
limit = 5
m = re.search(r'(\d+)\s*次', q)
if m:
limit = int(m.group(1))
query_sector_trend(names[0], limit)
if m: limit = int(m.group(1))
_print_sector_trend(names[0], limit)
return
# 资金净流入排行
if "净流入" in q and ("最多" in q or "排行" in q or "top" in q.lower()):
limit = 5
import re
m = re.search(r'(\d+)', q)
if m:
limit = int(m.group(1))
query_top_inflow(limit)
if m: limit = int(m.group(1))
_print_top_inflow(limit)
return
# 连续净流入
if "连续" in q and "净流入" in q:
days = 3
import re
m = re.search(r'(\d+)\s*天', q)
if m:
days = int(m.group(1))
query_consecutive_inflow(days)
if m: days = int(m.group(1))
_print_consecutive_inflow(days)
return
# 市场情绪
if "情绪" in q or "mood" in q.lower():
limit = 10
import re
m = re.search(r'(\d+)\s*次', q)
if m:
limit = int(m.group(1))
query_market_mood(limit)
if m: limit = int(m.group(1))
_print_market_mood(limit)
return
# 统计概览
if "概览" in q or "统计" in q or "stats" in q.lower():
query_stats()
_print_stats()
return
# 直接 SQL(以 SELECT 开头)
if q.upper().strip().startswith("SELECT"):
conn = get_conn()
try:
rows = conn.execute(q).fetchall()
if rows:
cols = rows[0].keys()
cols = [d[0] for d in conn.execute(q + " LIMIT 0").description]
print("\t".join(cols))
for r in rows:
print("\t".join(str(r[c]) for c in cols))
print("\t".join(str(c) for c in r))
else:
print("(empty)")
except Exception as e:
@@ -229,10 +151,7 @@ def route(query: str):
finally:
conn.close()
return
# 未匹配
print(f"未识别的查询: {q}")
print()
print(f"未识别的查询: {q}\n")
print("支持的查询模式:")
print(" 「半导体」最近5次采集的涨跌幅")
print(" 今天资金净流入最多的5个板块")
@@ -247,5 +166,4 @@ if __name__ == "__main__":
print("用法: python3 mofin_query.py \"查询语句\"")
print("示例: python3 mofin_query.py \"半导体最近5次采集的涨跌幅\"")
sys.exit(1)
route(sys.argv[1])