← fxLadderMM

fxLadderMM — Fixes

BLOCKEDAudited 2026-09-13· hand-written (no shared engine) engine· quantroduction· spot

Every defect the audit found and how it was repaired — the same measurements as the engineering record, without the code.

6 issues found and repaired — 3 defects, 3 warnings.

# Issue Severity Surface
1 Every quote was priced off the previous bar, and 40% of them were refused DEFECT order placement
2 A daily loss limit fed by an all-time drawdown — it stopped trading after the first week and never restarted DEFECT risk control
3 9,311 Order may have failed warnings about orders that were cancelled on purpose WARNING console
4 The maker-fee setting was decorative — it priced nothing DEFECT inputs
5 Twenty-seven panel rows had no explanation WARNING side panel
6 Twenty-seven settings still cannot be set to zero, deliberately NOTE inputs

Every fix was verified by re-running the full backtest across all 7 markets and comparing the result record against the pre-fix baseline. Where a fix is stated to change no behaviour, the two records are identical to the byte.


Every quote was priced off the previous bar, and 40% of them were refused

Severity DEFECT · Surface order placement · Sites 1

The file ends with execute(). That function is declared async and every order inside it is placed with await method.buyLimitPostOnly(...). Calling it without await returns at the first await and defers the rest: the strategy body finishes, the host moves on, and the deferred half places its quotes against a bar that has already passed.

The prices were computed from the mid the strategy read at the top of its cycle. By the time the orders arrived, that mid was gone.

Why it matters. A post-only order that would cross is refused outright, so a stale quote is not merely a worse quote — it is frequently no quote at all. The engine recorded the rejection reasons: on ADA alone, 4,445 buys and 3,519 sells refused as postonly_would_cross, out of 8,845 rejections in total. Across the seven markets the strategy tried to place 66,957 orders that never reached the book.

And the quotes that were accepted were no better founded — they simply happened to still rest at a mid that had moved. A market maker whose entire edge is where it places its quotes relative to the mid was placing them relative to yesterday's.

Orders refused by the exchange, 3 years × 7 markets

before after
Orders rejected 66,957 0
…as postonly_would_cross_buy 31,214 0
…as postonly_would_cross_sell 24,585 0
…as limit_insufficient_asset 11,158 0
Orders that reached the book 65,299 193,232

Confirmed directly rather than inferred: instrumenting the engine caught quotes priced at 0.29317 arriving at a bar whose mid was 0.2903. Spot-checked on five of the other eleven — cloudQuoterMM 1,067 → 0, volSurfaceMM 155 → 0, stablePegMM 60 → 0, and an exact no-op on the two that had no rejections to begin with.

The fix. await execute(). The strategy body already runs inside an async scope in both Gunbot and the backtest engine, so top-level await is legal — the file's own if (!_startup()) return relies on the same thing.

Effect on live behaviour. Changes behaviour, substantially and correctly. The strategy now places roughly three times as many orders because none are refused. It does not make it profitable — see the performance report — but every number measured before this fix was measured on a strategy that was, in effect, quoting one bar late.

Scope beyond this strategy. 12 strategies pack-wide, all of them quoting market makers — churnEngineMM, cloudQuoterMM, fxLadderMM, fxSessionRotatorMM, icebergSequentialMM, inventoryFlywheelMM, quoteFlipperMM, stablePegMM, stollReflectiveMM, tickToggleMM, twinHorizonMM, volSurfaceMM. All twelve are fixed. A further 60 files call a execute() that is not async, which is unaffected.


A daily loss limit fed by an all-time drawdown — it stopped trading after the first week and never restarted

Severity DEFECT · Surface risk control · Sites 1

peakEquity only ever rises, so ddPct: (peakEquity − equity) / peakEquity is a lifetime drawdown. It sets dailyLossLimit, which is cleared once per calendar day, labelled Daily loss limit in the side panel, and sits on the line next to a genuinely daily dailyPnL check.

Those cannot both be true. Once equity was 4% below its all-time high, the daily reset cleared the flag and the very next cycle re-set it, because the lifetime drawdown had not moved.

Why it matters. The strategy stopped trading permanently, and its results looked good because of it. It reported a −4% return with a 4.2% max drawdown on six of seven markets — the profile of a tightly risk-controlled market maker. It was nothing of the sort: it was a strategy that had quit.

This is the most dangerous shape a defect can take in this pack, because the metric that would normally raise the alarm is the one the bug improves.

How long it actually traded, before the fix — 1,157-day window

market last fill active for share of window
ADA day 9 206 bars 0.82%
BTC day 7 158 bars 0.63%
DOGE day 9 204 bars 0.81%
ETH day 5 122 bars 0.48%
LINK day 101 2,412 bars 9.59%
SOL day 5 109 bars 0.43%
XRP day 6 134 bars 0.53%

Six of seven markets: the strategy placed its last order inside the first nine days of a three-year window and never traded again.

The fix. Reset peakEquity in the same block that clears the flag, so MAX_DD_PCT means drawdown from today's peak — consistent with the flag's name, its reset cadence and its panel label.

Effect on live behaviour. Changes behaviour enormously, and makes the numbers far worse — which is the point. Median return goes from −4.0% to −95.9%. The −4% was never a result; it was the guard latching at exactly MAX_DD_PCT and never releasing. What this repair produced is the strategy's actual performance, which had been hidden behind a stuck switch.

Scope beyond this strategy. Found here. The pattern — a lifetime high-water mark feeding a flag that is cleared daily — is worth checking on every strategy in the pack that keeps a peakEquity.


9,311 Order may have failed warnings about orders that were cancelled on purpose

Severity WARNING · Surface console · Sites 2

rt.checkFills() can see only two things: how old a tracked order is, and how many orders are currently open. After ten cycles it calls an order FILLED if nothing is open, and MISSED — with an Order may have failed warning — if something is. It has no way to know the strategy cancelled the order itself.

A quoting engine cancels and re-places its whole ladder every cycle, so there is always something open and every tracked order eventually trips the warning.

Why it matters. A warning that fires during completely normal operation is worse than no warning, because it trains the operator to ignore the one that matters. On the sell side alone this produced 9,311 of them over the window.

The fix. Added forgetPending(side?) to quantroduction-runtime, which drops pending orders the strategy has deliberately cancelled, and called it at both of this strategy's cancel sites. Purely additive — a strategy that never calls it behaves exactly as before. The file's fallback rt shim carries a no-op version so a missing module cannot reintroduce the noise.

Effect on live behaviour. None on trading. Log-volume only.

Scope beyond this strategy. The new rt.forgetPending() is added to the shared quantroduction-runtime module, so all 79 strategies of that lineage can use it. Only fxLadderMM calls it so far; every cancel-and-re-quote strategy in the pack should.


The maker-fee setting was decorative — it priced nothing

Severity DEFECT · Surface inputs · Sites 1

FL_MAKER_REBATE was read into P and used for exactly one thing — adding an imaginary credit to makerRebatesEst for the side panel. It never touched a quote. The ladder quoted the same 0.5 / 1.5 / 2.5-basis-point tiers whatever the venue charged, and the panel accrued a rebate the account was not receiving.

Why it matters. At the shipped −0.015% rebate the tiers are fine. On a book charging 0.10% maker — the ordinary spot tier on Binance and Bybit, and what this pack's engine models — a round trip at the widest default tier grosses 0.05% against 0.20% of fees. Every completed round trip loses about 0.15% by construction, and no amount of signal can repair that.

The fix. Skip any tier whose one-way depth does not exceed the one-way maker cost, and say so once rather than silently quoting a thinner ladder. Negative cost is a rebate and clears every tier, so the shipped default is unchanged.

Effect on live behaviour. None at the shipped default. Setting FL_MAKER_REBATE to a real 0.10% moves the median return by 0.04 points — because the volatility widening already pushes effective depths well past 10 bps on crypto, so the gate rarely binds there. It binds on the FX pairs the strategy is actually written for, where vol is a tenth of this.

Scope beyond this strategy. Same family as the MM_fee defect repaired across the fifteen mm-dca strategies: a fee setting exposed in the GUI that never reaches the price.


Twenty-seven panel rows had no explanation

Severity WARNING · Surface side panel · Sites 20

27 of 29 data rows rendered with a value and no tooltip.

Why it matters. This strategy's panel is dense with terms that are not self-explanatory even to an experienced operator — Pip size, Base depth, Tier step, r-shift. A number with no explanation is a number nobody can check.

The fix. Filled 20 from the shared label glossary. Seven remain — Pip size, Tiers, Base depth, Tier step and three others — because the glossary refuses to invent text for a label it does not know, which is the correct behaviour and is why the count is not zero.

Effect on live behaviour. None — presentation only.

Scope beyond this strategy. 1,433 rows across 392 distinct labels pack-wide.


Twenty-seven settings still cannot be set to zero, deliberately

Severity NOTE · Surface inputs

Every tunable was read through a fallback idiom meaning use the operator's value, or the baked default., so a setting of 0 silently becomes the default.

Why it matters. The fixer skips any file where an affected knob is used as a divisor, and this file divides by PIP_BPS, weightSum and NUM_TIERS. Repairing the pattern blindly would let FL_PIP_BPS: 0 turn a working strategy into a division by zero — a worse defect than the one being fixed.

The fix. Not applied. Repairing this file needs per-knob treatment: honour zero for the knobs where zero means off, and range-check the three divisors rather than letting the the fallback accident guard them. Recorded here so it is not mistaken for an oversight.

Effect on live behaviour. None — nothing was changed.

Scope beyond this strategy. The fixer repaired 1,743 sites across 70 files pack-wide. It skipped this one.