量化选股——基于动量因子的行业风格轮动策略(第2部分—策略回测)

动量因子的概述与测算,阿隆指标测算请参考:https://blog.csdn.net/weixin_35757704/article/details/128767040

1. 交易策略

阿隆动量因子策略构建:

  1. 相同权重,满仓持有5个指数;每卖出1个或多个指数后,都会等权重用现金买入指数,始终保持5个指数的持仓
  2. 买入:AroonDown+AroonUp>50,且当AroonUp > AroonDown时买入
  3. 卖出:AroonDown+AroonUp>50,且当AroonDown > AroonUp时卖出
  4. 买入时:
    1. 计算【aroon差额】= AroonUp-AroonDown;得到上涨与下跌动量的差额
    2. 每个指数按照 aroon差额 从大到小的顺序排序;为了买入上涨动量最强,且下跌动量最弱的指数
    3. 如果多个指数的aroon差额值相同,则按照15年到20年测算的胜率高低,按照历史测算时综合胜率的先后关系排序
    4. 然后从上到下依次买入指数,最终保持始终持仓5个指数

以 1/1000 作为摩擦成本,不计算管理费

2. Backtrader回测程序

这里我们使用backtrader回测框架,回测的内容除了在 量化策略——准备3 数据、Backtrader回测框架与quantstats评价指标 中提供的一些方法外,核心的策略代码如下:

class AroonStrategy(TemplateStrategy):
    params = (("start_date", None), ('end_date', None),)

    def __init__(self):
        super().__init__()
        # 基本配置
        self.max_hold = 5
        self.this_month = self.params.start_date.month
        total_bond_code = []
        for this_data in self.datas:
            if type(this_data).__name__ == "StockData":
                total_bond_code.append(this_data._name)
        self.total_bond_code = total_bond_code
        self.vic_dict = {'801210.SI': 0, '801110.SI': 1, '801750.SI': 2, '801120.SI': 3, '801890.SI': 4, '801080.SI': 5,
                         '801200.SI': 6, '801140.SI': 7, '801160.SI': 8, '801730.SI': 9, '801010.SI': 10,
                         '801130.SI': 11, '801760.SI': 12, '801770.SI': 13, '801050.SI': 14, '801040.SI': 15,
                         '801180.SI': 16, '801720.SI': 17, '801710.SI': 18, '801030.SI': 19, '801880.SI': 20,
                         '801170.SI': 21, '801790.SI': 22, '801150.SI': 23, '801230.SI': 24, '801740.SI': 25,
                         '801950.SI': 26, '801780.SI': 27}

    def next(self):
        """最核心的触发策略"""
        hold_bond_name = [_p._name for _p in self.broker.positions if self.broker.getposition(_p).size > 0]  # 查看持仓
        # 计算指标
        _candidate_dict = {}
        for _candidate_code in self.total_bond_code:
            _candidate_dict[_candidate_code] = {
                "aroondown": self.getdatabyname(_candidate_code).aroondown[0],
                "aroonup": self.getdatabyname(_candidate_code).aroonup[0],
            }
        candidate_df = pd.DataFrame(_candidate_dict).T
        candidate_df['aroo_energy'] = candidate_df['aroondown'] + candidate_df['aroonup']
        candidate_df['aroo_mines'] = candidate_df['aroonup'] - candidate_df['aroondown']
        candidate_df = pd.merge(candidate_df, pd.DataFrame(self.vic_dict, index=['rank']).T,
                                left_index=True, right_index=True)
        candidate_df = candidate_df.sort_values(['aroo_mines', "rank"], ascending=[False, True])

        if candidate_df['aroo_energy'].sum() == 0:
            return
        if len(hold_bond_name) < self.max_hold:
            self.get_buy_bond(candidate_df, self.max_hold - len(hold_bond_name))
        # 卖出的逻辑
        for _index, _series in candidate_df.iterrows():
            if _index in hold_bond_name:
                if _series['aroonup'] < _series['aroondown']:
                    self.sell(data=_index, size=self.getpositionbyname(_index).size,
                              valid=self.getdatabyname(_index).datetime.date(1))

    def get_buy_bond(self, candidate_df, buy_num):
        hold_bond_name = [_p._name for _p in self.broker.positions if self.broker.getposition(_p).size > 0]
        for index, series in candidate_df.iterrows():
            if series["aroo_energy"] <= 50:  # 当 AroonDown + AroonUp > 50时才执行判操作
                continue
            if index in hold_bond_name:
                continue
            buy_data = self.getdatabyname(index)
            if len(buy_data) >= buy_data.buflen():
                continue
            if series['aroonup'] > series['aroondown']:
                buy_cost_value = self.broker.getcash() / (self.max_hold - len(hold_bond_name)) * (
                        1 - self.broker.comminfo[None].p.commission)
                buy_size = buy_cost_value / self.getdatabyname(index).close[0]
                self.buy(data=buy_data, size=buy_size, exectype=bt.Order.Limit,
                         price=buy_data.close[0],
                         valid=buy_data.datetime.date(1))
                logger.debug("买入 {} size:{} 预计费用:{}".format(index, buy_size, buy_cost_value))
                buy_num -= 1
            if buy_num == 0:
                break

    def stop(self):
        # 绘制净值曲线
        wealth_curve_data = {}
        for _k, _v in self.value_record.items():
            wealth_curve_data[_k] = _v / self.broker.startingcash
        self.plot_wealth_curve(wealth_curve_data, "arron_{}_{}".format(
            self.params.start_date.strftime("%Y-%m-%d"), self.params.end_date.strftime("%Y-%m-%d")))
        # 最终结果
        daily_return = cal_daily_return(pd.Series(self.value_record))
        _, record_dict = cal_rolling_feature(daily_return)
        print(record_dict)
        print('a')

在策略中,当【aroon差额】相同时,按照测算的胜率从大到小依次买入,下面的字典便是每个行业指数胜率从大到小的排名,用于辅助排序:

{'801210.SI': 0, '801110.SI': 1, '801750.SI': 2, '801120.SI': 3, '801890.SI': 4, '801080.SI': 5,
 '801200.SI': 6, '801140.SI': 7, '801160.SI': 8, '801730.SI': 9, '801010.SI': 10,
 '801130.SI': 11, '801760.SI': 12, '801770.SI': 13, '801050.SI': 14, '801040.SI': 15,
 '801180.SI': 16, '801720.SI': 17, '801710.SI': 18, '801030.SI': 19, '801880.SI': 20,
 '801170.SI': 21, '801790.SI': 22, '801150.SI': 23, '801230.SI': 24, '801740.SI': 25,
 '801950.SI': 26, '801780.SI': 27}

3. 回测效果

3.1 2020年1月1日 - 2021年1月1日

  • 最终净值:1.21
  • 复合年增长: 0.226
  • 夏普比率: 0.885
  • 索蒂诺: 1.167
  • omega: 1.175
  • 最大回撤: -0.173
  • 年波动率: 0.253
    在这里插入图片描述

3.2 2021年1月1日 — 2022年1月1日

  • 最终净值:0.909
  • 复合年增长: -0.092
  • 夏普比率: -0.481
  • 索蒂诺: -0.663
  • omega: 0.924
  • 最大回撤: -0.191
  • 年波动率: 0.205
    在这里插入图片描述

3.3 2022年1月1日 — 2023年1月1日

  • 最终净值:0.74
  • 复合年增长: -0.255
  • 夏普比率: -1.681
  • 索蒂诺: -2.073
  • omega: 0.741
  • 最大回撤: -0.258
  • 年波动率: 0.186

在这里插入图片描述

### 量化选股算法实现方法 #### 数据准备 量化选股的第一步是收集并整理数据。这包括但不限于历史价格数据、财务报表数据以及宏观经济指标等。这些数据构成了后续建模的基础[^1]。 ```python import pandas as pd import numpy as np # 假设我们已经获取了一组股票的历史数据 data = pd.read_csv('stock_data.csv') print(data.head()) ``` #### 多因子模型构建 多因子模型是量化选股的核心工具之一,其主要目的是通过一系列因子衡量股票的表现潜力。常见的因子类别有市值、估值、动量、质量及波率等[^4]。具体来说: - **市值**:反映公司规模。 - **估值**:如市盈率(P/E)、市净率(P/B),用于评估股票是否被低估或高估。 - **动量**:考察近期股价走势趋势。 - **质量**:关注公司的盈利能力与资产健康状况。 - **波率**:衡量风险水平。 对于每只股票,可以通过计算上述各类因子得分来进行初步筛选[^2]。 #### 打分法 vs 归法 在实际应用过程中,存在两种主流的多因子选股方式——打分法和归法。 ##### 打分法 此方法通过对各项因子赋予特定权重后累加得出综合评分,从而决定哪些个股值得买入持有。相比而言更为稳定可靠,尤其当面对异常值干扰时表现更佳。 ```python def calculate_score(row, weights): factors = ['market_cap', 'pe_ratio', 'momentum'] score = sum([row[factor]*weights[i] for i,factor in enumerate(factors)]) return score weights = [0.3, 0.5, 0.2] data['score'] = data.apply(lambda row: calculate_score(row, weights), axis=1) top_stocks = data.nlargest(10,'score') # 获取前十大股票 print(top_stocks[['ticker','score']]) ``` ##### 归法 利用线性或其他形式归分析过去一段时间内某支股票相对于某些选定因素的变化关系建立预模型,并据此推未来收益可能性。尽管该技术能迅速适应新的市场环境改变,但它也更容易受个别极端案例影响而失准。 #### 策略编写与验证 完成前期准备工作之后便进入到了制定交易规则环节。这里需要明确定义入场条件(何时购入)、出场准则(什么时候卖出),同时还应考虑成本费用等因素的影响。随后借助历史行情资料模拟执行整个流程以检验预期成效如何[^3]。 ```python from backtesting import Backtest, Strategy class MultiFactorStrategy(Strategy): def init(self): self.scores = self.I(calculate_scores) def next(self): if not self.position and len(self.data.Close)>self.params.lookback_period: top_stock_index = np.argmax(self.scores[-self.params.lookback_period:]) buy_price = self.data.Open[top_stock_index+1] self.buy(size=self.equity/buy_price) elif self.position.pl_pct < -stop_loss_threshold or \ (self.data.index[-1]-self.trades[0].entry_bar)>=max_holding_days: self.sell() bt = Backtest(data, MultiFactorStrategy, cash=cash_amount, commission=commission_rate, exclusive_orders=True) stats = bt.run() print(stats) ``` #### 高级机器学习模型的应用实例 除了传统的统计学手段外,近年来也有不少研究尝试引入先进的ML/DL框架进一步优化选股效率。比如采用LightGBM这样的梯度提升树算法训练分类器区分优质股同劣质股之间的差异特征[^5]。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆萌的代Ma

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值