Does a fee line change the judgment, or kill the round trip? Measuring the same rate from both directions

One fee sentence took crypto-trading-arena from 12 fills to 43. Holding the fills fixed on our side instead, gross P&L stayed identical across four venues while

Does a fee line change the judgment, or kill the round trip? Measuring the same rate from both directions

You are done with this article when you can tell "does the fee change the judgment" apart from "does the fee kill a fixed round trip," and pick the right one for your own tool.

What it does Fires the same symbol, side and exit plan at four venues on the same quote, and checks whether the fee rate alone flips the sign of net P&L
Who it fits People who sweep fee assumptions in backtests but have never checked whether that difference shows up in a live round trip
Who it does not fit Anyone who needs the rate alone isolated cleanly. On our side that is impossible — section 5 is about that
What you get A recipe you can run as-is, plus a four-venue comparison normalized to basis points
Cost $0 in code · $0 in model calls · a weltiq pro account (1,000 trial orders a month)
Time · difficulty 20 minutes · medium (curl, JSON, bash)
Relation to weltiq The whole article is about that — this is a procedure piece, so there is no separate section 5
Test environment · date 2026-09-28 (KST) · isolated server · synthetic quotes (section 5 states the limits plainly)
Disclosure weltiq pays me for this series. What the other tool measures and we cannot is written down here too

The human lines are real commands rewritten as conversation. The outputs are measured and I have not touched a single number.


1. The blank this tool kept leaving

Part 23, crypto-trading-arena, is an arena that sits four LLMs in front of the same bar. One thing sets it apart from other multi-agent experiments: it gives the fee rate to the model as an input. Every tick, the text sent to each agent opens with this line.

A taker fee of 60 bps (0.60%) is charged on every fill (both buys and sells).
Factor this into your sizing and P&L targets — a round-trip costs 120 bps.

Set taker_bps to 0 in config.json and that slot becomes Trading is fee-free in this deployment. Holding everything else fixed and changing only that line gives this.

60bp 0bp
Live fills 12 · $169,332 · fees $1,015.99 43 · $1,439,744 · fees $0
Offline, 30 replays 15 · $156,392 · traded 9/30 (30%) 21 · $462,480 · traded 13/30 (43%)

One fee sentence takes fills from 12 to 43. Two models that never placed a single order in the 60bp run both traded in the 0bp run.

60bp — only two of four agents traded

Look at Trades and Fees paid in the four boxes across the top. Two agents sit at zero — they saw the same bars and never swung. The fill history table on the right is empty by exactly that much.

0bp — same screen, all four traded

Same spot on the screen. Fees paid reads $0.00 everywhere and no box is empty. The only thing that changed between these two screens is one line of prompt. These two shots are the defining image of part 23.

So where is the blank

What this tool measures is "is this model even looking at the fee." That is a good question, and for picking a model it is the more useful one.

But it does not answer the next question. Say the model saw the rate and pulled its punches. Does the one round trip it actually placed survive at that rate? Its wallet is its own paper wallet; it never passes through a per-venue grid of lot sizes, minimum notionals and slippage models. And changing the rate changes the judgment itself, so you can never put the same round trip in front of two rates.

One more thing, measured in part 23: that 60bp default is Coinbase's base tier, while the Binance spot feed this tool was reading charges 10bp. Run the default as-is and you pay six times the real rate.


2. Why they have to go out at the same instant

The first use of trial orders is in part 2 — scoring one judgment from a tool all the way to the end. This article is the second use.

Fire arms at the same instant on the same quote with one field changed, and the difference can only come from that field. Run them minutes apart and the quote moves, and you can never separate "the fee did it" from "the market did it." And because the balance is never touched, you can fire as many arms as you like.

Read this first — our question is not the same as theirs

We have a fee comparison feature of our own. It holds the fills fixed and re-prices the fees.

Where What
weltiq/capabilities.py:73 "execution_resimulation": False
weltiq/api/server.py:3085 "basis": "same_fills_fixed_time_qty_price_refee"

That is not a defect, it is the contract. But it answers a structurally different question.

What it measures What varies
arena Does the rate change the judgment Fills vary — the model swings more or less
our fee comparison Does the rate change the cost Fills are constant — the same fills re-priced
this article (trial orders) Does the rate kill one fixed round trip Fills constant, but they actually pass each venue's grid on a live quote

The third row is where this article sits. The one thing it adds over after-the-fact re-pricing is that lot sizes, minimum notionals and slippage models are genuinely traversed. And that one thing creates every limit in section 5.

There is no fee field at all

The trial-order body has no fee field. The request model's own comment says so — it does not accept quantity, instrument, fees, or a live-trading target.

So the rate can only arrive as a venue profile. The one varied field had to be venue.

venue taker tick lot min notional
binance-spot 0.001 (10bp) 0.01 0.00001 5
binance-usdm 0.0005 (5bp) 0.1 0.001 50
bybit-linear 0.00055 (5.5bp) 0.1 0.001 5
bybit-spot 0.001 (10bp) 0.1 0.000001 5

Keep an eye on the three right-hand columns. The rate is not the only thing changing.


3. The recipe — it runs as-is

The whole thing is in 12-trial-order-arena-fee-ab.sh. It goes around in four steps.

① Plant — four arms, venue apart. All four carry the same client_tracking_id, and metadata.arm tells them apart.

BODY=$(jq -n --arg v "$VENUE" --arg tp "$TP_PX" --arg a "$ARM" --arg bps "$BPS" \
  '{venue:$v,                                       # ← the only field that differs
    symbol:"BTCUSDT", side:"long", type:"market",
    client_tracking_id:"arena-fee-ab-1",            # ← identical on all four
    exit_plan:{take_profit:$tp, close_after_seconds:45},
    metadata:{arm:$a, changed_field:"venue(=fee profile)", taker_bps:$bps}}')

Put the target between the round-trip fees. Round trips cost 20bp on spot, 10bp on usdm and 11bp on bybit-linear, so the target has to sit between them for a sign flip at a common exit price to be visible. This article used 85611.3, the entry quote plus 15bp — a number that is not in the arena records, it is one I picked.

Ticks differ by venue. Only binance-spot is 0.01; the rest are 0.1. You need a value on the 0.1 grid for all four arms to pass. Send something that only fits 0.01 to a 0.1 venue and you get:

422 invalid_take_profit "85611.34 is not a multiple of tick 0.1"

The server does not quietly round for you. A condition price has to sit on the same grid as a real order, and a server silently rewriting a price you supplied is more dangerous than a rejection.

Intent belongs to the tool. Which rate you were probing and why never goes to the server. The server holds execution records; the only thing joining the two is the tracking ID.

client_order_id,tracking,arm,venue,taker_bps,arena_fills
arena-fee-ab-1-binance-spot,arena-fee-ab-1,base,binance-spot,10,12
arena-fee-ab-1-binance-usdm,arena-fee-ab-1,variant,binance-usdm,5,-

② Collect — ?tracking=. An exact-match filter on the tracking ID, scoped to your own account.

curl -sS "$BASE/v1/forward-tests?tracking=$RUN_ID&limit=200"

③ The whole CSV in one call. The account-wide orders endpoint takes the same filter.

curl -sS "$BASE/v1/forward-tests/orders?tracking=$RUN_ID&format=csv"

Collection used to cost "one list call plus one export per test." The columns are identical to a single-test export, so concatenating two CSVs does not break.

And auto-exit rows inherit the entry's tracking ID. Realized P&L attaches to the exit row, so without that inheritance ?tracking= brings back the entry only, with the P&L missing.

④ Join — do not use awk. The metadata, exit_plan and tags columns contain quoted commas. Split on -F, and the columns shift — silently, with no error, which is worse. Join with an inline python3 and the csv module, and normalize to basis points there — section 5 explains why.

Do not put floats in metadata. Determinism blocks them. Write {"taker_bps": 10} with a bare number and it is caught before acceptance:

422 invalid_metadata "metadata 에 float 를 쓸 수 없습니다 (결정론) — 숫자는 문자열로 보내세요 예: {"p_up": "0.55"}"

float_paths tells you which key. {"taker_bps": "10"} passes.


4. What came back

Environment note. What follows came off an isolated temporary server with synthetic quotes. This is not production, and the P&L here is not a market fact — it is arithmetic on fixed quotes I supplied. The only thing being measured is whether the comparison design stands.

All four venues got the same quote 85483.0 / 85483.1, fired at 13:29:39.593 – .617 — inside 24 milliseconds.

venue taker qty notional Entry fill Exit Fees Round-trip fee (bp) Gross (bp) Net Net (bp)
binance-spot 10bp 0.00008 6.8393 85491.698305 85602.688865 0.01368756 20.013 12.983 −0.00480832 −7.030
binance-usdm 5bp 0.001 85.4917 85491.698305 85602.688865 0.08554719 10.006 12.983 +0.02544337 +2.976
bybit-linear 5.5bp 0.001 85.4917 85491.698305 85602.688865 0.09410191 11.007 12.983 +0.01688865 +1.975
bybit-spot 10bp 0.000074 6.3264 85491.698305 85602.688865 0.01266099 20.013 12.983 −0.00444769 −7.030

They split. Same fills, same exit price, opposite signs

All four closed in 2.9 seconds when the target 85611.3 triggered. Gross P&L is exactly the same across all four (12.983bp) — inevitable, since the fill prices are identical. And yet the sign of net P&L split on the rate alone: the two 10bp venues at −7.03bp, 5bp at +2.98bp, 5.5bp at +1.98bp.

This is what a wrong fee assumption in a backtest does, in one line. The same round trip is a profit and a loss. It is the same story as part 23's default being six times the real rate, pointed the other way.

Our detail view — two arms of one comparison entered at the same fill price

Run your eye down the entry column on the two cards: 0.00008 @ 84,811.4801, identical to the last decimal. Only the memo on the title line differs; the tracking ID and the ab-compare tag are the same. A fee comparison looks exactly like this on screen, and the only column that splits is cost.**

The evidence that the quote was the same

Three fields lined up.

Field Measured
qty_basis.reference_px 85483.05 on all four
the fill's expected_px 85483.1 on all four
the fill's published_at 13:29:40.456 – .457 — inside 4 milliseconds

One published quote filled all four.

That said, quote_source differs by venue — binance versus bybit. That field means "my own venue's feed," not "the same snapshot," so you cannot use it to argue that several venues saw the same quote.

Export — the auto-exit row carries the tracking ID too

Look at the two rows in the order table. The top one is my entry (shot-31), the bottom one is the server's auto-exit (exit-plan:…), and both carry 추적 ID: tool-a-run-42. The net P&L of −0.01524498 attaches to that exit row — if that cell were blank, collecting by tracking ID would not bring the P&L with it. The CSV 내보내기 button on the left is the on-screen version of recipe step ③.

The quota behaved

Four arms = 4 uses. The account ledger snapshot was identical before and after — not a single row moved across all eight account and trade tables. On pro's 1,000 a month, a four-arm comparison runs 250 times.


5. The limits of this combination

① Isolating the rate alone is impossible on our side. This is the most important line in the article.

Change venue and the lot size and minimum notional change with it, so the quantity the server picks changes. Look at the table again: 0.00008 / 0.001 / 0.001 / 0.000074, a notional range of 6.83 to 85.49 USDT — a 13x spread.

Normalized to basis points the comparison stands; in absolute P&L it does not. That is why step ④ of the recipe emits bp. As long as there is no fee field in a trial order, the one field I changed is not the rate but the entire venue profile. Writing it up as "I changed only the fee" would be a lie.

② What arena measures, we still cannot. Part 23's 12→43 was "the model swings more or less after reading the fee sentence." A trial order goes out with the judgment pinned by me — what it measures is whether one fixed round trip survives at each rate.

The two are parallel, not substitutes. Use theirs to pick a model; use this to find out how much fee that pick can take.

③ It is also not the same as our fee comparison. With execution_resimulation: False, that path holds fills fixed and re-prices. All this article adds is that each venue's lot, minimum notional and slippage model are genuinely traversed on a live quote. The price of that is the quantity spread in ①. It is not free.

④ There is no quote snapshot identifier. A BBO has no id, so you cannot assert from the responses alone that these four fills came from one and the same quote. published_at inside 4 milliseconds is the best available, and that is circumstantial, not identification.

⑤ An unfilled arm leaves no record of the quote it saw. All four filled this time, but limit arms will leave some unfilled. The acceptance response carries no separate bid/ask, so you cannot read anywhere which quote that arm saw when it failed to fill. That is a gap we should open.

⑥ Synthetic quotes. The P&L above is not a market fact. What was confirmed is the wiring and the contract, not "Binance takes this much." The rate figures themselves (10bp, 5bp, 5.5bp) come from the venue profiles.

⑦ One correction on the collection path. When this run happened, GET /v1/forward-tests?q=<tracking id> returned 0 results (at the time q did not look at client_tracking_id), and what actually recovered the experiment was the tag. The exact-match ?tracking= filter opened immediately after this run. The recipe in section 3 uses that new contract, but I did not run that code — check for yourself how many results come back the first time you run it.


6. When to use this and when not to

Use it when

Do not use it when

And do not use this comparison to say which venue is better. All the table says is that the same round trip turns into a profit or a loss depending on the rate. Which venue to use depends on much more than the rate, and this experiment does not measure any of it.


Evidence for this article: measurement log · raw data · recipe. Every number above came from that record, and so did the limits in section 5.