# -*- coding: utf-8 -*- """ SQLite 数据访问层:线程安全连接 + 轻量查询封装 表结构:stocks / stock_daily / news / institutions / inst_ratings / fund_holdings / watchlist(自选) / analysis_cache(深度报告缓存) """ import sqlite3 import threading from contextlib import contextmanager from config import DB_PATH _local = threading.local() SCHEMA = """ CREATE TABLE IF NOT EXISTS stocks ( code TEXT PRIMARY KEY, name TEXT NOT NULL, industry TEXT DEFAULT '', board TEXT DEFAULT '主板', total_shares REAL DEFAULT 0, -- 总股本(亿股) float_shares REAL DEFAULT 0, -- 流通股本(亿股) list_date TEXT DEFAULT '', pe REAL DEFAULT 0, -- 市盈率(动) pb REAL DEFAULT 0, -- 市净率 market_cap REAL DEFAULT 0, -- 总市值(亿元) description TEXT DEFAULT '' -- 公司简介 ); CREATE TABLE IF NOT EXISTS stock_daily ( code TEXT NOT NULL, date TEXT NOT NULL, open REAL DEFAULT 0, high REAL DEFAULT 0, low REAL DEFAULT 0, close REAL DEFAULT 0, volume REAL DEFAULT 0, -- 成交量(万股) amount REAL DEFAULT 0, -- 成交额(万元) change_pct REAL DEFAULT 0, -- 涨跌幅 % PRIMARY KEY (code, date) ); CREATE INDEX IF NOT EXISTS idx_daily_date ON stock_daily(date); CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT DEFAULT '', source TEXT DEFAULT '', category TEXT DEFAULT '市场', publish_date TEXT DEFAULT '', related_stocks TEXT DEFAULT '', -- 关联股票代码,逗号分隔 sentiment REAL DEFAULT 0, -- 情感 -1~1 is_positive INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE INDEX IF NOT EXISTS idx_news_date ON news(publish_date); CREATE TABLE IF NOT EXISTS institutions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, type TEXT DEFAULT '', -- 公募基金/券商研究/保险资管/外资机构/私募游资 description TEXT DEFAULT '' ); CREATE TABLE IF NOT EXISTS inst_ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, inst_id INTEGER, inst_name TEXT, stock_code TEXT, rating TEXT DEFAULT '中性', -- 买入/增持/中性/减持 target_price REAL DEFAULT 0, rating_date TEXT DEFAULT '', prev_rating TEXT DEFAULT '' ); CREATE INDEX IF NOT EXISTS idx_ratings_code ON inst_ratings(stock_code); CREATE INDEX IF NOT EXISTS idx_ratings_date ON inst_ratings(rating_date); CREATE TABLE IF NOT EXISTS fund_holdings ( id INTEGER PRIMARY KEY AUTOINCREMENT, inst_id INTEGER, inst_name TEXT, stock_code TEXT, quarter TEXT, -- 2026Q2 hold_shares REAL DEFAULT 0, -- 持仓(万股) hold_value REAL DEFAULT 0, -- 持仓市值(万元) change_shares REAL DEFAULT 0, -- 环比增减(万股) change_pct REAL DEFAULT 0, -- 环比增减 % UNIQUE(inst_id, stock_code, quarter) ); CREATE INDEX IF NOT EXISTS idx_holdings_code ON fund_holdings(stock_code); CREATE TABLE IF NOT EXISTS watchlist ( code TEXT PRIMARY KEY, added_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE TABLE IF NOT EXISTS analysis_cache ( code TEXT PRIMARY KEY, report TEXT, created_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE TABLE IF NOT EXISTS analysis_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, stock_name TEXT DEFAULT '', focus TEXT DEFAULT '', report TEXT, sources TEXT DEFAULT '{}', -- JSON:大模型参考的数据源(RAG新闻/概况/指标/评级/持仓/提示词) created_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE INDEX IF NOT EXISTS idx_history_code ON analysis_history(code); CREATE TABLE IF NOT EXISTS strategy_backtests ( strategy TEXT NOT NULL, code TEXT NOT NULL, stock_name TEXT DEFAULT '', metrics TEXT DEFAULT '{}', -- JSON:收益/回撤/夏普/胜率等 equity TEXT DEFAULT '[]', -- JSON:[{date,value,bh}, ...] 净值曲线 trades TEXT DEFAULT '[]', -- JSON:交易明细 run_at TEXT DEFAULT (datetime('now','localtime')), PRIMARY KEY (strategy, code) ); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT DEFAULT '' ); CREATE TABLE IF NOT EXISTS notification_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, news_id INTEGER DEFAULT 0, title TEXT DEFAULT '', category TEXT DEFAULT '', sentiment REAL DEFAULT 0, importance REAL DEFAULT 0, related TEXT DEFAULT '', status TEXT DEFAULT 'sent', -- sent / failed message TEXT DEFAULT '', sent_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE TABLE IF NOT EXISTS tracking_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, target_type TEXT DEFAULT 'stock', -- stock(股票) / concept(概念主题) code TEXT NOT NULL, stock_name TEXT DEFAULT '', industry TEXT DEFAULT '', report TEXT DEFAULT '', -- 产业链/概念深度分析(markdown) meta TEXT DEFAULT '{}', -- JSON:significance/impact_score/summary/新闻统计 sources TEXT DEFAULT '{}', -- JSON:个股/上游/下游/同业 采集的资讯 + 指标 + 评级 status TEXT DEFAULT 'done', created_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE INDEX IF NOT EXISTS idx_tracking_code ON tracking_reports(code); CREATE TABLE IF NOT EXISTS watch_targets ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT DEFAULT 'concept', -- concept(概念/主题) / stock(股票) code TEXT DEFAULT '', -- stock 时的股票代码 name TEXT NOT NULL, -- 概念/主题名 或 股票名 keywords TEXT DEFAULT '', -- concept 的检索关键词(逗号分隔) enabled INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE TABLE IF NOT EXISTS report_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT DEFAULT '', -- premarket / postmarket subject TEXT DEFAULT '', brief_len INTEGER DEFAULT 0, detail_len INTEGER DEFAULT 0, status TEXT DEFAULT 'sent', message TEXT DEFAULT '', sent_at TEXT DEFAULT (datetime('now','localtime')) ); CREATE TABLE IF NOT EXISTS market_index ( date TEXT PRIMARY KEY, sh REAL DEFAULT 0, -- 上证指数(点) sz REAL DEFAULT 0, -- 深证成指(点) cy REAL DEFAULT 0 -- 创业板指(点) ); CREATE TABLE IF NOT EXISTS global_markets ( date TEXT PRIMARY KEY, data TEXT DEFAULT '{}' -- JSON:全球主要指数 {key: {value, chg}} ); """ def get_conn(): conn = getattr(_local, "conn", None) if conn is None: conn = sqlite3.connect(DB_PATH, timeout=30) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA busy_timeout=30000") _local.conn = conn return conn @contextmanager def db(): conn = get_conn() try: yield conn conn.commit() except Exception: conn.rollback() raise def init_db(): with db() as conn: conn.executescript(SCHEMA) # 迁移:老库补列 cols = {r[1] for r in conn.execute("PRAGMA table_info(tracking_reports)")} if "target_type" not in cols: conn.execute("ALTER TABLE tracking_reports ADD COLUMN target_type TEXT DEFAULT 'stock'") def query(sql, args=()): with db() as conn: cur = conn.execute(sql, args) return [dict(r) for r in cur.fetchall()] def query_one(sql, args=()): rows = query(sql, args) return rows[0] if rows else None def execute(sql, args=()): with db() as conn: cur = conn.execute(sql, args) return cur.lastrowid def executemany(sql, seq): with db() as conn: conn.executemany(sql, seq) def table_count(name): return query_one(f'SELECT COUNT(*) AS c FROM "{name}"')["c"] def wipe_all(): """清空业务表(保留结构)+ 重置自增序列,用于重灌数据""" for t in ("stock_daily", "inst_ratings", "fund_holdings", "news", "institutions", "stocks", "watchlist", "analysis_cache", "analysis_history", "market_index", "strategy_backtests", "notification_log", "tracking_reports", "watch_targets", "global_markets", "report_log"): with db() as conn: conn.execute(f'DELETE FROM "{t}"') with db() as conn: conn.execute("DELETE FROM sqlite_sequence") # 重置 AUTOINCREMENT,保证重灌后 ID 从 1 开始 # settings 保留(用户配置不清空)