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>
184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
"""
|
||
统一数据源模块 — 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
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 日线行情 — 双源 fallback(Tushare → 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(), # 未来
|
||
# }
|