【ptrade】行业轮动 · 回测实录

12_行业轮动 · 回测实录

10只行业ETF按5日涨幅排序取最强2只,每周第一个交易日调仓。

策略类型 :ETF轮动 / 周频

回测批次 :2026-10-04(本地 QuantStudio 回测引擎)

数据来源 :QuantStudio 本地行情库(etf_daily / stock_daily)


一、策略思路

相比01号策略,这版做了两处调整:一是把回看窗口从20日缩短到5日,反应更快,更贴近"行业风口"的短周期特征;二是调仓频率从每5个交易日改成每周一次,用固定的日历节奏换手,而不是机械地数交易日。行业ETF之间相关性低、轮动特征明显,是动量策略最经典的试验场。

关键参数

参数 取值
LOOKBACK 5
TOP_N 2
EXPOSURE 0.98

二、回测设置

项目 设置
策略文件 12_行业轮动.py
回测区间 2024-01-01 ~ 2026-09-02
初始资金 100000 元
佣金费率 0.00035(最低 5.0 元)
印花税率 0.001
过户费率 1e-05
滑点 0.0
撮合价格 close
基准 000300
交易日数 647

三、回测结果

本图有两条策略线:灰色虚线是回测引擎的原始记录,红色实线是剔除数据污染后的校正净值。请只看红线------灰线里那条垂直往上跳的台阶,不是策略赚的,是数据库里一只房地产ETF的价格断层凭空"送"的。

⚠️ 重要:本策略的原始回测净值受数据断层污染,不可直接引用。

数据库中 512200(房地产ETF)在 2024-08-12 存在唯一一处价格断层:当日 preClose 与前一日收盘的比值约为 2.79 ,即该标的的价格在这一天整体跳到了约 2.79 倍。策略恰好在这一天卖出该标的,成交价被相应抬高,等于向账户凭空注入了一大笔现金。

其结果是:此后账户净值被整体放大约 1.86 倍 。由于这笔注入是一次性的,策略的日收益率序列不变,因此正确的还原方式是把断层日之后的净值整体除以 1.86。

下表中「记录值」为引擎原始输出,「校正值」为剔除该污染后的结果。请以校正值为准。

绩效指标

指标 记录值 校正值
累计收益 113.44% 14.95%
年化收益 34.04% 5.53%
最大回撤 35.03% 35.03%
年化波动率 62.17% 31.98%
夏普比率 0.519 0.328
索提诺比率 1.438 0.493
卡玛比率 0.972 0.158
回合胜率 49.30% ---
日胜率 48.22% ---
盈亏比 137.7% ---
盈利回合数 106 ---
亏损回合数 109 ---
平均持仓天数 230.2 ---
成交笔数 704 ---

「盈亏比」= 平均盈利 ÷ 平均亏损 > 100%,表示平均每笔赚的比亏的多。

「回合」指一次完整的买入→卖出(round_trips),「成交笔数」是买卖双方分开计数的原始成交记录条数。
⚠️ 本窗口基准数据不完整 :引擎的基准序列尾部为占位填充值,据此算出的沪深300收益为 0.00%,该数字不可信;净值图中的基准曲线已截断至有效区间(至 2026-07-31)。因此本策略的 α、β、超额收益、信息比率等相对指标一律不可引用,只能用绝对收益与回撤来判断。

分年度收益(校正后)

年份 区间收益 交易日
2024 2.70% 242
2025 5.60% 243
2026 6.63% 162

四、交易明细

⚠️ 本节所有金额为引擎记录口径 ,已被上述数据断层整体放大约 1.86 倍,绝对金额不可直接引用(尤其盈利榜首那几笔)。盈亏的方向与回合之间的相对排序仍然有效。

  • 完整交易回合数:215 个(盈利 106 / 亏损 109)
  • 单笔平均盈亏:+544.84 元 ;最大单笔盈利 +57605.44 元 ;最大单笔亏损 -23721.49 元
  • 持仓天数:平均 230.2 天 ,最长 657 天 ,最短 6 天

盈利最多的 5 个回合

买入日 卖出日 标的 盈亏(元) 持仓天数
2024-05-06 2024-08-19 512200.SH +57605.44 105
2024-05-06 2024-08-12 512200.SH +28684.89 98
2025-02-24 2026-04-27 515880.SH +28161.04 427
2024-07-08 2024-10-14 512200.SH +22913.07 98
2025-02-10 2026-06-01 512480.SH +20449.75 476

亏损最多的 5 个回合

买入日 卖出日 标的 盈亏(元) 持仓天数
2025-02-24 2026-07-20 512480.SH -23721.49 511
2025-02-24 2026-08-03 512480.SH -19930.86 525
2024-06-03 2024-11-18 512660.SH -13533.28 168
2024-04-29 2024-10-14 512690.SH -13109.93 168
2025-07-28 2026-06-15 515220.SH -10513.28 322

五、完整回测代码

文件名:12_行业轮动.py(共 111 行)

引擎:QuantStudio 本地回测;全部为公开 API,无平台专有调用。

python 复制代码
# -*- coding: utf-8 -*-
"""
12_行业轮动策略 (QuantStudio 本地回测版)
========================================
核心逻辑:10只行业ETF按5日涨幅排序,取最强2只,每周第一个交易日调仓。
周频行业轮动,降低换手率,捕捉行业中期趋势。
"""

import numpy as np

# 10只行业ETF
SECTOR_ETF_POOL = [
    '512880.SS',  # 证券
    '512800.SS',  # 银行
    '512690.SS',  # 酒(消费)
    '512170.SS',  # 医疗
    '512480.SS',  # 半导体
    '515880.SS',  # 通信
    '512660.SS',  # 军工
    '516160.SS',  # 新能源
    '512200.SS',  # 房地产
    '515220.SS',  # 煤炭
]

LOOKBACK = 5         # 动量回看天数
TOP_N = 2            # 持仓数量
EXPOSURE = 0.98


def initialize(context):
    set_benchmark('000300.SS')
    g.last_rebalance_week = None
    run_daily(context, trade, time='09:30')
    log.info('12_行业轮动初始化: 行业ETF=%d只, 回看=%d日, Top%d, 周频调仓',
             len(SECTOR_ETF_POOL), LOOKBACK, TOP_N)


def _get_momentum(code):
    try:
        h = get_history(LOOKBACK + 1, '1d', field=['close'],
                        security_list=code, fq='pre', include=False, is_dict=True)
        if h is None or code not in h:
            return None
        closes = np.asarray(h[code]['close'], dtype=float)
        closes = closes[np.isfinite(closes)]
        if len(closes) < LOOKBACK + 1 or closes[0] <= 0:
            return None
        return float(closes[-1] / closes[0] - 1.0)
    except Exception:
        return None


def _is_first_trading_day_of_week(context):
    """判断是否为本周第一个交易日(用ISO周编号去重)"""
    try:
        dt = context.current_dt
        iso_cal = dt.isocalendar()
        current_week = (iso_cal[0], iso_cal[1])
        if g.last_rebalance_week is None or current_week != g.last_rebalance_week:
            g.last_rebalance_week = current_week
            return True
        return False
    except Exception:
        # 兜底:每5个交易日调仓
        if not hasattr(g, 'day_counter'):
            g.day_counter = 0
        g.day_counter += 1
        return g.day_counter % 5 == 0


def trade(context):
    # 只在每周第一个交易日调仓
    if not _is_first_trading_day_of_week(context):
        return

    # 计算所有行业ETF的动量
    scores = {}
    for code in SECTOR_ETF_POOL:
        mom = _get_momentum(code)
        if mom is not None:
            scores[code] = mom

    if not scores:
        log.info('无有效动量数据,跳过')
        return

    # 取TopN
    ranked = sorted(scores.keys(), key=lambda c: scores[c], reverse=True)
    target = ranked[:TOP_N]

    # 卖出不在目标中的
    held = list(context.portfolio.positions.keys())
    for code in held:
        if code not in target:
            order_target_value(code, 0)
            log.info('行业轮动卖出 %s (动量=%.2f%%)', code, scores.get(code, 0) * 100)

    # 等权买入
    if not target:
        return
    total_value = context.portfolio.total_value
    per_value = total_value * EXPOSURE / len(target)
    for code in target:
        order_target_value(code, per_value)
        log.info('行业轮动买入 %s (动量=%.2f%%, 目标市值=%.0f)',
                 code, scores[code] * 100, per_value)


def handle_data(context, data):
    pass

六、风险提示

5日动量非常短,容易把一两天的消息面波动误判成趋势,在行业快速切换的市场里会来回打脸。周频调仓意味着信号有一周的执行延迟。另外本策略标的池中的房地产ETF存在数据质量问题,阅读收益数字时须以校正值为准。


免责声明:本文所有数字来自本地历史回测,回测收益不代表未来表现,也不构成任何投资建议或个股/基金推荐。策略代码仅供研究学习,实盘执行还需额外考虑滑点、冲击成本、流动性、涨跌停与平台差异等因素。投资有风险,决策需谨慎。

相关推荐
RunsenLIu3 小时前
【ptrade】放量突破 回测实录
区块链
梦帮科技3 小时前
【3.0修订版】# 创作者激励池与空投分发:AirdropDistributor 与 20% 生态分配
去中心化·区块链·智能合约·同态加密·共识算法·信任链·分布式账本
RunsenLIu4 小时前
【ptrade】网格交易 回测实录
区块链
梦帮科技4 小时前
【3.0修订版】跨链桥架构与资产守恒:BSCBridge、BSCMirrorBridge 与 Lock-and-Mint 模型
架构·去中心化·区块链·智能合约·共识算法·信任链
梦帮科技1 天前
【3.0修订版】创世与节点工程:genesis.json 与 node.toml 解剖
数据结构·算法·json·去中心化·区块链·智能合约·信任链
梦帮科技1 天前
【3.0修订版】 RNS 代币架构:ERC20 五件套扩展与六钱包分配
数据结构·后端·算法·架构·node.js·区块链·php
怒放de生命20102 天前
区块链为啥死磕去中心化”
去中心化·区块链
微三云马玮均—GEO源码系统 私有化部署3 天前
消费返物业费:消费+服务趋势的必然产物!
大数据·人工智能·物联网·区块链·生活