文章声明:本内容为个人的业余研究,和任何单位,机构没有关系,文章出现的股票代码,全部只是测试例子,不做投资参考,投资有风险,代码学习使用,不做商业用途
最近在看动量模型,市场常用的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
:获取特定证券的持仓情况。
策略优点
简单有效:策略逻辑清晰,易于理解和实现。动量因子是市场公认的有效因子。
风险分散:ETF池覆盖多资产类别,减少单一市场风险,符合全天候投资理念。
质量动量:通过R平方过滤噪音,优先选择趋势稳定的ETF,避免高波动资产,可能提高胜率。
及时调仓:每日调仓能快速响应市场变化,捕捉动量转换。
低交易成本:ETF交易成本较低,且调仓频率适中(每日),适合实战。
策略缺点与风险
动量失效风险:动量策略在市场转折点(如趋势反转)可能表现不佳,导致追高杀跌。
参数敏感性:动量窗口(25天)是固定的,可能不适合所有市场环境。不同周期可能导致表现差异。
黑天鹅事件:极端市场事件(如金融危机)可能使动量策略失效,所有资产相关性增加。
单一持仓风险:只持有一个ETF,虽然集中了收益,但也放大了风险。如果该ETF短期回调,策略会遭受损失。
忽略基本面:策略纯技术面驱动,忽略宏观经济和基本面因素,可能在某些环境下失效。
改进建议
动态动量窗口:测试不同动量窗口(如20天、50天)的表现,或使用波动率自适应窗口。
多ETF持仓:考虑持有top 2或top 3 ETF,分散单一持仓风险。
加入止损机制:设置回撤止损,例如当ETF从高点下跌一定比例时卖出,控制下行风险。
结合市场状态:引入市场情绪指标(如VIX)或宏观经济数据,在风险偏好低时减少仓位。
优化资金管理:使用风险平价或波动率加权分配资金,而不是等分现金。
回测验证:在不同市场周期(牛、熊、震荡)进行回测,确保策略稳健性。
总结
“西蒙斯全天候动量模型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
)内的每日收盘价数据。这些数据是后续所有计算的基础。
第三步:数据预处理与转换
获取到原始价格数据后,函数会进行两项关键的转换,为接下来的数学建模做准备:
对数转换:将每日的收盘价转换为它们的自然对数值。在金融分析中,对数收益率比简单收益率在数学性质上更优越(例如,可加性),并且能更好地消除价格水平本身带来的偏差,使得时间序列更平稳,更适合做线性回归分析。
创建时间序列:生成一个简单的从0开始递增的整数序列,代表每一个交易日。例如,
[0, 1, 2, 3, ..., n]
。这个序列将作为线性回归模型中的自变量(X),代表“时间”。
第四步:执行线性回归分析
函数使用一元线性回归模型来拟合经过预处理的数据。它将上一步创建的时间序列(X)作为自变量,将对数价格序列(Y)作为因变量,进行拟合。
拟合的结果会得到两个关键参数:
斜率:回归线的斜率。这个值至关重要,它代表了价格对数随时间变化的平均日增长率。一个正的斜率表示整体上涨趋势,负的斜率则表示下跌趋势。斜率的绝对值越大,趋势越强。
截距:回归线的截距,在本次分析中重要性较低。
第五步:计算年化收益率
将回归得到的斜率(日对数增长率)进行转换,计算出年化收益率。
首先,通过对斜率取指数函数(
math.exp(slope)
),将对数日增长率转换回简单的日收益率。然后,基于金融领域通常假设一年有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 np
import pandas as pd
import 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 df
def 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 data
def 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 data
def 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策略之核心资产轮动
# 作者:wywy1995
import numpy as np
import 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/26142
def 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策略之核心资产轮动
# 作者:wywy1995
import numpy as np
import 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/26142
def 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))