Compare commits

..
12 Commits
11 changed files with 2251 additions and 17 deletions
Binary file not shown.
+283
View File
@@ -0,0 +1,283 @@
"""
数据加载辅助模块 — 从量化数据库加载回测所需数据
支持的数据源:
- 日线行情 (daily + adj_factor → 后复权价格)
- 每日指标 (daily_basic → PE / PB / 市值 / 换手率)
- 财务指标 (fina_indicator → ROE / ROA / 毛利率)
- 指数行情 (index_daily → 基准收益)
若数据库连接失败,自动回退到随机模拟数据,便于 Notebook 演示完整研究流程。
用法:
from data_loader import DataLoader
loader = DataLoader()
price_data = loader.load_prices('2020-01-01', '2024-12-31')
pe_data = loader.load_factor('pe', '2020-01-01', '2024-12-31')
roe_data = loader.load_factor('roe', '2020-01-01', '2024-12-31')
bench = loader.load_benchmark('000300.SH', '2020-01-01', '2024-12-31')
"""
import sys
from pathlib import Path
from typing import Dict, List, Optional
import numpy as np
import pandas as pd
# 项目根目录 (quanxiel/)
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
class DataLoader:
"""量化数据库数据加载器(数据库不可用时自动回退到模拟数据)"""
# 默认股票池(沪深300中代表性的蓝筹成分股)
DEFAULT_STOCKS: List[str] = [
# 银行 / 非银
"600000.SH", "600015.SH", "600016.SH", "600036.SH", "601166.SH",
"601288.SH", "601398.SH", "601318.SH", "601601.SH", "601628.SH",
# 消费 / 医药
"600519.SH", "000858.SZ", "000651.SZ", "000333.SZ", "600887.SH",
"600276.SH", "601888.SH",
# 工业 / 制造
"600031.SH", "600048.SH", "600104.SH", "600309.SH", "600585.SH",
"601668.SH", "601088.SH", "600028.SH", "601857.SH",
# 科技 / 成长
"000063.SZ", "002415.SZ", "300750.SZ", "600009.SH", "600050.SH",
]
# daily_basic 表可用字段
DAILY_BASIC_FIELDS = {
"pe", "pe_ttm", "pb", "ps", "ps_ttm", "dv_ratio", "dv_ttm",
"total_mv", "circ_mv", "turnover_rate", "volume_ratio",
}
# fina_indicator 表可用字段
FINA_FIELDS = {
"roe", "roe_dt", "roa", "roa2", "roic", "gross_margin",
"eps", "dt_eps", "current_ratio", "quick_ratio", "debt_to_assets",
"assets_turn", "cfo_to_or", "ocfps",
}
def __init__(self, stocks: Optional[List[str]] = None, seed: int = 42):
self.stocks = stocks or self.DEFAULT_STOCKS
self.seed = seed
self._conn = None
self._db_ok: Optional[bool] = None
# ==================================================================
# 公开接口
# ==================================================================
def load_prices(self, start_date: str, end_date: str) -> pd.DataFrame:
"""
加载后复权收盘价。
返回 DataFrame: index=date, columns=ts_code
"""
if self._ensure_db():
try:
return self._load_prices_db(start_date, end_date)
except Exception as e:
print(f"[DataLoader] 价格数据加载失败,回退到模拟数据: {e}")
return self._simulate_prices(start_date, end_date)
def load_factor(self, name: str, start_date: str, end_date: str) -> pd.DataFrame:
"""
加载指定因子/指标截面数据。
返回 DataFrame: index=date, columns=ts_code
"""
if name in self.DAILY_BASIC_FIELDS and self._ensure_db():
try:
return self._load_daily_basic_field(name, start_date, end_date)
except Exception as e:
print(f"[DataLoader] 指标 {name} 加载失败,回退到模拟数据: {e}")
elif name in self.FINA_FIELDS and self._ensure_db():
try:
return self._load_fina_field(name, start_date, end_date)
except Exception as e:
print(f"[DataLoader] 财务指标 {name} 加载失败,回退到模拟数据: {e}")
return self._simulate_factor(name, start_date, end_date)
def load_factors(self, names: List[str], start_date: str, end_date: str) -> Dict[str, pd.DataFrame]:
"""批量加载多个因子,返回 {name: DataFrame}"""
return {n: self.load_factor(n, start_date, end_date) for n in names}
def load_benchmark(
self, code: str = "000300.SH", start_date: str = "", end_date: str = ""
) -> pd.Series:
"""
加载基准指数收盘价。
返回 Series: index=date, values=close
"""
if start_date and end_date and self._ensure_db():
try:
return self._load_index_db(code, start_date, end_date)
except Exception as e:
print(f"[DataLoader] 指数 {code} 加载失败,回退到模拟数据: {e}")
# 模拟基准:用股票池均值价格代替
prices = self._simulate_prices(start_date or "2020-01-01", end_date or "2024-12-31")
return prices.mean(axis=1).rename(code)
def get_trade_dates(self, start_date: str, end_date: str) -> pd.DatetimeIndex:
"""获取交易日列表(数据库不可用时使用工作日)"""
if self._ensure_db():
try:
conn = self._get_conn()
query = """
SELECT DISTINCT cal_date FROM trade_cal
WHERE is_open = 1 AND cal_date BETWEEN %s AND %s
ORDER BY cal_date
"""
dates = pd.read_sql(query, conn, params=[start_date, end_date])
return pd.to_datetime(dates["cal_date"])
except Exception:
pass
return pd.bdate_range(start_date, end_date)
# ==================================================================
# 数据库连接
# ==================================================================
def _get_conn(self):
if self._conn is None:
sys.path.insert(0, str(_PROJECT_ROOT / "quantitative_data"))
from config import DB_CONFIG
import psycopg2
self._conn = psycopg2.connect(
host=DB_CONFIG["host"],
port=DB_CONFIG["port"],
database=DB_CONFIG["database"],
user=DB_CONFIG["user"],
password=DB_CONFIG["password"],
connect_timeout=5,
)
return self._conn
def _ensure_db(self) -> bool:
"""检测数据库是否可用(结果缓存)"""
if self._db_ok is not None:
return self._db_ok
try:
self._get_conn()
self._db_ok = True
except Exception:
self._conn = None
self._db_ok = False
print("[DataLoader] 数据库连接失败,使用模拟数据。"
"请检查 quantitative_data/.env 配置。")
return self._db_ok
# ==================================================================
# 数据库加载实现
# ==================================================================
def _stock_sql_placeholders(self) -> str:
return ",".join(["%s"] * len(self.stocks))
def _load_prices_db(self, start_date: str, end_date: str) -> pd.DataFrame:
conn = self._get_conn()
placeholders = self._stock_sql_placeholders()
query = f"""
SELECT d.ts_code, d.trade_date,
d.close * a.adj_factor AS adj_close
FROM daily d
JOIN adj_factor a
ON d.ts_code = a.ts_code AND d.trade_date = a.trade_date
WHERE d.ts_code IN ({placeholders})
AND d.trade_date BETWEEN %s AND %s
ORDER BY d.trade_date, d.ts_code
"""
df = pd.read_sql(query, conn, params=[*self.stocks, start_date, end_date])
if df.empty:
raise ValueError("查询无数据")
pivot = df.pivot(index="trade_date", columns="ts_code", values="adj_close")
pivot.index = pd.to_datetime(pivot.index)
pivot = pivot.sort_index()
# 只保留有数据的列
return pivot.dropna(how="all", axis=1)
def _load_daily_basic_field(self, field: str, start_date: str, end_date: str) -> pd.DataFrame:
conn = self._get_conn()
placeholders = self._stock_sql_placeholders()
query = f"""
SELECT ts_code, trade_date, {field}
FROM daily_basic
WHERE ts_code IN ({placeholders})
AND trade_date BETWEEN %s AND %s
ORDER BY trade_date, ts_code
"""
df = pd.read_sql(query, conn, params=[*self.stocks, start_date, end_date])
if df.empty:
raise ValueError(f"查询 {field} 无数据")
pivot = df.pivot(index="trade_date", columns="ts_code", values=field)
pivot.index = pd.to_datetime(pivot.index)
pivot = pivot.sort_index()
return pivot.replace([np.inf, -np.inf], np.nan)
def _load_fina_field(self, field: str, start_date: str, end_date: str) -> pd.DataFrame:
conn = self._get_conn()
placeholders = self._stock_sql_placeholders()
query = f"""
SELECT ts_code, end_date, {field}
FROM fina_indicator
WHERE ts_code IN ({placeholders})
AND end_date BETWEEN %s AND %s
ORDER BY end_date, ts_code
"""
df = pd.read_sql(query, conn, params=[*self.stocks, start_date, end_date])
if df.empty:
raise ValueError(f"查询财务指标 {field} 无数据")
pivot = df.pivot(index="end_date", columns="ts_code", values=field)
pivot.index = pd.to_datetime(pivot.index)
# 重采样到交易日并向前填充
trade_dates = self.get_trade_dates(start_date, end_date)
pivot = pivot.sort_index().reindex(trade_dates).ffill()
return pivot.replace([np.inf, -np.inf], np.nan)
def _load_index_db(self, code: str, start_date: str, end_date: str) -> pd.Series:
conn = self._get_conn()
query = """
SELECT trade_date, close
FROM index_daily
WHERE ts_code = %s AND trade_date BETWEEN %s AND %s
ORDER BY trade_date
"""
df = pd.read_sql(query, conn, params=[code, start_date, end_date])
if df.empty:
raise ValueError(f"指数 {code} 无数据")
s = pd.Series(df["close"].values, index=pd.to_datetime(df["trade_date"]))
return s.sort_index().rename(code)
# ==================================================================
# 模拟数据(数据库不可用时的回退)
# ==================================================================
def _simulate_prices(self, start_date: str, end_date: str) -> pd.DataFrame:
np.random.seed(self.seed)
dates = pd.bdate_range(start_date, end_date)
n_dates, n_stocks = len(dates), len(self.stocks)
# 几何布朗运动模拟股价
rets = np.random.randn(n_dates, n_stocks) * 0.02
rets[:, 0] *= 0.5 # 让第一只股票波动小(制造多样化)
rets[:, 1] *= 1.5 # 让第二只股票波动大
prices = 100 * np.exp(np.cumsum(rets, axis=0))
df = pd.DataFrame(prices, index=dates, columns=self.stocks)
return df
def _simulate_factor(self, name: str, start_date: str, end_date: str) -> pd.DataFrame:
np.random.seed(self.seed)
dates = pd.bdate_range(start_date, end_date)
n = len(self.stocks)
data = np.random.randn(len(dates), n)
# 部分指标需要为正(估值/基本面)
positive_fields = {
"pe", "pe_ttm", "pb", "ps", "ps_ttm", "dv_ratio", "dv_ttm",
"total_mv", "circ_mv", "turnover_rate", "volume_ratio",
"roe", "roe_dt", "roa", "roa2", "roic", "gross_margin",
"eps", "dt_eps", "current_ratio", "quick_ratio", "debt_to_assets",
"assets_turn", "cfo_to_or", "ocfps",
}
if name in positive_fields:
data = np.abs(data) + 0.5
return pd.DataFrame(data, index=dates, columns=self.stocks)
+296
View File
@@ -0,0 +1,296 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 价值投资策略 — 构建与回测\n",
"\n",
"## 策略思路\n",
"价值投资(Value Investing):买入相对于基本面被低估的股票。\n",
"本策略使用 **市盈率 PE 倒数(EP)** 与 **市净率 PB 倒数(BP** 合成价值因子,\n",
"每月调仓,持有估值最低(价值因子最高)的一批股票。\n",
"\n",
"## 研究流程\n",
"1. 加载数据(价格 + PE/PB 估值指标)\n",
"2. 价值因子计算与 IC 分析\n",
"3. 分层回测验证因子有效性\n",
"4. 构建策略并回测\n",
"5. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from alpha.config import AlphaConfig\n",
"from alpha.factors import FactorAnalyzer\n",
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
"from alpha.backtest import BacktestEngine\n",
"from alpha.evaluation import PerformanceEvaluator\n",
"from alpha.data_loader import DataLoader\n",
"\n",
"%matplotlib inline\n",
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print('模块导入成功')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 加载数据\n",
"\n",
"价值因子需要估值数据(PE / PB),通过 `DataLoader.load_factor` 加载。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"START_DATE = '2020-01-01'\n",
"END_DATE = '2024-12-31'\n",
"\n",
"loader = DataLoader()\n",
"\n",
"# 价格数据 + 估值数据\n",
"price_data = loader.load_prices(START_DATE, END_DATE)\n",
"pe_data = loader.load_factor('pe_ttm', START_DATE, END_DATE) # 市盈率 TTM\n",
"pb_data = loader.load_factor('pb', START_DATE, END_DATE) # 市净率\n",
"\n",
"print(f'价格数据: {price_data.shape}')\n",
"print(f'PE(TTM)数据: {pe_data.shape}')\n",
"print(f'PB数据: {pb_data.shape}')\n",
"\n",
"# 展示某天的估值截面\n",
"sample = pd.DataFrame({\n",
" 'PE_TTM': pe_data.iloc[-1],\n",
" 'PB': pb_data.iloc[-1],\n",
"}).dropna()\n",
"sample.head(10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 价值因子计算与 IC 分析\n",
"\n",
"价值因子 = 标准化后的 (1/PE + 1/PB)\n",
"$$\n",
"Value = Z\\left(\\frac{1}{PE}\\right) + Z\\left(\\frac{1}{PB}\\right)\n",
"$$\n",
"PE/PB 越低 → 价值因子越高 → 越被低估。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 剔除 PE/PB 非正值(亏损或净资产为负)\n",
"ep = 1.0 / pe_data.where(pe_data > 0)\n",
"bp = 1.0 / pb_data.where(pb_data > 0)\n",
"\n",
"# 截面标准化后合成\n",
"def zscore_cross_section(df):\n",
" return (df - df.mean(axis=1)) / (df.std(axis=1) + 1e-12)\n",
"\n",
"value_factor = zscore_cross_section(ep) + zscore_cross_section(bp)\n",
"\n",
"# 未来 20 日收益率(价值因子预测周期较长)\n",
"fwd_20d = price_data.pct_change(20).shift(-20)\n",
"\n",
"factor_stacked = value_factor.stack()\n",
"fwd_stacked = fwd_20d.stack()\n",
"\n",
"analyzer = FactorAnalyzer(factor_stacked, fwd_stacked)\n",
"ic = analyzer.compute_ic(method='rank')\n",
"\n",
"print('价值因子 IC 汇总:')\n",
"for k, v in analyzer.ic_summary().items():\n",
" print(f' {k}: {v:.4f}')\n",
"\n",
"ic.plot(figsize=(12, 3), title='价值因子 Rank IC 时间序列')\n",
"plt.axhline(y=0, color='r', linestyle='--')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 分层回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
"print('各分位组平均收益 (Q1=最贵, Q5=最便宜):')\n",
"print(quantile_ret)\n",
"\n",
"quantile_ret['avg_return'].plot(kind='bar', figsize=(8, 4), color='steelblue')\n",
"plt.title('价值因子分层收益 (未来20日)')\n",
"plt.ylabel('平均收益率')\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 构建策略并回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测配置\n",
"config = AlphaConfig(\n",
" initial_cash=1_000_000,\n",
" commission_rate=0.0003,\n",
" slippage=0.001,\n",
" stamp_tax=0.001,\n",
")\n",
"\n",
"# 价值策略:持有价值因子最高的 20% 股票(最被低估)\n",
"strategy = Strategy(\n",
" name='价值投资策略-EP+BP',\n",
" factors=[],\n",
" signal_generator=QuantileSignal(\n",
" n_quantiles=5, long_quantile=5, short_quantile=0,\n",
" ),\n",
" weight_allocator=EqualWeightAllocator(max_positions=10),\n",
" description='每月持有EP+BP合成的价值因子最高的20%股票'\n",
")\n",
"\n",
"engine = BacktestEngine(config)\n",
"equity_curve = engine.run(\n",
" strategy=strategy,\n",
" price_data=price_data,\n",
" factor_data={'value': value_factor},\n",
" rebalance_freq='M',\n",
")\n",
"\n",
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 净值曲线与回撤"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
"\n",
"# 净值曲线\n",
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='purple')\n",
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
"axes[0].set_title('价值投资策略净值曲线')\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# 回撤曲线\n",
"nav = equity_curve['nav']\n",
"running_max = nav.cummax()\n",
"drawdown = (nav - running_max) / running_max\n",
"axes[1].fill_between(equity_curve.index, 0, drawdown.values, color='red', alpha=0.3)\n",
"axes[1].set_title('回撤曲线')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 基准:股票池等权组合\n",
"bench_ret = price_data.pct_change().mean(axis=1)\n",
"\n",
"evaluator = PerformanceEvaluator(equity_curve, benchmark_returns=bench_ret, risk_free_rate=0.03)\n",
"print(evaluator.summary())\n",
"\n",
"report_df = evaluator.full_report()\n",
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 单独估值指标对比\n",
"\n",
"对比单独使用 EP、BP 与合成价值因子的效果:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 单独因子的 IC 对比\n",
"for name, fac in [('EP', ep), ('BP', bp), ('EP+BP', value_factor)]:\n",
" a = FactorAnalyzer(fac.stack(), fwd_stacked)\n",
" s = a.ic_summary()\n",
" print(f'{name}: IC均值={s[\"IC_Mean\"]:.4f}, ICIR={s[\"IR\"]:.4f}, '\n",
" f'IC>0占比={s[\"IC>0_Ratio\"]:.2%}')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+277
View File
@@ -0,0 +1,277 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 低波动率策略 — 构建与回测\n",
"\n",
"## 策略思路\n",
"低波动率异常(Low Volatility Anomaly)指波动率较低的股票,长期来看风险调整后收益往往优于高波动股票。\n",
"本策略使用 **20 日波动率倒数** 作为低波因子,每月调仓,买入波动率最低的一批股票。\n",
"\n",
"## 研究流程\n",
"1. 加载数据\n",
"2. 低波因子计算与 IC 分析\n",
"3. 分层回测验证因子有效性\n",
"4. 构建策略并回测\n",
"5. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from alpha.config import AlphaConfig\n",
"from alpha.factors import FactorAnalyzer\n",
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
"from alpha.backtest import BacktestEngine\n",
"from alpha.evaluation import PerformanceEvaluator\n",
"from alpha.data_loader import DataLoader\n",
"\n",
"%matplotlib inline\n",
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print('模块导入成功')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 加载数据"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测区间\n",
"START_DATE = '2020-01-01'\n",
"END_DATE = '2024-12-31'\n",
"\n",
"loader = DataLoader()\n",
"\n",
"# 后复权收盘价\n",
"price_data = loader.load_prices(START_DATE, END_DATE)\n",
"print(f'价格数据: {price_data.shape}, 日期: {price_data.index[0]} ~ {price_data.index[-1]}')\n",
"price_data.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 低波因子计算与 IC 分析"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 低波因子: 20日波动率的倒数(波动越小 → 因子值越大)\n",
"VOL_WINDOW = 20\n",
"daily_ret = price_data.pct_change()\n",
"volatility = daily_ret.rolling(VOL_WINDOW).std()\n",
"low_vol_factor = -volatility # 取负号使低波动为正\n",
"\n",
"# 未来 10 日收益率(验证预测能力,低波策略通常持有期较长)\n",
"fwd_10d = price_data.pct_change(10).shift(-10)\n",
"\n",
"factor_stacked = low_vol_factor.stack()\n",
"fwd_stacked = fwd_10d.stack()\n",
"\n",
"analyzer = FactorAnalyzer(factor_stacked, fwd_stacked)\n",
"ic = analyzer.compute_ic(method='rank')\n",
"\n",
"print('低波因子 IC 汇总:')\n",
"for k, v in analyzer.ic_summary().items():\n",
" print(f' {k}: {v:.4f}')\n",
"\n",
"# IC 曲线\n",
"ic.plot(figsize=(12, 3), title='低波因子 Rank IC 时间序列')\n",
"plt.axhline(y=0, color='r', linestyle='--')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 分层回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 分 5 组,观察单调性(Q1=高波动, Q5=低波动)\n",
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
"print('各分位组平均收益 (Q1=最高波动, Q5=最低波动):')\n",
"print(quantile_ret)\n",
"\n",
"quantile_ret['avg_return'].plot(kind='bar', figsize=(8, 4), color='steelblue')\n",
"plt.title('低波因子分层收益 (未来10日)')\n",
"plt.ylabel('平均收益率')\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 构建策略并回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测配置\n",
"config = AlphaConfig(\n",
" initial_cash=1_000_000,\n",
" commission_rate=0.0003,\n",
" slippage=0.001,\n",
" stamp_tax=0.001,\n",
")\n",
"\n",
"# 低波策略:持有波动率最低的 20% 股票,等权\n",
"strategy = Strategy(\n",
" name='低波动率策略-20日',\n",
" factors=[],\n",
" signal_generator=QuantileSignal(\n",
" n_quantiles=5, long_quantile=5, short_quantile=0, # 只做多\n",
" ),\n",
" weight_allocator=EqualWeightAllocator(max_positions=10),\n",
" description='每月持有过去20日波动率最低的20%股票,等权配置'\n",
")\n",
"\n",
"engine = BacktestEngine(config)\n",
"equity_curve = engine.run(\n",
" strategy=strategy,\n",
" price_data=price_data,\n",
" factor_data={'low_vol': low_vol_factor},\n",
" rebalance_freq='M',\n",
")\n",
"\n",
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 净值曲线与回撤"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
"\n",
"# 净值曲线\n",
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='green')\n",
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
"axes[0].set_title('低波动率策略净值曲线')\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# 回撤曲线\n",
"nav = equity_curve['nav']\n",
"running_max = nav.cummax()\n",
"drawdown = (nav - running_max) / running_max\n",
"axes[1].fill_between(equity_curve.index, 0, drawdown.values, color='red', alpha=0.3)\n",
"axes[1].set_title('回撤曲线')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 基准:股票池等权组合\n",
"bench_ret = price_data.pct_change().mean(axis=1)\n",
"\n",
"evaluator = PerformanceEvaluator(equity_curve, benchmark_returns=bench_ret, risk_free_rate=0.03)\n",
"print(evaluator.summary())\n",
"\n",
"report_df = evaluator.full_report()\n",
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 波动率窗口敏感性分析\n",
"\n",
"不同波动率计算窗口对因子表现的影响:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 不同波动率窗口对比\n",
"for win in [5, 10, 20, 40]:\n",
" vol = daily_ret.rolling(win).std()\n",
" fac = -vol\n",
" a = FactorAnalyzer(fac.stack(), fwd_stacked)\n",
" summary = a.ic_summary()\n",
" print(f'窗口 {win}日: IC均值={summary[\"IC_Mean\"]:.4f}, ICIR={summary[\"IR\"]:.4f}, '\n",
" f'IC>0占比={summary[\"IC>0_Ratio\"]:.2%}')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+307
View File
@@ -0,0 +1,307 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 动量策略 — 构建与回测\n",
"\n",
"## 策略思路\n",
"动量效应(Momentum Effect)指过去一段时间表现良好的股票,未来一段时间内往往继续表现良好。\n",
"本策略使用 **20 日收益率** 作为动量因子,每月调仓,买入过去表现最好的一个分位组股票。\n",
"\n",
"## 研究流程\n",
"1. 加载数据\n",
"2. 动量因子计算与 IC 分析\n",
"3. 分层回测验证因子有效性\n",
"4. 构建策略并回测\n",
"5. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from alpha.config import AlphaConfig\n",
"from alpha.factors import FactorAnalyzer\n",
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
"from alpha.backtest import BacktestEngine\n",
"from alpha.evaluation import PerformanceEvaluator, ReportGenerator\n",
"from alpha.data_loader import DataLoader\n",
"\n",
"%matplotlib inline\n",
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print('模块导入成功')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 加载数据"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测区间\n",
"START_DATE = '2020-01-01'\n",
"END_DATE = '2024-12-31'\n",
"\n",
"loader = DataLoader()\n",
"\n",
"# 后复权收盘价\n",
"price_data = loader.load_prices(START_DATE, END_DATE)\n",
"print(f'价格数据: {price_data.shape}, 日期: {price_data.index[0]} ~ {price_data.index[-1]}')\n",
"print(f'股票数量: {price_data.shape[1]}')\n",
"price_data.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 动量因子计算与 IC 分析"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 动量因子: 20日收益率(过去20个交易日涨跌幅)\n",
"MOM_WINDOW = 20\n",
"momentum = price_data.pct_change(MOM_WINDOW)\n",
"\n",
"# 未来 5 日收益率(验证短期预测能力)\n",
"fwd_5d = price_data.pct_change(5).shift(-5)\n",
"\n",
"# 堆叠为长表 (MultiIndex: date, stock)\n",
"factor_stacked = momentum.stack()\n",
"fwd_stacked = fwd_5d.stack()\n",
"\n",
"analyzer = FactorAnalyzer(factor_stacked, fwd_stacked)\n",
"ic = analyzer.compute_ic(method='rank')\n",
"\n",
"print('动量因子 IC 汇总:')\n",
"for k, v in analyzer.ic_summary().items():\n",
" print(f' {k}: {v:.4f}')\n",
"\n",
"# IC 曲线\n",
"ic.plot(figsize=(12, 3), title='动量因子 Rank IC 时间序列')\n",
"plt.axhline(y=0, color='r', linestyle='--')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 分层回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 分 5 组,观察单调性\n",
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
"print('各分位组平均收益 (Q1=最低动量, Q5=最高动量):')\n",
"print(quantile_ret)\n",
"\n",
"# 绘制分组收益条形图\n",
"quantile_ret['avg_return'].plot(kind='bar', figsize=(8, 4), color='steelblue')\n",
"plt.title('动量因子分层收益 (未来5日)')\n",
"plt.ylabel('平均收益率')\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 构建策略并回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测配置\n",
"config = AlphaConfig(\n",
" initial_cash=1_000_000,\n",
" commission_rate=0.0003,\n",
" slippage=0.001,\n",
" stamp_tax=0.001,\n",
")\n",
"\n",
"# 动量策略:Top 20% (Q5) 长期持有,等权\n",
"strategy = Strategy(\n",
" name='动量策略-20日Top20%',\n",
" factors=[],\n",
" signal_generator=QuantileSignal(\n",
" n_quantiles=5, long_quantile=5, short_quantile=0, # 只做多\n",
" ),\n",
" weight_allocator=EqualWeightAllocator(max_positions=10),\n",
" description='每月持有过去20日涨幅最高的20%股票,等权配置'\n",
")\n",
"\n",
"# 回测引擎\n",
"engine = BacktestEngine(config)\n",
"equity_curve = engine.run(\n",
" strategy=strategy,\n",
" price_data=price_data,\n",
" factor_data={'momentum': momentum},\n",
" rebalance_freq='M', # 月频调仓\n",
")\n",
"\n",
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 净值曲线与回撤"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
"\n",
"# 净值曲线\n",
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='steelblue')\n",
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
"axes[0].set_title('动量策略净值曲线')\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# 回撤曲线\n",
"nav = equity_curve['nav']\n",
"running_max = nav.cummax()\n",
"drawdown = (nav - running_max) / running_max\n",
"axes[1].fill_between(equity_curve.index, 0, drawdown.values, color='red', alpha=0.3)\n",
"axes[1].set_title('回撤曲线')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 基准:股票池等权组合\n",
"bench_ret = price_data.pct_change().mean(axis=1)\n",
"\n",
"evaluator = PerformanceEvaluator(equity_curve, benchmark_returns=bench_ret, risk_free_rate=0.03)\n",
"print(evaluator.summary())\n",
"\n",
"# 导出报告\n",
"report_df = evaluator.full_report()\n",
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. 交易记录分析"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"trades = engine.portfolio.trades\n",
"if trades:\n",
" trade_df = pd.DataFrame([\n",
" {'日期': t.date, '股票': t.stock, '方向': t.side,\n",
" '数量': t.quantity, '价格': t.price, '佣金': t.commission}\n",
" for t in trades\n",
" ])\n",
" print(f'总交易数: {len(trade_df)}')\n",
" print(f'买入: {(trade_df[\"方向\"]==\"buy\").sum()}, 卖出: {(trade_df[\"方向\"]==\"sell\").sum()}')\n",
" display(trade_df.head(20))\n",
"else:\n",
" print('无交易记录(可能数据量不足)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 参数敏感性分析\n",
"\n",
"动量窗口是影响策略表现的关键参数,可尝试不同窗口:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 不同动量窗口对比\n",
"for win in [10, 20, 40, 60]:\n",
" mom = price_data.pct_change(win)\n",
" fwd = price_data.pct_change(5).shift(-5)\n",
" a = FactorAnalyzer(mom.stack(), fwd.stack())\n",
" summary = a.ic_summary()\n",
" print(f'窗口 {win}日: IC均值={summary[\"IC_Mean\"]:.4f}, ICIR={summary[\"IR\"]:.4f}, '\n",
" f'IC>0占比={summary[\"IC>0_Ratio\"]:.2%}')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+274
View File
@@ -0,0 +1,274 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 均线趋势策略 — 构建与回测\n",
"\n",
"## 策略思路\n",
"趋势跟踪(Trend Following):价格位于均线上方且均线向上时做多,反之做空/空仓。\n",
"本策略使用 **股价偏离 20 日均线的 Z-Score** 作为趋势强度因子,\n",
"每月调仓,买入趋势强度排名靠前的股票。\n",
"\n",
"## 研究流程\n",
"1. 加载数据\n",
"2. 趋势因子计算与 IC 分析\n",
"3. 分层回测验证因子有效性\n",
"4. 构建策略并回测\n",
"5. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from alpha.config import AlphaConfig\n",
"from alpha.factors import FactorAnalyzer\n",
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
"from alpha.backtest import BacktestEngine\n",
"from alpha.evaluation import PerformanceEvaluator\n",
"from alpha.data_loader import DataLoader\n",
"\n",
"%matplotlib inline\n",
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print('模块导入成功')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 加载数据"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"START_DATE = '2020-01-01'\n",
"END_DATE = '2024-12-31'\n",
"\n",
"loader = DataLoader()\n",
"price_data = loader.load_prices(START_DATE, END_DATE)\n",
"print(f'价格数据: {price_data.shape}, 日期: {price_data.index[0]} ~ {price_data.index[-1]}')\n",
"price_data.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 趋势因子计算与 IC 分析\n",
"\n",
"趋势因子 = 价格偏离 20 日均线的幅度(百分比):\n",
"$$Trend = \\frac{Close}{MA_{20}} - 1$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"MA_WINDOW = 20\n",
"ma = price_data.rolling(MA_WINDOW).mean()\n",
"trend_factor = price_data / ma - 1 # 正偏离 = 上升趋势\n",
"\n",
"# 未来 5 日收益率\n",
"fwd_5d = price_data.pct_change(5).shift(-5)\n",
"\n",
"factor_stacked = trend_factor.stack()\n",
"fwd_stacked = fwd_5d.stack()\n",
"\n",
"analyzer = FactorAnalyzer(factor_stacked, fwd_stacked)\n",
"ic = analyzer.compute_ic(method='rank')\n",
"\n",
"print('趋势因子 IC 汇总:')\n",
"for k, v in analyzer.ic_summary().items():\n",
" print(f' {k}: {v:.4f}')\n",
"\n",
"ic.plot(figsize=(12, 3), title='趋势因子 Rank IC 时间序列')\n",
"plt.axhline(y=0, color='r', linestyle='--')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 分层回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
"print('各分位组平均收益 (Q1=最强下跌趋势, Q5=最强上升趋势):')\n",
"print(quantile_ret)\n",
"\n",
"quantile_ret['avg_return'].plot(kind='bar', figsize=(8, 4), color='steelblue')\n",
"plt.title('趋势因子分层收益 (未来5日)')\n",
"plt.ylabel('平均收益率')\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 构建策略并回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测配置\n",
"config = AlphaConfig(\n",
" initial_cash=1_000_000,\n",
" commission_rate=0.0003,\n",
" slippage=0.001,\n",
" stamp_tax=0.001,\n",
")\n",
"\n",
"# 趋势策略:持有趋势最强(均线偏离最大)的 20% 股票\n",
"strategy = Strategy(\n",
" name='均线趋势策略-MA20',\n",
" factors=[],\n",
" signal_generator=QuantileSignal(\n",
" n_quantiles=5, long_quantile=5, short_quantile=0,\n",
" ),\n",
" weight_allocator=EqualWeightAllocator(max_positions=10),\n",
" description='每月持有价格高于20日均线幅度最大的20%股票'\n",
")\n",
"\n",
"engine = BacktestEngine(config)\n",
"equity_curve = engine.run(\n",
" strategy=strategy,\n",
" price_data=price_data,\n",
" factor_data={'trend': trend_factor},\n",
" rebalance_freq='M',\n",
")\n",
"\n",
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 净值曲线与回撤"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
"\n",
"# 净值曲线\n",
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='orange')\n",
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
"axes[0].set_title('均线趋势策略净值曲线')\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# 回撤曲线\n",
"nav = equity_curve['nav']\n",
"running_max = nav.cummax()\n",
"drawdown = (nav - running_max) / running_max\n",
"axes[1].fill_between(equity_curve.index, 0, drawdown.values, color='red', alpha=0.3)\n",
"axes[1].set_title('回撤曲线')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 基准:股票池等权组合\n",
"bench_ret = price_data.pct_change().mean(axis=1)\n",
"\n",
"evaluator = PerformanceEvaluator(equity_curve, benchmark_returns=bench_ret, risk_free_rate=0.03)\n",
"print(evaluator.summary())\n",
"\n",
"report_df = evaluator.full_report()\n",
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 均线周期敏感性分析\n",
"\n",
"不同均线周期对趋势因子的影响:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 不同均线周期对比\n",
"for win in [5, 10, 20, 60]:\n",
" ma_tmp = price_data.rolling(win).mean()\n",
" fac = price_data / ma_tmp - 1\n",
" a = FactorAnalyzer(fac.stack(), fwd_stacked)\n",
" summary = a.ic_summary()\n",
" print(f'MA{win}: IC均值={summary[\"IC_Mean\"]:.4f}, ICIR={summary[\"IR\"]:.4f}, '\n",
" f'IC>0占比={summary[\"IC>0_Ratio\"]:.2%}')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+320
View File
@@ -0,0 +1,320 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 多因子选股策略 — 构建与回测\n",
"\n",
"## 策略思路\n",
"多因子模型(Multi-Factor Model)综合多个维度的因子,分散单一因子的风险,提升选股稳健性。\n",
"本策略合成 **四大类因子**\n",
"\n",
"| 因子类别 | 具体因子 | 代表逻辑 |\n",
"|---------|---------|---------|\n",
"| 动量 | 20日收益率 | 强者恒强 |\n",
"| 低波 | 20日波动率倒数 | 低波动溢价 |\n",
"| 价值 | EP (PE倒数) | 低估值 |\n",
"| 质量 | ROE | 高盈利能力 |\n",
"\n",
"每月调仓,综合得分最高的股票入选。\n",
"\n",
"## 研究流程\n",
"1. 加载数据\n",
"2. 各因子计算与 IC 分析\n",
"3. 等权合成多因子 + 分层回测\n",
"4. 构建策略并回测\n",
"5. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from alpha.config import AlphaConfig\n",
"from alpha.factors import FactorAnalyzer\n",
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
"from alpha.backtest import BacktestEngine\n",
"from alpha.evaluation import PerformanceEvaluator\n",
"from alpha.data_loader import DataLoader\n",
"\n",
"%matplotlib inline\n",
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print('模块导入成功')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 加载数据"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"START_DATE = '2020-01-01'\n",
"END_DATE = '2024-12-31'\n",
"\n",
"loader = DataLoader()\n",
"\n",
"# 价格 + 估值 + 财务\n",
"price_data = loader.load_prices(START_DATE, END_DATE)\n",
"pe_data = loader.load_factor('pe_ttm', START_DATE, END_DATE)\n",
"roe_data = loader.load_factor('roe', START_DATE, END_DATE)\n",
"\n",
"print(f'价格数据: {price_data.shape}')\n",
"print(f'PE(TTM)数据: {pe_data.shape}')\n",
"print(f'ROE数据: {roe_data.shape}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 各因子计算与 IC 分析"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ---- 截面标准化函数 ----\n",
"def zscore_cross_section(df):\n",
" return (df - df.mean(axis=1)) / (df.std(axis=1) + 1e-12)\n",
"\n",
"# ---- 1. 动量因子 (20日收益率) ----\n",
"momentum = price_data.pct_change(20)\n",
"z_mom = zscore_cross_section(momentum)\n",
"\n",
"# ---- 2. 低波因子 (20日波动率倒数) ----\n",
"daily_ret = price_data.pct_change()\n",
"volatility = daily_ret.rolling(20).std()\n",
"z_lowvol = -zscore_cross_section(volatility) # 波动越小越好\n",
"\n",
"# ---- 3. 价值因子 (EP = 1/PE) ----\n",
"ep = 1.0 / pe_data.where(pe_data > 0)\n",
"z_value = zscore_cross_section(ep)\n",
"\n",
"# ---- 4. 质量因子 (ROE) ----\n",
"z_quality = zscore_cross_section(roe_data)\n",
"\n",
"# ---- 汇总各因子 ----\n",
"factors = {\n",
" '动量': momentum,\n",
" '低波': -volatility,\n",
" '价值': ep,\n",
" '质量': roe_data,\n",
"}\n",
"\n",
"# 未来 10 日收益率\n",
"fwd_10d = price_data.pct_change(10).shift(-10)\n",
"\n",
"# 各因子 IC 对比\n",
"print('各因子 IC 汇总 (未来10日):')\n",
"ic_results = {}\n",
"for name, fac in factors.items():\n",
" a = FactorAnalyzer(fac.stack(), fwd_10d.stack())\n",
" s = a.ic_summary()\n",
" ic_results[name] = s\n",
" print(f' {name}: IC均值={s[\"IC_Mean\"]:.4f}, ICIR={s[\"IR\"]:.4f}, '\n",
" f'IC>0占比={s[\"IC>0_Ratio\"]:.2%}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 合成多因子与分层回测\n",
"\n",
"等权合成综合得分:\n",
"$$Score = \\frac{1}{4}(Z_{Momentum} + Z_{LowVol} + Z_{Value} + Z_{Quality})$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 等权合成综合得分\n",
"multi_factor = (z_mom + z_lowvol + z_value + z_quality) / 4\n",
"\n",
"# 分层回测\n",
"analyzer = FactorAnalyzer(multi_factor.stack(), fwd_10d.stack())\n",
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
"print('多因子分层收益 (Q1=综合得分最低, Q5=最高):')\n",
"print(quantile_ret)\n",
"\n",
"quantile_ret['avg_return'].plot(kind='bar', figsize=(8, 4), color='steelblue')\n",
"plt.title('多因子合成分层收益 (未来10日)')\n",
"plt.ylabel('平均收益率')\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 构建策略并回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测配置\n",
"config = AlphaConfig(\n",
" initial_cash=1_000_000,\n",
" commission_rate=0.0003,\n",
" slippage=0.001,\n",
" stamp_tax=0.001,\n",
")\n",
"\n",
"# 多因子策略:持有综合得分最高的 20% 股票\n",
"strategy = Strategy(\n",
" name='多因子选股策略-动量+低波+价值+质量',\n",
" factors=[],\n",
" signal_generator=QuantileSignal(\n",
" n_quantiles=5, long_quantile=5, short_quantile=0,\n",
" ),\n",
" weight_allocator=EqualWeightAllocator(max_positions=10),\n",
" description='每月持有动量/低波/价值/质量四因子综合得分最高的20%股票'\n",
")\n",
"\n",
"engine = BacktestEngine(config)\n",
"equity_curve = engine.run(\n",
" strategy=strategy,\n",
" price_data=price_data,\n",
" factor_data={'multi': multi_factor},\n",
" rebalance_freq='M',\n",
")\n",
"\n",
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 净值曲线与回撤"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
"\n",
"# 净值曲线\n",
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='darkred')\n",
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
"axes[0].set_title('多因子选股策略净值曲线')\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# 回撤曲线\n",
"nav = equity_curve['nav']\n",
"running_max = nav.cummax()\n",
"drawdown = (nav - running_max) / running_max\n",
"axes[1].fill_between(equity_curve.index, 0, drawdown.values, color='red', alpha=0.3)\n",
"axes[1].set_title('回撤曲线')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 基准:股票池等权组合\n",
"bench_ret = price_data.pct_change().mean(axis=1)\n",
"\n",
"evaluator = PerformanceEvaluator(equity_curve, benchmark_returns=bench_ret, risk_free_rate=0.03)\n",
"print(evaluator.summary())\n",
"\n",
"report_df = evaluator.full_report()\n",
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 因子权重敏感性分析\n",
"\n",
"对比等权合成与单因子策略的 IC 表现:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 单因子 vs 多因子 IC 对比\n",
"all_factors = {\n",
" '动量': z_mom,\n",
" '低波': z_lowvol,\n",
" '价值': z_value,\n",
" '质量': z_quality,\n",
" '多因子(等权)': multi_factor,\n",
"}\n",
"\n",
"print(f\"{'因子':<12} {'IC均值':>8} {'ICIR':>8} {'IC>0占比':>10}\")\n",
"print('-' * 42)\n",
"for name, fac in all_factors.items():\n",
" a = FactorAnalyzer(fac.stack(), fwd_10d.stack())\n",
" s = a.ic_summary()\n",
" print(f\"{name:<12} {s['IC_Mean']:>8.4f} {s['IR']:>8.4f} {s['IC>0_Ratio']:>10.2%}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+298
View File
@@ -0,0 +1,298 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 质量因子策略 — 构建与回测\n",
"\n",
"## 策略思路\n",
"质量投资(Quality Investing):买入盈利能力强、财务稳健的公司。\n",
"本策略使用 **ROE、毛利率、资产负债率** 合成质量因子:\n",
"- ROE(净资产收益率)越高 → 质量越好\n",
"- 毛利率越高 → 质量越好\n",
"- 资产负债率越低 → 财务更稳健\n",
"\n",
"每月调仓,持有质量因子最高的一批股票。\n",
"\n",
"## 研究流程\n",
"1. 加载数据(价格 + 财务指标)\n",
"2. 质量因子计算与 IC 分析\n",
"3. 分层回测验证因子有效性\n",
"4. 构建策略并回测\n",
"5. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..')\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"from alpha.config import AlphaConfig\n",
"from alpha.factors import FactorAnalyzer\n",
"from alpha.strategy import Strategy, QuantileSignal, EqualWeightAllocator\n",
"from alpha.backtest import BacktestEngine\n",
"from alpha.evaluation import PerformanceEvaluator\n",
"from alpha.data_loader import DataLoader\n",
"\n",
"%matplotlib inline\n",
"plt.rcParams['font.sans-serif'] = ['SimHei']\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print('模块导入成功')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 加载数据\n",
"\n",
"质量因子需要财务指标(ROE / 毛利率 / 资产负债率),通过 `DataLoader.load_factor` 加载。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"START_DATE = '2020-01-01'\n",
"END_DATE = '2024-12-31'\n",
"\n",
"loader = DataLoader()\n",
"\n",
"# 价格数据 + 财务数据\n",
"price_data = loader.load_prices(START_DATE, END_DATE)\n",
"roe_data = loader.load_factor('roe', START_DATE, END_DATE) # 净资产收益率\n",
"gross_margin_data = loader.load_factor('gross_margin', START_DATE, END_DATE) # 毛利率\n",
"debt_data = loader.load_factor('debt_to_assets', START_DATE, END_DATE) # 资产负债率\n",
"\n",
"print(f'价格数据: {price_data.shape}')\n",
"print(f'ROE数据: {roe_data.shape}')\n",
"print(f'毛利率数据: {gross_margin_data.shape}')\n",
"print(f'资产负债率数据: {debt_data.shape}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 质量因子计算与 IC 分析\n",
"\n",
"质量因子 = Z(ROE) + Z(毛利率) + Z(-资产负债率)\n",
"\n",
"财务指标为季度数据,`DataLoader` 已重采样到交易日并做向前填充。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 截面标准化函数\n",
"def zscore_cross_section(df):\n",
" return (df - df.mean(axis=1)) / (df.std(axis=1) + 1e-12)\n",
"\n",
"z_roe = zscore_cross_section(roe_data)\n",
"z_gm = zscore_cross_section(gross_margin_data)\n",
"z_debt = -zscore_cross_section(debt_data) # 负债率越低越好\n",
"\n",
"quality_factor = z_roe + z_gm + z_debt\n",
"\n",
"# 未来 20 日收益率(质量因子预测周期较长)\n",
"fwd_20d = price_data.pct_change(20).shift(-20)\n",
"\n",
"factor_stacked = quality_factor.stack()\n",
"fwd_stacked = fwd_20d.stack()\n",
"\n",
"analyzer = FactorAnalyzer(factor_stacked, fwd_stacked)\n",
"ic = analyzer.compute_ic(method='rank')\n",
"\n",
"print('质量因子 IC 汇总:')\n",
"for k, v in analyzer.ic_summary().items():\n",
" print(f' {k}: {v:.4f}')\n",
"\n",
"ic.plot(figsize=(12, 3), title='质量因子 Rank IC 时间序列')\n",
"plt.axhline(y=0, color='r', linestyle='--')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 分层回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"quantile_ret = analyzer.quantile_returns(n_quantiles=5)\n",
"print('各分位组平均收益 (Q1=质量最差, Q5=质量最好):')\n",
"print(quantile_ret)\n",
"\n",
"quantile_ret['avg_return'].plot(kind='bar', figsize=(8, 4), color='steelblue')\n",
"plt.title('质量因子分层收益 (未来20日)')\n",
"plt.ylabel('平均收益率')\n",
"plt.grid(True, alpha=0.3)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 构建策略并回测"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 回测配置\n",
"config = AlphaConfig(\n",
" initial_cash=1_000_000,\n",
" commission_rate=0.0003,\n",
" slippage=0.001,\n",
" stamp_tax=0.001,\n",
")\n",
"\n",
"# 质量策略:持有质量因子最高的 20% 股票\n",
"strategy = Strategy(\n",
" name='质量因子策略-ROE+毛利率+低负债',\n",
" factors=[],\n",
" signal_generator=QuantileSignal(\n",
" n_quantiles=5, long_quantile=5, short_quantile=0,\n",
" ),\n",
" weight_allocator=EqualWeightAllocator(max_positions=10),\n",
" description='每月持有ROE高、毛利率高、负债率低的优质公司'\n",
")\n",
"\n",
"engine = BacktestEngine(config)\n",
"equity_curve = engine.run(\n",
" strategy=strategy,\n",
" price_data=price_data,\n",
" factor_data={'quality': quality_factor},\n",
" rebalance_freq='M',\n",
")\n",
"\n",
"print(f'回测完成, 共 {len(equity_curve)} 个交易日')\n",
"print(f'累计收益率: {(equity_curve[\"nav\"].iloc[-1] - 1) * 100:.2f}%')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 净值曲线与回撤"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig, axes = plt.subplots(2, 1, figsize=(14, 8))\n",
"\n",
"# 净值曲线\n",
"axes[0].plot(equity_curve.index, equity_curve['nav'], label='策略净值', color='brown')\n",
"axes[0].axhline(y=1.0, color='gray', linestyle='--')\n",
"axes[0].set_title('质量因子策略净值曲线')\n",
"axes[0].legend()\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# 回撤曲线\n",
"nav = equity_curve['nav']\n",
"running_max = nav.cummax()\n",
"drawdown = (nav - running_max) / running_max\n",
"axes[1].fill_between(equity_curve.index, 0, drawdown.values, color='red', alpha=0.3)\n",
"axes[1].set_title('回撤曲线')\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 绩效评估"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 基准:股票池等权组合\n",
"bench_ret = price_data.pct_change().mean(axis=1)\n",
"\n",
"evaluator = PerformanceEvaluator(equity_curve, benchmark_returns=bench_ret, risk_free_rate=0.03)\n",
"print(evaluator.summary())\n",
"\n",
"report_df = evaluator.full_report()\n",
"pd.DataFrame(list(report_df.items()), columns=['指标', '数值']).set_index('指标')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 质量分项指标对比\n",
"\n",
"对比各质量指标单独用于选股的效果:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 各分项因子 IC 对比\n",
"for name, fac in [\n",
" ('ROE', z_roe),\n",
" ('毛利率', z_gm),\n",
" ('低负债率', z_debt),\n",
" ('质量合成', quality_factor),\n",
"]:\n",
" a = FactorAnalyzer(fac.stack(), fwd_stacked)\n",
" s = a.ic_summary()\n",
" print(f'{name}: IC均值={s[\"IC_Mean\"]:.4f}, ICIR={s[\"IR\"]:.4f}, '\n",
" f'IC>0占比={s[\"IC>0_Ratio\"]:.2%}')"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.10.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
+187 -14
View File
@@ -98,6 +98,26 @@ def safe_float(val):
return None
def _get_table_columns(table_name: str, conn) -> set:
"""
查询数据库表的实际列名集合
"""
cursor = conn.cursor()
try:
cursor.execute(
"""
SELECT column_name
FROM information_schema.columns
WHERE table_name = %s
""",
(table_name,),
)
cols = {row[0] for row in cursor.fetchall()}
finally:
cursor.close()
return cols
def batch_insert(table_name: str, df: pd.DataFrame, conn, conflict_columns: List[str]):
"""
使用 execute_values 批量 UPSERT (INSERT ... ON CONFLICT)
@@ -105,15 +125,90 @@ def batch_insert(table_name: str, df: pd.DataFrame, conn, conflict_columns: List
- df: 待导入 DataFrame
- conn: psycopg2 连接
- conflict_columns: 冲突列 (唯一约束列),冲突时更新其他列
自动过滤 DataFrame 中数据库表不存在的列,避免
'column "xxx" of relation "yyy" does not exist' 错误。
"""
if df.empty:
logger.warning(f" {table_name}: 空数据,跳过")
return 0
# pd.NaT / pd.NaT / numpy NaN 等无法被 psycopg2 识别,统一替换为 Python None
df = df.where(pd.notna(df), None)
# ---- 动态过滤:只保留数据库表中存在的列 ----
db_columns = _get_table_columns(table_name, conn)
if not db_columns:
# 表可能不存在,回退到不过滤(后续会抛出真实错误)
logger.warning(f" {table_name}: 未查询到表结构,使用原始列")
else:
extra_cols = [c for c in df.columns if c not in db_columns]
if extra_cols:
logger.warning(
f" {table_name}: 过滤掉表中不存在的列 {extra_cols}"
)
df = df[[c for c in df.columns if c in db_columns]]
if df.empty:
logger.warning(f" {table_name}: 过滤后无可用列,跳过")
return 0
columns = list(df.columns)
# 过滤后验证冲突列仍存在
missing_conflict = [c for c in conflict_columns if c not in columns]
if missing_conflict:
logger.error(
f" {table_name}: 冲突列 {missing_conflict} 不在可用列中,跳过"
)
return 0
# ---- 按冲突列去重 ----
# PostgreSQL 的 ON CONFLICT DO UPDATE 不允许同一命令中出现重复冲突键:
# "ON CONFLICT DO UPDATE command cannot affect row a second time"
# Tushare 财务接口 (income/balancesheet/cashflow/fina_indicator) 对同一股票
# 同一报告期可能返回多行数据(如多次公告修正,ann_date 不同但冲突键相同),
# 必须先在批内去重。
#
# 为保证保留的是"最新公告"的数据而非仅依赖 Tushare 返回顺序:
# 若存在公告日期列 (ann_date / f_ann_date),先按公告日期升序排序,
# 再 drop_duplicates(keep="last") 即可稳定保留最新一条 (公告日期最大),
# 且缺失公告日期的行 (NaT) 会排在最后,仅当无公告日期时才被保留。
#
# 注意:去重必须在此处 (NaN->None 替换之前) 执行,
# 此时日期列仍为 datetime64 类型,sort_values(na_position="last")
# 能正确处理 NaT;若在替换之后排序,object 类型混合日期/None 排序不可靠。
#
# 关键修复:PostgreSQL 对 VARCHAR/数值列会做隐式类型转换,因此数据库眼里
# report_type = '1' (str) 与 report_type = 1 (int) 是"同一个约束值"
# 但 pandas 的 drop_duplicates 认为 '1' != 1,导致去重不彻底、仍报错。
# 因此去重前先将非日期冲突列统一转成 str 再判断重复 (不修改原始 DataFrame)。
dedup_cols = [c for c in conflict_columns if c in columns]
before_dedup = len(df)
date_cols = [c for c in ["f_ann_date", "ann_date"] if c in columns]
if date_cols:
df = df.sort_values(date_cols, na_position="last")
if dedup_cols:
# 规范化去重键:非 datetime64 列统一 astype(str),消除类型不一致的隐患
key_df = df[dedup_cols].copy()
for c in key_df.columns:
if not pd.api.types.is_datetime64_any_dtype(key_df[c]):
key_df[c] = key_df[c].astype(str)
dup_mask = key_df.duplicated(keep="last")
df = df[~dup_mask]
after_dedup = len(df)
if after_dedup < before_dedup:
logger.warning(
f" {table_name}: 检测到 {before_dedup - after_dedup} 行重复冲突键,"
f"已保留最新公告记录 (去重后 {after_dedup} 行)"
)
# pd.NaT / numpy NaN 等无法被 psycopg2 识别,统一替换为 Python None。
# 关键:必须先 astype(object)pandas 的 datetime64 列无法存储 None
# 直接 where(..., None) 时 pandas 会把 None 自动提升回 NaT
# 导致 psycopg2 生成 'NaT'::timestamp 非法 SQL
# (典型报错: fina_indicator 的 ann_date 为空时报
# "invalid input syntax for type timestamp: \"NaT\"")。
# 转为 object 后 None 可正常存储,NaT/NaN 会被真正替换为 NULL。
df = df.astype(object).where(pd.notna(df), None)
rows = [tuple(row) for row in df.itertuples(index=False)]
# 构建 ON CONFLICT 子句(使用 sql.Identifier 防止注入/语法错误)
@@ -153,6 +248,25 @@ def batch_insert(table_name: str, df: pd.DataFrame, conn, conflict_columns: List
return len(rows)
except Exception as e:
conn.rollback()
# 兜底:若批内仍存在重复冲突键 (如极端类型差异未被识别),
# 回退为逐行 INSERT。单行命令永远不会触发
# "ON CONFLICT DO UPDATE command cannot affect row a second time"。
if "cannot affect row a second time" in str(e):
logger.warning(
f" {table_name}: 批内仍存在重复冲突键,回退为逐行导入 ({len(rows)} 行)"
)
try:
inserted = 0
for r in rows:
execute_values(cursor, upsert_sql.as_string(cursor), [r], page_size=1)
inserted += 1
conn.commit()
logger.info(f" {table_name}: 逐行成功导入 {inserted} 条记录")
return inserted
except Exception as e2:
conn.rollback()
logger.error(f" {table_name}: 逐行导入失败 - {e2}")
raise
logger.error(f" {table_name}: 批量导入失败 - {e}")
raise
finally:
@@ -313,7 +427,7 @@ def import_daily_for_stock(ts_code: str, start_date: str, end_date: str, conn) -
pro = get_ts_pro()
def fetch():
return pro.daily(
return pro.daily_vip(
ts_code=ts_code,
start_date=start_date.replace("-", ""),
end_date=end_date.replace("-", ""),
@@ -371,7 +485,7 @@ def import_daily_by_date(
):
"""
按交易日批量导入日线行情 (高效模式)
使用 pro.daily(trade_date='YYYYMMDD') 一次性拉取全市场当日数据
使用 pro.daily_vip(trade_date='YYYYMMDD') 一次性拉取全市场当日数据
大幅减少 API 调用次数: 约250交易日/年 × 16年 ≈ 4000次 (原来需要 5000股票 × 16年 = 80000次)
参数:
@@ -462,7 +576,7 @@ def import_daily_by_date(
td_str = td.strftime("%Y%m%d") if hasattr(td, "strftime") else str(td).replace("-", "")
def fetch():
return pro.daily(trade_date=td_str)
return pro.daily_vip(trade_date=td_str)
df = fetch_with_retry(fetch, max_retries=3)
if df is None or df.empty:
@@ -565,7 +679,7 @@ def import_daily_basic(
kwargs["trade_date"] = trade_date.replace("-", "")
def fetch():
return pro.daily_basic(**kwargs)
return pro.daily_basic_vip(**kwargs)
df = fetch_with_retry(fetch, max_retries=2)
if df is None or df.empty:
@@ -631,7 +745,7 @@ def import_daily_basic_by_date(
pro = get_ts_pro()
def fetch_daily_basic():
return pro.daily_basic(trade_date=td)
return pro.daily_basic_vip(trade_date=td)
df = fetch_with_retry(fetch_daily_basic, max_retries=3)
if df is not None and not df.empty:
@@ -685,7 +799,7 @@ def import_adj_factor(
kwargs["ts_code"] = ts_code
def fetch():
return pro.adj_factor(**kwargs)
return pro.adj_factor_vip(**kwargs)
df = fetch_with_retry(fetch, max_retries=2)
if df is None or df.empty:
@@ -770,10 +884,10 @@ def import_financial_statements(
for i, ts_code in enumerate(stock_list, 1):
for table_name, fetch_method in [
("income", pro.income),
("balancesheet", pro.balancesheet),
("cashflow", pro.cashflow),
("fina_indicator", pro.fina_indicator),
("income", pro.income_vip),
("balancesheet", pro.balancesheet_vip),
("cashflow", pro.cashflow_vip),
("fina_indicator", pro.fina_indicator_vip),
]:
try:
if table_name == "fina_indicator":
@@ -855,7 +969,7 @@ def import_index_daily(
for idx_code in index_codes:
try:
def fetch():
return pro.index_daily(
return pro.index_daily_vip(
ts_code=idx_code,
start_date=start_date.replace("-", ""),
end_date=end_date.replace("-", ""),
@@ -886,6 +1000,62 @@ def import_index_daily(
# 8. 初始化数据库 Schema
# ============================================================
# ---- 表结构迁移映射: 为已存在的旧表补充新列 ----
# key: 表名, value: 需要确保存在的列 -> (列定义类型, 注释)
_SCHEMA_MIGRATIONS = {
"income": {
"fv_value_chg_gain": "NUMERIC(20,4)",
},
"balancesheet": {
"total_share": "NUMERIC(20,4)",
},
"cashflow": {
"finan_exp": "NUMERIC(20,4)",
},
"fina_indicator": {
"ca_turn": "NUMERIC(16,4)",
},
}
def _migrate_schema(conn):
"""
迁移已存在的旧表: 使用 ALTER TABLE ... ADD COLUMN IF NOT EXISTS
补齐 Tushare API 返回但旧 schema 缺失的列。
- 对已存在的表生效 (CREATE TABLE IF NOT EXISTS 不会修改旧表)
- ADD COLUMN IF NOT EXISTS 幂等,可安全重复执行
- 与其他存储引擎不同,PostgreSQL 的 ADD COLUMN 是 O(1) 元数据操作
"""
cursor = conn.cursor()
try:
for table_name, columns in _SCHEMA_MIGRATIONS.items():
# 先检查表是否存在
cursor.execute(
"SELECT to_regclass(%s)",
(table_name,),
)
if cursor.fetchone()[0] is None:
continue
for col_name, col_type in columns.items():
cursor.execute(
sql.SQL("ALTER TABLE {} ADD COLUMN IF NOT EXISTS {} {}").format(
sql.Identifier(table_name),
sql.Identifier(col_name),
sql.SQL(col_type),
)
)
logger.info(f" 迁移: {table_name}.{col_name} 列已确认 ({col_type})")
conn.commit()
except Exception as e:
conn.rollback()
logger.warning(f" 表结构迁移失败 (可忽略,导入时自动过滤): {e}")
finally:
cursor.close()
def init_database():
"""
执行 DDL,创建所有表结构
@@ -946,6 +1116,9 @@ def init_database():
conn.commit()
cursor.close()
logger.info(" Schema 初始化完成")
# 迁移已存在的旧表: 补齐 Tushare API 新增列
_migrate_schema(conn)
except Exception as e:
conn.rollback()
logger.error(f" Schema 初始化失败: {e}")
@@ -1268,7 +1441,7 @@ def resume_daily_by_date(
td_compact = td_str.replace("-", "")
def fetch():
return pro.daily(trade_date=td_compact)
return pro.daily_vip(trade_date=td_compact)
df = fetch_with_retry(fetch, max_retries=3)
if df is None or df.empty:
+8 -2
View File
@@ -216,7 +216,8 @@ CREATE TABLE IF NOT EXISTS income (
n_sec_uw_income NUMERIC(20,4), -- 证券承销业务净收入
n_asset_mg_income NUMERIC(20,4), -- 受托客户资产管理业务净收入
oth_b_income NUMERIC(20,4), -- 其他业务收入
fv_value_chg NUMERIC(20,4), -- 加:公允价值变动净收益
fv_value_chg NUMERIC(20,4), -- 加:公允价值变动净收益 (兼容旧版字段)
fv_value_chg_gain NUMERIC(20,4), -- 加:公允价值变动净收益 (Tushare income 实际字段)
invest_income NUMERIC(20,4), -- 加:投资净收益
ass_invest_income NUMERIC(20,4), -- 其中:对联营企业和合营企业的投资收益
forex_gain NUMERIC(20,4), -- 加:汇兑净收益
@@ -325,6 +326,9 @@ CREATE TABLE IF NOT EXISTS balancesheet (
decr_in_disbur NUMERIC(20,4), -- 待处理流动资产损益
oth_nca NUMERIC(20,4), -- 其他非流动资产
-- 股东权益相关
total_share NUMERIC(20,4), -- 总股本 (Tushare balancesheet 返回字段)
-- 负债
total_liab NUMERIC(20,4), -- 负债合计
total_cur_liab NUMERIC(20,4), -- 流动负债合计
@@ -363,7 +367,8 @@ CREATE TABLE IF NOT EXISTS cashflow (
-- 经营活动
net_profit NUMERIC(20,4), -- 净利润
fin_exp NUMERIC(20,4), -- 财务费用
fin_exp NUMERIC(20,4), -- 财务费用 (兼容旧版字段)
finan_exp NUMERIC(20,4), -- 财务费用 (Tushare cashflow 实际字段)
c_fr_sale_sg NUMERIC(20,4), -- 销售商品、提供劳务收到的现金
recp_tax_rends NUMERIC(20,4), -- 收到的税费返还
n_depos_incr_fi NUMERIC(20,4), -- 客户存款和同业存放款项净增加额
@@ -429,6 +434,7 @@ CREATE TABLE IF NOT EXISTS fina_indicator (
arturn_days NUMERIC(16,4), -- 应收账款周转天数
inv_turn NUMERIC(16,4), -- 存货周转率
ar_turn NUMERIC(16,4), -- 应收账款周转率
ca_turn NUMERIC(16,4), -- 流动资产周转率 (Tushare fina_indicator 实际字段)
assets_turn NUMERIC(16,4), -- 总资产周转率
-- 盈利能力
roe NUMERIC(16,4), -- 净资产收益率
+1 -1
View File
@@ -260,7 +260,7 @@
"---\n",
"## Step 4: 导入日线行情 (核心表,最耗时)\n",
"\n",
"> **新版改进:** 按交易日循环拉取全市场数据 `pro.daily(trade_date='20180810')`\n",
"> **新版改进:** 按交易日循环拉取全市场数据 `pro.daily_vip(trade_date='20180810')`VIP接口)\n",
"> API 调用从 ~80,000次 (5000只×16年) 降至 ~4,000次 (250交易日×16年),速度提升约 **20倍**。\n",
"\n",
"### 4.1 按年批量导入 (推荐)\n",