Initial commit: cc-cursor 全链路量化研究平台
7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
MariaDB 连接管理。
|
||||
|
||||
使用 SQLAlchemy 连接池,通过 SSH 隧道访问远程数据库。
|
||||
连接失败时自动尝试重建 SSH 隧道并重连。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from config.settings import MARIADB_CONFIG
|
||||
|
||||
_engine = None
|
||||
_SessionLocal = None
|
||||
|
||||
|
||||
def _build_url() -> str:
|
||||
cfg = MARIADB_CONFIG
|
||||
return (
|
||||
"mysql+pymysql://{}:{}"
|
||||
"@{}:{}/{}"
|
||||
"?charset={}"
|
||||
).format(cfg["user"], cfg["password"], cfg["host"], cfg["port"], cfg["database"], cfg["charset"])
|
||||
|
||||
|
||||
def _reconnect_ssh() -> bool:
|
||||
"""自动运行 autossh.sh 重建 SSH 隧道。每次调用都会尝试。"""
|
||||
|
||||
candidates = [
|
||||
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)))), "shared", "script", "autossh.sh"),
|
||||
os.path.expanduser("~/Downloads/cc-cursor/shared/script/autossh.sh"),
|
||||
]
|
||||
script = None
|
||||
for c in candidates:
|
||||
if os.path.exists(c):
|
||||
script = c
|
||||
break
|
||||
|
||||
if script is None:
|
||||
print("[DB] SSH 脚本未找到")
|
||||
return False
|
||||
|
||||
try:
|
||||
print("[DB] 尝试重建 SSH 隧道: {}".format(script))
|
||||
result = subprocess.run(["bash", script], capture_output=True, text=True, timeout=15)
|
||||
time.sleep(3) # 等待隧道建立
|
||||
if result.returncode == 0:
|
||||
print("[DB] SSH 隧道重建完成")
|
||||
return True
|
||||
else:
|
||||
print("[DB] SSH 隧道重建失败: {}".format(result.stderr[:200]))
|
||||
return False
|
||||
except Exception as e:
|
||||
print("[DB] SSH 执行异常: {}".format(e))
|
||||
return False
|
||||
|
||||
|
||||
def _test_engine(engine) -> bool:
|
||||
"""测试引擎连接是否存活。"""
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text("SELECT 1"))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_engine():
|
||||
"""获取 SQLAlchemy Engine(懒初始化,断连时自动重连)。"""
|
||||
global _engine, _SessionLocal
|
||||
|
||||
# 已有引擎但连接断开 → dispose 旧池 → SSH 重连 → 重建引擎
|
||||
if _engine is not None and not _test_engine(_engine):
|
||||
print("[DB] 连接已断开,尝试自动恢复...")
|
||||
_engine.dispose() # 杀死所有旧连接
|
||||
_engine = None
|
||||
if _reconnect_ssh():
|
||||
print("[DB] 连接已恢复")
|
||||
else:
|
||||
print("[DB] 自动恢复失败,将无法连接数据库")
|
||||
|
||||
# 创建引擎(首次或重建后)
|
||||
if _engine is None:
|
||||
cfg = MARIADB_CONFIG
|
||||
_engine = create_engine(
|
||||
_build_url(),
|
||||
pool_size=cfg["pool_size"],
|
||||
pool_recycle=cfg["pool_recycle"],
|
||||
pool_pre_ping=True, # 每次取连接前先 SELECT 1 验证存活
|
||||
echo=False,
|
||||
)
|
||||
# Session factory 下次获取时自动绑定新引擎
|
||||
_SessionLocal = None
|
||||
|
||||
return _engine
|
||||
|
||||
|
||||
def get_session():
|
||||
"""获取一个新的数据库会话。"""
|
||||
global _SessionLocal
|
||||
if _SessionLocal is None:
|
||||
_SessionLocal = sessionmaker(bind=get_engine())
|
||||
return _SessionLocal()
|
||||
|
||||
|
||||
def test_connection() -> bool:
|
||||
"""测试数据库连接是否正常。"""
|
||||
try:
|
||||
engine = get_engine()
|
||||
return _test_engine(engine)
|
||||
except Exception as e:
|
||||
print("[ERROR] 数据库连接失败: {}".format(e))
|
||||
return False
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
数据访问对象。
|
||||
|
||||
提供 DataFrame 级别的读写操作,屏蔽底层 ORM/SQL 细节。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import text
|
||||
|
||||
from database.connection import get_engine
|
||||
from database.models import StockBasic, StockDaily, StockFinancial, Report
|
||||
|
||||
# DB 表列名,供 DataManager 在写入前筛选
|
||||
_DAILY_COLS = [
|
||||
"ts_code", "trade_date", "open", "high", "low", "close",
|
||||
"pre_close", "change", "pct_chg", "vol", "amount", "turnover_rate",
|
||||
]
|
||||
_FINA_COLS = [
|
||||
"ts_code", "end_date", "eps", "bvps", "roe", "roe_diluted",
|
||||
"net_profit_margin", "debt_to_assets", "current_ratio", "quick_ratio",
|
||||
"total_revenue", "total_revenue_yoy", "net_profit", "net_profit_yoy",
|
||||
]
|
||||
|
||||
|
||||
def _df_to_db(df: pd.DataFrame, model_class, replace: bool = False) -> int:
|
||||
"""将 DataFrame 写入对应表,返回写入行数。"""
|
||||
if df.empty:
|
||||
return 0
|
||||
engine = get_engine()
|
||||
if_action = "replace" if replace else "append"
|
||||
# 统一字符串列,避免 MySQL 类型问题
|
||||
df = df.where(pd.notna(df), None)
|
||||
rows = len(df)
|
||||
df.to_sql(
|
||||
model_class.__tablename__,
|
||||
con=engine,
|
||||
if_exists=if_action,
|
||||
index=False,
|
||||
method="multi",
|
||||
chunksize=500,
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
# ── StockBasic ─────────────────────────────────────────────
|
||||
|
||||
def save_stock_list(df: pd.DataFrame) -> int:
|
||||
"""保存股票列表(replace 模式)。"""
|
||||
cols = ["ts_code", "name", "area", "industry", "market", "list_date", "is_hs"]
|
||||
df = df[[c for c in cols if c in df.columns]].copy()
|
||||
return _df_to_db(df, StockBasic, replace=True)
|
||||
|
||||
|
||||
def query_stock_list() -> pd.DataFrame:
|
||||
"""查询全部股票列表。"""
|
||||
engine = get_engine()
|
||||
return pd.read_sql(f"SELECT * FROM {StockBasic.__tablename__}", con=engine).set_index("ts_code")
|
||||
|
||||
|
||||
# ── StockDaily ─────────────────────────────────────────────
|
||||
|
||||
def save_daily(df: pd.DataFrame) -> int:
|
||||
"""批量写入日线数据。先删旧再插新,避免主键冲突。"""
|
||||
cols = [
|
||||
"ts_code", "trade_date", "open", "high", "low", "close",
|
||||
"pre_close", "change", "pct_chg", "vol", "amount", "turnover_rate",
|
||||
]
|
||||
df = df[[c for c in cols if c in df.columns]].copy()
|
||||
if df.empty:
|
||||
return 0
|
||||
# 删除即将写入的日期的旧数据
|
||||
engine = get_engine()
|
||||
ts_codes = df["ts_code"].unique().tolist()
|
||||
trade_dates = df["trade_date"].unique().tolist()
|
||||
if ts_codes and trade_dates:
|
||||
with engine.connect() as conn:
|
||||
conn.execute(
|
||||
text("DELETE FROM {} WHERE ts_code IN :codes AND trade_date IN :dates".format(
|
||||
StockDaily.__tablename__)),
|
||||
{"codes": tuple(ts_codes), "dates": tuple(trade_dates)},
|
||||
)
|
||||
conn.commit()
|
||||
return _df_to_db(df, StockDaily, replace=False)
|
||||
|
||||
|
||||
def query_daily(ts_code: str, start: str | None = None, end: str | None = None) -> pd.DataFrame:
|
||||
"""按股票代码和日期范围查询日线。"""
|
||||
engine = get_engine()
|
||||
table = StockDaily.__tablename__
|
||||
sql = f"SELECT * FROM {table} WHERE ts_code = :ts_code"
|
||||
params = {"ts_code": ts_code}
|
||||
if start:
|
||||
sql += " AND trade_date >= :start"
|
||||
params["start"] = start
|
||||
if end:
|
||||
sql += " AND trade_date <= :end"
|
||||
params["end"] = end
|
||||
sql += " ORDER BY trade_date ASC"
|
||||
df = pd.read_sql(text(sql), con=engine, params=params)
|
||||
if not df.empty:
|
||||
df["trade_date"] = df["trade_date"].astype(str)
|
||||
return df
|
||||
|
||||
|
||||
def get_latest_trade_date(ts_code: str) -> str | None:
|
||||
"""获取某股票在数据库中的最新交易日。"""
|
||||
engine = get_engine()
|
||||
table = StockDaily.__tablename__
|
||||
sql = f"SELECT MAX(trade_date) FROM {table} WHERE ts_code = :ts_code"
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(text(sql), {"ts_code": ts_code}).scalar()
|
||||
return result
|
||||
|
||||
|
||||
# ── StockFinancial ─────────────────────────────────────────
|
||||
|
||||
def save_financial(df: pd.DataFrame) -> int:
|
||||
"""批量写入财务数据(replace 模式:同报告期覆盖更新)。"""
|
||||
cols = [
|
||||
"ts_code", "end_date", "eps", "bvps", "roe", "roe_diluted",
|
||||
"net_profit_margin", "debt_to_assets", "current_ratio", "quick_ratio",
|
||||
"total_revenue", "total_revenue_yoy", "net_profit", "net_profit_yoy",
|
||||
]
|
||||
df = df[[c for c in cols if c in df.columns]].copy()
|
||||
return _df_to_db(df, StockFinancial, replace=True)
|
||||
|
||||
|
||||
def query_financial(ts_code: str) -> pd.DataFrame:
|
||||
"""查询某股票全部财务数据。"""
|
||||
engine = get_engine()
|
||||
table = StockFinancial.__tablename__
|
||||
sql = f"SELECT * FROM {table} WHERE ts_code = :ts_code ORDER BY end_date DESC"
|
||||
return pd.read_sql(text(sql), con=engine, params={"ts_code": ts_code})
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
ORM 模型定义。
|
||||
|
||||
所有表使用 mac_ 前缀,与现有表隔离。
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Date, DateTime, Float, BigInteger, Index, PrimaryKeyConstraint, Text
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from config.settings import TABLE_STOCK_BASIC, TABLE_STOCK_DAILY, TABLE_STOCK_FINANCIAL, TABLE_REPORT
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class StockBasic(Base):
|
||||
"""股票基本信息表。"""
|
||||
__tablename__ = TABLE_STOCK_BASIC
|
||||
|
||||
ts_code = Column(String(16), primary_key=True, comment="股票代码(如 000001.SZ)")
|
||||
name = Column(String(32), comment="股票名称")
|
||||
area = Column(String(16), comment="地区")
|
||||
industry = Column(String(32), comment="行业")
|
||||
market = Column(String(8), comment="市场(主板/创业板/科创板)")
|
||||
list_date = Column(String(8), comment="上市日期")
|
||||
is_hs = Column(String(1), comment="是否沪深港通")
|
||||
|
||||
|
||||
class StockDaily(Base):
|
||||
"""日线行情表。"""
|
||||
__tablename__ = TABLE_STOCK_DAILY
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("ts_code", "trade_date"),
|
||||
Index("idx_mac_daily_ts_code", "ts_code"),
|
||||
Index("idx_mac_daily_trade_date", "trade_date"),
|
||||
)
|
||||
|
||||
ts_code = Column(String(16), comment="股票代码")
|
||||
trade_date = Column(String(8), comment="交易日期")
|
||||
open = Column(Float, comment="开盘价")
|
||||
high = Column(Float, comment="最高价")
|
||||
low = Column(Float, comment="最低价")
|
||||
close = Column(Float, comment="收盘价")
|
||||
pre_close = Column(Float, comment="昨收价")
|
||||
change = Column(Float, comment="涨跌额")
|
||||
pct_chg = Column(Float, comment="涨跌幅(%)")
|
||||
vol = Column(Float, comment="成交量(手)")
|
||||
amount = Column(Float, comment="成交额(千元)")
|
||||
turnover_rate = Column(Float, comment="换手率(%)")
|
||||
|
||||
|
||||
class StockFinancial(Base):
|
||||
"""财务数据表(同花顺核心指标)。"""
|
||||
__tablename__ = TABLE_STOCK_FINANCIAL
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("ts_code", "end_date"),
|
||||
Index("idx_mac_fina_ts_code", "ts_code"),
|
||||
)
|
||||
|
||||
ts_code = Column(String(16), comment="股票代码")
|
||||
end_date = Column(String(8), comment="报告期 YYYYMMDD")
|
||||
eps = Column(Float, comment="基本每股收益")
|
||||
bvps = Column(Float, comment="每股净资产")
|
||||
roe = Column(Float, comment="净资产收益率(%)")
|
||||
roe_diluted = Column(Float, comment="净资产收益率-摊薄(%)")
|
||||
net_profit_margin = Column(Float, comment="销售净利率(%)")
|
||||
debt_to_assets = Column(Float, comment="资产负债率(%)")
|
||||
current_ratio = Column(Float, comment="流动比率")
|
||||
quick_ratio = Column(Float, comment="速动比率")
|
||||
total_revenue = Column(Float, comment="营业总收入")
|
||||
total_revenue_yoy = Column(Float, comment="营业总收入同比增长率(%)")
|
||||
net_profit = Column(Float, comment="净利润")
|
||||
net_profit_yoy = Column(Float, comment="净利润同比增长率(%)")
|
||||
|
||||
|
||||
class Report(Base):
|
||||
"""报告存储表。"""
|
||||
__tablename__ = TABLE_REPORT
|
||||
__table_args__ = (
|
||||
Index("idx_mac_report_date", "report_date"),
|
||||
Index("idx_mac_report_subject", "subject_type", "subject_code"),
|
||||
Index("idx_mac_report_active", "is_active"),
|
||||
)
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment="主键")
|
||||
report_date = Column(Date, nullable=False, comment="报告日期")
|
||||
title = Column(String(256), nullable=False, comment="报告标题")
|
||||
subject_type = Column(String(32), comment="研究对象类型: stock/index/sector/portfolio/daily")
|
||||
subject_code = Column(String(64), comment="研究对象代码")
|
||||
content = Column(Text, comment="报告内容 (markdown)")
|
||||
created_at = Column(DateTime, comment="报告生成时间")
|
||||
is_active = Column(Float, default=1.0, comment="1=有效, 0=已失效")
|
||||
|
||||
|
||||
def create_all_tables():
|
||||
"""创建所有 mac_ 开头的表。"""
|
||||
engine = __import__("database.connection", fromlist=["get_engine"]).get_engine()
|
||||
Base.metadata.create_all(engine)
|
||||
print("[OK] 所有表创建完成")
|
||||
Reference in New Issue
Block a user