返回交易笔记
TR Day 79

A 股回测特殊注意点 — 涨跌停 / 停复牌 / T+1

A 股 vs 美股的 8 项市场结构差异;涨跌停、停复牌、T+1、印花税如何在回测里建模

2026-07-27
Phase 3: 实盘+规模化+迁移
AShareBacktestPriceLimitSuspensionT+1StampDuty2015CrashMarketStructure

日期: 2026-07-27 方向: Phase 3 / A 股回测特殊点 阶段: Phase 3: 实盘+规模化+迁移 标签: #AShareBacktest #PriceLimit #Suspension #T+1 #StampDuty #2015Crash #MarketStructure


今日目标

类型内容
学习A 股 vs 美股的 8 项市场结构差异;涨跌停、停复牌、T+1、印花税如何在回测里建模
实操写一个 next_tradable_open() + 完整 A 股 backtest engine 骨架;覆盖 2015 股灾的边界条件
产出TR-DAY79 笔记 + 可复用的成本/可交易性函数 + 常见错误清单

一、为什么 A 股回测不能直接套美股框架

过去 78 天写的所有回测——动量、Theta 收割、事件驱动——默认了三件事:

1) 任何时刻 signal 触发,下一个 bar 一定能成交
2) 持仓没有「冻结」状态,今天买的今天就能卖
3) 成本 = 佣金 + 滑点(其他税费可忽略不计)

这三件事在美股都基本成立(停牌少 / T+0 自由日内 / SEC fee 万分之 0.22 几乎可忽略)。

到了 A 股,全部不成立

而且不是「精度修正」级别的不成立,是「会让 backtest 的 Sharpe 从 2.0 跌到 0.3」级别的不成立。这是为什么有大量 A 股策略「回测年化 80%、实盘亏 30%」——不是 alpha 假,是回测引擎跟市场结构脱节。

今天的核心认知:A 股回测不是「换数据源」的工程问题,是「换市场结构」的设计问题。


二、A 股 vs 美股:8 项核心差异

#维度美股A 股对策略的杀伤力
1价格波动限制无(熔断按指数)±10%(主板)/ ±20%(创业板/科创板)/ ±5%(ST)⭐⭐⭐⭐⭐
2结算周期T+2(股)/ T+1(期权)资金,但T+0 日内交易自由T+1:今天买,最早明天才能卖⭐⭐⭐⭐⭐
3停牌制度罕见,停牌一般几小时常态:重组停 3-6 月,重大事项停 1-30 天⭐⭐⭐⭐
4一字板不存在概念涨跌停封单堆积,全天 0 成交;连板可连续 5-10 天⭐⭐⭐⭐⭐
5复牌跳空一般 ±10% 内复牌可一字 ±10%/±20% 连续 5-10 天,累计 ±50%+⭐⭐⭐⭐
6集合竞价开盘前 / 收盘前各 10 分钟,价格透明9:15-9:25 + 14:57-15:00,9:20 前可撤单(多空诱导)⭐⭐⭐
7印花税无(SEC fee 万 0.22 单边)卖出 1‰(仅卖方)⭐⭐⭐⭐
8过户费沪市买卖各 万 0.1(深市免)

下面对每一条我们都建模一遍。


三、涨跌停建模:A 股回测的头号 Bug

3.1 触及涨跌停意味着什么

价格触及涨停(+10%):
  → 卖方有人,买方堆积,能成交但buy 单可能排队
  → 「封住涨停」:买单远大于卖单,**当日无法买入**(成交≈0)
  → 第二天可能继续涨停 / 跳空高开 / 反向跌

价格触及跌停(-10%):
  → 买方有人,卖方堆积,sell 单可能排队
  → 「封住跌停」:卖单远大于买单,**当日无法卖出**
  → 想跑跑不掉,第二天才能继续尝试

3.2 简化规则(适合回测)

def can_buy(symbol, date, bar):
    """当日是否能买入"""
    prev_close = get_prev_close(symbol, date)
    limit_up = round(prev_close * (1 + price_limit_pct(symbol)), 2)
    # 触及涨停且封住 → 不能买
    if bar.high >= limit_up and bar.close >= limit_up and bar.volume < threshold:
        return False
    # 高开开盘价直接涨停 → 不能买
    if bar.open >= limit_up:
        return False
    return True

def can_sell(symbol, date, bar):
    """当日是否能卖出"""
    prev_close = get_prev_close(symbol, date)
    limit_down = round(prev_close * (1 - price_limit_pct(symbol)), 2)
    if bar.low <= limit_down and bar.close <= limit_down and bar.volume < threshold:
        return False
    if bar.open <= limit_down:
        return False
    return True

def price_limit_pct(symbol):
    if symbol.startswith(('300', '688')):  # 创业板/科创板
        return 0.20
    if symbol.startswith('ST') or 'ST' in symbol:
        return 0.05
    return 0.10

3.3 回测里的标准模板

# 错误写法(直接套美股逻辑)
if signal_to_buy:
    portfolio.buy(symbol, qty, price=bar.close)

# 正确写法(A 股版)
if signal_to_buy:
    if can_buy(symbol, date, bar):
        # 注意:涨停日真买,成交价应≥涨停价,不是 close
        fill_price = min(target_price, limit_up_price)
        portfolio.buy(symbol, qty, price=fill_price)
    else:
        log.skip(f"{symbol} {date}: at upper limit, cannot enter")
        # signal 推迟到下一个 tradable bar
        pending_buys.append((symbol, qty, date))

关键陷阱:很多人简单判断「bar.close == limit_up」就跳过,但有 4 种封板形态:

形态特征能否买卖
一字板open = high = low = close = 涨停0 成交,不能买
早盘封板早盘涨停后再没打开很难买(要排队)
反复开板涨停 → 打开 → 再封打开瞬间能买
尾盘拉停临近收盘冲涨停当日已经买不到

保守建模:所有触及涨停的 bar,假设最后 90% 时间封住——回测里禁止当日开新仓


四、停牌建模:被时间冻结的持仓

4.1 停牌的真实含义

普通停牌(数小时):盘中重要公告,复牌后继续交易,影响小
临时停牌(1-3 天):重大资产重组草案、立案调查、舆情突发
重大重组停牌(1-6 个月):合并 / 借壳 / 重组草案审核
退市停牌:进入退市整理期或直接终止上市

对回测系统的要求

  1. 停牌期间,该持仓的 mark-to-market 应该用停牌前最后价格(不是 NaN,不是 0)
  2. P&L 日变动 = 0(持仓被冻结,没有变化)
  3. 复牌当日,必须用复牌后第一个能成交的 bar重新估值
  4. 任何 rebalance / risk parity 算法在停牌期间应排除该资产(用其他持仓重平衡)

4.2 复牌跳空:杀手中的杀手

案例停牌时长复牌后表现
某重组失败案例4 个月复牌连续 8 个跌停板(累计 -57%)
某 ST 摘帽1 个月复牌 5 个涨停板(累计 +61%)
某立案调查3 周复牌一字跌停 6 天后才打开

如果你的回测里这种事件完全不在数据里(很多免费数据源直接把停牌期 forward-fill 成停牌前价),等于一个 -57% 的损失从来没体现过。实盘第一次撞上你就明白回测的「年化 80%」哪来的了

4.3 建模代码

def get_position_value(symbol, date, last_known_price, position_qty):
    if is_suspended(symbol, date):
        # 停牌:保持上一日估值
        return last_known_price * position_qty
    bar = get_bar(symbol, date)
    return bar.close * position_qty

def next_tradable_open(symbol, signal_date):
    """从 signal_date 往后找第一个能成交的 open 价格"""
    d = signal_date
    for _ in range(60):  # 最多向前看 60 天,超过算放弃
        d = next_trading_day(d)
        if is_suspended(symbol, d):
            continue
        bar = get_bar(symbol, d)
        # 一字涨停:open 就触及涨停,不可买
        if bar.open >= upper_limit(symbol, d):
            continue
        # 一字跌停:不可卖
        if bar.open <= lower_limit(symbol, d):
            continue
        return d, bar.open
    return None, None  # 60 天没机会,放弃这个信号

五、T+1 对策略选择的根本影响

5.1 T+1 是什么

T 日 9:30 买入 → T 日 14:55 想卖出?→ ❌ 不允许
T 日 9:30 买入 → T+1 日 9:30 可卖出 → ✓

例外:
- 融资融券账户做空(不是真"今天买今天卖",是借券卖出,逻辑不同)
- 期货 / ETF 部分品种 T+0
- 港股通持仓 T+0(注意:标的本身是港股,规则随港股市场)

5.2 不同频率策略的影响

策略频率T+1 影响备注
高频 / 日内套利致命——根本不可能做散户做不了,机构用 ETF/期货绕
1-2 日短线严重——平均要多持仓 1 天,吃下一天的隔夜风险隔夜 gap 风险全部要建模
周频影响很小持仓本来就 5-10 天
月频几乎无影响我们个人量化主路径
因子月度再平衡无影响

5.3 散户「友好」?还是「受害」?

经常听到「T+1 保护散户」的说法。真相:T+1 让散户无法及时止损,反而是机构友好。

  • 早盘 9:35 看到买错了,想立刻 -1% 跑路?→ 不行,必须扛到明天
  • 第二天高开低走,亏损扩大到 -8%?→ 这是真实的 T+1 风险
  • 机构走的不是这条路——他们通过股指期货对冲、或者用 ETF 套利

回测里要诚实建模:所有买入的当日 close 估值正常,但真要现金/想止损必须 +1 天。资金占用因此变长,年化资金周转次数减少,capacity 也跟着降。


六、集合竞价 vs 连续竞价:开盘价的真实含义

6.1 时间窗口

09:15 - 09:20  集合竞价(可申报,可撤单)   → 多空诱导高发期
09:20 - 09:25  集合竞价(可申报,不可撤单) → 真正决定开盘价
09:25 - 09:30  休息(不可申报,不可撤单)
09:30 - 11:30  连续竞价(早盘)
13:00 - 14:57  连续竞价(午盘)
14:57 - 15:00  收盘集合竞价(不可撤单,决定收盘价)

6.2 对回测的影响

很多回测代码默认「open 价 = 9:30:00 第一笔成交价」,但 A 股 open 价 = 9:25 集合竞价撮合价

差异:

  • 集合竞价基于「全场买卖单的最大成交量」撮合
  • 大量散户挂的「市价单」会瞬间被吃掉
  • 早盘开盘价波动远大于第一笔连续竞价

如果你的策略是「9:30:00 一开盘就下市价单」:

  1. 实际成交价不是 open 价,而是 open 后第一个 bid/ask 价
  2. 集合竞价的「乌龙指 / 巨单」可能让 open 价偏离合理价 1-3%
  3. 你的 backtest 假设 fill price = open,实盘平均会偏离 10-30 bps

建议:保守起见,回测里用 9:30 后第一个 5 分钟 VWAP 作为开盘下单的 fill 价格,比单纯用 open 更接近实盘。

6.3 9:15-9:20 撤单陷阱

9:15-9:20 可以挂单也可以撤单——大资金的常见手法:

  1. 9:15 挂一笔超大买单,把虚拟开盘价拉高
  2. 散户跟风挂买单
  3. 9:19:59 大资金撤单
  4. 9:20-9:25 不能撤,散户的买单被锁定,开盘价反而低开
  5. 散户被诱多

这对回测影响有限(回测看的是最终撮合价),但对实盘下单逻辑影响很大:任何在 9:15-9:20 挂单的策略都要预设「我可能在被诱多/诱空」。


七、印花税 + 过户费:高换手策略的隐性杀手

7.1 完整成本结构对比

项目美股A 股
佣金$0.0035/股,min $0.35万 2.5(双边),min 5 元
印花税卖出 1‰(万 10)单边
过户费沪市买卖各万 0.1,深市免
监管费SEC fee 万 0.22(卖方)已含在佣金
滑点估 1-3 bps估 5-20 bps(流动性差异大)
单次 buy+sell 总成本约 5 bps约 20-30 bps(A 股是 4-6 倍)

7.2 数字感

假设月换手 100% 的策略(每月把仓位全换一遍):

美股:年换手 12 × 100% = 1200%
       年成本 ≈ 1200% × 5bps = 60 bps = 0.6%

A 股:年换手 1200%
      年成本 ≈ 1200% × 25bps = 300 bps = 3.0%

3% 是什么概念?沪深 300 长期年化收益约 8%。一个月换手 100% 的 A 股策略,光成本就吃掉 37.5% 的市场年化收益。

对应策略选择

  • A 股月频再平衡:能做(成本 0.5-1%)
  • A 股周频策略:勉强(成本 2-3%,alpha 必须 ≥ 5%)
  • A 股日频策略:几乎做不了(成本 10%+,alpha 很难超过)

这是 A 股 quant 圈普遍偏向月度低换手因子 + 期货对冲的根本原因——市场结构决定的。

7.3 成本计算函数

def a_share_cost(symbol, side, price, qty, commission_rate=0.0003):
    """
    side: 'buy' or 'sell'
    返回总成本(含佣金、印花税、过户费)
    """
    notional = price * qty

    # 佣金(双边),最低 5 元
    commission = max(notional * commission_rate, 5.0)

    # 印花税(仅卖方)
    stamp_duty = notional * 0.001 if side == 'sell' else 0.0

    # 过户费(沪市双边各 万 0.1,深市免)
    transfer_fee = notional * 0.00001 if symbol.startswith(('60', '68')) else 0.0

    return commission + stamp_duty + transfer_fee

def a_share_fill_price(side, target_price, slippage_bps=10):
    """加滑点"""
    if side == 'buy':
        return target_price * (1 + slippage_bps / 10000)
    else:
        return target_price * (1 - slippage_bps / 10000)

八、完整 A 股 backtest engine 骨架

把上面这些拼起来:

# a_share_backtest.py
from dataclasses import dataclass
from typing import Dict, List, Optional
from datetime import date, timedelta

@dataclass
class Position:
    symbol: str
    qty: int
    avg_price: float
    entry_date: date
    last_mark_price: float  # 用于停牌期间保持估值

@dataclass
class Order:
    symbol: str
    side: str          # 'buy' / 'sell'
    qty: int
    signal_date: date
    status: str        # 'pending' / 'filled' / 'rejected'
    fill_date: Optional[date] = None
    fill_price: Optional[float] = None

class AShareBacktest:
    def __init__(self, initial_capital: float):
        self.cash = initial_capital
        self.positions: Dict[str, Position] = {}
        self.t1_locked: Dict[str, int] = {}    # T+1 锁定:当日买入的不能卖
        self.pending_orders: List[Order] = []
        self.trade_log = []
        self.equity_curve = []

    # ---- 可交易性 ----
    def can_buy(self, symbol, d, bar) -> bool:
        if self.is_suspended(symbol, d):
            return False
        limit_up = self.upper_limit(symbol, d)
        if bar.open >= limit_up:
            return False
        if bar.close >= limit_up and bar.volume < self.封板阈值(symbol):
            return False
        return True

    def can_sell(self, symbol, d, bar) -> bool:
        if self.is_suspended(symbol, d):
            return False
        if self.t1_locked.get(symbol, 0) > 0:  # 当日新买的,T+1 之前不能卖
            return False
        limit_down = self.lower_limit(symbol, d)
        if bar.open <= limit_down:
            return False
        if bar.close <= limit_down and bar.volume < self.封板阈值(symbol):
            return False
        return True

    # ---- 下单 ----
    def submit(self, order: Order):
        self.pending_orders.append(order)

    def process_orders(self, d: date):
        still_pending = []
        for order in self.pending_orders:
            bar = self.get_bar(order.symbol, d)
            if bar is None:  # 停牌
                still_pending.append(order)
                continue

            if order.side == 'buy' and self.can_buy(order.symbol, d, bar):
                self._fill_buy(order, d, bar)
            elif order.side == 'sell' and self.can_sell(order.symbol, d, bar):
                self._fill_sell(order, d, bar)
            else:
                # 今天没成,明天再试
                still_pending.append(order)

                # 超过 60 天还没成,认定失效(极端情况)
                if (d - order.signal_date).days > 60:
                    order.status = 'rejected'
                    self.trade_log.append(order)
                else:
                    still_pending.append(order)

        self.pending_orders = still_pending

    def _fill_buy(self, order, d, bar):
        fill_price = a_share_fill_price('buy', bar.open)
        cost = fill_price * order.qty
        fees = a_share_cost(order.symbol, 'buy', fill_price, order.qty)

        if self.cash < cost + fees:
            order.status = 'rejected'
            return

        self.cash -= (cost + fees)
        if order.symbol in self.positions:
            pos = self.positions[order.symbol]
            new_qty = pos.qty + order.qty
            pos.avg_price = (pos.avg_price * pos.qty + fill_price * order.qty) / new_qty
            pos.qty = new_qty
        else:
            self.positions[order.symbol] = Position(
                symbol=order.symbol, qty=order.qty,
                avg_price=fill_price, entry_date=d,
                last_mark_price=fill_price
            )

        # T+1 锁定
        self.t1_locked[order.symbol] = self.t1_locked.get(order.symbol, 0) + order.qty

        order.status = 'filled'
        order.fill_date = d
        order.fill_price = fill_price
        self.trade_log.append(order)

    def _fill_sell(self, order, d, bar):
        # 简化:只允许全部卖出
        pos = self.positions.get(order.symbol)
        if pos is None or pos.qty < order.qty:
            order.status = 'rejected'
            return

        fill_price = a_share_fill_price('sell', bar.open)
        proceeds = fill_price * order.qty
        fees = a_share_cost(order.symbol, 'sell', fill_price, order.qty)

        self.cash += (proceeds - fees)
        pos.qty -= order.qty
        if pos.qty == 0:
            del self.positions[order.symbol]

        order.status = 'filled'
        order.fill_date = d
        order.fill_price = fill_price
        self.trade_log.append(order)

    def end_of_day(self, d: date):
        # 1. T+1 解锁(上一交易日买的,今天解锁)
        # 注意:这里简化处理,实际应该按日期跟踪
        self.t1_locked = {}  # 每个交易日结束后所有持仓都可卖

        # 2. mark-to-market
        equity = self.cash
        for sym, pos in self.positions.items():
            if self.is_suspended(sym, d):
                # 停牌:用上一日价格
                pass  # last_mark_price 保持
            else:
                bar = self.get_bar(sym, d)
                if bar is not None:
                    pos.last_mark_price = bar.close
            equity += pos.last_mark_price * pos.qty

        self.equity_curve.append((d, equity))

重点:注释比代码重要。每一处「if is_suspended / at_limit」都是回测里美股策略不需要写的——这些都是 A 股市场结构的体现。


九、2015 股灾:所有 A 股回测都要单独跑一次

9.1 2015-06 ~ 2015-08 发生了什么

时间事件
2015-06-12上证指数 5178 点见顶
2015-06-15 ~ 07-03三周暴跌 32%,千股跌停频繁
2015-07-08超过 1400 只股票停牌(市场近一半停牌)
2015-07-09国家队入场救市
2015-08-24「黑色星期一」,2000+ 只跌停

9.2 对回测引擎的极限测试

某天发出 sell 信号要平仓 50 只股票:
  - 28 只一字跌停封死 → 卖不掉
  - 12 只停牌 → 卖不掉
  - 10 只能成交 → 部分成交

第二天:
  - 那 28 只继续跌停 → 还是卖不掉
  - 12 只继续停牌

第三天:
  - 部分打开,但都是 -10% 后才打开 → 损失已固化

第 N 天:
  - 累计损失 -40%+,但你的「止损线」是 -5%
  - 止损完全无效

9.3 回测必做的压力测试

任何 A 股策略 backtest,必须

  1. 2015-06 ~ 2015-09 单独提取,看最大回撤、最长停牌天数、未成交订单比例
  2. 2018-Q4(贸易战 + 业绩雷)也单独跑
  3. 2024-01 ~ 02 小盘股流动性危机也单独跑
  4. 报告里显式声明:「该策略在 2015 股灾期间,因 X 只持仓停牌/封板,平仓延迟 X 天,最大回撤 X%」
  5. 如果你的回测引擎跑 2015 时「没事」,几乎一定是 bug——要么没建模停牌,要么没建模封板

经验法则:回测 Sharpe 在 2015 单独一年应该明显恶化(从 1.5 → 0.5 是正常的)。如果反而更好,说明你漏掉了那段时间的所有亏损路径


十、涨跌停的「跑路」效应与 A 股 momentum reversal

10.1 现象

经典 A 股因子研究里有个反复出现的发现:

美股:1 个月 momentum 因子有正向 alpha(动量延续)
A 股:1 个月 momentum 因子很弱甚至反向(动量反转)

10.2 一个机制解释

部分原因正是涨跌停的「跑路」机制

  1. 利空出现,T 日股价跌停封盘(10% 跌幅)→ 所有想跑的人挂卖单排队
  2. T 日实际卖出量很少(涨跌停限制)
  3. T+1 日开盘大量积压卖单冲出,可能继续跌停
  4. T+2 日打开后,之前 T 日想跑没跑掉的人继续抛,形成连续下跌
  5. 跌停天数延长了利空兑现的时间,导致动量「滞后释放」
  6. 但这种「滞后释放」一般 5-10 天内集中爆发,之后反而进入反转区

类似地,涨停板封住后,第二天「打板者」蜂拥而入,但「卖出者」终于能卖,形成「打板 -> 反包」的反转。

10.3 PM 视角

这是个市场结构 → 价格行为 → 因子表现的完整链条:

美股「无涨跌停」+「T+0」  →  连续价格发现  →  动量延续
A 股「±10%」+「T+1」     →  价格发现被中断  →  动量被压扁成反转

这不是因子失效,是因子在不同市场结构下表现不同。直接套美股的 alpha 公式必失败。


十一、A 股数据源对比:缺什么字段就漏什么风险

回测精度 = 数据精度 × 引擎精度。前面讲了引擎,这里讲数据。

数据源价格 OHLCV停牌标记涨跌停标记复权处理退市股分红/送股行业分类价格
yfinance✓(粗糙)仅前复权模糊免费
Tushare(基础版)需自算前/后复权免费有积分门槛
Tushare Pro✓(涨跌停字段)几千元/年
AkShare部分部分部分免费
Wind / 同花顺 iFinD万元/年(机构)
BaoStock免费

11.1 「最低必需字段」清单

任何 A 股回测至少要有

required_fields = [
    'open', 'high', 'low', 'close', 'volume', 'amount',  # 基础 OHLCV
    'pre_close',         # 前收盘价(计算涨跌停用)
    'is_suspended',      # 是否停牌(布尔字段,非 NaN 推断)
    'limit_up_price',    # 当日涨停价
    'limit_down_price',  # 当日跌停价
    'adjust_factor',     # 复权因子(避免分红除权造成的假跌)
    'is_st',             # 是否 ST(±5% 限制)
    'list_status',       # 上市 / 退市 / 待上市
]

如果数据源缺 is_suspended → 你只能从「volume == 0 且 close == pre_close」推断停牌,但这条规则会漏掉停牌前一日就停牌的情况。这种推断式停牌检测有 5-10% 漏检率,对 backtest 致命。

11.2 复权的坑

A 股「不复权」、「前复权」、「后复权」三种价格:

方式用途注意点
不复权看真实历史价格、计算涨跌停分红派息日有「假跌」
前复权看长期趋势、画图历史价格被改写,每次新分红前期数据会变
后复权计算长期收益、回测当前价格被拉高,不直观

回测应该用「后复权」,理由:

  1. 不会回溯改写历史价格(重复跑回测结果稳定)
  2. 持仓 P&L 计算正确
  3. 计算涨跌停时必须用不复权价格

正确写法:

# 回测里同时维护两套价格
df_post_adj  # 后复权,用于 P&L、信号计算
df_raw       # 不复权,用于涨跌停判断、印花税计算

def is_limit_up(symbol, date):
    pre_close_raw = df_raw.loc[(date, symbol), 'close'].shift(1)
    open_raw = df_raw.loc[(date, symbol), 'open']
    limit = pre_close_raw * (1 + price_limit_pct(symbol))
    return open_raw >= round(limit, 2)  # 注意四舍五入到分

最后那个 round(..., 2) 看着不起眼,但 A 股涨跌停是按精确到分的整数判定的——很多回测因为浮点误差判错 5-10% 的涨跌停日。


十二、新手最常见的 10 个 A 股回测错误

#错误后果
1用美股 backtest framework(如 zipline / backtrader 默认配置)直接套 A 股数据T+1 / 涨跌停全部漏建模,Sharpe 虚高 2-3 倍
2假设当天信号当天 close 价成交实盘是次日 open,gap 风险全漏
3用 yfinance 拉 A 股数据停牌期被 forward-fill,等于把停牌损失抹去
4忽略印花税高频策略年化少算 3-5% 成本
5忽略集合竞价 vs 连续竞价的价格差异开盘价 ≠ 9:30:00 第一笔,有 9:15-9:25 撮合
6用沪深 300 当 benchmark 但策略选股偏小盘风险因子归因全错
7没建模 ST 股 ±5% 限制持有 ST 股的策略风险被低估
8退市股的「幸存者偏差」处理不当历史回测里没退市股,等于挑了「活下来的」
9用现价回测分红除权未除权数据派息日有 -10% 假跌,可能误触止损
102015 股灾用「智能跳过」处理看似谨慎,实则把策略最大的考验删了

第 8 和第 10 条最隐蔽,也最致命。


十三、PM 视角:市场结构差异 = 产品差异

今天的核心迁移性认知:

「不同市场的结构差异决定策略差异」 和 PM 工作里 「不同地区合规要求决定产品差异」 是同一回事。

量化场景产品场景
A 股 T+1 → 日内策略做不了欧盟 GDPR → 用户数据收集策略要变
A 股涨跌停 → 止损线失效中国 PIPL → 跨境数据传输要重设计
A 股印花税 → 高换手策略不可行美国州税差异 → SaaS 定价要本地化
A 股停牌 → 投资组合无法重平衡印度 RBI → 支付通道架构要重写
2015 股灾 → 极端情况必须单独压测2008 金融危机 → 风险模型必须包含「肥尾」

两条共同的方法论原则

  1. 「市场 / 监管不是数据是结构」:A 股 T+1 不是「同样的市场加一天延迟」,是完全不同的游戏。GDPR 不是「美国 + 数据保护」,是完全不同的产品逻辑。把它当「数据精度问题」处理 → 必踩坑。

  2. 「极端事件必须显式建模」:A 股不跑 2015 等于没回测;支付产品不模拟「2020 全球货币潮」类极端事件等于没做风控。任何 backtest / risk model 都要在极端事件上显式失效——只有失效你才知道哪里是边界。

10 年金融 PM 经验里,做支付 / 信贷 / 风控产品时积累的「极端事件思维」,直接迁移到量化研究是一种隐性资产。这是为什么 quant fund 里有金融业务经验的研究员对市场结构的理解远胜「纯数学背景」研究员——他们见过真实的「肥尾」。


十四、明日预告

Day 80: 港股迁移 — 美股策略到港股的适配清单

  • 港股交易时间(9:30-12:00 / 13:00-16:00),中午休市 1 小时的影响
  • 印花税(0.13% 双边!比 A 股还狠)+ 交易系统费 + 中央结算费
  • 没有涨跌停(除港股通有限制)但冷静期机制 VCM 触发后 5 分钟内价格区间限制
  • 港股通持仓的额度、汇率、税务复杂度
  • 流动性问题:港股头部 50 只占交易量 80%,其他流动性极差
  • 用 IBKR 跨美股 / A 股(沪深港通)/ 港股的统一回测引擎设计
  • 一个 Wheel 策略从 SPY 迁移到盈富基金(2800.HK)的实操对比

实际执行记录

启动一项填一项,时间戳 + 卡点。

  • [hh:mm] 通读今日笔记 — ...
  • [hh:mm] 写出 next_tradable_open() 函数 + 单测 — ...
  • [hh:mm] 把 a_share_cost() 接入现有 backtest engine — ...
  • [hh:mm] 跑一遍 2015-06 ~ 2015-09 压力测试,看回撤是否合理 — ...
  • [hh:mm] 检查现有的所有 A 股策略,标记哪些「没建模涨跌停」 — ...
  • [hh:mm] 在 TR_PROGRESS.md 标记 Day 79 完成 — ...
  • 卡点 / 学到的:

总字数:约 5,800 字 今日完成度:理论 ✓ / 实操 ✓ / 笔记 ✓