第 3 章 · 01 London Breakout:跨时区信息套利 本节摘要:本节精读 London Breakout——一个外汇开盘区间突破策略。它的本质不是什么神秘形态,而是跨时区信息套利:FX 24 小时全球运转,东京盘和伦敦盘接力,GMT 7:00-7:59(伦敦开盘前最后 1 小时)是吸收隔夜全球信息的"凝结窗",作者把这个小时的高低价取出来当阈值,等伦敦 8:00 开盘后 30 分钟内一旦突破就入场。我们重点讲清楚 signalgeneration 里那段"时间复杂度极高"的 for 循环:它怎么用 保证只首次入场、怎么用 把异常跳空当假信号过滤、怎么在 EST 12pm 强制平仓。 涉及源码:原项目 (约 286 行),数据 (histdata.
本节摘要:本节精读 London Breakout——一个外汇开盘区间突破策略。它的本质不是什么神秘形态,而是跨时区信息套利:FX 24 小时全球运转,东京盘和伦敦盘接力,GMT 7:00-7:59(伦敦开盘前最后 1 小时)是吸收隔夜全球信息的"凝结窗",作者把这个小时的高低价取出来当阈值,等伦敦 8:00 开盘后 30 分钟内一旦突破就入场。我们重点讲清楚 signal_generation 里那段"时间复杂度极高"的 for 循环:它怎么用
cumsum保证只首次入场、怎么用risky_stop=0.01把异常跳空当假信号过滤、怎么在 EST 12pm 强制平仓。
涉及源码:原项目
London Breakout backtest.py(约 286 行),数据gbpusd.csv(histdata.com 1 分钟 bid/ask 平均值,EST 时区)。
⚠️ 注意:本节策略涉及外汇 24 小时多时区机制,务必先消化时区换算(EST/GMT)再看代码,否则会把
signals['date'][i].hour==2当成凌晨 2 点而不是"伦敦开盘前最后 1 小时"。
阅读完本节,你应当能够:
risky_stop=0.01 既是异常跳空过滤,又是 ±50bp 止盈止损的"半径"。cumsum 在本策略里承担"只首次入场"和"日终清仓"两重职责。外汇和股票不同,没有统一的交易所,它是全球银行间接力的 24 小时市场(工作日)。一天的接力顺序大致是:
悉尼 ──► 东京 ──► 伦敦 ──► 纽约 ──► (回悉尼)
作者开篇就表白:"this is to London, the greatest city in the world……i was a Londoner, proud of being Londoner"。他接着说出关键事实:伦敦是三大主力中成交量最大的中心("london is where the majority trades are executed")。所以 London 开盘的那一刻,往往是一天里信息密度最高、价格最容易出方向的时刻。
东京盘 GMT 0:00-8:59(夏令时与冬令时不同,作者咬牙切齿地骂"daylight saving time, what a stupid idea"),伦敦盘 GMT 8:00 开盘。两者在 GMT 8:00 那一刻交接棒。作者抓的不是东京整段,也不是伦敦整段,而是 GMT 7:00-7:59 这最后 1 小时——东京快收盘、伦敦快开盘,这 1 小时吸收了整个亚洲时段的隔夜信息,但伦敦的大单子还没进场,价格相对"干净"。
这 1 小时的高点和低点,就成了"隔夜全球共识"的上下沿:
upper = max(东京最后一小时所有价格) lower = min(东京最后一小时所有价格)
histdata.com 的数据用 EST(UTC-5) 时区,所以:
| 事件 | GMT | EST |
|---|---|---|
| 东京最后一小时开始 | 7:00 | 2:00 |
| 伦敦开盘 | 8:00 | 3:00 |
| 伦敦收盘 | 17:00 | 12:00 |
代码里所有 hour==2 / hour==3 / hour==12 都是 EST 视角。看到 signals['date'][i].hour==2,脑子里要立刻翻译成"东京最后一小时"。
定义风险半径 risky_stop=0.01(100 个基点,即 1%):
risky_stop(即跳空 100bp 以上)视为异常,放弃入场。executed_price 为中心,±risky_stop/2(50bp)触发平仓。作者比喻:"if it goes skyrocketing, say 200 basis points above, which is 100 above our risk tolerance, i personally wouldnt get in the market as its too risky"。一句话:跳得太凶的单子不接,因为我的止损只有 50bp,跳 200bp 进去等于把止损预算直接打爆。
延续第 2 章约定:signals 列 +1 做多、-1 做空、0 不动;cumsum 是 signals 的累计和,代表当前持仓方向与张数。本策略独有的是 upper/lower 两列阈值,只在窗口内非零。
London Breakout backtest.py:106-211 的 for 循环是全策略的灵魂,作者自陈"the time complexity for this part is extremely high……if u have a better idea to optimize it, plz let me know"。它按 signals['date'][i].hour 分 5 个分支。
分支① 收集 EST 2am 全部价格(L112-113):
if signals['date'][i].hour==2: tokyo_price.append(signals['price'][i])
整个 EST 2:00-2:59 的 60 根 1 分钟价格全部塞进 tokyo_price 列表。这 60 个价格就是"隔夜共识"的样本。
分支② EST 3:00 定阈值(L124-132):
elif signals['date'][i].hour==3 and signals['date'][i].minute==0: upper=max(tokyo_price) lower=min(tokyo_price) signals.at[i,'upper']=upper signals.at[i,'lower']=lower tokyo_price=[]
伦敦开盘第一根 K 线(EST 3:00 整)取出 max/min 作为上下沿,写进当前行的 upper/lower,然后清空 tokyo_price 给下一天备用。
分支③ 开盘 30 分钟内破阈入场(L140-185):
elif signals['date'][i].hour==3 and signals['date'][i].minute<open_minutes: signals.at[i,'upper']=upper signals.at[i,'lower']=lower # 破上沿做多 if signals['price'][i]-upper>0: signals.at[i,'signals']=1 signals['cumsum']=signals['signals'].cumsum() # 反直觉风控:跳空过深,假信号 if signals['price'][i]-upper>risky_stop: signals.at[i,'signals']=0 # cumsum>1 说明已入场,这次不算 elif signals['cumsum'][i]>1: signals.at[i,'signals']=0 else: executed_price=signals['price'][i] # 破下沿做空(对称) if signals['price'][i]-lower<0: signals.at[i,'signals']=-1 ...
这里两个反直觉点:
signals.at[i,'signals']=1,再分别检查"是否跳空过深"和"是否已入场",任一命中就把信号抹回 0。这是典型的"先乐观后过滤"写法。cumsum 每次重算:signals['cumsum']=signals['signals'].cumsum() 不是一次性算好,而是每次写完 signal 后整列重算——这样 cumsum[i] 永远是"截至当前时刻的累计持仓",用来判"我是不是已经入场过了"。>1 表示已经持多仓,这次破阈不算。分支④ EST 12pm 强制平仓(L191-193):
elif signals['date'][i].hour==12: signals['cumsum']=signals['signals'].cumsum() signals.at[i,'signals']=-signals['cumsum'][i]
GMT 17:00(伦敦收盘),把累计持仓的反向值作为信号——cumsum=1 就发 -1,cumsum=0 就发 -0=0(空仓不动)。一行代码实现"日终清仓"。
分支⑤ 持仓期 ±50bp 止盈止损(L201-209):
else: signals['cumsum']=signals['signals'].cumsum() if signals['cumsum'][i]!=0: if signals['price'][i]>executed_price+risky_stop/2: signals.at[i,'signals']=-signals['cumsum'][i] if signals['price'][i]<executed_price-risky_stop/2: signals.at[i,'signals']=-signals['cumsum'][i]
不在窗口、没到收盘的"持仓期",只要当前有仓(cumsum!=0),价格超出 executed_price±50bp 就反向平仓止盈或止损。注意上下两个 if 都用 -cumsum 平仓——止盈和止损在代码里完全对称,区别只在于"价格往上突破"还是"往下突破"。
把 5 个分支串起来,一天 GBPUSD 的策略行为是:
EST 2:00-2:59 分支① 收集 60 个价格到 tokyo_price EST 3:00:00 分支② upper=max / lower=min 定阈 EST 3:00-3:29 分支③ 破阈入场(只首次),记录 executed_price EST 3:30-11:59 分支⑤ 持仓期,±50bp 止盈止损 EST 12:00 分支④ 强制平仓

第二张图是开盘 40 分钟(2:50-3:30)的放大,棕色点是 upper threshold、橙色点是 lower threshold,可以直观看到价格突破阈值时绿色 LONG 三角或红色 SHORT 倒三角落在哪。

🎯 反直觉发现:
risky_stop=0.01在本策略里身兼二职——既是入场前的"异常跳空过滤半径"(超过 100bp 不入场),又是入场后的"止盈止损半径"(±50bp 平仓)。同一个参数,入场前用全值(100bp),入场后用半值(50bp)。这种"用一个风险预算参数串联前后两端"的设计很省心,但也意味着跳空过滤的容忍度是止损半径的 2 倍——这是作者刻意留的安全垫:宁可错过跳空行情,也不要在 200bp 跳空里被 50bp 止损反复打脸。
cumsum 一列两用:既判"是否已入场"(>1 或 <-1),又用于日终清仓(-cumsum 直接反向)。signals.at[i,'signals']=1 后再过滤:写信号前先乐观置 ±1,再用 if/elif 链抹回 0,代码比"先判条件再写"更紧凑。executed_price 仅在真入场时记录:避免被"假信号"覆盖,保证止盈止损基准价始终是真正成交价。open_minutes=30 可调:作者注释里提示,你也可以改成"在 EST 3:30 这一个固定时刻检查条件"——本策略是连续 30 分钟滚动检查,改法在 L138-139 的注释里。cumsum 保证只首次。risky_stop=0.01 身兼二职:入场前过滤异常跳空(100bp),入场后 ±50bp 止盈止损。-cumsum 一行实现日终清仓。下一节,我们看一个"亲戚"策略 Dual Thrust——同样是开盘区间突破,但它没有止盈止损,完全靠日终清仓和"翻仓"机制管理风险。这是另一种哲学。