【ptrade】网格交易 回测实录

05_网格交易 · 回测实录

单标的(沪深300ETF)5格网格,每格3%,跌一格买一份、涨一格卖一份。

策略类型 :震荡策略 / 网格

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

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


一、策略思路

网格交易是"震荡市神器"这一说法的原型。做法很机械:先取沪深300ETF近60日收盘价的中位数当基准线,上下各设5格、每格3%,共11档价格;首次运行时先建一半仓位的底仓,保证手里有货可卖;此后价格每跌破一格就买入一份(约总资金的10%),每涨过一格就卖掉持仓的20%。它不判断趋势、不预测方向,只希望价格在区间内来回震荡,好让它一格一格地把价差收进口袋。

关键参数

参数 取值
SECURITY 510300.SS
GRID_COUNT 5
GRID_PCT 0.03
BASELINE_LOOKBACK 60
EXPOSURE 0.95

二、回测设置

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

三、回测结果

这张图要看两件事:一是净值曲线长期是平的还是向下的。本窗口的结果是略微亏损(-3.09%)------在一段既有大涨也有大跌的四年半里都赚不到钱,说明"震荡套利"的收益被单边行情和交易摩擦吃掉了。二是回撤图中的坑:网格策略的坑通常是缓慢加深、然后长期不回填的那种,本图正是如此。

绩效指标

指标 数值
累计收益 -3.09%
年化收益 -0.69%
最大回撤 17.52%
年化波动率 6.97%
夏普比率 -0.550
索提诺比率 -0.741
卡玛比率 -0.039
回合胜率 55.86%
日胜率 51.42%
盈亏比 79.3%
盈利回合数 81
亏损回合数 64
平均持仓天数 48.0
成交笔数 292

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

「回合」指一次完整的买入→卖出(round_trips),「成交笔数」是买卖双方分开计数的原始成交记录条数。
注:上表「盈利/亏损回合数」为引擎口径(合计 145 笔),其中包含回测末日尚未平仓的持仓;「交易明细」中按已平仓成交统计的回合数为 142 笔。两者不影响收益率与回撤,仅统计范围不同。
⚠️ 本窗口基准数据不完整 :引擎的基准序列尾部为占位填充值,据此算出的沪深300收益为 0.00%,该数字不可信;净值图中的基准曲线已截断至有效区间(至 2026-07-31)。因此本策略的 α、β、超额收益、信息比率等相对指标一律不可引用,只能用绝对收益与回撤来判断。

分年度收益

年份 区间收益 交易日
2022 -12.59% 242
2023 -3.27% 242
2024 3.95% 242
2025 9.83% 243
2026 1.35% 161

四、交易明细

  • 完整交易回合数:142 个(盈利 78 / 亏损 64)
  • 单笔平均盈亏:-40.18 元 ;最大单笔盈利 +664.89 元 ;最大单笔亏损 -6966.51 元
  • 持仓天数:平均 48.0 天 ,最长 112 天 ,最短 1 天

盈利最多的 5 个回合

买入日 卖出日 标的 盈亏(元) 持仓天数
2024-08-05 2024-09-27 510300.SH +664.89 53
2024-07-30 2024-09-26 510300.SH +582.81 58
2025-10-10 2025-11-06 510300.SH +561.35 27
2025-09-04 2025-09-11 510300.SH +511.74 7
2025-08-06 2025-08-22 510300.SH +495.42 16

亏损最多的 5 个回合

买入日 卖出日 标的 盈亏(元) 持仓天数
2022-01-28 2022-03-15 510300.SH -6966.51 46
2022-08-18 2022-11-01 510300.SH -1084.48 75
2022-08-03 2022-10-12 510300.SH -995.93 70
2022-01-05 2022-02-07 510300.SH -880.15 33
2022-01-14 2022-02-15 510300.SH -737.89 32

五、完整回测代码

文件名:05_网格交易.py(共 129 行)

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

python 复制代码
# -*- coding: utf-8 -*-
"""
05_网格交易策略 (QuantStudio 本地回测版)
========================================
核心逻辑:在固定价格区间内设置网格,价格每下跌一格买入一份,每上涨一格卖出一份。
震荡市神器,单边市会被套牢或卖飞。
单标的(沪深300ETF),5格网格,每格3%。
"""

import numpy as np

SECURITY = '510300.SS'
GRID_COUNT = 5          # 网格数量
GRID_PCT = 0.03         # 每格幅度(3%)
BASELINE_LOOKBACK = 60  # 基准价回看天数
EXPOSURE = 0.95


def initialize(context):
    set_benchmark('000300.SS')
    g.baseline_price = None
    g.grid_levels = []
    g.last_grid_index = None
    run_daily(context, trade, time='09:30')
    log.info('05_网格交易初始化: 标的=%s, 格数=%d, 每格=%.0f%%',
             SECURITY, GRID_COUNT, GRID_PCT * 100)


def _calc_baseline(code):
    """计算基准价(60日收盘价中位数)"""
    try:
        h = get_history(BASELINE_LOOKBACK, '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) < 20:
            return None
        return float(np.median(closes))
    except Exception:
        return None


def _get_current_price(code):
    """获取当前价格(用昨日收盘价代理,日线模式下)"""
    try:
        h = get_history(1, '1d', field=['close'],
                        security_list=code, fq='pre', include=True, is_dict=True)
        if h is None or code not in h:
            return None
        closes = np.asarray(h[code]['close'], dtype=float)
        if len(closes) == 0:
            return None
        return float(closes[-1])
    except Exception:
        return None


def trade(context):
    # 每日重新计算基准价和网格
    baseline = _calc_baseline(SECURITY)
    if baseline is None or baseline <= 0:
        return
    g.baseline_price = baseline

    # 生成网格价格(基准价上下各GRID_COUNT格)
    g.grid_levels = []
    for i in range(-GRID_COUNT, GRID_COUNT + 1):
        price = baseline * (1.0 + i * GRID_PCT)
        g.grid_levels.append((i, price))

    current_price = _get_current_price(SECURITY)
    if current_price is None:
        return

    # 确定当前价格在哪个网格
    current_index = 0
    for idx, price in g.grid_levels:
        if current_price >= price:
            current_index = idx

    # 首次运行:记录当前网格
    if g.last_grid_index is None:
        g.last_grid_index = current_index
        # 初始建仓:按当前网格位置建底仓
        pos = get_position(SECURITY)
        if pos is None or pos.amount == 0:
            # 底仓 = 总资金的50%
            target_value = context.portfolio.total_value * 0.5
            order_target_value(SECURITY, target_value)
            log.info('网格初始建仓: 基准=%.3f, 当前=%.3f, 网格=%d, 底仓=%.0f',
                     baseline, current_price, current_index, target_value)
        return

    # 网格变化:下跌买入,上涨卖出
    if current_index < g.last_grid_index:
        # 价格下跌,买入
        grids_dropped = g.last_grid_index - current_index
        cash = context.portfolio.cash
        if cash > 1000:
            # 每下跌一格买入总资金的10%
            buy_value = context.portfolio.total_value * 0.10 * grids_dropped
            buy_value = min(buy_value, cash * 0.95)
            if buy_value > 100:
                order_value(SECURITY, buy_value)
                log.info('网格买入: 价格=%.3f, 网格%d→%d, 买入=%.0f',
                         current_price, g.last_grid_index, current_index, buy_value)

    elif current_index > g.last_grid_index:
        # 价格上涨,卖出
        grids_rised = current_index - g.last_grid_index
        pos = get_position(SECURITY)
        if pos is not None and pos.amount > 0:
            # 每上涨一格卖出持仓的20%
            current_value = pos.amount * current_price
            sell_value = current_value * 0.20 * grids_rised
            sell_value = min(sell_value, current_value * 0.9)
            if sell_value > 100:
                order_target_value(SECURITY, current_value - sell_value)
                log.info('网格卖出: 价格=%.3f, 网格%d→%d, 卖出=%.0f',
                         current_price, g.last_grid_index, current_index, sell_value)

    g.last_grid_index = current_index


def handle_data(context, data):
    pass

六、风险提示

这是本批里最需要警惕的一类。网格的收益结构是"很多次小赚 + 一次大亏":回合胜率高达55.9%,但最大单笔盈利只有665元,最大单笔亏损却有6967元------一次单边下跌就能吞掉十次盈利。更深层的问题在仓位方向:上涨卖出后仓位越来越轻、下跌买入后仓位越来越重,等于在下跌趋势里自动"越亏越加仓",与趋势跟踪的"截断亏损"正好相反。另外本回测的佣金费率被设为0(只按每笔最低5元计,四年半共1460元)------由于本策略单笔金额小、笔数多,"最低5元"其实比费率制更贵,所以这次的亏损并非费用低估造成的;真正的缺口在于未计入的冲击成本与滑点。


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

相关推荐
梦帮科技2 小时前
【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 天前
消费返物业费:消费+服务趋势的必然产物!
大数据·人工智能·物联网·区块链·生活
55873 生态系统3 天前
第 22 篇|社区聊天|55873 文明共建者的日常交流与协作界面
人工智能·区块链·55873全域文明生态体系·55873操作系统·55873社区聊天
IvorySQL3 天前
PostgreSQL 日报| relchecks 溢出导致表无法删除(9 月 28 日)
数据库·人工智能·ai·postgresql·区块链
怒放de生命20103 天前
【web3基础】区块链演化逻辑:从分布式账本到可编程世界
web3·区块链·分布式账本