1 背景
最近申请开通了QMT量化工具,正在学习相关操作技能,参考样例写了一个ETF场内基金的市值平衡策略,并对其进行了回测。初学乍到,还很懵懂,望有经验的大佬可以指点迷津。
2 市值平衡策略
市值平衡策略是一种基于格雷厄姆股债平衡理论的量化投资方法,其核心思想是通过动态调整股票和债券的配置比例,实现风险与收益的平衡。该策略的基本原理是:市场具有均值回归的特性,当股票市场上涨过多(估值偏高)时,后续可能出现回调;而当股票市场下跌过多(估值偏低)时,则有望迎来反弹。
具体操作流程如下:假设投资者在年初(1月1日)构建投资组合时,将资金平均分配于股票和债券两类资产,初始配置比例为50:50。随后,每隔一个固定周期(如3个月或6个月)对组合进行一次再平衡操作。当检查日(如3月31日)发现股票资产上涨导致股债比例偏离至60:40时,系统会自动卖出部分股票资产,并将所得资金用于买入债券资产,使股债比例重新回归50:50的目标配置。反之,若债券资产占比过高,则执行相反操作。通过这种定期再平衡的机制,策略能够在市场波动中保持相对稳定的风险敞口,同时实现"低买高卖"的效果。
这种策略的优势在于:1)通过纪律性的再平衡操作,避免投资者受情绪影响而追涨杀跌;2)利用资产间的负相关性,平滑组合波动;3)在长期投资中,能够有效控制下行风险,同时不错失市场上涨机会。对于普通投资者而言,市值平衡策略提供了一种简单有效的资产配置方法,适合追求稳健收益的投资者参考使用。
3 策略配置
这里选取了5至ETF基金,分别为:
159934 黄金ETF
513500 标普500ETF
513300 纳斯达克ETF
510300 沪深300ETF
159985 豆粕ETF
初始比例统一为20%,平均分配,通过每3个月调整一次持仓。
回测时间:2021年1月1日-2024年12月31日
比较基准:000300沪深300指数
4 部分代码
#encoding:gbk
'''
市值平衡策略
本策略事先设定好交易的股票篮子,按一定的交易交易对股票篮子中的持仓进行市值平衡
'''
import pandas as pd
import math
# 定义一个空类G,用于存储全局变量
class G:
pass
# 创建G类的实例g,用于存储全局数据
g = G()
def init(ContextInfo):
"""
初始化函数,在策略开始时执行,用于初始化全局变量和设置初始参数。
参数:
ContextInfo: 上下文信息对象,包含策略运行所需的各种信息
"""
# 定义股票池,包含要交易的ETF代码
g.code_list = ['159934.SZ', '513500.SH', '513300.SH', '510300.SH', '159985.SZ']
# 设置账户ID
ContextInfo.accID = 'test'
# 初始化可用资金,从上下文信息中获取初始资金
g.money = ContextInfo.capital
# 初始化持仓字典,记录每只股票的初始持仓为0
g.po = {}
for code in g.code_list:
g.po[code] = 0
# 将持仓字典转换为DataFrame,方便后续处理
g.position = pd.DataFrame.from_dict(g.po, orient="index").rename(columns={0: "position"}).reset_index()
# 重命名DataFrame的列名
g.position = g.position.rename(columns={'index': 'code'})
# 计算股票池中股票的数量
g.etf_num = len(g.code_list)
# 计算每只股票的目标市值,将总资金平均分配到每只股票
g.target_mark = g.money / g.etf_num
# 定义调仓日期,格式为月份和日期
g.tradedate = ['0331', '0630', '0930', '1231']
print('------------------Init finished------------------')
def handlebar(ContextInfo):
"""
处理每个bar的函数,在每个bar到达时执行,用于判断是否调仓并进行交易操作。
参数:
ContextInfo: 上下文信息对象,包含策略运行所需的各种信息
"""
# 获取当前bar的位置
index = ContextInfo.barpos
# 获取当前bar的时间标签、上一bar的时间标签和上两bar的时间标签
realtimetag = ContextInfo.get_bar_timetag(index)
last1timetag = ContextInfo.get_bar_timetag(index - 1)
last2timetag = ContextInfo.get_bar_timetag(index - 2)
# 将时间标签转换为日期时间格式
realtime = timetag_to_datetime(realtimetag, '%Y%m%d%H%M%S')
last1time = timetag_to_datetime(last1timetag, '%Y%m%d%H%M%S')
last2time = timetag_to_datetime(last2timetag, '%Y%m%d%H%M%S')
# 提取当前时间的月份和日期
realtimeym = timetag_to_datetime(realtimetag, '%m%d')
# 判断是否为调仓日,如果不是调仓日且当前有持仓,则直接返回
if (realtimeym not in g.tradedate) & (g.position['position'].sum() != 0):
return
print(f"调仓日:{realtime}")
# 获取上一日bar的收盘价格
close_df = get_price(realtime, ContextInfo, 'open')
# 获取当日bar的开盘价格
open_df = get_price(realtime, ContextInfo, 'open')
# 处理价格数据,合并持仓信息并计算目标持仓等
merge_df = data_process(open_df, close_df)
# 根据处理后的数据进行交易操作,调整持仓至目标持仓
trade(merge_df, ContextInfo, last1time)
def get_price(select_time, ContextInfo, field):
"""
获取指定时间的市场价格数据。
参数:
select_time: 要获取数据的时间
ContextInfo: 上下文信息对象,包含策略运行所需的各种信息
field: 要获取的价格字段,如'open'表示开盘价
返回:
包含价格数据的DataFrame
"""
# 从上下文信息中获取市场数据
close_dict = ContextInfo.get_market_data_ex(
[field],
stock_code=g.code_list,
end_time=select_time,
count=1,
period=ContextInfo.period,
dividend_type="front",
)
# 初始化一个空的DataFrame用于存储价格数据
close_df = pd.DataFrame()
# 遍历每个股票的价格数据,添加股票代码列并合并到DataFrame中
for k in close_dict.keys():
close_dict[k]['code'] = k
close_df = pd.concat([close_df, close_dict[k]])
return close_df
def data_process(open_df, close_df):
"""
处理价格数据,合并持仓信息并计算目标持仓、目标市值等。
参数:
open_df: 包含开盘价的DataFrame
close_df: 包含收盘价的DataFrame
返回:
处理后的DataFrame,包含持仓、价格、目标市值、目标持仓等信息
"""
# 合并持仓信息和开盘价数据
merge_df = pd.merge(g.position[['code', 'position']], open_df, on='code')
# 计算当前市值
merge_df['market_value'] = merge_df['position'] * merge_df['open']
# 如果当前持仓为0,则将目标市值设置为初始分配的目标市值
if merge_df['position'].sum() == 0:
merge_df['target_mark'] = g.target_mark
# 否则,将目标市值设置为当前市值的平均值
else:
merge_df['target_mark'] = merge_df['market_value'].mean()
# 计算目标持仓,向下取整到100的整数倍
merge_df['target_volume'] = merge_df.apply(lambda x: math.floor(x['target_mark'] / x['open'] / 100) * 100, axis=1)
# 计算需要调整的持仓数量
merge_df['volume'] = merge_df['target_volume'] - merge_df['position']
print(merge_df)
return merge_df
def trade(merge_df, ContextInfo, last1time):
"""
根据处理后的数据进行交易操作,调整持仓至目标持仓。
参数:
merge_df: 处理后的DataFrame,包含持仓、价格、目标市值、目标持仓等信息
ContextInfo: 上下文信息对象,包含策略运行所需的各种信息
last1time: 上一bar的时间
"""
# 遍历每只股票,判断是否需要卖出
for r, d in merge_df.iterrows():
if d['volume'] <= -100:
# 计算可卖出的最大数量
volume = min(-d['volume'], d['position'])
# 执行卖出订单,这里的passorder函数应该是自定义的下单函数
passorder(24, 1101, 'testS', d['code'], 5, d['open'], volume, ContextInfo)
# 更新持仓信息
merge_df.at[r, 'position'] = d['target_volume']
# 更新可用资金
g.money += volume * d['open']
print(last1time, '卖出', d['code'], ':', volume, '@', d['open'], 'money:', g.money)
# 遍历每只股票,判断是否需要买入
for r, d in merge_df.iterrows():
if d['volume'] >= 100:
# 计算可买入的最大数量
volume = min(d['volume'], math.floor(g.money / d['open'] / 100) * 100)
# 执行买入订单,这里的passorder函数应该是自定义的下单函数
passorder(23, 1101, 'testS', d['code'], 5, d['open'], volume, ContextInfo)
# 更新持仓信息
merge_df.at[r, 'position'] = d['target_volume']
# 更新可用资金
g.money -= volume * d['open']
print(last1time, '买入', d['code'], ':', volume, '@', d['open'], 'money:', g.money)
# 更新全局持仓信息
g.position = merge_df[['code', 'position']]
5 回测结果
基准年化收益率:-6.92%
策略年化收益率:11.3%
6 小结
可以看到策略比基准的收益要提高不少,当然其中的细节有待完善,比如没有加入交易成本,粗略地以开盘价作为交易价格(现实中一天内的价格会变化,还有滑点问题),没有异常处理等等,接下来会逐步优化。