astock-agent/backend/app/db/database.py
2026-06-10 08:36:25 +08:00

172 lines
12 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.

"""SQLAlchemy 异步数据库配置"""
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import event
from app.config import settings
# SQLite 异步需要 aiosqlite
db_url = settings.database_url.replace("sqlite:///", "sqlite+aiosqlite:///")
engine = create_async_engine(db_url, echo=False)
# 启用 WAL 模式:允许读写并发,写操作不阻塞读操作
@event.listens_for(engine.sync_engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA busy_timeout=5000") # 写锁最多等待 5 秒
cursor.close()
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@asynccontextmanager
async def get_db():
session = async_session()
try:
yield session
finally:
await session.close()
async def init_db():
"""创建所有表,并补充新增列"""
from app.db.tables import metadata
async with engine.begin() as conn:
await conn.run_sync(metadata.create_all)
# 补充新增列SQLite ALTER TABLE ADD COLUMN已存在会忽略
for col_sql in [
"ALTER TABLE recommendations ADD COLUMN supply_demand_score REAL DEFAULT 0",
"ALTER TABLE recommendations ADD COLUMN price_action_score REAL DEFAULT 0",
"ALTER TABLE recommendations ADD COLUMN position_score REAL",
"ALTER TABLE recommendations ADD COLUMN valuation_score REAL",
"ALTER TABLE recommendations ADD COLUMN risk_note TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN action_plan TEXT DEFAULT '观察'",
"ALTER TABLE recommendations ADD COLUMN trigger_condition TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN invalidation_condition TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN suggested_position_pct REAL DEFAULT 0",
"ALTER TABLE recommendations ADD COLUMN review_after_days INTEGER DEFAULT 3",
"ALTER TABLE recommendations ADD COLUMN lifecycle_status TEXT DEFAULT 'candidate'",
"ALTER TABLE recommendations ADD COLUMN data_freshness TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN llm_analysis TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN strategy TEXT DEFAULT 'momentum'",
"ALTER TABLE recommendations ADD COLUMN llm_score REAL",
"ALTER TABLE market_temperature ADD COLUMN max_streak INTEGER",
"ALTER TABLE market_temperature ADD COLUMN broken_rate REAL",
"ALTER TABLE recommendations ADD COLUMN entry_signal_type TEXT DEFAULT 'none'",
"ALTER TABLE recommendations ADD COLUMN entry_timing TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN recall_tags TEXT DEFAULT '[]'",
"ALTER TABLE recommendations ADD COLUMN prefilter_decision TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN prefilter_reason TEXT DEFAULT ''",
"ALTER TABLE recommendations ADD COLUMN focus_points TEXT DEFAULT '[]'",
"ALTER TABLE recommendations ADD COLUMN decision_trace TEXT DEFAULT '{}'",
"ALTER TABLE sector_heat ADD COLUMN stage TEXT",
"ALTER TABLE sector_heat ADD COLUMN board_type TEXT DEFAULT 'theme'",
"ALTER TABLE sector_heat ADD COLUMN theme_id TEXT DEFAULT ''",
"ALTER TABLE sector_heat ADD COLUMN theme_name TEXT DEFAULT ''",
"ALTER TABLE sector_heat ADD COLUMN theme_aliases TEXT DEFAULT '[]'",
"ALTER TABLE sector_heat ADD COLUMN days_continuous INTEGER",
"ALTER TABLE sector_heat ADD COLUMN member_count INTEGER",
"ALTER TABLE sector_heat ADD COLUMN leading_stocks TEXT",
"ALTER TABLE sector_heat ADD COLUMN pct_trend TEXT",
"ALTER TABLE sector_heat ADD COLUMN turnover_avg REAL",
"ALTER TABLE sector_heat ADD COLUMN main_force_ratio REAL",
"ALTER TABLE recommendation_tracking ADD COLUMN max_price REAL",
"ALTER TABLE recommendation_tracking ADD COLUMN min_price REAL",
"ALTER TABLE recommendation_tracking ADD COLUMN max_return_pct REAL",
"ALTER TABLE recommendation_tracking ADD COLUMN max_drawdown_pct REAL",
"ALTER TABLE recommendation_tracking ADD COLUMN days_since_recommendation INTEGER DEFAULT 0",
"ALTER TABLE recommendation_tracking ADD COLUMN close_reason TEXT DEFAULT ''",
"ALTER TABLE recommendation_tracking ADD COLUMN review_note TEXT DEFAULT ''",
"ALTER TABLE users ADD COLUMN email TEXT",
"ALTER TABLE users ADD COLUMN invite_code_used TEXT DEFAULT ''",
"ALTER TABLE stock_diagnoses ADD COLUMN diagnosis_mode TEXT DEFAULT 'entry'",
"ALTER TABLE user_watchlists ADD COLUMN note TEXT DEFAULT ''",
"ALTER TABLE user_watchlists ADD COLUMN watch_group TEXT DEFAULT 'observe'",
"ALTER TABLE user_watchlists ADD COLUMN cost_price REAL",
"ALTER TABLE user_watchlists ADD COLUMN is_active BOOLEAN DEFAULT 1",
"ALTER TABLE user_watchlists ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
"ALTER TABLE watchlist_analyses ADD COLUMN conclusion TEXT DEFAULT '观察'",
"ALTER TABLE watchlist_analyses ADD COLUMN advice TEXT DEFAULT ''",
"ALTER TABLE watchlist_analyses ADD COLUMN trigger_condition TEXT DEFAULT ''",
"ALTER TABLE watchlist_analyses ADD COLUMN risk_note TEXT DEFAULT ''",
"ALTER TABLE watchlist_analyses ADD COLUMN summary TEXT DEFAULT ''",
"ALTER TABLE watchlist_analyses ADD COLUMN full_analysis TEXT DEFAULT ''",
"ALTER TABLE watchlist_analyses ADD COLUMN score_reference REAL DEFAULT 0",
"ALTER TABLE watchlist_analyses ADD COLUMN analysis_mode TEXT DEFAULT 'scheduled'",
"ALTER TABLE strategy_configs ADD COLUMN evidence_json TEXT DEFAULT '{}'",
"ALTER TABLE strategy_configs ADD COLUMN effective_from DATETIME DEFAULT CURRENT_TIMESTAMP",
"ALTER TABLE prompt_configs ADD COLUMN evidence_json TEXT DEFAULT '{}'",
"ALTER TABLE strategy_config_changes ADD COLUMN prompt_key TEXT DEFAULT ''",
"ALTER TABLE news_items ADD COLUMN summary TEXT DEFAULT ''",
"ALTER TABLE news_items ADD COLUMN error TEXT DEFAULT ''",
"ALTER TABLE catalysts ADD COLUMN llm_reason TEXT DEFAULT ''",
"ALTER TABLE stock_research_notes ADD COLUMN disagreement TEXT DEFAULT ''",
"ALTER TABLE stock_research_notes ADD COLUMN invalid_condition TEXT DEFAULT ''",
"ALTER TABLE stock_research_notes ADD COLUMN generated_by TEXT DEFAULT 'rules'",
"ALTER TABLE stock_research_notes ADD COLUMN stock_role TEXT DEFAULT '待归类'",
"ALTER TABLE stock_research_notes ADD COLUMN theme TEXT DEFAULT '未归类'",
"ALTER TABLE stock_research_notes ADD COLUMN chain_node TEXT DEFAULT '未归类'",
"ALTER TABLE opportunity_cards ADD COLUMN stock_role TEXT DEFAULT '待归类'",
"ALTER TABLE opportunity_cards ADD COLUMN alpha_type TEXT DEFAULT '观察线索'",
"ALTER TABLE opportunity_cards ADD COLUMN alpha_score REAL DEFAULT 0",
"ALTER TABLE opportunity_cards ADD COLUMN beta_dependency TEXT DEFAULT ''",
"ALTER TABLE opportunity_cards ADD COLUMN beta_dependency_score REAL DEFAULT 0",
"ALTER TABLE opportunity_cards ADD COLUMN ambush_score REAL DEFAULT 0",
"ALTER TABLE opportunity_cards ADD COLUMN expectation_gap_score REAL DEFAULT 0",
"ALTER TABLE opportunity_cards ADD COLUMN risk_gate TEXT DEFAULT '通过'",
"ALTER TABLE opportunity_cards ADD COLUMN setup_quality TEXT DEFAULT '仅观察'",
"ALTER TABLE opportunity_cards ADD COLUMN alpha_reason TEXT DEFAULT ''",
"ALTER TABLE theme_knowledge ADD COLUMN lifecycle_status TEXT DEFAULT '观察期'",
"ALTER TABLE theme_knowledge ADD COLUMN stage TEXT DEFAULT 'mid'",
"ALTER TABLE theme_knowledge ADD COLUMN is_active BOOLEAN DEFAULT 1",
"ALTER TABLE theme_knowledge ADD COLUMN sort_order INTEGER DEFAULT 0",
"ALTER TABLE theme_knowledge ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
"ALTER TABLE theme_chain_knowledge ADD COLUMN related_stocks TEXT DEFAULT '[]'",
"ALTER TABLE theme_chain_knowledge ADD COLUMN leader_stocks TEXT DEFAULT '[]'",
"ALTER TABLE theme_chain_knowledge ADD COLUMN node_role TEXT DEFAULT ''",
"ALTER TABLE theme_chain_knowledge ADD COLUMN is_active BOOLEAN DEFAULT 1",
"ALTER TABLE theme_chain_knowledge ADD COLUMN sort_order INTEGER DEFAULT 0",
"ALTER TABLE theme_chain_knowledge ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP",
]:
try:
await conn.execute(
__import__("sqlalchemy").text(col_sql)
)
except Exception:
pass # 列已存在,忽略
for index_sql in [
"CREATE UNIQUE INDEX IF NOT EXISTS idx_news_items_dedup_key ON news_items(dedup_key)",
"CREATE INDEX IF NOT EXISTS idx_news_items_status_time ON news_items(status, published_at)",
"CREATE INDEX IF NOT EXISTS idx_catalysts_source_url ON catalysts(source, url)",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_theme_knowledge_name ON theme_knowledge(theme_name)",
"CREATE INDEX IF NOT EXISTS idx_theme_knowledge_active_order ON theme_knowledge(is_active, sort_order)",
"CREATE INDEX IF NOT EXISTS idx_theme_chain_knowledge_theme ON theme_chain_knowledge(theme_name, is_active, sort_order)",
"CREATE INDEX IF NOT EXISTS idx_scan_process_session_time ON scan_process_logs(scan_session, created_at)",
"CREATE INDEX IF NOT EXISTS idx_scan_process_stage_time ON scan_process_logs(stage, created_at)",
"CREATE INDEX IF NOT EXISTS idx_research_observations_session_score ON research_observations(scan_session, final_score)",
"CREATE INDEX IF NOT EXISTS idx_research_observations_code_time ON research_observations(ts_code, created_at)",
"CREATE INDEX IF NOT EXISTS idx_research_observations_theme_time ON research_observations(theme_name, created_at)",
"CREATE INDEX IF NOT EXISTS idx_research_reports_session_time ON research_reports(scan_session, created_at)",
"CREATE INDEX IF NOT EXISTS idx_research_reports_trade_date ON research_reports(trade_date, created_at)",
"CREATE INDEX IF NOT EXISTS idx_theme_maps_trade_date_score ON theme_maps(trade_date, heat_score)",
"CREATE INDEX IF NOT EXISTS idx_theme_chain_theme ON theme_chain_nodes(theme_name, trade_date)",
"CREATE INDEX IF NOT EXISTS idx_stock_research_code_time ON stock_research_notes(ts_code, created_at)",
"CREATE INDEX IF NOT EXISTS idx_risk_events_session_reject ON risk_events(scan_session, reject)",
"CREATE INDEX IF NOT EXISTS idx_opportunity_cards_session_score ON opportunity_cards(scan_session, score)",
]:
try:
await conn.execute(__import__("sqlalchemy").text(index_sql))
except Exception:
pass
try:
await conn.execute(
__import__("sqlalchemy").text(
"UPDATE users SET email = username WHERE email IS NULL OR email = ''"
)
)
except Exception:
pass