Two posts ago, I measured one-sided slippage. In the last post, I simulated the last-look window that creates it. Now let us put the signals together to answer the question every serious retail trader eventually asks: is my broker A-booking me or B-booking me, and does it matter for how I am filled?
Quick definitions, because the terms get thrown around loosely:
- A-book: your order is passed to an external liquidity provider. The broker earns from spread or commission and has no direct interest in whether you win or lose.
- B-book: your order is kept internally, and the broker takes the other side. Your losses become their revenue.
B-book is not automatically evil. Plenty of B-book flow fills cleanly because most retail accounts lose, and the desk has no reason to interfere. The real risk is undisclosed switching: your execution quietly degrading once you become profitable. That switch is what you can detect.
I build execution software at BJF Trading Group. These are the tests I would run on my own account, none of which need anything more than a timestamped trade log.
Signal 1: demo versus live, same strategy
Demo servers are almost always unfiltered and simulate near-perfect fills. Run the identical logic on demo and live in parallel, then compare the fill distributions.
def exec_profile(df):
return {
"fill_rate": (df["status"] == "filled").mean(),
"mean_slip": df["slip"].mean(),
"reject_rate": (df["status"] == "rejected").mean(),
"median_latency_ms": df["latency_ms"].median(),
}
print("demo:", exec_profile(demo))
print("live:", exec_profile(live))
A small gap is normal. A large, persistent gap (live fills much worse than demo on the same strategy) shows how your live flow really handles it.
Signal 2: does execution degrade when you get profitable?
This is the sharpest test. Sort your trades in time, split at the point your equity curve turned consistently positive, and compare execution before and after.
df = df.sort_values("timestamp")
turned_profitable = df["cum_profit"].cummax().ge(profit_threshold).idxmax()
before = df.loc[:turned_profitable]
after = df.loc[turned_profitable:]
print("before profitable:", exec_profile(before))
print("after profitable:", exec_profile(after))
If latency steps up, slippage turns one-sided, and rejects appear only after the account becomes profitable, that timing signals an account being reclassified and moved behind an execution filter. Markets do not know your equity curve. A risk desk does.
Signal 3: latency step-changes
Plot execution latency over time. Fair execution is low and stable. A sudden, permanent jump strongly suggests a server-side delay was added to your account.
import matplotlib.pyplot as plt
roll = df.set_index("timestamp")["latency_ms"].rolling("1D").median()
roll.plot()
plt.ylabel("median execution latency (ms)")
plt.title("Look for a step, not noise")
plt.show()
Noise is fine. A step is the tell.
Signal 4: size sensitivity
Execution that is clean at 0.1 lots but suddenly slips or rejects at larger size reveals a threshold in the risk logic. Fair execution degrades smoothly with available liquidity, not in a step at the point you become material.
by_size = df.groupby(pd.cut(df["lots"], [0, 0.1, 0.5, 1, 5]))
print(by_size["slip"].mean())
print(by_size["reject_rate"].mean() if "reject_rate" in df else "")
Putting it together into a score
No single signal is proof. The combination is. This is the logic behind BEQI, an open methodology that folds fill rate, slippage symmetry, rejection behavior, and latency into one comparable execution score, so you can rank two brokers on evidence instead of reputation. The method and reference code are here: Broker Audit Toolkit, and the broker-side mechanics are in How brokers really fill your orders.
The point of all four signals is the same: stop arguing about what a broker intends, and start measuring what it does. Your fill log already knows.
Next in the series: why an edge that looks great in a backtest can go negative in production, modeled in code.
I develop arbitrage and execution software at BJF Trading Group.
Top comments (0)