小果均值方差回测系统专家
一、系统概述
使用教程:https://gitcode.com/qq_50882340/xg_mean_var_backtrader
1.1 系统定位
小果均值方差最优资产组合权重再平衡回测系统(xg_mean_var_backtrader)是一套忠实还原马科维茨现代投资组合理论(Modern Portfolio Theory, MPT)的专业级量化资产配置回测框架。它以「过去 lookback_days 天的历史收益率」估计各资产的期望收益与协方差矩阵,通过「均值方差优化(SLSQP 最大化夏普比率)」在每只标的权重限制 [min_weight, max_weight] 内求解当期最优资产权重,随后每 interval 个交易日将实际持仓再平衡到该目标权重,把「该买多少 / 该卖多少」由量化出的组合风险收益最优解来界定。
与其它策略系统最大的不同在于:均值方差系统不做择时、不靠单标的方向判断,而是聚焦组合层面的资产配置——把资金在多个低相关资产间按「期望收益÷波动风险」最优配比,在给定风险偏好约束下最大化单位风险所获得的超额回报(夏普比率)。单一资金池整体运作(不把 cash 均分到各标的),每日记录各标的实际/目标权重,交易类型为「初始建仓」与「再平衡买入/卖出」,无做T、无止损止盈概念。
> 均值方差核心思想:不要把鸡蛋放在一个篮子里,且每个篮子的大小应由「收益÷风险」的科学配比而非拍脑袋决定。 组合的风险不仅取决于各资产自身波动,更取决于资产间的协方差(相关性)——低相关资产的搭配可在不牺牲太多收益的前提下显著降低组合波动。
1.2 核心策略逻辑(每 interval 交易日执行)
均值方差框架在每个共同交易日的运行分两阶段:组合初始化(一次性) 与 定期再平衡(每 interval 日):
一、组合初始化(首个共同交易日)
① 等权重初始建仓:按 stock_list 等分资金买入各标的(不足 1 手则跳过)
② 目标权重置为等权,调仓计数归零
二、每日主循环(遍历每个共同交易日)
① 当日市值 = 各标的 持仓 × 收盘价;总资产 = 现金 + 市值
② 若有当日最优权重 → 更新 target_weights
③ 每累计 interval 个交易日且总资产>0 → 触发再平衡:
├─ 对每只标的:target_value = 总资产 × target_weight
├─ diff = target_value − 当前市值
├─ |diff| < 总资产×0.5% → 忽略(避免频繁小额交易)
├─ diff>0 → 再平衡买入(≤可用现金)
└─ diff<0 → 按金额卖出(回收现金)
④ 记录每日:现金/持仓/市值/总资产/累计盈亏/各标的实际与目标权重
⑤ 依序补齐每日收益率/盈亏,并对照等权持有不动基准
每个 interval 个交易日对每只标的最多买卖一次;单日可多标的同时调仓;动作类型记录为「初始建仓 / 再平衡买入 / 再平衡卖出」。
1.3 均值方差优化引擎(关键概念)
均值方差系统的一切围绕「收益率矩阵」与「协方差风险」两个核心,权重由最大化夏普比率的最优化求得:
| 概念 | 公式/算法 | 说明 |
|---|---|---|
| 对齐数据 | 取各标的共同交易日期交集 | align_data 只保留全部标的同时有行情的日期,用于可比的收益/协方差计算 |
| 收益率矩阵 | 对齐后各标的 zdf 列(日涨跌幅) |
get_returns_matrix 截取 end_date 前最近 lookback_days 天 |
| 期望收益 | mean_returns = 日收益均值 × 252 |
年化期望收益估计 |
| 协方差矩阵 | cov = 日收益协方差 × 252 |
年化协方差,捕捉资产间相关结构 |
| 目标函数 | negative_sharpe = −(期望收益−rf) ÷ 组合波动 |
rf 默认 0.02(2%),最小化负夏普 = 最大化夏普 |
| 求解器 | scipy.optimize.minimize(method='SLSQP') |
在约束内求最优权重,maxiter=1000 |
| 权重约束 | 各权重 ∈ [min_weight, max_weight] 且 总和=1 |
避免单标过度集中/过度分散 |
| 初始猜测 | 等权 1/n |
迭代起点为平均配置 |
| 兜底 | 数据<30行 或 优化失败 → 退回等权 | 保证总有可用权重 |
> ⚠️ 权重优化用 zdf(日涨跌幅) 作为收益率代理、zdf.cov() 作为协方差来源;求解为无放空(weights 归零后归一化)的纯多头均值方差配置。
>
> 📌 注意:lambda_risk(风险厌恶系数)在当前版本只作参数存储/打印留档,并未直接进入 SLSQP 目标函数——实际求解目标是最大化夏普比率(negative_sharpe),而非 max return − lambda×risk 形式。如需风险厌恶形式可二次开发 optimize_max_sharpe。
1.4 现代组合理论(MPT) → 本框架参数映射
| MPT/资产配置概念(理论) | 本框架参数 | 默认 | 说明 |
|---|---|---|---|
| 历史样本期估计 | lookback_days |
60 | 用过去 N 个交易日估计收益/协方差 |
| 组合再平衡频率 | interval |
5 | 每 N 个交易日调一次仓 |
| 单标的最大配比上限 | max_weight |
0.6 | 任意标的不超组合 60% |
| 单标的最小配比下限 | min_weight |
0.05 | 任意标的至少配置 5% |
| 风险厌恶程度 | lambda_risk |
2.0 | 本版仅留档,未直接进目标函数 |
| 无风险利率 | rf(优化内固定) |
0.02 | negative_sharpe 中超额收益的减项 |
| 权重和为 1 | (约束硬性) | 1 | 资金全仓分配,不保留目标现金 |
| 调仓死区 | 总资产×0.5% | 0.005 | |diff| 低于此忽略,避免过度交易 |
1.5 设计哲学
- 解耦与复用:数据对齐(
align_data)、收益矩阵(get_returns_matrix)、优化目标(negative_sharpe)、求解(optimize_max_sharpe)、逐期权重(calculate_optimal_weights_ts)与交易撮合各环节解耦,可独立替换。 - 透明与可解释:每个调仓日记录目标权重/前权重/买卖权重差,
weight_history完整留存每日权重演变,决策可追溯。 - 风险优先:以协方差与组合波动作为约束标尺,追求「单位风险下最高收益」,天然适应不同波动率资产搭配。
- 灵活与扩展:支持自定义数据注入(标的+指数各 7 种接口)、
lookback/interval/weight 边界全参数化,可继承重写optimize_max_sharpe引入风险厌恶/无放空变体。
二、安装与运行环境
2.1 安装包
小果均值方差回测系统以单个类文件 xg_mean_var_backtrader.py 形式提供,无独立安装步骤。将脚本置于工作目录后,在同一目录(或旁路)下放置数据文件即可使用。
使用教程:https://gitcode.com/qq_50882340/xg_mean_var_backtrader
2.2 依赖
Python ≥ 3.8- 第三方库:
pandas、numpy、scipy(均值方差优化与统计必需) - 本地数据源可选依赖:
pyarrow(读取.parquet历史数据) - 无需安装
xg_tdx_func(本框架不涉及通达信公式引擎)
> ⚠️ 本系统强依赖 scipy(from scipy.optimize import minimize、from scipy import stats),与其它小果回测系统不同,务必安装 scipy。
安装依赖:
pip install pandas numpy scipy pyarrow
2.3 包结构
工作目录/
├── xg_mean_var_backtrader.py # 主类文件(class xg_mean_var_backtrader)
├── data/
│ ├── 历史数据/ # 本地标的日线 parquet(代码.parquet)
│ └── (可选) 指数历史文件 # 指数数据(未注入自定义指数时)
└── 策略模型/
└── 均值方差最优资产组合策略/<user>/ # save_backtrader_data 导出目录(自动创建)
2.4 导入方式
from xg_mean_var_backtrader.xg_mean_var_backtrader import xg_mean_var_backtrader
bt = xg_mean_var_backtrader(
start_date='20230101',
end_date='20261201',
stock_list=['159915.SZ', '513100.SH', '518880.SH'],
cash=1000000,
)
2.5 数据目录要求(使用内置数据源时)
当未注入自定义数据(use_custom_data=False)时,系统按固定路径查找单只标的日线 parquet:
data/历史数据/{stock_code}.parquet
找到即用;找不到该标的则加载失败并在结果中跳过。
2.6 数据字段要求
标的(股票/ETF)日线数据需包含以下字段:
| 字段 | 说明 | 是否必须 |
|---|---|---|
date |
交易日期 | ✅ 必须 |
close |
收盘价 | ✅ 必须(>0) |
open |
开盘价 | ✅ 必须(>0) |
high |
最高价 | ✅ 必须(缺失自动=close/NaN) |
low |
最低价 | ✅ 必须(缺失自动=close/NaN) |
volume |
成交量 | ✅ 必须 |
zdf |
涨跌幅 | ⚠️ 推荐(缺失自动用 close.pct_change() 补) |
preClose |
前收盘价 | ⚠️ 可选(有则补 return/adj_factor) |
amount |
成交金额 | ⚠️ 可选 |
证券代码 / 证券名称 |
代码/名称 | ⚠️ 可选(自动补) |
> 均值方差优化核心只用到 date + close + zdf;align_data 需各标的存在共同交易日期(交集非空),否则回测终止。数据行数至少需 > lookback_days + 30(calculate_optimal_weights_ts 要求历史≥30 行才跑优化,否则一直退回等权)效果最佳。
指数数据仅需 date + close(close>0),用于输出留档。
三、初始化参数完整参考
> 以下为 __init__ 方法全部参数的详细说明,按功能分组。
3.1 基础参数 (Base)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
start_date |
str |
'20260101' |
回测起始日期,格式 YYYYMMDD |
end_date |
str |
'20500101' |
回测结束日期,格式同上 |
stock_list |
list |
['159915.SZ','513100.SH','518880.SH'] |
候选资产池,元素需带交易所后缀 |
index_stock |
str |
'000300.SH' |
基准指数代码(用于输出留档) |
cash |
float |
100000 |
初始总资金(元,整体组合资金池,不均分) |
comm |
float |
0.0001 |
手续费率(万1 = 0.0001) |
max_workers |
int |
4 |
数据加载并发线程数 |
verbose |
bool |
False |
是否打印加载过程日志(构造末尾强制置 True) |
3.2 均值方差优化参数 (Mean-Variance)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
lookback_days |
int |
60 |
估计收益/协方差的历史回看天数 |
max_weight |
float |
0.6 |
单标的目标权重上限 |
min_weight |
float |
0.05 |
单标的目标权重下限 |
lambda_risk |
float |
2.0 |
风险厌恶系数(本版仅留档/打印,不直接进目标函数) |
interval |
int |
5 |
再平衡周期(每 N 个交易日调一次仓) |
3.3 数据源开关 (Data Source)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
use_custom_data |
bool |
False |
是否使用注入的自定义标的数据库(自动切换) |
use_custom_index_data |
bool |
False |
是否使用注入的自定义指数数据(自动切换) |
3.4 内部固定参数(构造时自动设置,一般不修改)
| 参数 | 默认值 | 说明 |
|---|---|---|
adj_type |
'none' |
复权类型(当前固定 none) |
min_shares |
100 |
单笔最小成交份数(1 手) |
share_multiple |
100 |
成交份数整数倍(按 100 手撮合) |
3.5 关键口径:整体资金池与再平衡死区
| 概念 | 公式 | 说明 |
|---|---|---|
| 初始建仓 | 现金 = cash,各标的目标 = 等权 1/n |
首日一次性全仓等权买入 |
| 单标目标市值 | total_asset × target_weight |
再平衡日的期望持仓市值 |
| 再平衡死区 | |diff| < total_asset × 0.005 |
差异过小忽略,避免频繁小额交易 |
| 目标现金 | 0 |
资金全部分配到标的(权重和为 1),无目标现金保留 |
| 权重约束 | Σweight=1 且 每只∈[min,max] |
SLSQP 硬约束 |
四、数据加载与处理详解
4.1 标的(股票/ETF)数据来源优先级
get_stock_data(stock_code) 依次尝试:
- 自定义数据源(
use_custom_data=True且代码在_custom_data_cache)——add_stock_data_from_*注入后优先; - 本地 parquet(
data/历史数据/{代码}.parquet,固定单路径); - 均无 → 返回空 DataFrame,标的被跳过。
加载后统一:日期过滤到 [start_date, end_date]、按日期升序、剔除 close≤0 或 open≤0 行、补算 zdf = close.pct_change().fillna(0)。
4.2 多线程加载与对齐
get_all_stock_data() 用 ThreadPoolExecutor(max_workers) 并发加载全部标的;_load_single_stock 仅要求非空 DataFrame。
align_data(all_data) 取全部标的共同交易日交集,产出对齐宽表:date + {code}_close + {code}_zdf。交集为空则回测终止。这是组合可比的基石——某只标的数据缺失日期过多会显著缩小回测区间。
4.3 指数数据来源
get_index_data() 优先返回注入的自定义指数数据(use_custom_index_data=True 且已注入),否则从本地文件读取 index_stock 对应指数并缓存。指数仅用于输出留档,策略对照基准实际是 calculate_benchmark_curve 生成的等权持有不动曲线。
五、均值方差优化详解
5.1 对齐与收益率矩阵(align_data / get_returns_matrix)
在 vectorized_backtest 中:先 align_data(all_data) 对齐共同日期;get_returns_matrix 从对齐表中取最近 lookback_days 天的 {code}_zdf 列(剔除缺列与 dropna),得到收益率矩阵(列=标的,行=交易日),供优化求解。
5.2 最优权重求解(optimize_max_sharpe)
对收益率矩阵:
mean_returns = 各列均值 × 252(年化期望收益);cov_matrix = 协方差 × 252(年化协方差);- 目标
negative_sharpe(weights):组合收益=(w·μ),组合波动=√(wᵀΣw),sharpe=(组合收益−rf)/组合波动,返回−sharpe供最小化(rf 固定 0.02); - 约束:
Σweights = 1(eq),每个权重∈ [min_weight, max_weight](bounds); - 初值等权
1/n,scipy.optimize.minimize(method='SLSQP', maxiter=1000); - 成功 → 结果权重 clip≥0 后归一化返回;失败 → 返回
None。
5.3 逐日权重时间序列(calculate_optimal_weights_ts)
遍历对齐表,对每个调仓参考日用「当日之前全部历史」求解该日最优权重;数据不足 30 行或求解失败 → 该日权重退回等权。产出一张 weight_df(列 = date + 各标的),即每日最优目标权重历史,存为 self.weight_history。
5.4 权重边界与再平衡执行口径
| 口径 | 说明 |
|---|---|
| 目标权重来源 | weight_map[trade_date](按日期查表,缺则沿用上一目标权重) |
| 触发频率 | rebalance_counter % interval == 0 才调仓 |
| 买入金额 | min(diff, 可用现金),撮合不足 1 手则跳过 |
| 卖出金额 | abs(diff),按金额撮合(不超持仓) |
| 交易死区 | |diff| < 总资产×0.5% 忽略 |
六、交易执行详解
6.1 买入撮合(calculate_buy_shares)
buy_amount = amount / (1 + comm) # 先扣手续费
shares = 向下取整(buy_amount / price) # 再按 share_multiple(100) 对齐
若 shares < min_shares(100) → 0
实际成本 = shares×price × (1+comm),循环确保 ≤ amount
6.2 卖出撮合(calculate_sell_shares_by_value)
按金额卖出:sell_shares 由 sell_value÷price 按 100 对齐,上界为持仓;actual_amount = 卖出额 − 手续费。用于再平衡卖出,不提供全仓卖出参数(卖出额由目标权重差值决定)。
6.3 成交记录字段(trade_log)
每笔成交记录:date / stock / type / price / shares / amount / commission / reason。
type 取值:初始建仓 / 再平衡买入 / 再平衡卖出;reason 示例:初始建仓,目标权重33.3%、目标权重40.0%,前权重25.0%,买入权重15.0%、目标权重20.0%,前权重35.0%,卖出权重15.0%。
> 📌 与信号类策略不同,本系统无手续费字段 zdf/做T 概念;每笔统一含 commission 字段。
七、完整函数清单
> 以下为 xg_mean_var_backtrader 类的全部方法,按功能分组。共 63 个方法(含私有方法),无模块级顶层函数。
7.1 构造与配置
| 方法签名 | 说明 |
|---|---|
__init__(start_date, end_date, stock_list, ...) |
构造回测实例,配置基础/均值方差/数据源参数 |
_convert_to_serializable(obj) |
将对象(Timestamp/datetime/ndarray/DataFrame/NaN)递归转 JSON 可序列化 |
get_config() -> dict |
获取当前策略的全部配置参数(含 lookback/max_weight/interval 与数据源状态) |
7.2 自定义标的注入(7 种接口)
| 方法签名 | 说明 |
|---|---|
add_stock_data_from_dataframe(stock_code, df) |
从 DataFrame 注入标的日线数据 |
add_stock_data_from_excel(stock_code, file_path, sheet_name=0, **kwargs) |
从 Excel 注入 |
add_stock_data_from_csv(stock_code, file_path, **kwargs) |
从 CSV 注入 |
add_stock_data_from_json(stock_code, file_path, orient='records', **kwargs) |
从 JSON 注入 |
add_stock_data_from_dict(stock_code, data) |
从字典注入 |
add_stock_data_from_parquet(stock_code, file_path, columns=None, **kwargs) |
从 Parquet 注入 |
add_stock_data_from_bytes(stock_code, file_bytes, file_type='csv', **kwargs) |
从字节流注入 |
7.3 自定义标的数据库管理
| 方法签名 | 说明 |
|---|---|
clear_custom_data() |
清空所有已注入的自定义标的数据 |
remove_stock_data(stock_code) |
移除指定标的的自定义数据 |
get_custom_data_keys() -> list |
获取所有已注入标的代码列表 |
has_custom_data(stock_code) -> bool |
检查指定标的是否已有自定义数据 |
7.4 自定义指数注入(7 种接口)
| 方法签名 | 说明 |
|---|---|
add_index_data_from_dataframe(df) |
从 DataFrame 注入指数数据(需 date+close) |
add_index_data_from_excel(file_path, sheet_name=0, **kwargs) |
从 Excel 注入 |
add_index_data_from_csv(file_path, **kwargs) |
从 CSV 注入(自动尝试编码) |
add_index_data_from_json(file_path, orient='records', **kwargs) |
从 JSON 注入 |
add_index_data_from_dict(data) |
从字典注入 |
add_index_data_from_parquet(file_path, columns=None, **kwargs) |
从 Parquet 注入 |
add_index_data_from_bytes(file_bytes, file_type='csv', **kwargs) |
从字节流注入 |
7.5 指数数据管理
| 方法签名 | 说明 |
|---|---|
clear_index_data() |
清空自定义指数数据 |
has_index_data() -> bool |
检查是否有自定义指数数据 |
get_index_data_info() -> dict |
获取指数数据信息(行数/日期范围等) |
7.6 数据加载与处理
| 方法签名 | 说明 |
|---|---|
_normalize_dataframe(df, stock_code=None) |
标准化 DataFrame(日期列识别、close/high/low/open/volume 补齐、补 zdf/复权因子) |
get_stock_data(stock_code) |
读取单标的(优先自定义,其次本地 parquet),日期过滤+列补齐 |
_load_single_stock(stock) |
多线程任务:加载单只标的(非空即接受) |
get_all_stock_data() |
多线程并发加载全部标的 |
get_index_data() |
获取指数数据(优先自定义,其次本地文件,带缓存) |
7.7 均值方差优化(核心)
| 方法签名 | 说明 |
|---|---|
align_data(data_dict) |
对齐所有标的数据到共同交易日(交集),产出 {code}_close/{code}_zdf 宽表 |
get_returns_matrix(data_dict=None, end_date=None, lookback_days=None) |
构建收益率矩阵(截取 end_date 前最近 lookback 天,剔除缺列) |
negative_sharpe(weights, mean_returns, cov_matrix, rf=0.02) |
负夏普比率(供 minimize 最小化) |
optimize_max_sharpe(returns_df) |
SLSQP 最大化夏普求解最优权重(含 min/max bounds + 和=1 约束) |
calculate_optimal_weights_ts(aligned_data) |
逐期时间序列计算每日最优权重(不足30行/失败退回等权) |
calculate_benchmark_curve(all_data, aligned_dates) |
计算等权重持有不动基准曲线 |
7.8 交易撮合计算
| 方法签名 | 说明 |
|---|---|
calculate_buy_shares(amount, price) |
计算买入份额与含费实际成本(先扣费、100 手对齐) |
calculate_sell_shares_by_value(shares, price, sell_value) |
按金额计算卖出份额与实得金额(不超持仓、扣手续费) |
7.9 回测执行
| 方法签名 | 说明 |
|---|---|
vectorized_backtest() |
向量化回测主函数(初始建仓 + interval 定期再平衡 + 每日记账),返回组合历史 portfolio_history |
run_backtest() |
执行完整回测(等价于 vectorized_backtest) |
7.10 绩效与数据生成
| 方法签名 | 说明 |
|---|---|
generate_equity_curve_data() |
生成净值曲线(含 net_value/drawdown/position_ratio/基准) |
generate_trade_data() |
生成成交明细与统计(trades+statistics) |
generate_position_data() |
生成每日持仓与统计(positions+statistics) |
generate_account_data() |
生成每日账户与统计(account_history+statistics) |
calculate_performance_metrics(df, df_index) |
计算核心绩效指标 |
calculate_annual_performance(portfolio_df) |
计算年度收益统计 |
7.11 数据获取接口(懒触发,自动 run_backtest)
| 方法签名 | 说明 |
|---|---|
get_annual_performance() |
获取年度收益统计 |
get_annual_performance_df() |
年度收益转 DataFrame(修复重复列名) |
get_trade_data() / get_position_data() / get_account_data() |
获取成交/持仓/账户数据 |
get_daily_positions() / get_trade_log() / get_portfolio_history() |
获取原始日持仓/成交/组合历史(dict records) |
get_performance_metrics() / get_equity_curve_data() |
获取绩效/净值 |
get_stock_results() |
获取各标的独立回测结果 |
get_weight_history() |
获取每日最优权重历史(DataFrame:date+各标的列) |
get_backtest_summary() |
获取回测摘要(config+绩效+年度+成交+净值+各标的+权重) |
get_all_data() |
获取全量数据(JSON 可序列化) |
get_backtrader_data() |
获取按分类组织的回测数据(指数/成交/持股/账户/绩效/权重等) |
7.12 结果保存与报告
| 方法签名 | 说明 |
|---|---|
save_backtrader_data(user='test') |
分类保存回测结果到 策略模型/均值方差最优资产组合策略/{user}/(Excel+JSON) |
save_to_json(filename=None, include_raw_data=False) |
保存回测结果为 JSON 文件 |
generate_report() |
生成格式化文本报告 |
八、输出数据结构详解
8.1 绩效指标字段(performance_metrics)
| 字段 | 类型 | 说明 |
|---|---|---|
start_date / end_date |
str |
回测起止日期 |
total_days |
int |
交易总天数 |
total_cash |
float |
初始总资金 |
final_value |
float |
最终总资产 |
total_return |
float |
总收益率(小数,如 0.25=25%) |
total_pnl |
float |
总盈亏额 |
total_commission |
float |
累计手续费 |
remaining_cash / final_holdings |
float |
期末现金 / 期末总持仓份数 |
annual_return |
float |
年化收益率 |
max_drawdown |
float |
最大回撤(负数) |
max_drawdown_date |
str |
最大回撤发生日 |
sharpe_ratio / sortino_ratio / calmar_ratio |
float |
夏普 / 索提诺 / 卡玛比率 |
win_rate |
float |
胜率(日收益为正占比) |
volatility |
float |
年化波动率 |
benchmark_final_value / benchmark_start_value |
float |
基准(等权持有)期末/期初净值 |
benchmark_total_return |
float |
基准总收益率 |
benchmark_max_drawdown |
float |
基准最大回撤 |
excess_return / excess_return_pct |
float |
相对基准超额收益(小数/百分比) |
interval / lookback_days / max_weight / min_weight / lambda_risk |
float/int |
优化参数留档 |
8.2 年度收益统计字段(annual_performance,按年份键)
| 字段 | 类型 | 说明 |
|---|---|---|
year / start_date / end_date / total_days |
int/str |
年份与年度区间 |
total_return / annual_return / total_pnl / final_value |
float |
年度收益指标 |
max_drawdown / volatility |
float |
年度风险指标 |
sharpe_ratio / sortino_ratio / calmar_ratio |
float |
年度风险调整收益 |
win_rate / t_trades / total_commission |
float/int |
年度胜率 / 成交笔数 / 手续费 |
benchmark_total_return / benchmark_* |
float |
年度基准各项 |
excess_return / excess_return_pct |
float |
年度超额收益 |
remaining_cash / final_holdings / total_cash |
float |
年度末资金/持仓/初始资金 |
8.3 净值曲线字段(equity_curve_data)
| 字段 | 类型 | 说明 |
|---|---|---|
dates |
list |
日期字符串列表 |
total_value / cumulative_pnl |
list |
每日总资产 / 累计盈亏 |
cash / holdings / market_value |
list |
现金 / 持仓 / 市值 |
net_value |
list |
策略净值(总资产/初始资金) |
drawdown |
list |
回撤(%) |
position_ratio |
list |
持仓市值占比(%) |
total_return / daily_return |
list |
累计 / 日收益率 |
benchmark_net_value / benchmark_return |
list |
基准净值 / 基准日收益率 |
statistics |
dict |
汇总(起止日/天数/初始/最大最小值/总收益/最大回撤) |
benchmark_statistics |
dict |
基准汇总(起止净值/总收益/最大回撤) |
8.4 成交统计(trade_data['statistics'])
total_trades / buy_trades / sell_trades / total_buy_amount / total_sell_amount / total_commission / first_trade_date / last_trade_date。
8.5 持仓统计(position_data['statistics'])
total_days / max_holdings / min_holdings / final_holdings / avg_holdings / first_date / last_date / final_return;每条 positions 含 stock_details(各标的 price/holdings/cash/market_value/weight/target_weight)。
8.6 账户统计(account_data['statistics'])
start_date / end_date / total_days / initial_cash / final_total_value / final_cash / final_holdings / total_pnl / total_return / max_total_value / min_total_value / max_net_value / min_net_value / volatility,含 benchmark_final_value / benchmark_total_return。
8.7 权重历史(get_weight_history)
DataFrame:列 = date + {各标的代码};每行给出该共同交易日的最优目标权重(和为 1)。是均值方差系统独有的核心诊断输出。
8.8 各标的独立结果(get_stock_results)
每标的含:cash / holdings / total_value(每日数组)、trades(该标成交)、以及 final_holdings / final_cash / final_value。
8.9 文本报告内容(generate_report())
打印策略配置、绩效指标、各标的表现、年度收益表格(含夏普/索提诺/卡尔玛/胜率/基准/超额)。
8.10 导出目录结构
策略模型/均值方差最优资产组合策略/<user>/
├── 指数数据/ 指数数据.xlsx + 指数数据.json
├── 成交数据/ 成交数据.xlsx + 成交数据.json
├── 持股数据/ 持股数据.xlsx + 持股数据.json
├── 账户数据/ 账户数据.xlsx + 账户数据.json
├── 绩效指标/ 绩效指标.xlsx + 绩效指标.json
├── 年度收益统计/ 年度收益统计.xlsx + 年度收益统计.json
├── 净值曲线/ 净值曲线.json
├── 策略参数/ 策略参数.xlsx + 策略参数.json
└── 权重历史/ 权重历史.xlsx + 权重历史.json ← 本系统独有
九、完整实战示例
9.1 示例1:经典均值方差再平衡回测(本地数据)
from xg_mean_var_backtrader.xg_mean_var_backtrader import xg_mean_var_backtrader
bt = xg_mean_var_backtrader(
start_date='20230101',
end_date='20261201',
stock_list=['159915.SZ', '513100.SH', '518880.SH'],
index_stock='000300.SH',
cash=1000000,
comm=0.0001,
# 均值方差优化参数
lookback_days=60, # 用过去60日估计收益/协方差
max_weight=0.6, # 单标权重上限60%
min_weight=0.05, # 单标权重下限5%
lambda_risk=2.0, # 风险厌恶(留档)
interval=5, # 每5个交易日调仓
max_workers=4,
)
result = bt.run_backtest()
print(bt.generate_report())
bt.save_backtrader_data(user="均值方差再平衡")
9.2 示例2:季度再平衡 + 高分散约束
bt = xg_mean_var_backtrader(
start_date='20230101',
end_date='20261201',
stock_list=['159915.SZ', '510300.SH', '518880.SH', '511010.SH', '159937.SZ'],
cash=500000,
lookback_days=120, # 长回看窗口
interval=63, # 约每季度(63交易日)调仓
max_weight=0.3, # 高分散:上限30%
min_weight=0.1, # 每只至少10%
)
bt.run_backtest()
print(bt.get_weight_history().tail()) # 查看最近权重分配
print(bt.generate_report())
9.3 示例3:自定义标的数据注入 + 均值方差回测
import pandas as pd
bt = xg_mean_var_backtrader(
start_date='20230101',
end_date='20261201',
stock_list=['159915.SZ', '513100.SH', '518880.SH'],
cash=1000000,
max_workers=4,
)
# 注入自定义数据(DataFrame 需含 date/open/high/low/close/volume)
# bt.add_stock_data_from_csv('159915.SZ', 'data/159915.csv')
# bt.add_stock_data_from_excel('513100.SH', 'data/513100.xlsx')
# bt.add_stock_data_from_dataframe('159915.SZ', pd.read_parquet('data/159915.parquet'))
bt.run_backtest()
bt.save_to_json('result/mean_var.json')
9.4 示例4:注入自定义指数数据
bt = xg_mean_var_backtrader(
start_date='20230101',
end_date='20261201',
stock_list=['159915.SZ'],
cash=500000,
)
# 构造或读取指数 DataFrame(含 date/close 即可)
import pandas as pd
index_df = pd.read_parquet('data/my_index.parquet') # 需 date/close 列
bt.use_custom_index_data = True
bt.add_index_data_from_dataframe(index_df)
print("是否有指数:", bt.has_index_data(), bt.get_index_data_info())
bt.run_backtest()
print(bt.generate_report())
9.5 示例5:提取结果做二次分析 + 画净值/回撤/权重图
bt.run_backtest()
metrics = bt.get_performance_metrics()
curve = bt.get_equity_curve_data()
trades = bt.get_trade_data()
weights = bt.get_weight_history()
print(f"总收益率: {metrics['total_return']*100:.2f}%")
print(f"年化收益率: {metrics['annual_return']*100:.2f}%")
print(f"最大回撤: {metrics['max_drawdown']*100:.2f}%")
print(f"夏普比率: {metrics['sharpe_ratio']:.3f}")
print(f"胜率: {metrics['win_rate']*100:.2f}%")
print(f"交易笔数: {trades['statistics']['total_trades']}")
import matplotlib.pyplot as plt
dates = curve['dates']
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
axes[0].plot(dates, curve['net_value'], label='策略净值', linewidth=2)
if curve.get('benchmark_net_value'):
axes[0].plot(dates, curve['benchmark_net_value'], label='基准(等权持有)', linestyle='--')
axes[0].legend(); axes[0].set_title('组合净值 vs 基准')
# 权重面积图
if weights is not None and not weights.empty:
wd = weights.set_index('date')
wd.plot.area(ax=axes[1], alpha=0.6)
axes[1].set_title('最优权重历史(均值方差优化)')
plt.xticks(rotation=45); plt.tight_layout(); plt.show()
9.6 示例6:按标的查看再平衡结果与权重分配
bt.run_backtest()
# 各标的独立结果
for stock, res in bt.get_stock_results().items():
print(f"=== {stock} ===")
print(f" 期末资产: {res['final_value']:.2f}, 持仓: {res['final_holdings']}份")
# 最新一期最优权重
weights = bt.get_weight_history()
if weights is not None and not weights.empty:
last_row = weights.iloc[-1]
print("\n最近一次最优权重分配:")
for stock in bt.stock_list:
print(f" {stock}: {last_row[stock]*100:.1f}%")
# 年度绩效
print("\n", bt.get_annual_performance())
十、自定义数据详解
10.1 标的注入后系统自动做的事
add_stock_data_from_* 会调用 _normalize_dataframe:识别日期列/索引→排序→补齐 close/high/low/open/volume→补 zdf→(有 preClose 时)补 return/adj_factor。注入成功后 use_custom_data 应设为 True(多数方法自动切换)。
10.2 指数注入说明
add_index_data_from_* 只需 date+close(自动剔除 close≤0、识别日期列);注入后 use_custom_index_data=True 时优先作为指数基准。
10.3 注入成功判定与验证
print("标的自定义键:", bt.get_custom_data_keys()) # 含代码即成功
print("has_custom_data:", bt.has_custom_data('159915.SZ'))
print("指数:", bt.has_index_data(), bt.get_index_data_info())
十一、常见问题与 FAQ
Q1: 提示「没有共同的交易日期」或回测终止?
align_data 需要全部标的共同交易日交集非空。检查各标的数据日期范围是否重叠足够;缺某标的长期停牌/无数据会显著压缩回测区间。
Q2: 为什么交易次数很少或某只总不交易?
再平衡仅在 interval 个交易日且 |diff|≥总资产×0.5% 时触发;若当前权重已贴近目标权重(尤其等权优化结果),差异小于死区则不交易,属正常。可调小 interval 或死区逻辑(代码内 0.005)。
Q3: 权重始终是等权,没有优化效果?
可能原因:数据总行数 ≤ lookback_days+30(calculate_optimal_weights_ts 内 len<30 恒退回等权);或 SLSQP 求解失败。增大历史数据 / 检查数据质量。
Q4: 如何理解 lambda_risk 参数?
当前版本 lambda_risk 仅存储与打印,目标函数是最大化夏普比率(negative_sharpe),未用 return − lambda×risk 形式。想启用风险厌恶形式可继承重写 optimize_max_sharpe。
Q5: 自定义数据注入后仍走本地目录?
确认注入返回 True,用 get_custom_data_keys() 验证,并把 use_custom_data=True(或确认注入方法自动切换)。
Q6: 收益率为 0 或异常?
检查价格列是否全为正、cash 初始值、stock_list 是否都有数据、成交撮合是否因手续费过高导致 actual_cost>amount 而 0 份、以及是否因 zdf 缺失导致收益矩阵全 NaN。
Q7: 单标的权重会超 max_weight 吗?
优化层有 bounds 约束,SLSQP 结果理论上不超 max_weight。但因价格跳动与整数手撮合,实际持仓权重会略偏离目标,需靠下个再平衡周期拉回。
Q8: 某标的首日无价格 / 建仓失败?
initial_weights 等权下若首日该标无有效价格则跳过建仓;若全部标的都无法建仓,回测结果将只剩现金。确认首日各标的有行情。
十二、高级技巧与二次开发
12.1 参数调优建议
| 参数 | 建议范围 | 说明 |
|---|---|---|
lookback_days |
30-250 | 越长估计越稳但对市场变化越滞后;过短(<30)恒退回等权 |
interval |
5-63 | 越小调仓越频繁、手续费越高;63≈季度再平衡 |
max_weight |
0.2-0.6 | 越小越分散;取决于资产池相关性与集中度偏好 |
min_weight |
0.02-0.1 | 下限越高越强制分散 |
cash / 资产池 |
3-6 只 | 太少无法体现分散优势,太多约束过紧难优化 |
12.2 均值方差策略设计经验
- 资产低相关是前提:均值方差的核心收益来自低相关资产搭配,尽量混入股/债/商品/黄金等不同风险资产。
- 收益/协方差估计:
zdf均值×252 与协方差×252 是朴素年化估计,实盘可用更长窗口或收缩估计(可在get_returns_matrix扩展)。 - 再平衡纪律:死区与
interval缺一不可,避免过度交易吞噬收益。
12.3 自定义优化目标(风险厌恶/效用最大化)
继承 xg_mean_var_backtrader,重写 optimize_max_sharpe 引入 lambda_risk:
from scipy.optimize import minimize
import numpy as np
class MyMeanVar(xg_mean_var_backtrader):
def negative_utility(self, w, mu, cov, lam):
ret = np.dot(w, mu)
vol = np.sqrt(np.dot(w.T, np.dot(cov, w)))
return -(ret - 0.5 * lam * vol ** 2) # 均值-方差效用(负)
def optimize_max_sharpe(self, returns_df):
n = len(returns_df.columns)
mu = returns_df.mean() * 252
cov = returns_df.cov() * 252
cons = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
bnds = tuple((self.min_weight, self.max_weight) for _ in range(n))
res = minimize(self.negative_utility, np.array([1/n]*n),
args=(mu, cov, self.lambda_risk),
method='SLSQP', bounds=bnds, constraints=cons)
w = res.x if res.success else np.array([1/n]*n)
return np.maximum(w, 0) / np.maximum(np.sum(np.maximum(w, 0)), 1e-12), mu, cov
12.4 自定义交易/成本规则
重写 calculate_buy_shares(改买入逻辑)、calculate_sell_shares_by_value(改卖出逻辑)、或 calculate_performance_metrics(加自定义绩效指标)。
十三、技术说明与提示
- 单一整体资金池:
cash不均分到各标的,全仓按权重分配,累计收益率按总初始资金cash折算。 - 依赖 scipy:均值方差优化需
scipy(minimize/stats),安装时务必带上。 - 无模块级函数:全部逻辑在类内,63 个方法覆盖对齐/优化/撮合/绩效全流程。
- 权重约束口径:
min_weight/max_weight作用于优化目标权重;实际因整数手撮合略有偏离。 - 指数数据仅用于输出留档,策略对照基准为「等权持有不动」曲线(
calculate_benchmark_curve)。 - 交易类型仅
初始建仓 / 再平衡买入 / 再平衡卖出,无做T/止损/信号概念。 - 源码当前固定
adj_type='none',若需复权需二次开发_normalize_dataframe。
小果均值方差回测系统,让现代组合理论落地更高效、更透明。如需源码、更多因子库或技术支持,欢迎联系小果(微信:xg_quant)。