量化研究---ptrade西蒙斯全天候动量模型

发布于:2025-08-30 ⋅ 阅读:(20) ⋅ 点赞:(0)

文章声明:本内容为个人的业余研究,和任何单位,机构没有关系,文章出现的股票代码,全部只是测试例子,不做投资参考,投资有风险,代码学习使用,不做商业用途

最近在看动量模型,市场常用的2个计算一个是收益,一个是斜率,波动率排序,本身是一样的计算趋势强度,但是这个跟参数的时间有关系,太长的时间调整慢,回撤大,时间参数小,轮动快。回撤小一点,我后面利用我的想法优化一下,控制回撤提高收益

这个是聚合宽的回撤,和pt回撤感觉差别有一点大,我在检测原因

图片

pt的回测数据,我后面优化一下2.0板块

图片

看策略的原理

策略分析:西蒙斯全天候动量模型2

策略概述

“西蒙斯全天候动量模型2”是一个基于动量效应的ETF轮动策略,旨在通过量化指标从多个资产类别中选取趋势最强、最稳定的ETF进行投资。策略的核心思想是结合年化收益率趋势稳定性(R平方) 来评估ETF的动量质量,从而实现资产配置的优化。该策略每天调仓,确保及时捕捉动量变化,适用于全天候投资环境。

策略组件详细分析
1. 投资标的与资产配置
  • ETF池:策略覆盖4个主要资产类别,实现分散化投资:

    • 518880.SS(黄金ETF):代表大宗商品,具有避险属性。

    • 513100.SS(纳指100ETF):代表海外科技股,增长潜力大。

    • 159915.SZ(创业板100ETF):代表国内成长股、科技股和中小盘。

    • 510180.SS(上证180ETF):代表国内价值股、蓝筹股和中大盘。

  • 资产覆盖:策略通过这4个ETF覆盖了商品、海外市场、成长股和价值股,降低了单一市场风险,符合“全天候”理念。

2. 动量计算机制(get_rank函数)
  • 数据获取:对于每个ETF,获取过去25天(g.m_days)的每日收盘价。

  • 对数转换:对收盘价取自然对数(np.log),将价格序列转换为对数空间,更适合线性模型分析。

  • 线性回归:使用np.polyfit进行一元线性回归,拟合时间(自变量x)与对数价格(因变量y)的关系,得到斜率(slope)和截距(intercept)。

  • 年化收益率计算

    • 公式:annualized_returns = math.pow(math.exp(slope), 250) - 1

    • 逻辑:斜率slope近似每日对数收益率,通过指数化和年化(250个交易日)得到年化收益率。这衡量了ETF的趋势强度。

  • R平方(R²)计算

    • 公式:r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))

    • 逻辑:R平方衡量回归模型的拟合优度,值越接近1表示趋势越稳定、噪音越小。这衡量了ETF趋势的可靠性。

  • 动量得分:得分 = 年化收益率 × R平方。这个综合指标同时考虑了收益和稳定性,得分高的ETF代表“高质量动量”(即收益高且趋势稳定)。

  • 排序:根据得分从高到低对ETF排序,返回排序后的ETF列表。

3. 交易逻辑(trade函数)
  • 目标持仓:每次只持有得分最高的1个ETF(target_num = 1),集中投资于最佳动量资产。

  • 调仓流程

    • 卖出:如果当前持有的ETF不在目标列表中,则全部卖出。

    • 买入:如果目标ETF不在当前持仓中,则使用可用现金买入目标ETF(等分现金,但由于只持有一个ETF,所有现金用于买入一个)。

  • 每日执行:策略每天在开盘时(9:30)运行,确保及时捕捉动量变化。

4. 辅助函数
  • get_xg_account:获取账户信息,包括现金、总资产、持仓价值等,用于资金管理。

  • get_xg_position:获取当前持仓详情,包括股票代码、数量、价格等。

  • get_xg_order:获取委托订单信息,用于监控交易执行。

  • get_xg_position_on:获取特定证券的持仓情况。

策略优点
  1. 简单有效:策略逻辑清晰,易于理解和实现。动量因子是市场公认的有效因子。

  2. 风险分散:ETF池覆盖多资产类别,减少单一市场风险,符合全天候投资理念。

  3. 质量动量:通过R平方过滤噪音,优先选择趋势稳定的ETF,避免高波动资产,可能提高胜率。

  4. 及时调仓:每日调仓能快速响应市场变化,捕捉动量转换。

  5. 低交易成本:ETF交易成本较低,且调仓频率适中(每日),适合实战。

策略缺点与风险
  1. 动量失效风险:动量策略在市场转折点(如趋势反转)可能表现不佳,导致追高杀跌。

  2. 参数敏感性:动量窗口(25天)是固定的,可能不适合所有市场环境。不同周期可能导致表现差异。

  3. 黑天鹅事件:极端市场事件(如金融危机)可能使动量策略失效,所有资产相关性增加。

  4. 单一持仓风险:只持有一个ETF,虽然集中了收益,但也放大了风险。如果该ETF短期回调,策略会遭受损失。

  5. 忽略基本面:策略纯技术面驱动,忽略宏观经济和基本面因素,可能在某些环境下失效。

改进建议
  1. 动态动量窗口:测试不同动量窗口(如20天、50天)的表现,或使用波动率自适应窗口。

  2. 多ETF持仓:考虑持有top 2或top 3 ETF,分散单一持仓风险。

  3. 加入止损机制:设置回撤止损,例如当ETF从高点下跌一定比例时卖出,控制下行风险。

  4. 结合市场状态:引入市场情绪指标(如VIX)或宏观经济数据,在风险偏好低时减少仓位。

  5. 优化资金管理:使用风险平价或波动率加权分配资金,而不是等分现金。

  6. 回测验证:在不同市场周期(牛、熊、震荡)进行回测,确保策略稳健性。

总结

“西蒙斯全天候动量模型2”是一个简单但有效的动量轮动策略,通过综合评估收益和趋势稳定性来选择ETF。它适合趋势明显的市场环境,能在资产类别间灵活切换,追求超额收益。然而,策略也存在动量失效和单一持仓风险,投资者应充分回测并结合自身风险偏好使用。总体而言,策略体现了量化投资的核心理念——基于数据驱动决策,实现系统化交易。

详细的内容说明量化研究---年化30%ptrade西蒙斯全天候动量模型https://mp.weixin.qq.com/s/7zZ_dlRnPg7rX-SqXKEmcw

核心的计算打分函数说明

def get_rank(etf_pool):    '''    基于年化收益和判定系数打分的动量因子轮动    '''    score_list = []    for etf in etf_pool:        print(etf,'**************************')        df = get_history(g.m_days, frequency="1d", field="close", security_list=etf )        y = df['log'] = np.log(df.close)        x = df['num'] = np.arange(df.log.size)        slope, intercept = np.polyfit(x, y, 1)        annualized_returns = math.pow(math.exp(slope), 250) - 1        r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))        score = annualized_returns * r_squared        score=round(score)        score_list.append(score)    df = pd.DataFrame(index=etf_pool, data={'score':score_list})    df = df.sort_values(by='score', ascending=False)    rank_list = list(df.index)        df['证券代码']=df.index.tolist()    print(df)    df['名称']=df['证券代码'].apply(lambda x:g.name_dict.get(x,x))    print(df)         return rank_list

该函数的主要目的是对一个给定的ETF基金池进行综合排名。它并非仅仅依据传统的收益率进行排序,而是采用了一个独特的综合评价指标(得分),该指标同时考虑了收益水平趋势的稳定性与可靠性。最终,函数会按照这个综合得分从高到低返回ETF代码的排名列表。


工作原理分步详解

函数的工作原理可以分解为以下几个核心步骤:

第一步:循环处理每一只ETF

函数接收一个包含多只ETF代码的列表作为输入。然后,它会遍历这个列表,对列表中的每一只ETF单独进行计算和分析。

第二步:获取历史价格数据

对于当前正在处理的ETF,函数会调用一个外部接口(get_history函数),获取该ETF在过去指定天数(g.m_days)内的每日收盘价数据。这些数据是后续所有计算的基础。

第三步:数据预处理与转换

获取到原始价格数据后,函数会进行两项关键的转换,为接下来的数学建模做准备:

  1. 对数转换:将每日的收盘价转换为它们的自然对数值。在金融分析中,对数收益率比简单收益率在数学性质上更优越(例如,可加性),并且能更好地消除价格水平本身带来的偏差,使得时间序列更平稳,更适合做线性回归分析。

  2. 创建时间序列:生成一个简单的从0开始递增的整数序列,代表每一个交易日。例如,[0, 1, 2, 3, ..., n]。这个序列将作为线性回归模型中的自变量(X),代表“时间”。

第四步:执行线性回归分析

函数使用一元线性回归模型来拟合经过预处理的数据。它将上一步创建的时间序列(X)作为自变量,将对数价格序列(Y)作为因变量,进行拟合。
拟合的结果会得到两个关键参数:

  • 斜率:回归线的斜率。这个值至关重要,它代表了价格对数随时间变化的平均日增长率。一个正的斜率表示整体上涨趋势,负的斜率则表示下跌趋势。斜率的绝对值越大,趋势越强。

  • 截距:回归线的截距,在本次分析中重要性较低。

第五步:计算年化收益率

将回归得到的斜率(日对数增长率)进行转换,计算出年化收益率

  1. 首先,通过对斜率取指数函数(math.exp(slope)),将对数日增长率转换回简单的日收益率。

  2. 然后,基于金融领域通常假设一年有250个交易日,将日收益率复利投资250天,从而得到年化收益率((1 + 日收益率)^250 - 1)。这个值代表了如果该ETF保持当前趋势,理论上一年可能获得的收益率。

第六步:计算趋势可靠性(R平方)

为了衡量第五步计算出的收益率是否可靠,函数计算了回归模型的R平方值。

  • R平方是一个统计学指标,取值范围在0到1之间。它表示因变量(价格对数)的变化中有多大比例可以由自变量(时间)的变化来解释。

  • R平方值越高(越接近1),说明价格的变化越能用一个清晰的、稳定的时间趋势来解释,模型的拟合度越好,我们对该趋势就越有信心。

  • R平方值越低(越接近0),说明价格变化更多地是随机波动,而不是一个明确的趋势,之前计算出的高斜率(高收益率)可能只是噪音,并不可靠。

第七步:计算综合得分

这是整个函数最核心的一步。它将收益性可靠性两个维度结合起来,生成一个综合得分
综合得分 = 年化收益率 × R平方

  • 设计逻辑:一个理想的投资标的,应该既有高的年化收益率(斜率高),又有高的趋势稳定性(R平方高)。这个乘法公式确保了:

    • 一只收益率极高但波动巨大、趋势不明显的ETF(高收益率 × 低R平方)得分会降低。

    • 一只趋势非常稳定但收益率极低甚至为负的ETF(低收益率 × 高R平方)得分也会降低。

    • 只有那些同时具备较高收益和较高趋势稳定性的ETF,才能获得最高的综合得分。

第八步:结果整理与输出

所有ETF都计算完毕后,函数将所有ETF代码及其对应的综合得分整理成一个表格,并按照得分从高到低进行排序。此外,为了增强结果的可读性,它还将ETF代码转换成了具体的基金名称(从一个预设的字典g.name_dict中查找),并将完整的排名表格打印出来。最终,函数返回排序后的ETF代码列表。

代码我直接给大家学习使用不做商业用途,ptrade的代码,给我点赞转发就可以

'''西蒙斯全天候动量模型利用,斜率,波动率排序,打分作者:西蒙斯量化微信:xg_quant'''import numpy as npimport pandas as pdimport math#初始化函数 def initialize(context):    # 参数    g.etf_pool = [        '518880.SS', #黄金ETF(大宗商品)        '513100.SS', #纳指100(海外资产)        '159915.SZ', #创业板100(成长股,科技股,中小盘)        '510180.SS', #上证180(价值股,蓝筹股,中大盘)    ]    g.etf_name=[        '黄金ETF',        '纳指100',        '创业板100',        '上证180'    ]    g.name_dict=dict(zip(g.etf_pool,g.etf_name))    g.m_days = 25 #动量参考天数    run_daily(context,trade, time='9:30') #每天运行确保即时捕捉动量变化          def get_rank(etf_pool):    '''    基于年化收益和判定系数打分的动量因子轮动    '''    score_list = []    for etf in etf_pool:        print(etf,'**************************')        df = get_history(g.m_days, frequency="1d", field="close", security_list=etf )        y = df['log'] = np.log(df.close)        x = df['num'] = np.arange(df.log.size)        slope, intercept = np.polyfit(x, y, 1)        annualized_returns = math.pow(math.exp(slope), 250) - 1        r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))        score = annualized_returns * r_squared        score=round(score)        score_list.append(score)    df = pd.DataFrame(index=etf_pool, data={'score':score_list})    df = df.sort_values(by='score', ascending=False)    rank_list = list(df.index)        df['证券代码']=df.index.tolist()    print(df)    df['名称']=df['证券代码'].apply(lambda x:g.name_dict.get(x,x))    print(df)         return rank_list# 交易def trade(context):    # 获取动量最高的一只ETF    target_num = 1        target_list = get_rank(g.etf_pool)[:target_num]    # 卖出       hold_stock=get_xg_position(context)    if hold_stock.shape[0]>0:        hold_stock=hold_stock[hold_stock['持有数量']>=10]        if hold_stock.shape[0]>0:            hold_list=hold_stock['证券代码'].tolist()        else:            hold_list=[]    else:        hold_list=[]                for etf in hold_list:        if etf not in target_list:            order_target_value(etf, 0)            print('卖出' + str(etf))        else:            print('继续持有' + str(etf))    # 买入            if len(hold_list) < target_num:        value = context.portfolio.cash / (target_num - len(hold_list))        for etf in target_list:            if etf not in hold_list:                order_target_value(etf, value)                print('买入' + str(etf))            else:                print('继续持有' + str(etf))def get_xg_account(context):    '''    获取小果账户数据    '''    df=pd.DataFrame()    df['可用金额']=[context.portfolio.cash]    df['总资产']=[context.portfolio.portfolio_value]    df['持仓价值']=[context.portfolio.positions_value]    df['已使用现金']=[context.portfolio.capital_used]    df['当前收益比例']=[context.portfolio.returns]    df['初始账户总资产']=[context.portfolio.pnl]    df['开始时间']=[context.portfolio.start_date]    return dfdef get_xg_position(context):    '''    获取小果持股数据    '''    data=pd.DataFrame()    positions=context.portfolio.positions    stock_list=list(set(positions.keys()))    print('持股数量{}'.format(len(stock_list)))    for stock in stock_list:        df=pd.DataFrame()        df['证券代码']=[positions[stock].sid]        df['可用数量']=[positions[stock].enable_amount]        df['持有数量']=[positions[stock].amount]        df['最新价']=[positions[stock].last_sale_price ]        df['成本价']=[positions[stock].cost_basis ]        df['今日买入']=[positions[stock].today_amount ]        df['持股类型']=[positions[stock].business_type  ]        data=pd.concat([data,df],ignore_index=True)    '''    if data.shape[0]>0:        if g.is_del=='是':            print('开始策略隔离**********')            data['隔离']=data['证券代码'].apply(lambda x: '是' if x in g.stock_list else '不是')            data=data[data['隔离']=='是']        else:            print('不开启策略隔离*********')    '''    return datadef get_xg_order(context):    '''    获取小果委托数据    '''    orders=get_orders()    print("委托数量{}".format(len(orders)))    data=pd.DataFrame()    if len(orders)>0:        for ors in orders:            df=pd.DataFrame()            df['订单号']=[ors.id]            df['订单产生时间']=[ors.dt]            df['指定价格']=[ors.limit ]            df['证券代码']=[ors.symbol ]            df['委托数量']=[ors.amount ]            df['订单生成时间']=[ors.created ]            df['成交数量']=[ors.filled ]            df['委托编号']=[ors.entrust_no]            df['盘口档位']=[ors.priceGear ]            df['订单状态']=[ors.status ]            data=pd.concat([data,df],ignore_index=True)            else:        data=data    return datadef get_xg_position_on(context,security=''):    ''''    获取单股的持股情况    '''    pos=get_positions(security=security)    df=pd.DataFrame()    if len(pos)>0:        df['证券代码']=[pos[security].sid]        df['可以数量']=[pos[security].enable_amount]        df['持有数量']=[pos[security].amount]        df['最新价']=[pos[security].last_sale_price ]        df['成本价']=[pos[security].cost_basis ]        df['今日买入']=[pos[security].today_amount ]        df['持股类型']=[pos[security].business_type  ]    else:        df=df    return df'''聚宽源代码# 克隆自聚宽文章:https://www.joinquant.com/post/42673# 标题:【回顾3】ETF策略之核心资产轮动# 作者:wywy1995import numpy as npimport pandas as pd#初始化函数 def initialize(context):    # 设定基准    set_benchmark('000300.XSHG')    # 用真实价格交易    set_option('use_real_price', True)    # 打开防未来函数    set_option("avoid_future_data", True)    # 设置滑点 https://www.joinquant.com/view/community/detail/a31a822d1cfa7e83b1dda228d4562a70    set_slippage(FixedSlippage(0.000))    # 设置交易成本    set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.0002, close_commission=0.0002, close_today_commission=0, min_commission=5), type='fund')    # 过滤一定级别的日志    log.set_level('system', 'error')    # 参数    g.etf_pool = [        '518880.XSHG', #黄金ETF(大宗商品)        '513100.XSHG', #纳指100(海外资产)        '159915.XSHE', #创业板100(成长股,科技股,中小盘)        '510180.XSHG', #上证180(价值股,蓝筹股,中大盘)    ]    g.m_days = 25 #动量参考天数    run_daily(trade, '9:30') #每天运行确保即时捕捉动量变化# 基于年化收益和判定系数打分的动量因子轮动 https://www.joinquant.com/post/26142def get_rank(etf_pool):    score_list = []    for etf in etf_pool:        df = attribute_history(etf, g.m_days, '1d', ['close'])        y = df['log'] = np.log(df.close)        x = df['num'] = np.arange(df.log.size)        slope, intercept = np.polyfit(x, y, 1)        annualized_returns = math.pow(math.exp(slope), 250) - 1        r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))        score = annualized_returns * r_squared        score_list.append(score)    df = pd.DataFrame(index=etf_pool, data={'score':score_list})    df = df.sort_values(by='score', ascending=False)    rank_list = list(df.index)        print(df)         record(黄金 = round(df.loc['518880.XSHG'], 2))    record(纳指 = round(df.loc['513100.XSHG'], 2))    record(成长 = round(df.loc['159915.XSHE'], 2))    record(价值 = round(df.loc['510180.XSHG'], 2))    return rank_list# 交易def trade(context):    # 获取动量最高的一只ETF    target_num = 1        target_list = get_rank(g.etf_pool)[:target_num]    # 卖出        hold_list = list(context.portfolio.positions)    for etf in hold_list:        if etf not in target_list:            order_target_value(etf, 0)            print('卖出' + str(etf))        else:            print('继续持有' + str(etf))    # 买入    hold_list = list(context.portfolio.positions)    if len(hold_list) < target_num:        value = context.portfolio.available_cash / (target_num - len(hold_list))        for etf in target_list:            if context.portfolio.positions[etf].total_amount == 0:                order_target_value(etf, value)                print('买入' + str(etf))'''

聚宽的回测代码

​​​​​​​

# 克隆自聚宽文章:https://www.joinquant.com/post/42673# 标题:【回顾3】ETF策略之核心资产轮动# 作者:wywy1995import numpy as npimport pandas as pd#初始化函数 def initialize(context):    # 设定基准    set_benchmark('000300.XSHG')    # 用真实价格交易    set_option('use_real_price', True)    # 打开防未来函数    set_option("avoid_future_data", True)    # 设置滑点 https://www.joinquant.com/view/community/detail/a31a822d1cfa7e83b1dda228d4562a70    set_slippage(FixedSlippage(0.000))    # 设置交易成本    set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.0002, close_commission=0.0002, close_today_commission=0, min_commission=5), type='fund')    # 过滤一定级别的日志    log.set_level('system', 'error')    # 参数    g.etf_pool = [        '518880.XSHG', #黄金ETF(大宗商品)        '513100.XSHG', #纳指100(海外资产)        '159915.XSHE', #创业板100(成长股,科技股,中小盘)        '510180.XSHG', #上证180(价值股,蓝筹股,中大盘)    ]    g.m_days = 25 #动量参考天数    run_daily(trade, '9:30') #每天运行确保即时捕捉动量变化# 基于年化收益和判定系数打分的动量因子轮动 https://www.joinquant.com/post/26142def get_rank(etf_pool):    score_list = []    for etf in etf_pool:        df = attribute_history(etf, g.m_days, '1d', ['close'])        y = df['log'] = np.log(df.close)        x = df['num'] = np.arange(df.log.size)        slope, intercept = np.polyfit(x, y, 1)        annualized_returns = math.pow(math.exp(slope), 250) - 1        r_squared = 1 - (sum((y - (slope * x + intercept))**2) / ((len(y) - 1) * np.var(y, ddof=1)))        score = annualized_returns * r_squared        score_list.append(score)    df = pd.DataFrame(index=etf_pool, data={'score':score_list})    df = df.sort_values(by='score', ascending=False)    rank_list = list(df.index)        print(df)         record(黄金 = round(df.loc['518880.XSHG'], 2))    record(纳指 = round(df.loc['513100.XSHG'], 2))    record(成长 = round(df.loc['159915.XSHE'], 2))    record(价值 = round(df.loc['510180.XSHG'], 2))    return rank_list# 交易def trade(context):    # 获取动量最高的一只ETF    target_num = 1        target_list = get_rank(g.etf_pool)[:target_num]    # 卖出        hold_list = list(context.portfolio.positions)    for etf in hold_list:        if etf not in target_list:            order_target_value(etf, 0)            print('卖出' + str(etf))        else:            print('继续持有' + str(etf))    # 买入    hold_list = list(context.portfolio.positions)    if len(hold_list) < target_num:        value = context.portfolio.available_cash / (target_num - len(hold_list))        for etf in target_list:            if context.portfolio.positions[etf].total_amount == 0:                order_target_value(etf, value)                print('买入' + str(etf))

网站公告

今日签到

点亮在社区的每一天
去签到