← adxTrendMM

adxTrendMM — Fixes

BLOCKEDAudited 2026-09-14· 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.

4 issues found and repaired — 4 defects, 0 warnings.

# Issue Severity Surface
1 It switched itself off the moment the account fell below one order, and the published −56.8% was that floor DEFECT behaviour
2 The trailing stop is arranged to lose — and it is checked so late that it never once ran DEFECT behaviour
3 The side panel's “Round trips” counted only the winners — 89% of them were invisible DEFECT side panel
4 It silenced its own logger every cycle, so LOG_LEVEL could not turn the log back on DEFECT console

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.


It switched itself off the moment the account fell below one order, and the published −56.8% was that floor

Severity DEFECT · Surface behaviour · Sites 2

The strategy decides whether it may buy, and then how much to buy, using two tests that contradict each other.

The first allows a cycle to run while the account still holds half an order's worth of cash. The second, at the point of actually placing the order, refuses to place anything at all unless a whole order's worth is there — and the order is never scaled down to fit what is available.

Every balance between those two levels passes the first test and fails the second. And because the order size can only grow — it starts at the configured trading limit and is raised by a share of realised profit, never lowered — an account that drifts into that band stays there for the rest of the run. The strategy stops trading, permanently, and says nothing about it.

The same contradiction sits on the averaging-down path a few lines above.

Why it matters. This does not degrade a result. It terminates one, and the terminated result is then published as the strategy's performance.

The pre-fix run reports a median −56.8% across seven markets. Six of the seven end holding within 0.25% of exactly one trading limit. Six markets landing within a quarter of a percent of each other is not a market outcome — it is the account arithmetic: capital 231, order size 100, and 231 → 100 is −56.7%.

It also makes trading activity a function of the fee rate, because fees decide how fast the floor is reached. That is precisely what the pack's cost-independence invariant exists to catch, and this strategy fails it: at zero fees it places roughly twice as many orders, worst ratio 0.46 against a 0.50 threshold.

Where the account ended, against one order size of 100

market final equity return orders at 0.10% fee orders at 0% fee ratio
ADA 99.84 −56.8% 507 950 0.53
BTC 99.83 −56.8% 556 1,085 0.51
DOGE 99.89 −56.8% 519 1,119 0.46
ETH 99.96 −56.7% 569 955 0.60
LINK 117.08 −49.3% 477 487 0.98
SOL 99.77 −56.8% 584 798 0.73
XRP 99.87 −56.8% 584 1,152 0.51

LINK is the one market that never reached the floor, and it is also the only market whose order count barely moves when the fees are removed — 0.98 against 0.46–0.73 everywhere else. The two facts are the same fact: a strategy that is not locked out does not care how fast it would have got there.

The fix. Size the order to the cash actually available, and apply the strategy's own half-order threshold at the point of placement so that the two tests agree.

The fraction is taken from the file's own allowance rather than chosen, so the repair implements what the two lines together already described. Applied to both buy paths.

Effect on live behaviour. PENDING_REMEASURE

Scope beyond this strategy. 24 strategies in the pack carry this, found by the audit tooling. Two are measured doing it — this one and donchianBreakoutMM. On the other 22 it is latent at the capital each is calibrated to, and active on any smaller account. Each of the 24 gets the repair inside its own audit with its own measurement, because it changes behaviour.


The trailing stop is arranged to lose — and it is checked so late that it never once ran

Severity DEFECT · Surface behaviour · Sites 1

Two things are wrong with this exit, and the second is why the first never shows up in the result.

It is arranged to lose. The trailing stop arms once the position is up 0.4%, then exits when the price gives back 0.5% from its peak. The give-back is wider than the gain that arms it, so a position that arms and immediately turns is sold 0.102% below where it was bought — before any cost at all — and 0.342% below after fees and slippage on both sides.

And it never runs. The exit rules are checked in a fixed order and this one sits fifth, behind the hard stop, the time limit, and two trend tests that fire constantly at one-minute cadence. Measured over six months across seven markets, it fired zero times out of 3,766 exits.

Why it matters. The header presents this as the strategy's considered profit-taking design: the fixed profit target is switched off by default, with the comment "OFF by default — let winners run", and the trailing stop is what winners are meant to run into.

It is not what happens. 87.6% of exits are the bearish DI cross and 10.3% are the ADX-weakness test; the remaining 2.1% are hard stops. The trailing stop, the fixed target and the time-to-live account for none of them.

So an operator reading the file — or the settings panel, which exposes ADX_TRAIL_ARM and ADX_TRAIL_PCT as live controls — would reasonably believe they are tuning how this strategy takes profit. They are tuning nothing. Both settings are inert at the shipped exit ordering, and the one that would take effect if they ever did is arranged to sell below cost.

Every exit over six months across seven markets, and what the trailing stop would do if it ever fired

exit path count share checked
DI_CROSS_BEARISH — −DI crosses above +DI 3,298 87.6% 3rd
ADX_WEAK — ADX falls below ADX_EXIT 388 10.3% 4th
STOP_LOSS — hard stop at ADX_MAX_DD 80 2.1% 1st
TRAIL_STOP — the trailing stop 0 0% 5th
GAIN_EXIT — fixed target, opt-in and off 0 0% 6th
TTL — time-to-live at 200 cycles 0 0% 2nd

The arithmetic of the unreachable branch, for completeness: it arms at +0.400% and gives back 0.500%, which is −0.102% gross and −0.342% net. To break even it would need a 0.398% trail at the shipped 0.4% arm, or a 0.744% arm at the shipped 0.5% trail. Both are close enough to the shipped values that the settings look plausible, which is how a losing exit rule survives review.

The fix. Not applied. Two separate changes are implied, and neither should be made without its own measurement.

Re-ordering the exits so the trailing stop can be reached would change what the strategy does on essentially every trade, and it would switch on a rule that currently loses by construction — so the ordering and the thresholds have to move together or not at all.

What this audit claims is narrower and provable: at the shipped settings, two of this strategy's six exit paths have never executed, a third is switched off, and the trailing stop would sell below cost if it were ever reached.

Effect on live behaviour. None — nothing was changed.

Scope beyond this strategy. The unreachable-branch half is specific to this file's exit ordering. The shape worth checking elsewhere is the general one: a trailing stop whose give-back is wider than the gain that arms it sells below cost by construction, and it is not visible from either number alone — only from the pair.


The side panel's “Round trips” counted only the winners — 89% of them were invisible

Severity DEFECT · Surface side panel · Sites 6

Every exit path books its outcome on one line, and the round-trip counter is incremented only in the winning branch. The hard stop-loss, which has no winning branch at all because a stop is always a loss, never incremented it either.

The side panel then prints that counter under the label Round trips, with the tooltip: “Number of complete buy-then-sell cycles the strategy has closed. Each round trip is one win or one loss.” The counter and its own tooltip disagree, and the counter is the one that is wrong.

Why it matters. An operator reading the panel would have seen 403 round trips where the engine counted 3,766. Read at face value beside the win/loss row, the panel describes a strategy that closed 403 cycles and won all of them.

This strategy is the sharpest case in the pack precisely because it loses so often: at a 9–13% win rate, 89.3% of its closed round trips left no trace in its own reporting.

Round trips the engine counted, against what the panel would have shown

engine panel invisible
adxTrendMM, 7 markets × 6 months 3,766 403 3,363 (89.3%)
donchianBreakoutMM 748 172 576 (77.0%)
kalmanGrid 113 74 39 (34.5%)
meanRevOscillator 205 200 5 (2.4%)
volRegimeMM / volHarvester 78 77 1 (1.3%)

The gap is exactly the loss count, so it is widest on the strategies that lose most often — which are the ones an operator most needs an accurate panel for.

The fix. Add the increment to the loss branch rather than hoisting the winning one above the test.

The obvious repair is to hoist it so both branches share it, and that is wrong here: meanRevOscillator books its counters inside a break-even-guard check, so hoisting would start counting round trips for sells the guard refused to place. A blocked sell is not a round trip. Putting the new statement directly beside losses++ means every guard the file already applies carries over untouched.

Effect on live behaviour. None — proven, not assumed. roundTrips is display-only in all 22 files; nothing reads it in a trading decision. Verified on volRegimeMM by re-running all seven markets and diffing against the pre-fix record: buys, sells and return identical to the last decimal on every market.

Scope beyond this strategy. 22 strategies carried this; 21 were repaired in the same pass and the last is this one. On meanRevOscillator it is not cosmetic at all — the same counter is the divisor of the reported average holding time.


It silenced its own logger every cycle, so LOG_LEVEL could not turn the log back on

Severity DEFECT · Surface console · Sites 1

One line, unconditional, once per cycle, immediately after the startup check, replaces the strategy's own INFO logger with no-ops. rt.warn and rt.error survive; everything at INFO dies, including every exit message the file contains.

Why it matters. The motivation was real — at 1-minute cadence a line per cycle is 263,000 lines per pair over this window — but the runtime already solves it: it reads the operator's LOG_LEVEL, defaulting to NORMAL, and prints ordinary lines at NORMAL and above and debug lines only at DEBUG.

The gag overrides that choice in the direction the operator cannot undo. Setting LOG_LEVEL to DEBUG in the GUI did nothing at all.

The fix. Gate the existing line on the operator's own setting, so it only silences when they have not asked for logs. The shipped default is still silent, so nothing changes for anyone who has not gone looking.

The matcher was widened at the same time: it now matches the structure of the line — three assignments inside one try/catch, with every run of whitespace optional — rather than a literal, so a fourth bulk edit with different spacing cannot reopen it.

Effect on live behaviour. None at the shipped default, which is still silent. Proven by the byte-identical re-run below.

Scope beyond this strategy. Six files still carried this, and they were missed because a third spelling of the line exists. The fixer matched two literal spellings and reported “0 files to change”, which reads as “all done”.