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:
2026-06-07 15:59:05 +08:00
co-authored by Claude Opus 4.7
commit 271a9343a5
293 changed files with 59598 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
"""
DataManager — 统一数据管理层。
策略/模型层通过 DataManager 获取数据,不直接访问 AkShare/Tushare 或数据库。
优先从 DB 读取,缺失时依次尝试 AkShare → Tushare 拉取并入库。
"""
import time
import pandas as pd
from config.settings import DEFAULT_START_DATE, DEFAULT_END_DATE
from data.sources.akshare_source import AkShareSource
from data.sources.tushare_source import TushareSource
from database import dao
from database.models import create_all_tables
class DataManager:
"""统一数据管理。双数据源:AkShare(主)+ Tushare(备)。"""
def __init__(self):
self._ak_source: AkShareSource | None = None
self._ts_source: TushareSource | None = None
@property
def ak(self) -> AkShareSource:
if self._ak_source is None:
self._ak_source = AkShareSource()
return self._ak_source
@property
def ts(self) -> TushareSource:
if self._ts_source is None:
self._ts_source = TushareSource()
return self._ts_source
# ── 初始化 ────────────────────────────────────────────
def init_db(self) -> None:
create_all_tables()
# ── 内部:双源 try ────────────────────────────────────
def _try_fetch(self, method_name: str, *args, **kwargs):
"""
依次尝试 AkShare → Tushare 调用同一方法名。
method_name: 'fetch_daily' | 'fetch_stock_list' | 'fetch_financial'
返回: (result_df, source_name) 或 (empty_df, None)
如果 ts_code 是指数代码,自动路由到 fetch_index_daily。
"""
from data.sources.akshare_source import is_index_code
# 指数自动路由
if method_name == "fetch_daily" and args:
ts_code = args[0]
if is_index_code(ts_code):
method_name = "fetch_index_daily"
for label, source in [("Tushare", self.ts), ("AkShare", self.ak)]:
try:
if label == "Tushare" and not source.available:
continue
fn = getattr(source, method_name)
df = fn(*args, **kwargs)
if df is not None and not df.empty:
return df, label
except Exception as e:
print(" [{}] {} 失败: {}".format(label, method_name, e))
return pd.DataFrame(), None
# ── 股票列表 ──────────────────────────────────────────
def get_stock_list(self, force_refresh: bool = False) -> pd.DataFrame:
if not force_refresh:
df = dao.query_stock_list()
if not df.empty:
print("[DataManager] 从 DB 读取股票列表: {}".format(len(df)))
return df
print("[DataManager] 拉取股票列表 (AkShare → Tushare)...")
df, src = self._try_fetch("fetch_stock_list")
if df.empty:
print("[DataManager] 所有数据源均无法获取股票列表")
return pd.DataFrame()
print("[DataManager] 股票列表已入库 ({}): {}".format(src, len(df)))
dao.save_stock_list(df)
time.sleep(2)
return df
# ── 日线数据 ──────────────────────────────────────────
def get_daily(
self,
ts_code: str,
start: str | None = None,
end: str | None = None,
force_refresh: bool = False,
) -> pd.DataFrame:
start = start or DEFAULT_START_DATE
end = end or DEFAULT_END_DATE or time.strftime("%Y%m%d")
if not force_refresh:
df = dao.query_daily(ts_code, start, end)
if not df.empty:
return df
# DB 未命中 → 从数据源拉取
df, src = self._try_fetch("fetch_daily", ts_code, start, end)
if df.empty:
print("[WARN] {} 日线获取失败 (AkShare+Tushare 均不可用)".format(ts_code))
return pd.DataFrame()
# 保存到 DB
try:
# 筛选 DB 需要的列
cols = [c for c in dao._DAILY_COLS if c in df.columns]
dao.save_daily(df[cols])
except Exception as e:
print("[WARN] 日线入库失败: {}".format(e))
return df
def sync_daily(self, ts_code: str) -> int:
latest = dao.get_latest_trade_date(ts_code)
today = time.strftime("%Y%m%d")
if latest and latest >= today:
print("[DataManager] {} 数据已是最新 ({})".format(ts_code, latest))
return 0
start = latest or DEFAULT_START_DATE
df, src = self._try_fetch("fetch_daily", ts_code, start, today)
if df.empty:
print("[WARN] {} sync_daily 失败 (AkShare+Tushare 均不可用)".format(ts_code))
return 0
cols = [c for c in dao._DAILY_COLS if c in df.columns]
dao.save_daily(df[cols])
print("[DataManager] {} 同步 {} 条日线 ({})".format(ts_code, len(df), src))
return len(df)
def sync_all_daily(self) -> int:
stock_list = self.get_stock_list()
total = 0
for i, ts_code in enumerate(stock_list.index):
try:
total += self.sync_daily(ts_code)
if (i + 1) % 50 == 0:
print("[DataManager] 进度: {}/{}".format(i + 1, len(stock_list)))
time.sleep(1)
except Exception as e:
print("[WARN] {} 同步失败: {}".format(ts_code, e))
print("[DataManager] 全量同步完成,新增 {}".format(total))
return total
# ── 财务数据 ──────────────────────────────────────────
def get_financial(self, ts_code: str) -> pd.DataFrame:
df = dao.query_financial(ts_code)
if not df.empty:
return df
df, src = self._try_fetch("fetch_financial", ts_code)
if not df.empty:
try:
cols = [c for c in dao._FINA_COLS if c in df.columns]
dao.save_financial(df[cols])
except Exception as e:
print("[WARN] 财务数据入库失败: {}".format(e))
return df