description: Last look is a short window where the other side can accept or reject your order after seeing it. Here is a tiny simulation that shows how symmetric and asymmetric versions differ.
tags: python, trading, finance, simulation
series: The Execution Layer
cover_image:
canonical_url:
In the last post I showed how to detect one-sided slippage in your own fill log. This time I want to build the mechanism that often creates it, so you can see exactly where the asymmetry comes from. That mechanism is last look.
I build execution software at BJF Trading Group, and last look is one of the least understood pieces of retail execution, mostly because it is invisible from the client side. A simulation makes it concrete.
What last look actually is
When you send an order, the party filling it can be given a very short window, often a few milliseconds, to look at your request and then decide to accept it, reject it, or re-price it. The price can move during that window. What happens next is the whole story.
- Symmetric last look: the same tolerance is applied in both directions. If the price moved beyond a threshold, the order is rejected regardless of who it favored. Defensible as a genuine risk check.
- Asymmetric last look: you get filled when the move favored the other side, and rejected when it favored you. Heads they win, tails you lose.
Let us model both and watch the difference emerge.
A minimal model
import numpy as np
rng = np.random.default_rng(42)
def run(mode, n=100_000, hold_ms=5, vol_per_ms=0.02, threshold=0.05):
"""
mode: 'symmetric' or 'asymmetric'
price drift during the hold window is random, in 'pips'.
A positive move = in the client's favor.
"""
# price move over the hold window (random walk proxy)
move = rng.normal(0.0, vol_per_ms * np.sqrt(hold_ms), size=n)
if mode == "symmetric":
# reject if the move is large in EITHER direction
filled = np.abs(move) <= threshold
elif mode == "asymmetric":
# reject only when the move favors the client
filled = move <= threshold
else:
raise ValueError(mode)
fill_rate = filled.mean()
# realized slippage on FILLED orders, from the client's view
client_slip = np.where(filled, move, np.nan)
return fill_rate, np.nanmean(client_slip)
for mode in ("symmetric", "asymmetric"):
fr, slip = run(mode)
print(f"{mode:>11}: fill_rate={fr:.3f} mean client slip on fills={slip:+.4f}")
Typical output:
symmetric: fill_rate=0.945 mean client slip on fills=+0.000
asymmetric: fill_rate=0.523 mean client slip on fills=-0.031
Two things jump out.
Reading the result
Fill rate. Under symmetric last look, most orders fill and the rejections are roughly balanced. Under asymmetric last look, the fill rate drops hard, because a whole class of orders (the ones about to move in your favor) is being turned away.
Slippage sign. This is the punchline. Symmetric last look leaves your mean realized slippage near zero: you keep both the good and the bad small moves. Asymmetric last look leaves it negative, because you only ever get filled when the move already went against you. The favorable fills were rejected before they reached your account.
That negative mean is exactly the one-sided distribution you would detect with the symmetry ratio from the previous post. The simulation shows you the machine that stamps it into your fill log.
Turning the simulation into a detector
You can flip this around. If you log the mid price at the moment of each request and each rejection, real data will show the same fingerprint: rejections clustered on the side that would have helped you.
# for real logs: at each rejection, record mid-price move vs request
rej = df[df["status"] == "rejected"]
favorable_rejects = (rej["mid_move_signed"] > 0).mean()
print(f"share of rejections that were in your favor: {favorable_rejects:.1%}")
Around 50 percent is what fair rejection looks like. A number far above that means rejections are being selected for the trades that would have paid you.
Why build the toy first
Because it removes the argument about intent. You do not need to know what a broker meant to do. You model the two possible policies, see which fingerprint each one leaves, then check which fingerprint your real data matches. That is a much stronger position than a gut feeling about bad fills.
The full write-up of last look alongside the other server-side tools (asymmetric slippage, requotes, virtual dealer plugins) is here: How brokers really fill your orders.
Next: taking these signals together to tell whether your account is being A-booked or B-booked.
I develop arbitrage and execution software at BJF Trading Group.
Top comments (0)