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
+183
View File
@@ -0,0 +1,183 @@
"""
统一数据源模块 — djapi/api/stock/ 的单一数据入口。
归一化 Tushare / AkShare / MySQL 三种数据源。
所有模块通过此入口获取数据连接,不再各自创建 pro 实例。
用法:
from .data_source import get_tushare_pro, get_mysql_db
pro = get_tushare_pro()
df = pro.daily(ts_code='000001.SZ', ...)
db = get_mysql_db()
rows = db.query("SELECT * FROM xwlb_daily WHERE ...")
扩展新数据源:
class NewSource: ...
_sources['new'] = NewSource()
def get_new_source(): return _sources['new']
"""
import os
import threading
import tushare as ts
# 延迟加载 AkShare(避免不必要的导入开销)
_akshare = None
def _get_akshare():
global _akshare
if _akshare is None:
import akshare as ak
_akshare = ak
return _akshare
# ═══════════════════════════════════════════════════════════
# Token 加载
# ═══════════════════════════════════════════════════════════
def get_ts_token() -> str:
"""获取 Tushare Token,优先级:环境变量 TUSHARE_TS_TOKEN > TUSHARE_TOKEN > config.py"""
token = os.getenv("TUSHARE_TS_TOKEN", "") or os.getenv("TUSHARE_TOKEN", "")
if not token:
try:
from .config import TS_TOKEN
token = TS_TOKEN
except ImportError:
try:
from config import TS_TOKEN
token = TS_TOKEN
except ImportError:
pass
return token
# ═══════════════════════════════════════════════════════════
# Tushare Pro 连接池(线程安全单例)
# ═══════════════════════════════════════════════════════════
_pro_lock = threading.Lock()
_pro = None
def get_tushare_pro():
"""获取 Tushare pro_api 实例(全局单例,线程安全)。"""
global _pro
if _pro is not None:
return _pro
with _pro_lock:
if _pro is not None:
return _pro
token = get_ts_token()
ts.set_token(token)
_pro = ts.pro_api()
return _pro
def reset_tushare_pro():
"""重置 Tushare 连接(token 变更时调用)。"""
global _pro
with _pro_lock:
_pro = None
# ═══════════════════════════════════════════════════════════
# MySQL 连接
# ═══════════════════════════════════════════════════════════
_mysql_db = None
def get_mysql_db():
"""获取 MySQLDB 实例(全局单例)。"""
global _mysql_db
if _mysql_db is not None:
return _mysql_db
try:
from ..utils.mysql_handler import MySQLDB
except (ImportError, ValueError):
try:
from utils.mysql_handler import MySQLDB
except ImportError:
return None
_mysql_db = MySQLDB()
return _mysql_db
# ═══════════════════════════════════════════════════════════
# 日线行情 — 双源 fallbackTushare → AkShare
# ═══════════════════════════════════════════════════════════
def get_daily(ts_code: str, start_date: str, end_date: str, source: str = "tushare"):
"""
获取个股日线行情。
参数:
ts_code: 如 '000001.SZ'
start_date: YYYYMMDD
end_date: YYYYMMDD
source: 'tushare' | 'akshare' | 'auto' (tushare优先)
返回:
pd.DataFrame (trade_date, open, high, low, close, vol, amount, ...)
"""
import pandas as pd
if source == "auto":
# Tushare 优先
try:
df = get_daily(ts_code, start_date, end_date, source="tushare")
if df is not None and not df.empty:
return df
except Exception:
pass
return get_daily(ts_code, start_date, end_date, source="akshare")
if source == "tushare":
pro = get_tushare_pro()
df = pro.daily(
ts_code=ts_code, start_date=start_date, end_date=end_date,
fields="ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"
)
if df is not None and not df.empty:
df["trade_date"] = df["trade_date"].astype(str)
return df
if source == "akshare":
symbol = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
ak = _get_akshare()
df = ak.stock_zh_a_hist(
symbol=symbol, period="daily",
start_date=start_date, end_date=end_date, adjust="qfq"
)
if df is not None and not df.empty:
df = df.rename(columns={
"日期": "trade_date", "开盘": "open", "收盘": "close",
"最高": "high", "最低": "low", "成交量": "vol", "成交额": "amount",
"涨跌幅": "pct_chg", "涨跌额": "change",
})
df["ts_code"] = ts_code
df["trade_date"] = df["trade_date"].astype(str)
return df if df is not None else pd.DataFrame()
raise ValueError("Unknown source: {}".format(source))
# ═══════════════════════════════════════════════════════════
# 扩展点:未来新增数据源
# ═══════════════════════════════════════════════════════════
#
# 1. 在 _sources dict 中注册新源
# 2. 实现与 get_daily() 相同签名的函数
# 3. 在 get_daily(source=...) 中添加路由
#
# _sources = {
# "tushare": TushareDailySource(),
# "akshare": AkShareDailySource(),
# "wind": WindDailySource(), # 未来
# "joinquant": JoinQuantSource(), # 未来
# }