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:
+35
-117
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user