散户量化实战——动量因子

动量因子,是指根据过去一段时间内资产价格的表现,来决定未来投资的方向。动量因子认为强者恒强,市场走势具有惯性,近期上涨的资产往往会继续上涨,而近期下跌的资产可能会继续下跌。

在量化领域这个因子叫动量(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()
本表以Fama-French三因子资产定价模型为依据,提供市场溢酬因子(Rm-Rf),市值因子(SMB)和账面市值比因子(HML)的月序列数据。 表中计算所用的无风险收益数据选择标准为:开始--2002年8月6日用三个月期定期银行存款利率; 2002年8月7日--2006年10月7日用三个月期中央银行票据的票面利率; 2006年10月8日--当前,用上海银行间3个月同业拆放利率。 三因子数据包括: 市场溢酬因子__流通市值加权 Rm-Rf 市值因子__流通市值加权 SMB 账面市值比因子__流通市值加权 HML 市场溢酬因子__总市值加权 Rm-Rf 市值因子__总市值加权 SMB 账面市值比因子__总市值加权 HML 有以下3种方式计算的月惯性因子(又称动量因子)。 计算方法1:惯性因子=前n个月累积收益最高的30%的所有股票组合加权收益率-前n个月累积收益最低的30%的所有股票组合加权收益率。 计算方法2:惯性因子=前n个月累积收益最高的10%的所有股票组合加权收益率-前n个月累积收益最低的10%的所有股票组合加权收益率。 计算方法3:惯性因子=前n个月累积收益大于零的所有股票组合加权收益率-前n个月累积收益小于零所有股票组合加权收益率。 其中,n=3、4、5、6、7、8、9、10、11、12、18、24;加权方式为等权、流通市值加权、总市值加权。 在Carhart四因子模型经典文献中,惯性因子=前11个月累积收益最高的30%的股票组合等权收益率-前11个月累积收益最低的30%的股票组合等权收益率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值