Files
stock-advisor/database.py
T

244 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 '{}', -- JSONsignificance/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 market_index (
date TEXT PRIMARY KEY,
sh REAL DEFAULT 0, -- 上证指数(点)
sz REAL DEFAULT 0, -- 深证成指(点)
cy REAL DEFAULT 0 -- 创业板指(点)
);
"""
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"):
with db() as conn:
conn.execute(f'DELETE FROM "{t}"')
with db() as conn:
conn.execute("DELETE FROM sqlite_sequence") # 重置 AUTOINCREMENT,保证重灌后 ID 从 1 开始
# settings 保留(用户配置不清空)