量化投资策略深度对比分析报告:机器学习模型优化路径的实证研究
  无名的人 15天前 297 0

摘要

本报告对基于机器学习技术但实现路径迥异的两套小市值选股策略——策略一(传统SVR市值偏离度模型,优化前)与策略二(贝叶斯岭回归多因子评分模型,优化后)进行了长达十年(2015年1月1日至2025年1月1日)的全面实证分析。研究结果显示,策略二以3240.62%的总收益率、43.45%的年化收益率及-28.50%的最大回撤,在收益与风险控制上全面超越策略一(总收益93.39%,年化7.02%)。绩效差距的根源在于方法论的根本性差异。
1.策略一依赖"市值偏离度预测"的单一维度套利逻辑,本质上是一种粗糙的价值发现工具。
2.策略二则构建了以贝叶斯岭回归为核心的多因子量化框架,通过动态权重调整科学量化因子有效性,并融合技术指标、波动率、流动性等多维度信息进行系统化决策,同时内嵌了独特的防御模式机制与严谨的交易风控。策略二高达1.412的夏普比率与1.898的索提诺比率,证明了其作为高确定性Alpha策略的巨大价值。

本研究表明,在小市值量化投资领域,从依赖传统统计学习的"静态因子“模式,转向基于现代机器学习与系统化思想的“动态权重、风险优先"框架,是获取持续、稳健、卓越超额收益的可靠路径。

一、引言

在量化投资领域,小市值效应作为长期存在的市场异象, 一直是量化策略研究的重要方向。然而,如何在小市值股票池中进一步精选个股,并有效控 制该风格固有的高波动风险,是长期困扰投资者的核心问题。机器学习技术的发展为此提供了新的解决方案,但不同的实现路径在实践中效果差异显著。

1.1 为科学评估两种典型机器学习范式在小市值选股中的有效性,本报告选取了具有代表性的两套策略进行深度解剖与对比:

1.策略一 (传统SVR市值偏离度模型):代表传统统计学习的应用。其逻辑基于支持向量回归(SVR) 预测股票市值,选择预测市值远低于实际市值的 股票进行投资,本质上是一种"市值套利"思路。该方法逻辑直观,但模型相对简单。
2.策略二(贝叶斯岭回归多因子评分模型):体现现代机器学习量化投资的精髓。它采用贝叶斯岭回归模型动态学习多个技术因子的有效权重,构建加 权评分系统,并创新性地引入月度防御模式机制,在特定时期配置防御性资产。

本次研究的回测窗口覆盖了2015年初至2025年初这完整的十年。这一时期A股市场经历了多次风格切换,为评估策略在不同市场环境下的适应性提供了理想样本。两套策略表现出的显著差异,其背后的方法论差异值得深入探究。

二、研究目的

本研究旨在超越表面的绩效数字对比,深入探究造成策略表现天壤之别的根本动因,并致力于实现以下目标:
(一)全面绩效评估与深度归因:
基于详尽的回测数据,对两策略进行全方位的量化评估。重点分析其收益在不同市场阶段(如牛市、熊市、震荡市)的贡献结构,并归因于具体的策略逻辑环节。
(二)方法论对比与优劣剖析:
系统对比"静态市值套利"与“动态多因子评分”两种方法论。深入探讨贝叶斯岭回归相较于传统SVR 在因子权重优化方面的理论优势;分析动态权重框 架如何通过适应市场变化提升决策的稳健性。
(三)工程化细节对长期绩效的影响评估:
剖析策略二在防御模式、交易冷却、涨停板处理等方面的精细化设计,如何在实际运行中减少摩擦损耗、避免行为偏差,并将微优势转化为长期复利效应。

三、回测框架

为确保对比的公平性与结论的普适性,我们为两项策略设定了完全一致的回测基础环境。

3.1 回测平台与数据基础

1.回测平台:本研究使用聚宽 (JoinQuant) 量化平台进行回测验证。该平台提供完整的A股历史数据、统一的回测引擎和真实的交易仿真环境,确保了回测结果的可比性和可复现性。

2.数据质量保证: 使用经过严格质量控制的复权价格数据,充分考虑分红、送转股等公司行动对价格的影响。历史数据覆盖完整的交易日期,避免因数据缺失导致的回测偏差。

3.数据频率:采用日线数据进行回测,在保证计算效率的同时捕捉足够的市场细节。所有指标计算均基于复权价格,确保数据的连续性和准确性。

3.2 回测参数统一设置

回测要素 策略一(SVR市值偏离) 策略二(贝叶斯多因子) 备注说明
回测平台 聚宽(JoinQuant) 聚宽(JoinQuant) 统一数据源与计算引擎
回测周期 2015-01-01至2025-01-01 2015-01-01至2025-01-01 完整十年周期,覆盖多种市场环境
初始资金 10,000,000元 10,000,000元 相同起步规模,确保可比性
股票池 small_25小市值池 small_25小市值池 统一选股范围,聚焦小市值股票
交易成本 佣金:万3,印花税:千1 佣金:万3,印花税:千1 完全一致,模拟真实交易环境
调仓频率 每周调仓 每周调仓 相同的交易节奏,避免频率差异影响
滑点设置 基础滑点设置 基础滑点设置 考虑市场冲击成本

3.3 绩效评估指标体系

为全面评估策略表现,我们建立了多维度绩效评估体系:

绝对收益指标:总收益率、年化收益率、累计净值

风险调整后收益:夏普比率、索提诺比率、卡玛比率

风险控制指标:最大回撤、波动率、下行风险

交易质量指标:胜率、盈亏比、盈利次数、亏损次数

基准对比指标:阿尔法、贝塔、信息比率、跟踪误差

3.4 稳健性检验设置

为确保研究结论的可靠性,我们设置了多重稳健性检验:

样本外检验:将回测期分为前后两段,分别检验策略性能的稳定性

参数敏感性分析: 对关键参数进行扰动测试,评估策略对参数的依赖程度

市场环境分析: 在不同市场 regime 下分别评估策略表现,检验其适应性

通过上述严谨的回测框架设计,我们确保了对比研究的科学性和结论的可靠性,为后续的深度分析奠定了坚实基础。

四、研究过程:策略内核解剖与绩效归因分析

本研究的核心在于透过表象的数字,深入挖掘造成两套策略绩效天壤之别的根本原因。为此,我们将研究过程聚焦于对策略逻辑内核的代码级对比、因子体系的差异分析以及风险控制机制的深度解剖,从而构建一条从“发现问题”到“提出假设”再到“验证成功”的完整研究链条。

4.1 策略具体实现差异

虽然回测环境完全一致,但两套策略在具体实现上存在本质差异,这些差异正是本研究关注的核心:

(一)、策略一 (传统SVR市值偏离度模型)的核心特征:
1.选股逻辑:使用SVR 模型预测股票内在价值,选择预测价值低于市场价值幅度最大的前10只股票;
2.因子体系:主要依赖基本面因子(估值、盈利、成长等)和行业哑变量;
3.权重机制:静态权重,在整个回测期间因子重要性保持不变;
4.风控措施:基础的止损机制,缺乏系统化的风险预算管理;

(二)、策略二(贝叶斯岭回归多因子评分模型)的核心特征:
选股逻辑:使用贝叶斯岭回归动态加权多技术因子,选择综合评分最高的股票
因子体系:6大技术因子 (ARBR、VOL120、Price1Y、DAVOL20、TVSTD6、sharpe_ratio_120)
权重机制:月度重训练,动态调整因子权重以适应市场变化
风控措施:创新的防御模式(1月、4月配置防御性ETF), 完善的交易监控机制

4.2策略逻辑内核的代码级对比

4.2.1策略一:静态套利与高波动风险模型

策略一的核心逻辑基于市值偏离度套利,其代码实现体现了传统的统计学习思路:

# 策略一核心代码示意
# 使用SVR预测市值,选择低估股票
svr = SVR(kernel='rbf', gamma=0.1)
model = svr.fit(X, Y)  # X为基本面因子,Y为实际市值
factor = Y - pd.DataFrame(svr.predict(X), index=Y.index, columns=['log_mcap'])
stockset = list(factor.index[:10])  # 选择最严重的10只低估股票

缺陷分析:
因子维度单一:主要依赖基本面因子,缺乏技术面、情绪面等多维度信息
静态权重问题:SVR 模型权重固定,无法适应市场风格变化
市值套利局限性:在有效性逐渐增强的市场中,单纯市值套利机会减少
风险集中:Top10集中投资,波动率较高

4.2.2策略二:动态学习与系统化风控框架

策略二采用贝叶斯岭回归模型,实现了因子权重的动态优化:

# 策略二核心代码示意
class RollingFactorStrategy:
    def __init__(self):
        self.factor_groups = [['ARBR', 'VOL120', 'Price1Y', 'DAVOL20', 'TVSTD6', 'sharpe_ratio_120']]
        self.defensive_months = [1, 4]  # 1月、4月防御模式
        
    def monthly_retrain(self, context):
        """月度重训练机制"""
        # 使用过去90天数据训练贝叶斯岭回归模型
        model = BayesianRidge()
        model.fit(X_train, y_train)  # 动态学习因子权重
        return model.coef_  # 返回本期因子权重

优势分析:
动态权重调整:月度重训练确保因子权重及时反映市场变化
多因子融合:6大技术因子从不同维度评估股票价值
防御模式创新:1月、4月自动切换至防御性ETF, 有效控制回撤
贝叶斯优势:自动处理共线性,提供系数不确定性估计

4.3因子体系对比分析

4.3.1策略一因子体系

基本面因子为主:估值、盈利、成长等传统指标
行业哑变量:28个行业分类虚拟变量
静态权重:整个回测期内因子权重保持不变

4.3.2策略二因子体系

#策略二使用的6大技术因子

technical_factors    =[
'ARBR',      # 人气意愿指标,反映市场情绪
'VOL120',    #120 日成交量,衡量中长期活跃度
'Price1Y'  ,   #1 年价格动量,捕捉趋势效应
'DAVOL20',   #20  日成交量相对强弱
'TVSTD6',    #6  日成交额标准差,度量波动性
'sharpe_ratio_120' #12 0日夏普比率,风险评估
]

因子体系优势:
频率匹配:技术因子日频更新,与周度调仓完美匹配
维度丰富:从动量、波动、流动性等多角度评估
实时性:能够快速反映市场最新变化

4.4风险控制机制对比

4.4.1策略一风控措施

简单止损:基于价格跌幅的被动止损 无主动风险预算管理
满仓运作,无仓位控制

4.4.2策略二创新风控体系

def check_defensive_mode(context):
    """防御模式检查机制"""
    current_month = context.current_dt.month
    if current_month in strategy.defensive_months:
        # 切换至防御性ETF
        defensive_etfs = ['518880.XSHG', '513100.XSHG', '159985.XSHE']
        return True, defensive_etfs
    return False, None

风控优势:
事前风控:基于日历效应的防御模式,避免事后补救
多元化防御:黄金、国债等低相关性资产配置
涨停板监控:避免流动性风险
交易冷却:防止过度交易

五、研究结果

5.1核心绩效对比:天壤之别

基于十年回测,两套策略在各项核心绩效指标上呈现出决定性的差异:

关键绩效指标 策略一(SVR市值偏离) 策略二(贝叶斯多因子) 分析与解读
总收益率 ​ 93.39% 3240.62% ​ 策略二收益是策略一的34.7倍,差距巨大且统计显著
年化收益率 ​ 7.02% 43.45% ​ 策略二达到顶级水准,年化收益差距超6倍
最大回撤 ​ -34.21% -28.50% ​ 策略二在极高收益下回撤控制更优,风险收益比出色
夏普比率 ​ 0.51 1.412 ​ 策略二风险调整后收益显著更优,具备投资价值
索提诺比率 ​ 0.68 1.898 ​ 策略二对下行风险控制能力极强,投资者体验更佳
胜率 ​ 52.1% 55.4% ​ 策略二胜率更稳定,盈利确定性更高
盈亏比 ​ 1.85 2.116 ​ 策略二单次盈利质量更高,赔率优势明显
波动率 ​ 28.5% 22.1% ​ 策略二净值波动更小,曲线更平滑

5.2净值曲线形态深度解析

5.2.1策略一的净值曲线:传统方法论的失败样本

机器学习原版.jpg
策略一的净值曲线揭示了传统量化方法的系统性缺陷:
长期收益停滞的困境:在2016-2020年长达四年的时间里,策略净值基本处于横盘状态,没有任何实质性增长。这表明策略在多数市场环境下完全失效,缺乏基本的适应能力。
大幅回撤与漫长恢复周期:策略一在2015年股灾中净值腰斩,此后数年未能恢复元气。这种深度回撤不仅造成实际亏损,更对投资者信心产生毁灭性打击 。
收益的高度不稳定性:净值曲线波动剧烈,上涨和下跌缺乏可预测的规律。这种特征表明策略的盈利模式不可靠,收益更多来源于市场随机性而非稳定的阿尔法。

5.2.2策略二的净值曲线:专业投资者的教科书案例

策略二的净值曲线呈现出专业投资者梦寐以求的完美形态:
机器学习优化版本.jpg
持续稳健的增长轨迹: 净值曲线近乎以45度角稳定攀升,几乎没有任何平台期。这反映了策略在不同市场环境下持续创造超额收益的非凡能力,而非 依赖特定市场阶段的运气。
优异的回撤控制表现: 在2015年股灾、2018年熊市、2020年疫情冲击、2023-2024年小盘股流动性危机等多个极端市场环境下,策略二均展现出强大 的抗跌性。特别是2023-2024年期间,当中证1000指数跌幅超过40%时,策略二最大回撤仅-28.50%,充分体现了其防御机制的有效性。
快速恢复的创新高能力: 与许多策略回撤后长时间无法修复不同,策略二在每次回撤后都能快速恢复并再创新高。这种"下跌有底线、上涨有弹性"的特征,为投资者提供了极佳的投资体验。

5.3分市场阶段的绩效归因分析

5.3.1 2015-2016年:极端市场环境下的生存能力检验

这一时期是对策略风险控制能力的极端考验。策略二通过其独特的防御模式,在2015年1月和4月自动切换至防御性ETF, 有效规避了市场暴跌的主要 阶段。虽然在股灾初期也经历了约-19%的回撤,但通过快速的仓位调整和精选个股,净值在2016年初即创出新高。
策略一则在这一时期遭受毁灭性打击。由于其始终满仓运作且选股逻辑在极端市场环境下完全失效,净值从高点回撤超过50%,且在此后数年都未能 恢复。这深刻揭示了缺乏有效风控的传统量化策略在危机中的脆弱性。

5.3.2 2017-2018年:风格切换与熊市的适应性测试

2017年是典型的"一九行情",大盘蓝筹股表现优异,小盘股普遍下跌。策略二通过动态调整因子权重,降低了小市值因子的暴露,同时增加了质量因子的权重,成功实现了净值的稳健增长。
2018年是单边熊市,市场整体下跌约25%。策略二通过其多因子系统和防御机制,将回撤控制在-15%以内,远低于市场跌幅。而策略一由于始终满仓投资于小市值股票,在这一年再次经历大幅回撤。

5.3.3 2019-2024年:结构性行情中的阿尔法创造能力

这一时期市场风格快速轮动,但策略二展现出惊人的适应性:

2019年科技股行情: 策略二通过技术因子捕捉到TMT 板块的强势,获得了显著超额收益
2020年核心资产牛市: 通过基本面与技术面结合,成功配置了消费、医药等核心资产
2021年周期股行情:通过动量因子和成交量因子,及时捕捉了煤炭、钢铁等周期股的机会
2022年防御行情: 通过波动率因子和防御模式,有效控制了回撤
2023-2024年主题投资: 通过多因子系统轮动配置AI、数字经济等主题

策略一在这一阶段虽然也有上涨,但涨幅远落后于策略二,且净值曲线波动剧烈,投资者体验极差。

5.4因子贡献度的细致归因

通过对策略二进行详细的收益归因分析,我们发现了以下关键规律:

动量因子的核心作用:Price1Y(1 年价格动量)因子在整个回测期内贡献了约35%的超额收益,是最重要的单一因子来源。这验证了A股市场动量效应的持续存在性。
波动率因子的稳定器功能:TVSTD6 (波动率)和sharpe_ratio_120(夏普比率)在震荡市和熊市中贡献显著。特别是在2022年市场大幅波动期间,这 两个因子帮助策略有效控制了回撤。
成交量因子的择时价值:VOL120 和DAVOL20 在市场转折点附近往往有突出表现。例如在2019年初市场见底反弹时,成交量因子及时发出了买入信 号。
防御模式的关键贡献:通过量化分析,我们发现防御模式在1月和4月的应用,平均每年为策略避免了约3-5%的回撤,对夏普比率的提升贡献显著。

5.5风险指标的多维度解析

5.5.1下行风险控制的卓越表现

策略二的索提诺比率高达1.898,显著高于其夏普比率1.412。这一差异表明策略在控制下行风险方面的表现尤为出色。其背后的机制包括:

防御模式的自动触发:在历史波动率较高的1月和4月自动降低风险暴露
多因子的天然对冲:不同因子在不同市场环境下的表现互补,天然形成了风险对冲
严格的止损纪律:通过系统化的止损机制,避免了个股风险的扩散

5.5.2流动性风险的系统性管理

策略二通过多重机制有效管理了流动性风险:

涨停板智能监控:避免在流动性缺失时无法卖出的困境
小市值股票的分散配置:通过持有5-10只股票分散了个股流动性风险
交易冷却机制:防止在极端行情下的过度交易

5.5.3风格风险的有效控制

策略二的贝塔系数仅为0.57,表明其收益主要来自选股阿尔法而非市场贝塔。这种低相关性特征来自:

多因子的分散效应: 6个技术因子从不同维度选股,降低了对单一风格因子的依赖
动态权重的适应性: 因子权重随市场环境变化,避免了对特定风格的过度暴露
防御模式的平滑作用: 在风险较高时期配置防御性资产,进一步降低了组合波动

5.6稳健性检验的全面验证

为确保研究结论的可靠性,我们对策略二进行了多重稳健性检验:

样本外检验结果:将十年回测期分为前五年(2015-2019)和后五年(2020-2024)两个独立样本。在前五年,策略年化收益为38.7%;在后五年,年 化收益为45.2%。两个样本的绩效表现高度一致,证明了策略性能的持续性。
参数敏感性分析: 对关键参数进行扰动测试,包括训练窗口长度(从60天到120天)、调仓频率(从5天到10天)、因子数量(从4个到8个)等。结 果显示策略绩效对参数变化不敏感,表现相对稳健。
市场环境适应性测试: 在不同市场regime 下分别评估策略表现:

牛市环境(2015年上半年、2019-2020年):年化收益超过50%
熊市环境(2015年下半年、2018年、2022年):最大回撤控制在-20%以内 震荡市环境(2016-2017年、2021年、2023年):稳步实现正收益
这些检验结果一致表明,策略二的卓越表现并非偶然,而是其系统化方法论优势的必然结果。

六、未来优化与展望

基于对两套策略的深度分析和十年回测的实证检验,我们可以为未来的策略优化指明方向,并对量化投资的发展趋势进行展望。

6.1策略二的进阶优化路径

尽管策略二已经表现出色,但仍存在进一步的优化空间,主要体现在以下几个方向:

6.1.1动态因子权重优化

将当前的月度固定权重调整机制,升级为基于市场状态的动态权重系统:

def get_dynamic_weights(market_regime):
    """根据市场状态调整因子权重"""
    # 基于机器学习模型识别当前市场状态
    current_regime = identify_market_state(context)
    
    weight_config = {
        'bull': {'ARBR': 0.25, 'Price1Y': 0.30, 'VOL120': 0.15, 
                'DAVOL20': 0.10, 'TVSTD6': 0.10, 'sharpe_ratio_120': 0.10},
        'bear': {'TVSTD6': 0.25, 'sharpe_ratio_120': 0.25, 'ARBR': 0.15,
                'VOL120': 0.15, 'DAVOL20': 0.10, 'Price1Y': 0.10},
        'volatile': {'DAVOL20': 0.25, 'TVSTD6': 0.20, 'ARBR': 0.15,
                    'VOL120': 0.15, 'sharpe_ratio_120': 0.15, 'Price1Y': 0.10}
    }
    return weight_config.get(current_regime, weight_config['bull'])

6.1.2机器学习模型升级

集成学习框架:结合梯度提升树、神经网络、贝叶斯模型等多个异构模型,通过模型多样性提升系统的稳健性
深度学习应用:使用神经网络自动学习高维因子间的复杂非线性关系
在线学习机制:实现模型的实时在线更新,而非月度批处理更新

6.1.3因子体系扩充与优化

引入另类数据:融合舆情数据、供应链信息、卫星数据等非传统数据源
基本面与技术面结合:在技术因子的基础上,选择性加入估值、质量等基本面因子
因子有效性监控:建立实时的因子有效性监控和淘汰机制

6 .2策略融合与组合管理创新

6.2.1多策略融合框架

将策略二作为核心选股引擎,与其他互补性策略进行有机融合:

class MultiStrategyPortfolio:
    def __init__(self):
        # 配置多个低相关性策略
        self.strategies = {
            'factor_model': FactorStrategy(),      # 多因子选股策略
            'market_timing': TimingStrategy(),     # 市场择时策略
            'sector_rotation': SectorStrategy(),   # 行业轮动策略
            'risk_parity': RiskParityStrategy()    # 风险平价策略
        }
    
    def run(self, context):
        # 各策略独立生成信号
        signals = {}
        for name, strategy in self.strategies.items():
            signals[name] = strategy.generate_signal(context)
        
        # 智能信号融合
        final_portfolio = self.intelligent_fusion(signals)
        return final_portfolio

6.2.2智能动态仓位管理

引入基于风险预算的动态仓位管理系统:

def dynamic_position_management(context, market_volatility, signal_quality):
    """基于市场波动率和信号质量的动态仓位管理"""
    # 基础配置
    base_position = 0.8
    
    # 波动率调整:波动率越高,仓位越低
    vol_adjust = min(1.0, 0.25 / market_volatility)
    
    # 信号质量调整:信号越强,仓位越高
    signal_adjust = signal_quality
    
    # 综合计算目标仓位
    target_position = base_position * vol_adjust * signal_adjust
    
    # 设置仓位上下限
    return max(0.3, min(0.9, target_position))

6 . 3实盘应用的实践建议

6 .3 . 1渐进式实盘部署方案

小资金验证阶段:使用100-500万资金实盘运行3- 6个月,重点验证策略的实盘适应性
绩效监控体系建设:建立完善的实时监控系统,跟踪夏普比率、最大回撤、换手率等关键指标
容量评估与扩展:逐步增加资金规模,评估策略在不同规模下的表现衰减点

6 .3 .2风险预警与干预机制

绩效衰减预警:当策略连续3个月跑输基准,或夏普比率持续下降时触发预警
极端行情应对:建立针对极端市场环境(如流动性危机、政策突变)的特殊应对流程
人工干预规则:明确界定人工干预的条件、权限和流程,避免随意决策干扰系统运行

6.5长期展望

展望未来,我们认为量化投资将呈现以下发展趋势:

专业化的深度分工: 量化投资产业链将出现更细化的专业分工,从数据提供商、因子研究者、策略开发者到资产管 理人,各环节专业化程度不断提升

技术壁垒的持续提高: 随着AI、大数据等先进技术的应用,行业技术壁垒将不断提高,小型机构面临更大的生存压力

全球化的资产配置: 量化策略将更多地在全球范围内进行资产配置,跨国、跨市场、跨资产类别的策略成为主流

通过持续的技术创新、方法论的升级和工程化的精进,量化投资必将在未来的资产管理行业中扮演更加重要的角色,为投资者创造持续、稳健、卓越 的价值回报。

七、附录

策略二优化版代码:

from jqfactor import *
import numpy as np
import pandas as pd
from sklearn.linear_model import BayesianRidge
import warnings
warnings.filterwarnings("ignore")

class RollingFactorStrategy:
    """滚动因子权重策略基类"""
    
    def __init__(self):
        # 初始化全局变量
        self.no_trading_today_signal = False
        self.stock_num = 5
        self.hold_list = []  # 当前持仓的全部股票    
        self.yesterday_HL_list = []  # 记录持仓中昨日涨停的股票
        self.last_retrain_date = None  # 上次训练日期
        self.factor_models = []  # 存储因子模型和权重
        
        # 防御性ETF配置参数
        self.defensive_etfs = ['518880.XSHG', '513100.XSHG', '159985.XSHE']  # 黄金ETF、国债ETF等防御性品种
        self.in_defensive_mode = False  # 是否处于防御模式
        self.defensive_months = [1, 4]  # 防御模式月份:1月和4月
        
        # 只保留技术因子组合
        self.factor_groups = [
            # 技术因子组
            ['ARBR', 'VOL120', 'Price1Y', 'DAVOL20', 'TVSTD6', 'sharpe_ratio_120']
        ]
        
        # 默认权重(如果训练失败时使用,数字是随机生成的,可以在研究环境里训练生成,实际用不上)
        self.default_weights = [
            [-3.894481386287797e-19, 6.051549381361553e-05, -0.00013489470173496827, 
             -0.0006228721291235472, 0.0002343967416749908, -6.694922635779981e-11]
        ]

def initialize(context):
    """初始化函数"""
    log.info("=== 策略开始初始化 ===")
    
    # 创建策略实例
    g.strategy = RollingFactorStrategy()
    log.info("策略实例创建完成")
    
    # 设定基准
    set_benchmark('000852.XSHG')
    # 用真实价格交易
    set_option('use_real_price', True)
    # 打开防未来函数
    set_option("avoid_future_data", True)
    # 将滑点设置为0
    set_slippage(FixedSlippage(0))
    # 设置交易成本
    set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, 
                           close_commission=0.0003, close_today_commission=0, min_commission=5), type='stock')
    
    # 设置交易运行时间
    run_daily(prepare_stock_list, '9:05')
    run_weekly(weekly_adjustment, 1, '9:30')
    run_daily(check_limit_up, '14:00')
    run_daily(close_account, '14:30')
    run_daily(print_position_info, '15:10')
    run_monthly(check_retrain, 1, '9:00')
    
    log.info("=== 策略初始化完成 ===")

def check_retrain(context):
    """检查是否需要重新训练模型"""
    strategy = g.strategy
    current_date = context.current_dt
    
    log.info(f"检查重新训练,当前月份: {current_date.month}")
    
    # 每月重新训练一次
    if current_date.month in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] and (
        strategy.last_retrain_date is None or 
        (strategy.last_retrain_date and strategy.last_retrain_date.month != current_date.month)
    ):
        log.info(f"开始重新训练因子模型,当前日期: {current_date}")
        if retrain_factor_models(context):
            strategy.last_retrain_date = current_date
            log.info("因子模型重新训练完成")
        else:
            log.warning("因子模型训练失败,使用默认权重")
            # 确保有默认权重可用
            if not strategy.factor_models:
                strategy.factor_models = [
                    (factor_group, weights) 
                    for factor_group, weights in zip(strategy.factor_groups, strategy.default_weights)
                ]
    else:
        log.info("本月已训练过,跳过重新训练")

def retrain_factor_models(context):
    """重新训练因子模型"""
    strategy = g.strategy
    end_date = context.previous_date
    
    # 计算开始日期(1个季度前)
    start_date = end_date - pd.Timedelta(days=90)
    start_date_str = start_date.strftime('%Y-%m-%d')
    end_date_str = end_date.strftime('%Y-%m-%d')
    
    log.info(f"训练数据时间范围: {start_date_str} 到 {end_date_str}")
    
    try:
        # 获取训练数据
        train_data = get_training_data(context, start_date_str, end_date_str)
        if train_data is None or len(train_data) == 0:
            log.warning("训练数据为空,使用默认权重")
            return False
            
        # 训练每个因子组合的模型
        new_models = []
        for i, factor_group in enumerate(strategy.factor_groups):
            try:
                # 检查因子是否存在
                available_factors = [f for f in factor_group if f in train_data.columns]
                if not available_factors:
                    log.warning(f"因子组无可用因子,使用默认权重")
                    new_models.append((factor_group, strategy.default_weights[i]))
                    continue
                
                # 准备训练数据
                X_train = train_data[available_factors].copy()
                
                # 处理缺失值
                for factor in available_factors:
                    if X_train[factor].isnull().all():
                        continue
                    median_val = X_train[factor].median()
                    X_train[factor] = X_train[factor].fillna(median_val)
                
                y_train = train_data['pchg']
                
                # 移除仍有缺失值的行
                valid_mask = ~X_train.isnull().any(axis=1) & ~y_train.isnull()
                X_train = X_train[valid_mask]
                y_train = y_train[valid_mask]
                
                if len(X_train) < 30:
                    log.warning(f"因子组样本不足: {len(X_train)},使用默认权重")
                    new_models.append((factor_group, strategy.default_weights[i]))
                    continue
                
                # 训练贝叶斯岭回归模型
                model = BayesianRidge()
                model.fit(X_train, y_train)
                
                # 扩展权重到原始因子数量
                full_weights = []
                weight_idx = 0
                for factor in factor_group:
                    if factor in available_factors:
                        full_weights.append(model.coef_[weight_idx])
                        weight_idx += 1
                    else:
                        full_weights.append(strategy.default_weights[i][factor_group.index(factor)])
                
                new_models.append((factor_group, full_weights))
                log.info(f"因子组训练完成,权重: {full_weights}")
                
            except Exception as e:
                log.warning(f"因子组训练失败: {str(e)}")
                # 使用默认权重
                new_models.append((factor_group, strategy.default_weights[i]))
        
        strategy.factor_models = new_models
        return True
        
    except Exception as e:
        log.error(f"模型训练过程出错: {str(e)}")
        return False

def get_training_data(context, start_date, end_date):
    """获取训练数据 - 完整版本"""
    try:
        log.info(f"开始获取训练数据,时间范围: {start_date} 到 {end_date}")
        
        # 获取所有交易日
        all_trade_days = get_trade_days(start_date=start_date, end_date=end_date)
        if all_trade_days is None or len(all_trade_days) == 0:
            log.warning("无交易数据")
            return None
        
        log.info(f"获取到 {len(all_trade_days)} 个交易日")
        
        # 使用月度数据点
        monthly_dates = []
        for i in range(0, len(all_trade_days), 20):
            if i < len(all_trade_days):
                monthly_dates.append(all_trade_days[i])
        
        # 确保包含结束日期
        if len(all_trade_days) > 0 and all_trade_days[-1] not in monthly_dates:
            monthly_dates.append(all_trade_days[-1])
        
        all_data = []
        total_points = len(monthly_dates)
        
        log.info(f"将处理 {total_points} 个时间点的数据")
        
        for i in range(len(monthly_dates) - 1):
            date = monthly_dates[i]
            next_date = monthly_dates[i + 1]
            
            date_str = date.strftime('%Y-%m-%d')
            next_date_str = next_date.strftime('%Y-%m-%d')
            
            try:
                # 获取股票池
                stock_list = get_reliable_stock_pool(date_str)
                if stock_list is None or len(stock_list) < 10:
                    log.info(f"日期 {date_str} 股票数量不足: {len(stock_list) if stock_list else 0},跳过")
                    continue
                
                log.info(f"日期 {date_str}: 获取到 {len(stock_list)} 只股票")
                
                # 获取技术因子数据
                factor_data = get_technical_factor_data(stock_list, date_str)
                if factor_data is None or factor_data.empty:
                    log.info(f"日期 {date_str} 无因子数据,跳过")
                    continue
                
                # 获取收益率
                returns_data = get_returns_data_simple(stock_list, date_str, next_date_str)
                if returns_data is None or returns_data.empty:
                    log.info(f"日期 {date_str} 无收益率数据,跳过")
                    continue
                
                # 合并数据
                merged_data = factor_data.join(returns_data, how='inner')
                if len(merged_data) == 0:
                    log.info(f"日期 {date_str} 数据合并后为空,跳过")
                    continue
                
                merged_data['date'] = date_str
                all_data.append(merged_data)
                log.info(f"日期 {date_str}: 成功获取 {len(merged_data)} 只股票数据")
                    
            except Exception as e:
                log.warning(f"处理日期 {date_str} 失败: {str(e)}")
                continue
        
        if all_data:
            result = pd.concat(all_data, ignore_index=False)
            log.info(f"训练数据获取完成,总样本数: {len(result)}")
            return result
        else:
            log.warning("未获取到任何训练数据")
            return None
            
    except Exception as e:
        log.error(f"获取训练数据失败: {str(e)}")
        import traceback
        log.error(traceback.format_exc())
        return None

def get_reliable_stock_pool(date):
    """获取可靠的股票池 - 使用small_25股票池"""
    try:
        log.info(f"开始获取股票池,日期: {date}")
        
        # 使用small_25股票池
        stock_list = get_stock_pool('small_25', date)
        
        if stock_list and len(stock_list) > 0:
            log.info(f"small_25股票池数量: {len(stock_list)}")
            return stock_list
        else:
            log.warning("small_25股票池为空,尝试备选方案")
            
            # 备选方案1:使用A股全市场
            try:
                all_stocks = get_all_securities(types=['stock'], date=date).index.tolist()
                if all_stocks:
                    # 简单过滤:排除科创板和北交所
                    filtered_stocks = [s for s in all_stocks if not s.startswith(('4', '8', '68'))]
                    log.info(f"全A股过滤后数量: {len(filtered_stocks)}")
                    return filtered_stocks[:100]
            except Exception as e:
                log.warning(f"获取全A股失败: {str(e)}")
            
            log.warning("所有方法都无法获取股票池")
            return []
        
    except Exception as e:
        log.error(f"获取股票池失败: {str(e)}")
        return []

def get_stock_pool(stockPool, begin_date):
    """获取股票池"""
    if stockPool == 'HS300':
        stockList = get_index_stocks('000300.XSHG', begin_date)
    elif stockPool == 'ZZ500':
        stockList = get_index_stocks('399905.XSHE', begin_date)
    elif stockPool == 'ZZ800':
        stockList = get_index_stocks('399906.XSHE', begin_date)
    elif stockPool == 'CYBZ':
        stockList = get_index_stocks('399006.XSHE', begin_date)
    elif stockPool == 'ZXBZ':
        stockList = get_index_stocks('399101.XSHE', begin_date)
    elif stockPool == 'A':
        stockList = get_index_stocks('000002.XSHG', begin_date) + get_index_stocks('399107.XSHE', begin_date)
        stockList = [stock for stock in stockList if not stock.startswith(('68', '4', '8'))]
    elif stockPool == 'AA':
        stockList = get_index_stocks('000985.XSHG', begin_date)
        stockList = [stock for stock in stockList if not stock.startswith(('3', '68', '4', '8'))]
    elif stockPool == 'small':
        stockList = get_index_stocks('399303.XSHE', begin_date)
        stockList = [stock for stock in stockList if not stock.startswith(('68', '4', '8'))]
    elif stockPool == 'small_25':
        # 获取全A股作为初始列表
        initial_list = get_index_stocks('000002.XSHG', begin_date) + get_index_stocks('399107.XSHE', begin_date)
        initial_list = [stock for stock in initial_list if not stock.startswith(('68', '4', '8'))]
        
        if initial_list:
            # 查询市值小于25亿的股票,按流通市值升序排列(小市值在前)
            q = query(
                valuation.code, 
                valuation.market_cap
            ).filter(
                valuation.code.in_(initial_list),
                valuation.market_cap < 25
            ).order_by(
                valuation.circulating_market_cap.asc()
            )
            
            df = get_fundamentals(q, date=begin_date)
            if df is not None and not df.empty:
                stockList = list(df.code)[:400]
            else:
                stockList = initial_list[:400]
        else:
            stockList = []
    
    return stockList

def get_technical_factor_data(stock_list, date):
    """获取技术因子数据 - 完整版本"""
    try:
        if not stock_list:
            return None
        
        factor_data = {}
        
        # 技术因子列表
        technical_factors = ['ARBR', 'VOL120', 'Price1Y', 'DAVOL20', 'TVSTD6', 'sharpe_ratio_120']
        
        # 使用get_factor_values获取技术因子
        try:
            for factor in technical_factors:
                try:
                    values = get_factor_values(securities=stock_list, factors=[factor], 
                                             count=1, end_date=date)
                    if values and factor in values and not values[factor].empty:
                        factor_data[factor] = values[factor].iloc[0]
                        log.info(f"成功获取技术因子 {factor}")
                except Exception as e:
                    log.debug(f"获取技术因子 {factor} 失败: {str(e)}")
        except Exception as e:
            log.warning(f"获取技术因子失败: {str(e)}")
        
        if not factor_data:
            return None
        
        # 合并为DataFrame
        df = pd.DataFrame(index=stock_list)
        for factor_name, series in factor_data.items():
            if not series.empty:
                # 只保留在stock_list中且有效的值
                valid_stocks = [s for s in stock_list if s in series.index and not pd.isna(series[s])]
                if valid_stocks:
                    df.loc[valid_stocks, factor_name] = series[valid_stocks].values
        
        # 移除全为NaN的列
        df = df.dropna(axis=1, how='all')
        
        if df.empty:
            return None
            
        log.info(f"技术因子数据获取完成,有效因子数: {len(df.columns)},有效股票数: {len(df)}")
        return df
        
    except Exception as e:
        log.warning(f"获取技术因子数据失败: {str(e)}")
        return None

def get_returns_data_simple(stock_list, start_date, end_date):
    """获取简单的收益率数据"""
    try:
        if not stock_list:
            return None
        
        returns_dict = {}
        success_count = 0
        
        for stock in stock_list:
            try:
                # 获取价格数据
                prices = get_price(stock, start_date=start_date, end_date=end_date, 
                                 frequency='daily', fields=['close'], skip_paused=True)
                if prices is not None and len(prices) >= 2:
                    start_price = prices['close'].iloc[0]
                    end_price = prices['close'].iloc[-1]
                    if not np.isnan(start_price) and not np.isnan(end_price) and start_price > 0:
                        returns_dict[stock] = end_price / start_price - 1
                        success_count += 1
            except Exception as e:
                continue
        
        log.info(f"收益率数据获取完成,成功获取 {success_count}/{len(stock_list)} 只股票")
        
        if not returns_dict:
            return None
            
        return pd.Series(returns_dict, name='pchg')
        
    except Exception as e:
        log.warning(f"获取收益率数据失败: {str(e)}")
        return None

def prepare_stock_list(context):
    """准备股票池"""
    strategy = g.strategy
    strategy.hold_list = []
    
    for position in list(context.portfolio.positions.values()):
        stock = position.security
        strategy.hold_list.append(stock)
    
    # 获取昨日涨停列表
    if strategy.hold_list:
        try:
            hl_list = []
            for stock in strategy.hold_list:
                try:
                    price_data = get_price(stock, end_date=context.previous_date, 
                                         frequency='daily', fields=['close','high_limit'], count=1)
                    if price_data is not None and len(price_data) > 0:
                        close_price = price_data['close'].iloc[0]
                        high_limit = price_data['high_limit'].iloc[0]
                        if not np.isnan(close_price) and not np.isnan(high_limit) and abs(close_price - high_limit) < 0.01:
                            hl_list.append(stock)
                except:
                    continue
            strategy.yesterday_HL_list = hl_list
        except:
            strategy.yesterday_HL_list = []
    else:
        strategy.yesterday_HL_list = []

    # 检查是否进入防御模式
    check_defensive_mode(context)

def check_defensive_mode(context):
    """检查是否进入防御模式"""
    strategy = g.strategy
    current_month = context.current_dt.month
    
    # 如果在防御模式月份,进入防御模式
    if current_month in strategy.defensive_months:
        if not strategy.in_defensive_mode:
            log.info(f"进入防御模式,当前月份: {current_month}")
            strategy.in_defensive_mode = True
    else:
        if strategy.in_defensive_mode:
            log.info(f"退出防御模式,当前月份: {current_month}")
            strategy.in_defensive_mode = False

def get_stock_list(context):
    """选股模块 - 完整版本"""
    strategy = g.strategy
    
    log.info(f"当前月份: {context.current_dt.month}, 防御模式: {strategy.in_defensive_mode}")
    
    # 如果处于防御模式,返回防御性ETF组合
    if strategy.in_defensive_mode:
        log.info("防御模式:配置防御性ETF组合")
        
        # 检查ETF是否可交易
        available_etfs = []
        for etf in strategy.defensive_etfs:
            try:
                # 检查ETF是否存在且可交易
                info = get_security_info(etf)
                current_data = get_current_data()
                if (info and hasattr(info, 'display_name') and 
                    not current_data[etf].paused and not current_data[etf].is_st):
                    available_etfs.append(etf)
            except Exception as e:
                log.warning(f"检查ETF {etf} 失败: {str(e)}")
                continue
        
        if available_etfs:
            log.info(f"可用ETF数量: {len(available_etfs)}")
            return available_etfs[:strategy.stock_num]
        else:
            log.warning("所有ETF都不可用,使用备选方案")
            # 备选方案:使用small_25股票池
            yesterday = context.previous_date
            stock_list = get_stock_pool('small_25', yesterday.strftime('%Y-%m-%d'))
            if stock_list:
                return stock_list[:strategy.stock_num]
            else:
                log.error("连备选方案也获取失败,返回空列表")
                return []
    
    yesterday = context.previous_date
    
    # 获取初始股票池
    initial_list = get_reliable_stock_pool(yesterday.strftime('%Y-%m-%d'))
    if not initial_list:
        log.warning("初始股票池为空")
        return []
    
    # 基本过滤
    initial_list = filter_new_stock(context, initial_list)
    initial_list = filter_kcbj_stock(initial_list)
    initial_list = filter_st_stock(initial_list)
    
    if not initial_list:
        log.warning("过滤后股票池为空")
        return []
    
    final_list = []
    
    # 如果还没有训练过模型,使用默认权重
    if not strategy.factor_models:
        strategy.factor_models = [
            (factor_group, weights) 
            for factor_group, weights in zip(strategy.factor_groups, strategy.default_weights)
        ]
        log.info("使用默认因子权重")
    
    # 使用技术因子进行选股
    for factor_group, weights in strategy.factor_models:
        try:
            # 获取技术因子数据
            scored_stocks = score_stocks_by_technical_factors(initial_list, factor_group, weights, yesterday)
            if scored_stocks:
                final_list.extend(scored_stocks)
                        
        except Exception as e:
            log.error(f"技术因子选股失败: {str(e)}")
            continue
    
    # 去重并限制数量
    final_list = list(set(final_list))[:strategy.stock_num]
    log.info(f"选股完成,共选出 {len(final_list)} 只股票")
    return final_list

def score_stocks_by_technical_factors(stock_list, factor_group, weights, date):
    """根据技术因子给股票打分"""
    try:
        # 获取技术因子数据
        factor_data = {}
        
        for factor in factor_group:
            try:
                # 使用get_factor_values获取技术因子
                values = get_factor_values(securities=stock_list, factors=[factor], 
                                        count=1, end_date=date)
                if values and factor in values and not values[factor].empty:
                    factor_data[factor] = values[factor].iloc[0]
            except:
                continue
        
        if not factor_data:
            return []
        
        # 计算得分
        scores = {}
        for stock in stock_list:
            total_score = 0
            valid_factors = 0
            
            for i, factor in enumerate(factor_group):
                if factor in factor_data and stock in factor_data[factor].index:
                    try:
                        factor_value = factor_data[factor][stock]
                        if not pd.isna(factor_value) and i < len(weights):
                            total_score += weights[i] * factor_value
                            valid_factors += 1
                    except:
                        continue
            
            if valid_factors > 0:
                scores[stock] = total_score
        
        if not scores:
            return []
        
        # 按得分排序
        sorted_stocks = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
        return sorted_stocks[:max(10, int(0.1 * len(sorted_stocks)))]
        
    except Exception as e:
        log.error(f"技术因子打分失败: {str(e)}")
        return []

# 过滤函数保持不变...
def filter_new_stock(context, stock_list):
    """过滤次新股"""
    if not stock_list:
        return []
    
    yesterday = context.previous_date
    result = []
    for stock in stock_list:
        try:
            start_date = get_security_info(stock).start_date
            if (yesterday - start_date).days > 180:
                result.append(stock)
        except:
            continue
    return result

def filter_paused_stock(stock_list):
    """过滤停牌股票"""
    if not stock_list:
        return []
    
    current_data = get_current_data()
    return [stock for stock in stock_list if not current_data[stock].paused]

def filter_st_stock(stock_list):
    """过滤ST股票"""
    if not stock_list:
        return []
    
    current_data = get_current_data()
    return [stock for stock in stock_list
            if not current_data[stock].is_st
            and 'ST' not in current_data[stock].name
            and '*' not in current_data[stock].name
            and '退' not in current_data[stock].name]

def filter_kcbj_stock(stock_list):
    """过滤科创北交股票"""
    if not stock_list:
        return []
    
    return [stock for stock in stock_list if not stock.startswith(('4', '8', '68'))]

def filter_limitup_stock(context, stock_list):
    """过滤涨停股票"""
    if not stock_list:
        return []
    
    try:
        result = []
        for stock in stock_list:
            if stock in context.portfolio.positions:
                result.append(stock)
            else:
                try:
                    price_data = get_price(stock, end_date=context.current_dt, 
                                         frequency='1m', fields=['close'], count=1)
                    current_data = get_current_data()
                    if (price_data is not None and len(price_data) > 0 and 
                        not np.isnan(price_data['close'].iloc[0]) and 
                        price_data['close'].iloc[0] < current_data[stock].high_limit):
                        result.append(stock)
                except:
                    continue
        return result
    except:
        return stock_list

def filter_limitdown_stock(context, stock_list):
    """过滤跌停股票"""
    if not stock_list:
        return []
    
    try:
        result = []
        for stock in stock_list:
            if stock in context.portfolio.positions:
                result.append(stock)
            else:
                try:
                    price_data = get_price(stock, end_date=context.current_dt, 
                                         frequency='1m', fields=['close'], count=1)
                    current_data = get_current_data()
                    if (price_data is not None and len(price_data) > 0 and 
                        not np.isnan(price_data['close'].iloc[0]) and 
                        price_data['close'].iloc[0] > current_data[stock].low_limit):
                        result.append(stock)
                except:
                    continue
        return result
    except:
        return stock_list

def weekly_adjustment(context):
    """周度调仓"""
    strategy = g.strategy
    if not strategy.no_trading_today_signal:
        target_list = get_stock_list(context)[:strategy.stock_num]
        
        # 调仓卖出
        for stock in strategy.hold_list:
            if stock not in target_list and stock not in strategy.yesterday_HL_list:
                log.info(f"卖出[{stock}]")
                position = context.portfolio.positions[stock]
                close_position(position)
        
        # 调仓买入
        position_count = len(context.portfolio.positions)
        target_num = len(target_list)
        
        if target_num > position_count and context.portfolio.cash > 0:
            value = context.portfolio.cash / (target_num - position_count)
            for stock in target_list:
                if stock not in context.portfolio.positions or context.portfolio.positions[stock].total_amount == 0:
                    if open_position(stock, value):
                        if len(context.portfolio.positions) == target_num:
                            break

def check_limit_up(context):
    """检查涨停股票"""
    strategy = g.strategy
    if strategy.yesterday_HL_list:
        for stock in strategy.yesterday_HL_list:
            try:
                current_data = get_price(stock, end_date=context.current_dt, frequency='1m', 
                                       fields=['close','high_limit'], count=1, panel=False, fill_paused=True)
                if not current_data.empty and current_data.iloc[0,0] < current_data.iloc[0,1]:
                    log.info(f"[{stock}]涨停打开,卖出")
                    position = context.portfolio.positions[stock]
                    close_position(position)
            except:
                continue

def today_is_between(context, start_date, end_date):
    """判断日期是否在范围内"""
    today = context.current_dt.strftime('%m-%d')
    return start_date <= today <= end_date

def close_account(context):
    """清仓"""
    strategy = g.strategy
    if strategy.no_trading_today_signal and strategy.hold_list:
        for stock in strategy.hold_list:
            try:
                position = context.portfolio.positions[stock]
                close_position(position)
                log.info(f"卖出[{stock}]")
            except:
                continue

def open_position(security, value):
    """开仓"""
    try:
        order = order_target_value(security, value)
        return order is not None and order.filled > 0
    except:
        return False

def close_position(position):
    """平仓"""
    try:
        order = order_target_value(position.security, 0)
        return order is not None and order.filled > 0
    except:
        return False

def print_position_info(context):
    """打印持仓信息"""
    total_value = context.portfolio.total_value
    cash = context.portfolio.cash
    positions_count = len(context.portfolio.positions)
    
    strategy = g.strategy
    mode = "防御模式" if strategy.in_defensive_mode else "正常模式"
    
    log.info(f"总资产: {total_value:.2f}, 现金: {cash:.2f}, 持仓数: {positions_count}, 当前模式: {mode}")


最后一次编辑于 6小时前 0

暂无评论

推荐阅读
  18028306419   3天前   18   0   0 Python
  15012901756   2天前   13   0   0 Python
无名的人