实际成本 + 滑点估计细化
把 Day 22 的成本框架,从「单一保守值」升级为「分档敏感性 + 分布」,理解策略对 cost regime 的弹性
日期: 2026-06-12 方向: Phase 2 / 成本敏感性 阶段: Phase 2: 策略实战 + AI 信号 标签: #TransactionCost #Slippage #Turnover #TWAP #SensitivityAnalysis #MonteCarlo
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 把 Day 22 的成本框架,从「单一保守值」升级为「分档敏感性 + 分布」,理解策略对 cost regime 的弹性 |
| 实操 | 对 Day 28 双因子组合做完整成本敏感性 Monte Carlo,输出 Sharpe / CAGR / MaxDD 在 1bp / 3bp / 5bp / 10bp 下的分布 |
| 认知 | 「策略 cost-sensitivity」是 trade-off 选型的核心维度——cost-sensitive 策略不是「不好」,而是对执行能力的隐性押注 |
| 产出 | TR-DAY34 笔记 + IBKR Tiered 精确佣金函数 + 多档滑点 sweep + 散户「合理保守值」决策表 |
一、为什么 Day 22 不够 — 把「单值」升级为「分布」
1.1 Day 22 的局限
Day 22 我们建立了第一版完整成本栈:
单边成本 ≈ 佣金 + spread cost + market impact + 监管费
散户「合理保守值」 = 10 bp(股票市价单单边)
这套框架的正确性是 OK 的,但用法上有两个严重缺陷:
| 缺陷 | 表现 | 后果 |
|---|---|---|
| 单值假设 | 把 10 bp 当成「真值」 | 无法回答「如果实盘是 6 bp 会怎样 / 是 14 bp 会怎样」 |
| 静态假设 | 假设全年成本恒定 | 忽略月初月末 / Fed 日 / 流动性事件下 spread 翻倍 |
| 无置信区间 | Sharpe 报「0.8」一个数 | 不知道这是 0.5±0.3 还是 0.8±0.05 |
PM 视角的类比:这就像产品需求评审时只给「ROI = 1.5x」一个数字,而不是给「P50 = 1.5x, P10 = 0.6x, P90 = 2.8x」。单点估值是 demo 用的,分布估值才是决策用的。
1.2 Day 34 要做的事
把成本从「一个数」变成「一组场景 × 一组概率」:
情景 单边成本 年内占比 说明
----------------------------------------------------
最优 → 1 bp 20% SPY/QQQ 流动性最好时段
正常 → 3 bp 50% SP100 大盘股、市价单、正常时段
偏紧 → 5 bp 20% 中盘 / 月末再平衡 / 季度末
压力 → 10 bp 10% Fed 日 / 波动率 spike / 流动性枯竭
然后跑 Monte Carlo:每次回测随机抽 cost regime,看 Sharpe 分布是不是仍然能站在 1.0 上。
这才叫严肃的成本建模。
二、IBKR Tiered 佣金的精确建模
2.1 IBKR Tiered 规则(美股)
Day 22 记过公式,今天写成可执行函数。规则:
Per-share charge = $0.0035 / share
Minimum per order = $0.35
Maximum per order = 1.0% of trade value
Exchange / ECN fees: 单列,~$0.0001-0.003/share(与路由相关)
Pass-through fees:
SEC fee (卖出) = $22.10 / $1,000,000 = 0.00221% of sell value
FINRA TAF (卖出) = $0.000166 / share, max $8.30
注意三个容易踩坑的边界:
min $0.35是 per-order,不是 per-fill。如果一个 order 被拆成 3 个 fill,IBKR 通常会聚合按 order 算(但 dark pool 路由有例外)。max 1%是 trade value 的 1%,不是 commission cap of $100。<100 股极便宜票(如 $1 股 50 股 = $50 trade)就会触发:本来 0.0035×50 = $0.175 但 min $0.35,而 1% of $50 = $0.50 没触顶——所以这单实际是 $0.35。- Tiered 还有 monthly volume rebate:单月 ≥ 300k shares 之后 per-share 降到 $0.002。散户量级摸不到。
2.2 commission(shares, price) 函数
# tr_day34_commission.py
from dataclasses import dataclass
@dataclass
class IBKRTieredCost:
"""IBKR Pro Tiered 美股佣金 + 监管费精确建模。
返回单笔的「全口径」commission(含监管费),单位 USD。
side: 'buy' 或 'sell'(监管费只对 sell 收)。
"""
per_share: float = 0.0035
min_order: float = 0.35
max_pct: float = 0.01 # 1% of trade value
sec_rate: float = 22.10 / 1_000_000 # 0.00221% of sell value
taf_per_share: float = 0.000166
taf_max: float = 8.30
exchange_fee_per_share: float = 0.0003 # 经验中值(不同路由 0.0001-0.0005)
def commission(self, shares: int, price: float, side: str = 'buy') -> float:
assert shares > 0 and price > 0
trade_value = shares * price
# 1) per-share 主体
base = shares * self.per_share
# 2) min/max 约束
base = max(self.min_order, base)
base = min(self.max_pct * trade_value, base)
# 3) Exchange / ECN 路由费(双向都有)
exch = shares * self.exchange_fee_per_share
# 4) 监管费(只 sell 收)
if side.lower() == 'sell':
sec = self.sec_rate * trade_value
taf = min(self.taf_max, shares * self.taf_per_share)
else:
sec, taf = 0.0, 0.0
return base + exch + sec + taf
def commission_bps(self, shares: int, price: float, side: str = 'buy') -> float:
"""返回相对 trade value 的 bps,方便比较。"""
total = self.commission(shares, price, side)
return 1e4 * total / (shares * price)
# 一些 sanity check
cost = IBKRTieredCost()
# 案例 1: 买 100 股 SPY @ $500(trade value $50,000)
print(cost.commission(100, 500, 'buy')) # ~$0.35 + $0.03 = $0.38
print(cost.commission_bps(100, 500, 'buy')) # ~0.08 bps
# 案例 2: 卖 1000 股 AAPL @ $200(trade value $200,000)
print(cost.commission(1000, 200, 'sell')) # $3.5 + $0.3 + $0.44 + $0.17 ≈ $4.41
print(cost.commission_bps(1000, 200, 'sell')) # ~0.22 bps
# 案例 3: 买 10 股 NVDA @ $120(trade value $1,200,触发 min $0.35)
print(cost.commission(10, 120, 'buy')) # max(0.035, 0.35) + 10*0.0003 = 0.353
print(cost.commission_bps(10, 120, 'buy')) # ~2.94 bps 贵!
# 案例 4: 触发 1% max(极端)
print(cost.commission(5, 1.0, 'buy')) # trade $5, 1% max = $0.05;min $0.35 > $0.05
# 但 max 限制是上界,min 是下界 — 这里 min 占优 $0.35?
# IBKR 实际规则:max 优先 — 0.05 才是终值
# 我们的实现里 max 在 min 之后做了 clip,所以输出 $0.05 ✓
关键观察(这条值得 hard-code 到 PM 备忘录里):
| 单笔大小 | 全口径 bps(买) | 全口径 bps(卖) | 启示 |
|---|---|---|---|
| <$1k | 5-30 bps | 5-30 bps | 小单不可行 |
| $1k-5k | 1-5 bps | 1-5 bps | 临界区,月度组合勉强 OK |
| $5k-10k | 0.3-0.8 bps | 0.5-1.0 bps | 健康区间 |
| $10k-50k | <0.3 bps | <0.5 bps | 佣金可忽略 |
| >$50k | <0.1 bps | <0.3 bps | 散户里的「优等生」 |
结论:Day 28 双因子组合的 Top 10 持仓平均 $500 / position($5,000 NAV / 10),佣金 ~1-5 bps 单边。这一项不是主要成本——主要成本是滑点。
2.3 容易漏算的「小钱」
- 借券费:Day 22 已建模,今天不展开。但要记得 W-8BEN 不申报借券费降低吗? 不降低,这是 IBKR 内部费用,不走税。
- Fund interest on T+1 cash:IBKR 对未投资现金给 SOFR - 1.5% 利息(>$10k 起付),<$10k 没有。$5k 账户拿不到。这是「沉默成本」。
- 数据订阅:Day 1 订阅了 $16/月 ≈ $192/年 = 3.84% / $5k 账户/年。如果策略毛年化是 12%,光数据费就吃掉 3.84 / 12 = 32% 的毛利——必须建模到 strategy P&L 里。
三、滑点模型细化 — 从「常数」到「conditional」
3.1 Bid-Ask Spread 的真实分布
Day 22 我们说「SP500 大盘股典型 spread 1-2 bp」,今天具体到两个 cap 区间:
| Universe | Spread 中位数 | Spread P90 | 月末 / Fed 日 增量 |
|---|---|---|---|
| SP100(mega cap) | ~5 bps | ~10 bps | +50% (7.5 bps) |
| SP400(mid cap) | ~10 bps | ~25 bps | +100% (20 bps) |
| Russell 2000 small cap | ~20-50 bps | ~100 bps | +100-200% |
| SPY / QQQ ETF | <1 bp | ~2 bps | 几乎无变化 |
| 期权(流动性好) | 5-15 bps | 30-50 bps | +200% (盘前/盘后) |
注意:上面的「spread」是「bid-ask 差除以 mid 的全口径」,而**「成本影响」一般取一半(spread / 2)**,因为 mid-price 是回测的标准 benchmark,市价单成交在 ask(买)或 bid(卖),距离 mid 是 spread / 2。
所以散户市价单的「spread cost」 ≈ spread / 2:
SP100 → 2.5 bps / 单边
SP400 → 5 bps / 单边
SPY → <0.5 bps / 单边
3.2 用 yfinance 估算 spread(粗略)
yfinance 没有直接的实时 spread,但日线 OHLC 可以近似估:
import yfinance as yf
import pandas as pd
import numpy as np
def estimate_spread_bps(ticker: str, period: str = '1mo') -> dict:
"""用 Corwin-Schultz 估计 spread bps(高频估计法的日线近似)。
Corwin & Schultz (2012) 提出用 high/low 估 effective spread:
beta = E[(ln(H_t/L_t))^2 + (ln(H_{t+1}/L_{t+1}))^2]
gamma = E[(ln(max(H_t, H_{t+1}) / min(L_t, L_{t+1})))^2]
alpha = (sqrt(2*beta) - sqrt(beta)) / (3 - 2*sqrt(2)) - sqrt(gamma / (3 - 2*sqrt(2)))
spread ≈ 2 (exp(alpha) - 1) / (1 + exp(alpha))
"""
df = yf.download(ticker, period=period, interval='1d', progress=False)
h = np.log(df['High'])
l = np.log(df['Low'])
beta = ((h - l) ** 2).rolling(2).sum()
gamma = (np.log(df['High'].rolling(2).max() / df['Low'].rolling(2).min())) ** 2
denom = 3 - 2 * np.sqrt(2)
alpha = (np.sqrt(2 * beta) - np.sqrt(beta)) / denom - np.sqrt(gamma / denom)
spread = 2 * (np.exp(alpha) - 1) / (1 + np.exp(alpha))
spread_bps = (spread.dropna() * 1e4).clip(lower=0)
return {
'median_bps': float(spread_bps.median()),
'p90_bps': float(spread_bps.quantile(0.9)),
'mean_bps': float(spread_bps.mean()),
}
# 对 SPY / AAPL / 一只 SP400 中盘 / 一只小盘做对比
for ticker in ['SPY', 'AAPL', 'ROKU', 'PLTR']:
s = estimate_spread_bps(ticker, period='3mo')
print(f"{ticker:6s} median {s['median_bps']:6.1f} bps P90 {s['p90_bps']:6.1f} bps")
示例输出(数量级,会因时段不同浮动):
SPY median 0.8 bps P90 2.1 bps
AAPL median 4.2 bps P90 9.8 bps
ROKU median 12.4 bps P90 31.5 bps
PLTR median 8.3 bps P90 18.7 bps
关键认知:Corwin-Schultz 估计法系统性高估流动性差标的的 spread(因为 high/low 在低流动性下会被异常 print 拉宽),但对 SP100 的估计非常接近真实。这是个轻量级 sanity check 工具,不能替代 IBKR 实时 spread 数据。
3.3 月初月末 / Fed 日 / 财报日的 spread 放大
经验规律(基于 SPGMI / Reuters 的若干历史研究):
| 事件 | spread 增量 | 持续时间 |
|---|---|---|
| 月末 rebalance window(最后 2 天) | +50% 到 +100% | 2-3 天 |
| 季度末 | +100% 到 +200% | 1 周 |
| FOMC 决议日(14:00-16:00 ET) | +100% 到 +300% | 数小时 |
| Non-farm Payrolls 公布前 30 分钟 | +50% 到 +200% | 30-60 分钟 |
| 个股 earnings 公布前后 1 小时 | +200% 到 +500% | 数小时 |
| VIX > 30 时段 | +50% 到 +100% | VIX 高的整段 |
对回测的启示:
- 月度 rebalance 默认下到月末最后一个交易日——这是 spread 最差的时点之一。学术回测常常用 month-end close 价,但实盘要么改到月初第二个交易日(spread 已恢复),要么用 TWAP 拆 3 天。
- Day 28 的双因子组合 rebalance 频率是月度,实际成本可能比 Day 22 假设的高 30-50%(因为月末效应)。这正是今天要测的。
3.4 滑点模型 v2 — Conditional
import numpy as np
import pandas as pd
class SlippageModel:
"""Conditional slippage:base + market regime + cap size。"""
BASE_BPS = { # 单边 mid-relative bps(不是 spread 全口径)
'mega': 2.5, # SP100
'large': 3.5, # SP500 但非 SP100
'mid': 5.0, # SP400
'small': 12.0, # Russell 2000
'etf': 0.5, # SPY/QQQ/IWM
}
REGIME_MULT = {
'calm': 1.0, # 60% of trading days
'normal': 1.3, # 25% of trading days
'stressed': 2.5, # 12% of trading days (VIX > 25)
'crisis': 5.0, # 3% of trading days (VIX > 40)
}
EVENT_MULT = {
'month_end_window': 1.5,
'fomc_day': 2.0,
'earnings_day': 3.0,
'normal': 1.0,
}
def __init__(self, seed: int = 42):
self.rng = np.random.default_rng(seed)
def slippage_bps(self,
cap_tier: str = 'mega',
regime: str = 'normal',
event: str = 'normal',
order_type: str = 'market') -> float:
base = self.BASE_BPS[cap_tier]
mult = self.REGIME_MULT[regime] * self.EVENT_MULT[event]
s = base * mult
# 限价单:spread 全省,但有 ~20% 不成交风险(机会成本不在这里建模)
if order_type == 'limit':
s *= 0.3
# 加 10% 随机噪声
s *= self.rng.normal(1.0, 0.1)
return max(0.5, s)
# 实用:跑一个 rebalance 日的滑点分布
m = SlippageModel()
samples = [m.slippage_bps(cap_tier='mega', regime='normal', event='month_end_window')
for _ in range(10_000)]
print(f"P50 = {np.percentile(samples, 50):.2f} bps")
print(f"P90 = {np.percentile(samples, 90):.2f} bps")
# P50 ≈ 4.9 bps, P90 ≈ 5.6 bps → 月末再平衡的现实成本
四、多档滑点扫描 — 1bp / 3bp / 5bp / 10bp
把 Day 28 双因子组合在 4 个固定 cost regime 下跑一遍,得到 Sharpe 敏感性曲线:
# tr_day34_cost_sweep.py
import pandas as pd
import numpy as np
from day28_pipeline import run_dual_factor_backtest # 假设 Day 28 模块化输出
cost_regimes = {
'1 bp (best case)': {'slippage_bps_round_trip': 2, 'comm_model': 'tiered'},
'3 bp (realistic)': {'slippage_bps_round_trip': 6, 'comm_model': 'tiered'},
'5 bp (conservative)':{'slippage_bps_round_trip': 10, 'comm_model': 'tiered'},
'10 bp (stressed)': {'slippage_bps_round_trip': 20, 'comm_model': 'tiered'},
}
results = []
for label, cfg in cost_regimes.items():
r = run_dual_factor_backtest(cost_config=cfg)
results.append({
'regime': label,
'CAGR': r['cagr'],
'Vol': r['vol'],
'Sharpe': r['sharpe'],
'MaxDD': r['max_dd'],
'Turnover (yr)': r['turnover_yearly'],
'Cost drag': r['cost_drag_yearly'],
})
print(pd.DataFrame(results).to_string(index=False))
预期输出(基于 Day 28 设定:年换手 ~240%,monthly rebalance,SP100 universe):
| Cost regime | CAGR | Vol | Sharpe | MaxDD | Cost drag |
|---|---|---|---|---|---|
| 1 bp (best) | 13.5% | 11.0% | 1.23 | -16.2% | -0.5% |
| 3 bp (realistic) | 12.1% | 11.0% | 1.10 | -16.5% | -1.4% |
| 5 bp (conservative) | 10.8% | 11.0% | 0.98 | -16.8% | -2.4% |
| 10 bp (stressed) | 7.6% | 11.0% | 0.69 | -17.5% | -4.8% |
| 20 bp (worst-case) | 2.4% | 11.0% | 0.22 | -19.0% | -9.6% |
核心观察(这表是今天最有信息量的一张):
- Sharpe 在 1-5 bp 区间衰减是线性的(每 bp ≈ -0.06 Sharpe);5-10 bp 开始非线性(每 bp ≈ -0.06 Sharpe 但相对值放大)。
- MaxDD 几乎不变——成本是「侵蚀 alpha」,不是「放大风险」。这是关键区分。
- Cost drag = 年换手率 × 单边 round-trip 成本:240% × 2 × 1 bp = 0.5%; 240% × 2 × 10 bp = 4.8%。记这个公式:
年化成本拖累 ≈ 年化换手率 × 2 × 单边成本(bp)
4.1 Sharpe 弹性的产品视角
定义 「成本弹性」 = ΔSharpe / Δcost_bp:
| Day 28 双因子 | 弹性 ≈ -0.06 Sharpe / bp | | 月度低波动单因子 | 弹性 ≈ -0.04(换手低) | | 周度动量 | 弹性 ≈ -0.25(换手高) | | 日内反转 | 弹性 ≈ -2.0(换手极高) |
这个数能直接告诉你「策略在 cost 上的 trade-off 弹性」:
- 低弹性策略 = robust to execution(即便实盘比回测差 5 bp,结果不会崩)
- 高弹性策略 = bet on execution(必须搭配 TWAP / 限价 / 大资金路由)
PM 视角的认知迁移:这就是「margin sensitivity to ad cost」在量化里的形态。Web2 广告团队都熟悉「CAC payback 对 CTR / CVR / CPM 的弹性表」——量化的「Sharpe 对 bp 弹性表」是一回事。
4.2 我的「合理保守值」决策
回测里默认用哪一档?
| 资产 | 默认 round-trip 成本 | 说明 |
|---|---|---|
| 股票(SP100) | 10 bp | 5 bp spread × 2 + 佣金 - rebate ≈ 10 bp 单边 round-trip |
| 股票(SP400 / 中盘) | 20 bp | spread 大、impact 大 |
| ETF(SPY/QQQ) | 2 bp | spread <1 bp 主导,几乎免费 |
| 期权(流动性好) | 20 bp of premium | bid-ask 是 5-10 bps of premium,加佣金 |
| 加密(BTC/ETH on Coinbase) | 40 bp | spread + slippage + maker/taker 0.4% |
| 加密(山寨) | >100 bp | 不要碰 |
Day 28 默认用 5 bp round-trip 是「乐观但可接受」。今天的 sweep 告诉我们:Day 28 报的 Sharpe 1.10 在「实际可能更差」的情境下会落到 0.69——Phase 2 paper trade 之前要心理上接受这个区间。
五、换手率(Turnover)的隐性成本
5.1 把换手率算清楚
定义:
单边 turnover = sum(|w_t - w_{t-1}|) / 2
年化 turnover = 月度 turnover × 12(如果 monthly rebalance)
Day 28 双因子组合的真实换手率分解:
| 来源 | 月度贡献 | 年化贡献 |
|---|---|---|
| 动量信号轮换 | ~12% | ~144% |
| 低波动信号轮换 | ~5% | ~60% |
| Vol target rescale | ~2% | ~24% |
| 现金流入流出 / 分红再投 | ~1% | ~12% |
| 合计 | ~20% | ~240% |
5.2 换手率 vs Sharpe — 全 frontier
关键问题:「如果我把 rebalance 频率改成 quarterly,turnover 减半,Sharpe 会变好还是变差?」
两个力量在拔河:
| 减少换手的「好处」 | 减少换手的「坏处」 |
|---|---|
| 成本下降 → Sharpe ↑ | 信号衰减更慢 → 但跟单不及时 → Sharpe ↓ |
回测一下:
freq_results = []
for freq in ['weekly', 'monthly', 'quarterly', 'semi-annual']:
r = run_dual_factor_backtest(rebalance_freq=freq, cost_config={'slippage_bps_round_trip': 10})
freq_results.append({'freq': freq, **r})
print(pd.DataFrame(freq_results).to_string(index=False))
预期输出:
| Freq | Turnover/yr | Gross Sharpe | Net Sharpe (10bp) | Cost drag |
|---|---|---|---|---|
| Weekly | 800% | 1.25 | 0.45 | -8.0% |
| Monthly | 240% | 1.18 | 0.98 | -2.4% |
| Quarterly | 90% | 1.05 | 0.95 | -0.9% |
| Semi-annual | 50% | 0.85 | 0.80 | -0.5% |
核心观察:
- Weekly 是坑:Gross Sharpe 看着最高(1.25),net 却是最差(0.45)。这就是「过度拟合换手」的典型。
- Monthly 和 Quarterly 几乎打平:在 10 bp 成本下,月度的 alpha 更新优势刚好补回成本劣势。如果成本是 20 bp,Quarterly 会赢。
- Semi-annual 是 robustness 下限:Sharpe 0.80 还能站,证明这个策略的核心 alpha 是真实的。
决策:Day 28 选 monthly 是对的,但 Phase 2 paper trade 阶段并行跑 quarterly 版本作为 robustness check。
5.3 PM 视角:换手率 = 运营 SLA 的隐性绑定
我做 10 年金融零售 PM,「运营频率 = 系统可靠性的隐性 SLA」:
| 量化术语 | PM 等价物 |
|---|---|
| 月度 rebalance | 每月 1 次后台批处理 |
| 每周 rebalance | 每周 5 次实时调度 |
| 日内 rebalance | 7×24 实时风控引擎 |
每提高一个频率档,对系统的可靠性要求是数量级跳升。我已经知道——金融系统里「周末批处理出问题,周一补」是常态,但「实时系统宕机 30 秒,全天单据混乱」也是常态。量化策略选 monthly 不是因为月度信号更准,而是因为 monthly 给了我「容错的运营窗口」。
这也是为什么 Phase 2 paper trade 我会选 monthly + quarterly 双轨——不是因为不相信周度信号,而是因为我现在还没有「能撑周度调度的基础设施」。
六、Implementation 优化 — 执行端的杠杆
成本不只是「策略选什么频率」决定的,还由「具体怎么下单」决定。
6.1 TWAP / VWAP — 拆单的概念
Time-Weighted Average Price (TWAP):把一个大单按时间均匀拆开。
原单:买 1000 股 AAPL 在 14:30 一次成交
TWAP:买 100 股 / 分钟,从 14:30 到 14:39 共 10 笔
Volume-Weighted Average Price (VWAP):按当天成交量分布拆。
日内成交量分布:开盘 / 收盘高,午盘低
VWAP:在成交量高的时段下大头、低的时段下小头
核心收益:把「冲击成本」从「集中一次 50 bp」摊薄成「分散 10 次 5 bp 平均」。
散户量级用不上吗?取决于单笔大小:
| 单笔 | 是否值得 TWAP |
|---|---|
| <$5k SPY 类 ETF | 不值得,spread <1 bp 直接市价 |
| <$5k SP100 大盘 | 不值得,市价单 ≈ 5 bps,TWAP 收益 <1 bp 但增加操作 |
| $5k-50k 中盘股 | 值得,可以用 IBKR 的 Adaptive 算法单(IBKR 内置 TWAP/VWAP) |
| >$50k 任何标的 | 必须,否则冲击 >20 bp |
对我们当前 <$5k 账户:Phase 2 阶段不用 TWAP,但要在 Phase 3 / 资金 ≥$50k 时切换。先把代码占位写好:
# 占位:未来切到 TWAP / VWAP 时的执行接口
def place_order(ib, contract, qty, direction, exec_algo='market'):
if exec_algo == 'market':
order = MarketOrder(direction, qty)
elif exec_algo == 'adaptive_fast':
order = MarketOrder(direction, qty)
order.algoStrategy = 'Adaptive'
order.algoParams = [TagValue('adaptivePriority', 'Urgent')]
elif exec_algo == 'twap':
order = MarketOrder(direction, qty)
order.algoStrategy = 'Twap'
order.algoParams = [TagValue('startTime', '14:30:00'),
TagValue('endTime', '14:45:00'),
TagValue('allowPastEndTime', 'true')]
return ib.placeOrder(contract, order)
6.2 限价单 vs 市价单 — 散户最重要的杠杆
这是散户能立刻拿到的成本优化。
| 维度 | 市价单 | 限价单(mid) | 限价单(aggressive: bid+1 tick) |
|---|---|---|---|
| 成交率 | 100% | ~30% | ~85% |
| 平均滑点(bp) | spread/2 + 冲击 | -spread/2(可能拿 maker rebate) | 0 ~ spread/4 |
| 时间到成交 | <1 秒 | 数分钟到不成交 | 数秒到数分钟 |
| 适用场景 | 信号衰减快 / 单子大 | 不急的 rebalance | 默认推荐 |
经验法则:
快信号(日内 / 事件驱动):市价单 — 不要省那 2 bp,错过更贵
慢信号(月度因子):限价单 aggressive — 省下来的 2-5 bp 全是利润
极慢信号(季度 rebalance):限价单 mid — 不成交就等下一天
Day 28 月度 rebalance 应该用 aggressive limit(bid+1 tick / ask-1 tick)。预期成本节省 30-50%,把 10 bp round-trip 压到 ~6 bp。
6.3 Rebalance 时点的微优化
**「14:00 ET 之后」**是常识里成本最低的窗口:
| 时段 | spread | impact | 适合 |
|---|---|---|---|
| Open (9:30-10:00) | 高 | 高 | 避开 |
| Morning (10:00-12:00) | 中 | 中 | 一般 |
| Lunch (12:00-13:30) | 低 | 中(流动性低) | 避开(看似 spread 小但深度差) |
| Afternoon (14:00-15:30) | 低 | 低 | 首选 |
| Close (15:30-16:00) | 中 | 高(MOC 单冲击) | 避开(除非要 close print) |
散户实操:月度 rebalance 锁定在每月最后/第一个交易日的 14:30-15:00 ET 窗口。
七、完整 Monte Carlo 代码 — Day 28 + Cost Regime 分布
把以上所有放在一起:
# tr_day34_montecarlo.py
import numpy as np
import pandas as pd
from day28_pipeline import run_dual_factor_backtest
# 1. Cost regime 分布(基于 Day 22 经验 + 今天 SP100 spread 估计)
COST_DISTRIBUTION = [
{'name': 'best', 'prob': 0.20, 'rt_bps': 2, 'fomc_mult': 1.0},
{'name': 'normal', 'prob': 0.50, 'rt_bps': 6, 'fomc_mult': 1.0},
{'name': 'tight', 'prob': 0.20, 'rt_bps': 10, 'fomc_mult': 1.5},
{'name': 'stressed', 'prob': 0.10, 'rt_bps': 20, 'fomc_mult': 2.5},
]
def sample_yearly_cost(rng):
"""每个月独立采样一个 regime,年化是 12 个月的加权平均。"""
monthly_bps = []
for _ in range(12):
r = rng.choice(COST_DISTRIBUTION, p=[d['prob'] for d in COST_DISTRIBUTION])
monthly_bps.append(r['rt_bps'] * r['fomc_mult'])
return np.mean(monthly_bps)
# 2. Monte Carlo — 1000 次回测
N_SIMS = 1000
rng = np.random.default_rng(42)
results = []
for i in range(N_SIMS):
cost_bps = sample_yearly_cost(rng)
r = run_dual_factor_backtest(cost_config={'slippage_bps_round_trip': cost_bps})
results.append({
'sim_id': i,
'cost_bps': cost_bps,
'CAGR': r['cagr'],
'Sharpe': r['sharpe'],
'MaxDD': r['max_dd'],
})
df = pd.DataFrame(results)
# 3. 输出 Sharpe 分布的关键 percentile
print("\n=== Sharpe 分布 ===")
for p in [5, 10, 25, 50, 75, 90, 95]:
print(f" P{p:2d} = {df['Sharpe'].quantile(p/100):.2f}")
print(f"\n Mean Sharpe = {df['Sharpe'].mean():.2f}")
print(f" Median Sharpe = {df['Sharpe'].median():.2f}")
print(f" P(Sharpe>1) = {(df['Sharpe']>1).mean():.1%}")
print(f" P(Sharpe>0.5) = {(df['Sharpe']>0.5).mean():.1%}")
# 4. 输出 Sharpe vs cost_bps 散点图(看弹性)
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 5))
ax.scatter(df['cost_bps'], df['Sharpe'], alpha=0.5, s=10)
# 拟合线性
z = np.polyfit(df['cost_bps'], df['Sharpe'], 1)
xs = np.linspace(df['cost_bps'].min(), df['cost_bps'].max(), 100)
ax.plot(xs, z[0]*xs + z[1], 'r-', label=f'slope = {z[0]:.3f} Sharpe / bp')
ax.set_xlabel('Round-trip cost (bps)')
ax.set_ylabel('Net Sharpe')
ax.set_title('Day 28 双因子组合的 Cost-Sharpe 弹性')
ax.legend()
ax.grid(True, alpha=0.3)
plt.savefig('day34_mc_elasticity.png', dpi=150)
预期输出:
=== Sharpe 分布 ===
P 5 = 0.42
P10 = 0.55
P25 = 0.78
P50 = 1.02
P75 = 1.15
P90 = 1.22
P95 = 1.27
Mean Sharpe = 0.96
Median Sharpe = 1.02
P(Sharpe>1) = 53%
P(Sharpe>0.5) = 89%
弹性 slope ≈ -0.061 Sharpe / bp(和 Day 28 单点测量一致)
核心结论(这一段是今天写给自己的「决策依据」):
- Sharpe 的中位数 1.02——和 Day 28 单点测量 1.10 略低,但是更接近真实。
- P(Sharpe > 1) = 53%——「这个策略一半时间能跑出 Sharpe > 1」,但另一半时间会落到 0.5-1.0。
- P(Sharpe > 0.5) = 89%——「这策略 89% 的概率能跑出 acceptable 表现」。这是 Phase 2 paper trade 的「go/no-go」依据。
- P5 = 0.42——「**最坏 5%**情况下 Sharpe = 0.42」。即便这种情况,也没有负 Sharpe——策略本身是有信号的,不是 noise。
八、散户的「合理保守值」 — 我的决策表
把今天所有内容总结成一张随手可查的备忘录:
| 资产 | 单边 spread cost | 单边 commission | 单边 round-trip | 回测默认值 |
|---|---|---|---|---|
| ETF (SPY/QQQ) | 0.5 bp | <0.1 bp | ~1 bp | 2 bp |
| SP100 大盘股 | 2.5 bp | 0.2 bp | ~3 bp | 5 bp |
| SP400 中盘股 | 5 bp | 0.5 bp | ~6 bp | 10 bp |
| Russell 2000 小盘 | 12 bp | 1 bp | ~14 bp | 25 bp |
| 期权(流动性好) | 5 bp of premium | 0.5 bp | ~6 bp | 15 bp |
| 期权(OTM 远期) | 50+ bp of premium | 1 bp | ~55 bp | 80 bp |
| 加密(BTC/ETH on Coinbase) | 5 bp | 30 bp (maker/taker) | ~35 bp | 40 bp |
| 加密(山寨) | 100+ bp | 30 bp | ~130 bp | 不回测 |
注:以上是「回测默认保守值」,用于 sweep 时的中位档。Monte Carlo 应该围绕这些值 ±50% 抽样。
这张表的用法:
- 写新策略代码时——把对应资产的「回测默认值」hard-code 为
default_round_trip_bps参数。 - 看到别人 Sharpe 1.5 的回测时——问「成本用了多少」。如果他用 1 bp 而我用 10 bp,他的 Sharpe 1.5 对我等价于 ~0.9,已经很普通。
- 跨资产组合时——按持仓权重加权计算综合成本:
组合 cost = sum(w_i × cost_i)
九、PM 视角:「unit economics 敏感性分析」的迁移
9.1 量化的「成本敏感性」 = PM 的「最坏 case 增长」
我在金融零售产品的 10 年里,每一次大需求评审都要给三种 case:
| 案例 | 量化等价 |
|---|---|
| Base case:预期 ROI 1.5x | 中位 Sharpe 1.02 |
| Best case:广告 CTR 高于预期 | P90 Sharpe 1.22 |
| Worst case:CAC 翻倍 | P10 Sharpe 0.55 |
好 PM 的特征:永远先讲 worst case,再讲 base,最后 best。坏 PM 反过来,结果是把团队带进「侥幸心理」陷阱。
量化在这条原则上更不宽容:你可以容忍产品 worst case 损失 30%,但量化 worst case 是真金白银。所以回测必须默认偏保守,实盘惊喜永远比意外好。
9.2 「成本」不是「损耗」,是「Trade-off 的另一面」
很多人把成本看成「毛收益的减法」——错。成本是「这个策略对执行能力的隐性押注」:
| 策略 | 成本敏感性 | 押注内容 |
|---|---|---|
| 月度因子(Day 28) | 低 | 押 alpha 真实存在 |
| 周度动量 | 中 | 押 alpha + 执行 OK |
| 日内反转 | 极高 | 押 alpha + 执行优秀 + 算力 + colo |
| HFT | 超高 | 押 alpha + 执行 + colo + FPGA + tech team |
散户的「执行能力上限」就是 IBKR Adaptive 算法 + 限价单。所以散户的「可行策略空间」就被「成本敏感性」自动筛选了——不是「我选不做 HFT」,是「HFT 在我这里 unit economics 不成立」。
这也是为什么 Day 28 选月度 + Day 34 验证 cost robustness 是关键的两步——前者选了一个 unit economics 健康的 weight class,后者证明这个 weight class 在压力下仍然 work。
9.3 给自己的执行原则(这一条我会贴在桌面)
1. 任何策略,回测时默认成本 = 表里的「保守值」
2. 任何策略上线前,必须跑 Monte Carlo cost regime,
且 P10 Sharpe 必须 > 0.5(最坏 10% 情况下还有 alpha)
3. 实盘 vs 回测的 Sharpe gap 超过 0.3,必须 root cause(不是 noise)
4. 不做任何「成本弹性 > -0.3 Sharpe/bp」的策略
十、Day 34 实际执行 Checklist
- (0) 复习 Day 22 / Day 28 笔记,把「成本框架」串起来
- (1) 实现
IBKRTieredCost类,跑通 4 个 sanity check 案例 - (2) 用 yfinance + Corwin-Schultz 估 5 只 ticker 的 spread(SPY / AAPL / ROKU / PLTR / 自选)
- (3) 实现
SlippageModelv2 类,支持 conditional regime - (4) 把 Day 28 pipeline 模块化导出
run_dual_factor_backtest(cost_config=...)接口 - (5) 跑 4 档 cost sweep,输出 Sharpe / CAGR / MaxDD / Cost drag 表
- (6) 跑 4 档 frequency sweep(weekly / monthly / quarterly / semi-annual)
- (7) 跑 1000 次 Monte Carlo,输出 Sharpe 分布 percentile 和 cost-Sharpe 弹性散点图
- (8) 把「散户合理保守值」决策表打印贴到桌面
- (9) 更新
docs/daily/TR_PROGRESS.mdWeek 5 / Day 34 标 ✅ - (10) 记录踩坑:本笔记最后加「实际执行记录」段
十一、明日预告
Day 35: Walk-Forward Analysis 实战验证 + 双因子组合 OOS 稳定性测试
- 把 Day 25 的 WFA 框架用到 Day 28 双因子组合 + Day 34 的真实成本上
- 5-fold rolling WFA:2014-2024 划分 IS / OOS
- 关键测试:IS Sharpe / OOS Sharpe 比值是否 > 0.6(学术稳健性门槛)
- 关键测试:OOS 期间最大 single-fold Sharpe 下降幅度
- 决策:基于 WFA 结果决定「Day 28 + Day 34 cost 调整后的策略是否进入 Phase 2 paper trade」
- 写一个 fold-by-fold 可视化 + 子集稳定性热力图
- 整理 Day 31-35 「成本 + WFA」整周笔记作为 Phase 2 Week 5 阶段性输出
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] IBKRTieredCost 实现 + sanity check — ...
- [hh:mm] Corwin-Schultz spread 估计 — ...
- [hh:mm] 4 档 cost sweep — ...
- [hh:mm] Frequency sweep — ...
- [hh:mm] Monte Carlo 1000 次 — ...
- [hh:mm] 弹性散点图 — ...
- 卡点 / 学到的:
总字数:约 6,200 字 今日完成度:理论 ✓ / 实操(你自己执行)/ 笔记 ✓