动量因子,是指根据过去一段时间内资产价格的表现,来决定未来投资的方向。动量因子认为强者恒强,市场走势具有惯性,近期上涨的资产往往会继续上涨,而近期下跌的资产可能会继续下跌。
在量化领域这个因子叫动量(momentum),其实平时大家也经常听到这种策略的俗称,比如:跟随趋势、追涨杀跌。
下面通过一个案例来实践这个因子:
从上证50、创业板指、十年国债这3个指数的ETF中,每天选出近1个月(22个交易日)涨幅最大的那只,如果已经持有该基金则继续持仓,如果未持有,则清仓持有的基金全仓买入该基金,如果这3只基金近1个月都下跌就清仓。
聚宽上该策略的实现代码:ETF动量轮动
下面给出在本地通过akshare获取数据,进行动量轮动回测的代码:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import akshare as ak
# 读取指数基金行情
# 上证50指数基金513050 2019-2023数据
sz50_df = ak.fund_etf_hist_em(symbol="513050", period="daily", start_date="20190101", end_date="20231231", adjust="qfq")
sz50_df['trade_date'] = pd.to_datetime(sz50_df['日期'], format='%Y-%m-%d')
sz50_df.rename(columns={'开盘': 'open', '收盘': 'close'}, inplace = True)
# 创业板指数基金159915 2019-2023数据
cyb_df = ak.fund_etf_hist_em(symbol="159915", period="daily", start_date="20190101", end_date="20231231", adjust="qfq")
cyb_df['trade_date'] = pd.to_datetime(cyb_df['日期'], format='%Y-%m-%d')
cyb_df.rename(columns={'开盘': 'open', '收盘': 'close'}, inplace = True)
# akshare无法获取国债etf数据,用黄金etf替代
bond_df = ak.fund_etf_hist_em(symbol="518880", period="daily", start_date="20190101", end_date="20231231", adjust="qfq")
bond_df['trade_date'] = pd.to_datetime(bond_df['日期'], format='%Y-%m-%d')
bond_df.rename(columns={'开盘': 'open', '收盘': 'close'}, inplace = True)
# 计算近x日收益率
def compare_return(index1, index2, index3, days=22):
return_pct1 = index1["close"].iloc[-1] / index1["close"].iloc[-days] - 1
return_pct2 = index2["close"].iloc[-1] / index2["close"].iloc[-days] - 1
return_pct3 = index3["close"].iloc[-1] / index3["close"].iloc[-days] - 1
return return_pct1, return_pct2, return_pct3
下面构造轮动执行策略:
# 选择区间收益率最高的指数基金
def select_index(sz50_data, cyb_data, bond_data):
sz50_return, cyb_return, bond_return = compare_return(sz50_data, cyb_data, bond_data)
if sz50_return > cyb_return and sz50_return > bond_return and sz50_return > 0:
return "sz50"
elif cyb_return > sz50_return and cyb_return > bond_return and cyb_return > 0:
return "cyb"
elif bond_return > sz50_return and bond_return > cyb_return and bond_return > 0:
return "bond"
else:
return "cash"
# 运行策略 计算每日持仓
def run_strategy(sz50_data, cyb_data, bond_data, start_date, end_date):
holdings = []
current_holding = ""
for date in sz50_data["trade_date"]:
if date < start_date or date > end_date:
continue
selected_index = select_index(sz50_data[sz50_data["trade_date"] < date],
cyb_data[cyb_data["trade_date"] < date],
bond_data[bond_data["trade_date"] < date])
if selected_index != current_holding:
current_holding = selected_index
holdings.append({"trade_date": date, "holding": current_holding})
return pd.DataFrame(holdings)
下面计算策略运行的指标,并绘制净值曲线:
# 计算每日收益率
def calculate_daily_returns(holdings_df, sz50_data, cyb_data, bond_data, slippage=0):
holdings_df["open"] = 0
holdings_df["close"] = 0
merged_data = pd.merge(holdings_df, sz50_data, on="trade_date", how="left", suffixes=('', '_sz50'))
merged_data = pd.merge(merged_data, cyb_data, on="trade_date", how="left", suffixes=('', '_cyb'))
merged_data = pd.merge(merged_data, bond_data, on="trade_date", how="left", suffixes=('', '_bond'))
daily_returns = []
daily_returns.append(0)
for i in range(1, len(merged_data)):
holding = holdings_df.iloc[i]["holding"]
prev_holding = holdings_df.iloc[i - 1]["holding"]
# 当日非空仓
if holding != "cash":
# 当日调仓且前一日未空仓
if holding != prev_holding and prev_holding != "cash":
# 计算当日开盘卖出昨日持仓的收益
if not pd.isna(merged_data.iloc[i][f"open_{prev_holding}"]):
open_return_pct = merged_data.iloc[i][f"open_{prev_holding}"] / merged_data.iloc[i - 1][f"close_{prev_holding}"] - 1
else: # 昨日持仓停牌则收益计为0
open_return_pct = 0
# 计算当日开盘买入新持仓的收益
close_return_pct = merged_data.iloc[i][f"close_{holding}"] / merged_data.iloc[i][f"open_{holding}"] - 1
# 计算当日收益
return_pct = (1 + open_return_pct) * close_return_pct
return_pct -= slippage
else:
return_pct = merged_data.iloc[i][f"close_{holding}"] / merged_data.iloc[i - 1][f"close_{holding}"] - 1
else: # 'cash'
return_pct = 0
daily_returns.append(return_pct)
merged_data["daily_return"] = daily_returns
# 每日持仓保存csv文件
merged_data[["trade_date", "holding", "daily_return"]].to_csv('data/每日持仓收益.csv', index=False)
return daily_returns
# 计算策略表现
def calculate_portfolio_performance(daily_returns):
cumulative_returns = np.cumprod(np.nan_to_num(np.array(daily_returns), nan=0) + 1) - 1
total_return = cumulative_returns[-1]
annualized_return = (1 + total_return) ** (252 / len(daily_returns)) - 1
max_drawdown = 0
for i in range(len(cumulative_returns)):
for j in range(i + 1, len(cumulative_returns)):
drawdown = (cumulative_returns[j] - cumulative_returns[i]) / (1 + cumulative_returns[i])
if drawdown < max_drawdown:
max_drawdown = drawdown
return total_return, max_drawdown, annualized_return, cumulative_returns
# 自定义滑点大小
slippage = 0.001
# 指定统计起始日期和结束日期
start_date = pd.to_datetime('20200101', format='%Y%m%d')
end_date = pd.to_datetime('20231231', format='%Y%m%d')
holdings_df = run_strategy(sz50_df, cyb_df, bond_df, start_date, end_date)
daily_returns = calculate_daily_returns(holdings_df, sz50_df, cyb_df, bond_df, slippage)
total_return, max_drawdown, annualized_return, cumulative_returns = calculate_portfolio_performance(daily_returns)
print(f"区间收益率: {total_return * 100:.2f}%")
print(f"最大回撤: {max_drawdown * 100:.2f}%")
print(f"年化收益率: {annualized_return * 100:.2f}%")
# 绘制净值曲线
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来在图中正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来在图中正常显示负号
plt.plot(holdings_df["trade_date"], cumulative_returns + 1)
plt.xlabel("日期")
plt.ylabel("净值")
plt.title("净值走势")
plt.show()