
Most write-ups about latency arbitrage stop at the idea: a fast reference price moves, your broker's quote lags for a moment, you trade the stale price. True — but useless on its own, because whether there's any edge at all depends entirely on your specific broker, and that is a measurable property, not a vibe.
So let's build the thing that measures it: a small tick-divergence detector that compares two price feeds and records every moment they disagree — the size of the gap, its direction, and how long it lasts. It's ~60 lines of Python, it runs, and it's the same core idea behind any serious broker-latency tool.
The one number that matters
A latency-arbitrage system is only interesting if you can answer three questions about a broker:
- How large are the gaps between the broker's quote and the true market? (magnitude)
- How long do they persist? (duration — do you have time to act?)
- When do they happen? (session/timing)
Everything else — the VPS, the execution stack, the strategy code — is downstream of those numbers. If the gaps are tiny and vanish in single-digit milliseconds, no infrastructure will manufacture an edge. If they're wide and last 100+ ms during liquid hours, you have something. You cannot know which world you're in without measuring.
Modelling a "divergence"
Two feeds:
- a reference feed — a fast, aggregated view of the market's true price, with minimal delay;
- a broker feed — the quote you can actually trade, which lags because of aggregation delay, network distance, or deliberate throttling.
At any instant, define the gap as reference_mid - broker_mid. When the reference jumps and the broker hasn't caught up, that gap exceeds some threshold and a divergence window opens. It stays open until the broker catches up and the gap collapses back under the threshold. For each window we record:
-
max_gap— the largest absolute gap seen (your theoretical edge) -
direction—buythe broker when it's stale-low (ref > broker),sellwhen stale-high -
duration— how long the window stayed open
Here are the data structures:
from dataclasses import dataclass
PIP = 0.0001 # 5-digit FX symbol, e.g. EUR/USD
@dataclass
class Divergence:
start: float # arrival time (s) the gap opened
end: float # arrival time (s) the gap closed
max_gap: float # largest |ref - broker| during the window (price units)
direction: str # 'buy' broker (ref above) or 'sell' broker (ref below)
@property
def duration_ms(self):
return (self.end - self.start) * 1000
@property
def gap_pips(self):
return self.max_gap / PIP
The detector
The detector is a tiny state machine. It remembers the latest reference mid, and on every incoming tick it re-evaluates the gap. Above threshold → a window is open (and we keep the running max). Back below threshold → close the window and emit an event.
class DivergenceDetector:
"""Feed it interleaved reference/broker ticks in arrival-time order."""
def __init__(self, threshold_pips=0.8):
self.threshold = threshold_pips * PIP
self.ref_mid = None
self._open = None # currently open divergence
self.events = []
def on_reference(self, t, mid):
self.ref_mid = mid
self._reevaluate(t)
def on_broker(self, t, mid):
self.broker_mid = mid
self._reevaluate(t, broker=mid)
def _reevaluate(self, t, broker=None):
if self.ref_mid is None or not hasattr(self, "broker_mid"):
return
b = broker if broker is not None else self.broker_mid
gap = self.ref_mid - b # >0 => broker stale-low => buy broker
if abs(gap) >= self.threshold:
direction = "buy" if gap > 0 else "sell"
if self._open is None:
self._open = {"start": t, "max": abs(gap), "dir": direction}
else:
self._open["max"] = max(self._open["max"], abs(gap))
elif self._open is not None:
o = self._open
self.events.append(Divergence(o["start"], t, o["max"], o["dir"]))
self._open = None
Note the important detail: ticks are processed in arrival-time order, not in the order the market produced them. The whole point is that the broker's tick for a given price arrives later than the reference's. That ordering is the phenomenon.
A runnable simulation
To exercise it without a live feed, simulate one true price (a random walk) observed by two feeds with different delays — a ~5 ms reference and a ~150 ms broker. Merge both streams by arrival time and push them through the detector.
import random, statistics
def simulate(n=20000, ref_delay=0.005, broker_delay=0.150, seed=42):
rnd = random.Random(seed)
t, price = 0.0, 1.1000
arrivals = []
for _ in range(n):
t += rnd.expovariate(1 / 0.05) # ~20 ticks/sec avg
price += rnd.gauss(0, 0.6) * PIP # small random step
arrivals.append((t + ref_delay, "ref", price))
arrivals.append((t + broker_delay, "broker", price))
arrivals.sort(key=lambda a: a[0]) # merge by ARRIVAL time
det = DivergenceDetector(threshold_pips=0.8)
for at, kind, mid in arrivals:
(det.on_reference if kind == "ref" else det.on_broker)(at, mid)
return det.events
ev = simulate()
gaps = [e.gap_pips for e in ev]
durs = [e.duration_ms for e in ev]
buys = sum(e.direction == "buy" for e in ev)
print(f"divergences detected : {len(ev)}")
print(f"median gap (pips) : {statistics.median(gaps):.2f}")
print(f"median duration (ms) : {statistics.median(durs):.0f}")
print(f"direction split : {buys} buy / {len(ev) - buys} sell")
Output on my machine:
divergences detected : 5122
median gap (pips) : 1.27
median duration (ms) : 54
direction split : 2593 buy / 2529 sell
Play with broker_delay. Drop it to 0.02 (a fast broker) and the divergences mostly disappear — which is exactly the point: the edge is a function of the broker's lag, and nothing else. Widen it to 0.3 and both the gap size and duration grow. You've just built a knob that models "how exploitable is this broker."
The roughly even buy/sell split is a good sanity check too: a pure lag against a symmetric random walk shouldn't be directionally biased. A real broker feed that comes back skewed is telling you something about how it's quoting.
Get the code
The full, tested version — with a CLI, per-symbol pip sizes, an examples/ delay sweep and unit tests — is open-source (MIT) on GitHub:
→ github.com/bjftradinggroup-inc/forex-latency-divergence-detector
git clone https://github.com/bjftradinggroup-inc/forex-latency-divergence-detector
cd forex-latency-divergence-detector
python latency_detector.py --broker-delay 0.15
If it's useful, a ⭐ helps other people find it.
What the toy model conveniently ignores
This is where measurement stops being cute and starts being engineering. A production detector has to deal with:
-
Clock discipline. You're comparing timestamps from two sources. If their clocks drift, your "latency" is fiction. Real setups stamp events against a monotonic clock and, at the serious end, discipline the host clock with PTP. Never trust wall-clock
datetime.now()for this. - Last look. A measured gap is a theoretical opportunity, not a fill. Many liquidity providers get a brief final window to reject an order after you send it — so the gaps you measure and the trades you actually get filled on are two different distributions. Measuring the first without modelling the second is how people overestimate an edge.
-
Per-symbol reality.
PIPisn't a constant; gold, JPY crosses and majors all differ. Thresholds, spreads and tick rates are per-symbol. Gold (XAU/USD) in particular tends to show far larger gaps than EUR/USD because it's more volatile and more loosely quoted. - Spread and costs. A 1.3-pip gap on a 0.6-pip spread is not a 1.3-pip edge. The detector measures the raw divergence; the tradeable part is what's left after spread, commission and slippage.
- Throughput. Real feeds are tens of thousands of messages per second. The hot path has to be allocation-light; the version above is for clarity, not for a colocated box.
The meta-point: a measured opportunity is not realized profit. The detector tells you whether a broker is even worth pursuing. It does not promise the money is capturable. That distinction is the difference between people who treat a backtest as a track record and people who don't.
From a detector to a decision
Scale this up — run it live against many brokers, across many symbols, for months — and the event stream becomes a dataset you can actually make decisions with: which brokers lag enough to matter, on which instruments, at which hours. That's the difference between choosing a broker on marketing and choosing one on evidence, and it's the single highest-value step before spending a cent on infrastructure.
If you'd rather not build and host the whole pipeline, this is exactly what a free forex arbitrage scanner does — it records real broker latency with open methodology so you can inspect the gap distributions directly. For the strategy context around it, see the deep dives on algorithmic & automated forex trading and latency arbitrage.
The takeaway for engineers: in this corner of trading, the interesting problem isn't "what's the strategy" — it's "how do I measure the market honestly enough to know whether a strategy can exist at all." Start there.
Educational content, not financial advice. Trading involves significant risk; measured theoretical opportunities do not guarantee tradable or profitable results.
Top comments (0)