feat: 量化引擎加固 — 新增测试 + 数据/因子/回测层优化

- 新增 finance/tests/ 6 个测试套件(agents/backtest/dao_upsert/factors/features/fundamental_lookahead)
- 数据层: data_manager / dao 优化,新增 upsert 逻辑
- 因子层: 基本面因子抽象定位 _mapping、ROE/PE/PB 重构
- 回测层: vectorbt/engine 大改动(251 行),report 增强
- ML 层: features/backtest_integration 特征工程与回测优化
- CLI: agent_cli 重构
- config/settings 扩充配置项
This commit is contained in:
Simon
2026-08-31 14:01:06 +08:00
parent 6acf938caf
commit 73d191b43a
28 changed files with 1418 additions and 373 deletions
+52 -22
View File
@@ -5,6 +5,7 @@ DataManager — 统一数据管理层。
优先从 DB 读取,缺失时依次尝试 AkShare → Tushare 拉取并入库。
"""
import logging
import time
import pandas as pd
@@ -15,6 +16,27 @@ from data.sources.tushare_source import TushareSource
from database import dao
from database.models import create_all_tables
logger = logging.getLogger(__name__)
def _rollback_days(yyyymmdd: str, days: int) -> str:
"""把 'YYYYMMDD' 往前回退 days 个自然日,返回 'YYYYMMDD'。"""
from datetime import datetime, timedelta
dt = datetime.strptime(yyyymmdd, "%Y%m%d") - timedelta(days=days)
return dt.strftime("%Y%m%d")
def _safe_save_daily(df: pd.DataFrame, skip_exc: bool = True) -> None:
"""把拉取到的日线入库;失败时记日志(可选抛错)。"""
cols = [c for c in dao._DAILY_COLS if c in df.columns]
try:
dao.save_daily(df[cols])
except Exception as e:
if skip_exc:
logger.exception("[DataManager] 日线入库失败(数据已获取,未持久化):%s", e)
else:
raise
class DataManager:
"""统一数据管理。双数据源:AkShare(主)+ Tushare(备)。"""
@@ -68,7 +90,7 @@ class DataManager:
if df is not None and not df.empty:
return df, label
except Exception as e:
print(" [{}] {} 失败: {}".format(label, method_name, e))
logger.warning(" [%s] %s 失败: %s", label, method_name, e)
return pd.DataFrame(), None
# ── 股票列表 ──────────────────────────────────────────
@@ -77,15 +99,15 @@ class DataManager:
if not force_refresh:
df = dao.query_stock_list()
if not df.empty:
print("[DataManager] 从 DB 读取股票列表: {} 只".format(len(df)))
logger.info("[DataManager] 从 DB 读取股票列表: %s 只", len(df))
return df
print("[DataManager] 拉取股票列表 (AkShare → Tushare)...")
logger.info("[DataManager] 拉取股票列表 (AkShare → Tushare)...")
df, src = self._try_fetch("fetch_stock_list")
if df.empty:
print("[DataManager] 所有数据源均无法获取股票列表")
logger.warning("[DataManager] 所有数据源均无法获取股票列表")
return pd.DataFrame()
print("[DataManager] 股票列表已入库 ({}): {} 只".format(src, len(df)))
logger.info("[DataManager] 股票列表已入库 (%s): %s 只", src, len(df))
dao.save_stock_list(df)
time.sleep(2)
return df
@@ -105,40 +127,48 @@ class DataManager:
if not force_refresh:
df = dao.query_daily(ts_code, start, end)
if not df.empty:
# 检查 DB 窗口尾部是否明显落后于请求的 end(>5 个自然日)。
# 若是,说明存在数据缺口,触发一次增量补拉,而不是把陈旧数据当完整返回。
latest = str(df["trade_date"].astype(str).max())
if end >= "20200101" and latest < _rollback_days(end, 7):
logger.warning(
"%s DB 窗口尾部 %s 明显落后于请求日 %s,触发补拉", ts_code, latest, end)
refreshed, src = self._try_fetch("fetch_daily", ts_code, latest, end)
if not refreshed.empty and src:
_safe_save_daily(refreshed)
return df
# DB 未命中 → 从数据源拉取
df, src = self._try_fetch("fetch_daily", ts_code, start, end)
if df.empty:
print("[WARN] {} 日线获取失败 (AkShare+Tushare 均不可用)".format(ts_code))
logger.warning("%s 日线获取失败 (AkShare+Tushare 均不可用)", 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))
# 保存到 DB(失败仅记日志,不阻断已获取到的数据返回)
_safe_save_daily(df)
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))
logger.info("[DataManager] %s 数据已是最新 (%s)", 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))
logger.warning("[DataManager] %s sync_daily 失败 (AkShare+Tushare 均不可用)", 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))
try:
dao.save_daily(df[cols])
except Exception as e:
logger.exception("[DataManager] %s 日线入库失败:%s", ts_code, e)
raise
logger.info("[DataManager] %s 同步 %s 条日线 (%s)", ts_code, len(df), src)
return len(df)
def sync_all_daily(self) -> int:
@@ -148,11 +178,11 @@ class DataManager:
try:
total += self.sync_daily(ts_code)
if (i + 1) % 50 == 0:
print("[DataManager] 进度: {}/{}".format(i + 1, len(stock_list)))
logger.info("[DataManager] 进度: %s/%s", i + 1, len(stock_list))
time.sleep(1)
except Exception as e:
print("[WARN] {} 同步失败: {}".format(ts_code, e))
print("[DataManager] 全量同步完成,新增 {} 条".format(total))
logger.warning("[DataManager] %s 同步失败: %s", ts_code, e)
logger.info("[DataManager] 全量同步完成,新增 %s 条", total)
return total
# ── 财务数据 ──────────────────────────────────────────
@@ -167,5 +197,5 @@ class DataManager:
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))
logger.exception("财务数据入库失败: %s", e)
return df