Commit graph Warrior_EA/Expert
Author SHA1 Message Date
AnimateDread
94628ba7a9 feat(risk): make the tight stop rungs falsifiable - break-even slippage per hit
The stop frontier on all three charts says a TIGHT stop improves the ATR book: EURUSD -0.125 ->
-0.075, USDJPY -0.055 -> -0.014, NAS100 +0.040 -> +0.121, all at a 0.50 ATR stop. That is the
opposite of the mechanism it was built to test - it is cutting LOSERS early, not letting winners run
- and it rests entirely on the one thing the report does not model.

At an 80-85% hit rate, slippage lands on five of every six trades. A caveat is not good enough for a
number that would otherwise look like the best result of the day, so it is now falsifiable:

    BEslip = advantage over no-stop / hit rate

the slippage PER HIT that erases a rung entire advantage. A rung whose whole advantage is 0.05 ATR
at an 84% hit rate dies at 0.06 ATR of slippage, which on an H1 stop-out is an ordinary number
rather than a pessimistic one. If BEslip comes back smaller than a realistic slip, the rung is an
artefact of the frictionless assumption and must be read as one.

This is the same discipline the cost-split and horizon reports were held to: state the assumption
that is doing the work, then print the number at which it breaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:23:24 -04:00
AnimateDread
01c3a1f2d8 feat(risk): measure what the STOP is costing - the exact counterfactual, in R
Operator asked whether the NN magnitude signal can drive dynamic SL/TP to stop cutting winners
early. Two prior findings shape the answer, and both are respected here rather than re-litigated:

  * DIRECTION IS CLOSED, but excursion SIZE clears at 3.9-5.4x its null on three instruments. So
    magnitude is the right thing to lean on - for RISK CONTROL, which is what the excursion verdict
    already says it is good for.
  * THE RATIO IS EV-NEUTRAL for a driftless walk (reaching +m before -k has probability k/(k+m),
    which IS break-even). Geometry cannot manufacture alpha, so nothing here claims it does.

What CAN be true is the operator premise: with Exit_On_Leg_Flip and NO take-profit, the stop is the
ONLY thing that can end a trade early, so a stop inside the ride typical adverse excursion silently
converts winners into losses. Nothing in the era report could see that - g_ensVoteUpR/DnR are
excursions over a FIXED horizon, not over the ride.

So the missing measurement is what each ride had to SURVIVE. LegRideLabel already walks every bar
between entry and the flip to find it; it now also tracks the worst adverse excursion on that walk
(lowest low for a long, highest high for a short, from the entry close, in ATR at entry) at one
comparison per bar and no extra pass. Cached per bar, carried onto each scored vote row.

The STOP FRONTIER then reports, at the certified rung: the book with NO stop at all, and the book
with a stop at each of 9 distances, with hit rate. THE COUNTERFACTUAL IS EXACT, not modelled - a
stop at S fired iff the measured ride MAE >= S.

REPORTED IN R, NOT ATR, and that is the point. A wider stop means a proportionally SMALLER position
at the same risk percentage, so comparing stop distances in ATR compares trades of different size
and always flatters the widest. Dividing by the stop distance is what the account actually
experiences.

Slippage is not modelled - a hit is priced at exactly -S - so the TIGHT rungs are flattered, and the
report says so rather than hiding it.

ALSO CONFIRMED, NOT BUILT: the operator second request (reject a trade when even the smallest lot
breaches the risk threshold) ALREADY EXISTS and is correct - MoneyRiskBase.mqh:147, "BELOW-MINIMUM
MEANS NO TRADE, NOT A BIGGER TRADE", refusing rather than letting TCNormalizeVolume round a
risk-derived 0.05 up to a 0.10 minimum and double the intended risk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:33:11 -04:00
AnimateDread
af59b41820 fix(features): a Wyckoff sentinel was 1137x wider than every other column
FEATURE HEALTH now reports SCALE, and the first thing it found was severe:

    median column range 2, widest/median 1137x
    wyckoffEvent[4]=2274 (x1137)   wyckoffEvent[5]=2265 (x1133)

ZoneTop and ZoneBottom ARE ATR-normalised - the defect is that the indicator reports "no active
zone" as 0, and `(0 - close) / atr` then evaluates to -close/atr. That ratio is scale-free, so it is
about -580 on EURUSD and -580 on NAS100 alike, injected into a vector where every other column lives
in +/-2.

WHY IT MATTERS MORE THAN A COSMETIC OUTLIER:

  * It owns the COVARIANCE MATRIX. This is the entire explanation for the redundancy report claiming
    "1 of 92 columns carry 95% of the variance (top component alone 100%)". The data is not
    one-dimensional; one column is three orders of magnitude wider than the rest. Every PCA or
    correlation-prune decision taken on that report would have been taken on an artefact.
  * It owns the FIRST LAYER GRADIENTS. A column that much larger dominates every weight update, so
    the network was learning mostly from a sentinel and drowning the other 90 columns.

Same defect class as CArrayDouble::At returning DBL_MAX for a negative index, found earlier today: an
out-of-band marker that arithmetic consumes without complaint. Guarded via WarriorZoneDistAtr, which
returns a NEUTRAL 0 for an absent level - the honest encoding of "there is no zone here" - and also
rejects EMPTY_VALUE and non-finite input, because an indicator with no data must not be read as a
price of zero. Width contract unchanged.

The SCALE half of FEATURE HEALTH is the durable part. The report has always named CONSTANT and
mostly-zero columns and never said how WIDE they are, so a scale outlier was invisible to it while
being the most damaging thing a column can be. It now prints the median range, the widest/median
ratio and the five widest columns by name, and says plainly that columns must be comparable in
magnitude before PCA or a correlation prune means anything.

Still outstanding from the same report: ichimoku[3,4,6] at 17-21x the median. Plausible for real
(close-cloud)/atr on small-ATR bars rather than a sentinel, so it is left measured and unpatched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:12:01 -04:00
AnimateDread
d2574f4c3a feat(label): measure the horizon frontier - and it says the horizon is NOT the lever
The label horizon has been called "the only lever that raises evidence" for weeks and was never
measured. This measures it, from PRICE and the leg replica alone - no network, no training, no era.

What it computes per ZigZag depth: legs (which IS the effective sample size, because every bar
inside a leg shares that leg outcome), mean leg life (= the label overlap), the ORACLE ride a
perfect caller takes, the round-turn cost over the same window, the break-even capture, and the
total at 2/5/10% capture.

EURUSD, 60k bars:

  depth  4: legs 8994 life  6.7 oracle 2.055 BE-capture 1.62% @2%=+71  @5%=+625 @10%=+1549
  depth  6: legs 6780 life  8.8 oracle 2.556 BE-capture 1.33% @2%=+116 @5%=+635 @10%=+1502
  depth  8: legs 5330 life 11.3 oracle 3.046 BE-capture 1.13% @2%=+141 @5%=+628 @10%=+1440
  depth 12: legs 3763 life 15.9 oracle 3.865 BE-capture 0.90% @2%=+160 @5%=+597 @10%=+1324
  depth 16: legs 2887 life 20.8 oracle 4.581 BE-capture 0.75% @2%=+165 @5%=+562 @10%=+1223
  depth 24: legs 1662 life 36.1 oracle 6.347 BE-capture 0.52% @2%=+156 @5%=+473 @10%=+1000

THREE RESULTS, TWO OF WHICH KILL MY OWN FRAMING:

1. Cost never binds. Break-even capture is 0.39-1.62% at every horizon, against rides of 2-8 ATR.
   The "shorter horizon sells payoff to buy evidence" trade-off this report was designed around
   barely exists.
2. At the ~2% capture this fleet has demonstrated, the horizon is nearly FLAT: +141 to +165 across
   depths 8-36, with the SHIPPED depth 12 already within 3% of the peak. Shortening to depth 6 makes
   it WORSE (+116). The horizon is not the lever.
3. Capture rate is first-order - 2% -> 5% roughly quadruples the total at any depth - and shorter
   horizons only win once capture is high (at 10%, depth 4 is worth 2x depth 16).

The first cut of this report ranked by the ORACLE and therefore picked depth 4 on every chart, which
is simply wrong: it credits a horizon with money no model here has ever taken. Ranking now uses the
demonstrated capture, and the capture columns are the ones to read.

Also fixed while here: LEG_STATE_DEPTH was NOT in the model fingerprint, though it sets where every
pivot falls and therefore which leg each bar belongs to, its direction, its ride, and whether it is
Buy/Sell/Neutral at all. Two models at depth 12 and depth 6 train on completely different targets and
were sharing a key. Now TGT:LEG1:<minride>:D<depth>. The ZigZag replica takes depth/deviation/backstep
as arguments defaulting to the shipped #defines, so every existing caller - including the in-situ
verification against the stock indicator - is byte-identical.

WHERE THIS POINTS: capacity is dominated by WIDTH, not horizon. Depth 12 gives 3763 independent legs;
the mask-off change took inputs 180 -> 552, so the first dense layer is ~17,600 weights against 3,763
observations. Halving the horizon buys 1.8x observations; tripling the width already spent 3x. The fix
is the redundancy filter - correlation between INPUTS, no label involved - which is designed in
project_pca_reduction_plan and never built: measured worst pair |r|=0.999, PCA takes obs/param 0.49 ->
11.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:58:42 -04:00
AnimateDread
2a82b0bd9e fix(ichimoku): read the Senkou spans RAW - stop depending on the wrapper shifted buffers
CiIchimoku serves Tenkan and Kijun (buffers 0/1, no offset) perfectly and always has. The two SPAN
buffers carry Offset(kijun), which makes CIndicatorBuffer::Refresh issue
CopyBuffer(handle, num, -m_offset, m_size, ...) - a NEGATIVE start_pos - and THAT is the only path
that has ever failed here. Six hypotheses died to measurement before this was accepted as the fix:

  stdlib cannot serve the buffers        - false, a probe read them fine
  full-history count at negative start   - false, returns 179048 err=0
  a second Create corrupts the buffers   - false, after 2 Creates spanA still ok
  primer/tuner parameter mismatch        - false, tunedKijun=26 matched the primer
  the primer was missing                 - false, it ran and the spans were still EMPTY
  buffer starvation from re-init         - live state was Total=10 spanA(0)=EMPTY

The live object fails in a way a constructed one will not reproduce, and I could not pin it. So the
dependency is removed instead of diagnosed further: the spans are copied ONCE PER ERA from the raw
handle at start_pos 0 - measured reliable on all three charts, 179022 values, err=0 - into our own
series-indexed arrays, and the kijun shift is applied by the READER. Same semantics the wrapper
Offset(kijun) provided, now stated where it can be read instead of inside a base class.

The raw handle is created with the SAME TUNED parameters as the wrapper, because a different triple
is a different indicator instance - reading 9/26/52 does nothing for a chart whose tuner moved to
9/30/52 - and the old handle is released first so ReInitADIndicators cannot leak one per call.

VERIFIED LIVE at the full 552-input width: 0 rejections, 0 stalls, all three charts training
(EURUSD era 14 in 18s, USDJPY era 3 in 11s, NAS100 era 6 in 10s), combined vote scoring at era 13.

Also: compile staging now excludes .venv/.git/docs/pdb/obj, which cuts the per-compile copy from
505 MB to 7.4 MB. It does NOT speed the compile - MetaEditor genuinely takes ~83s on this codebase -
it just stops copying a 386 MB Python virtualenv into the terminal folder every time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:41:07 -04:00
AnimateDread
9550f94997 fix(ichimoku): the throwaway probe turned out to be the fix - it is now a deliberate primer
The operator was right that Ichimoku is a stdlib indicator like every other, and it is. Three
hypotheses of mine died to measurement today:

  1. "MT5 will not serve the Senkou buffers"  - false; the wrapper reads them fine
  2. "the full-history count at a negative start_pos fails" - false; returns 179048, err=0
  3. "a second Create corrupts the buffers"    - false; after 2 Creates, Total=10, spanA still ok

What actually correlates with the fix is the one thing the probe started DOING rather than
reporting. While it copied 64 values the feature path still failed every bar with spanA/spanB
EMPTY and no chart completed an era. The moment it began issuing FULL-HISTORY CopyBuffer calls on
buffers 2 and 3 at init, the feature path started working on all three charts - 0 rejections, 0
stalls, eras in 21-34s, combined vote scoring - with no other functional change between those two
builds.

The reading: the Senkou plots are shifted kijun bars FORWARD, and that shifted region needs one
full-range materialisation before CIndicatorBuffer::Refresh - which asks at start_pos = -offset for
m_size values - will serve it. Priming costs two CopyBuffer calls per init. Not priming cost the
fleet hours across two days.

CAUSALITY IS INFERRED FROM SEQUENCE, NOT PROVEN BY ISOLATION, and the header says so. It is renamed
from WarriorProbeIchimokuBuffers to WarriorPrimeIchimokuBuffers and marked DO NOT DELETE AS
SCAFFOLDING, because I was one turn away from removing it as spent diagnostics - which would have
re-broken the fleet and left no trace of why. Buffer 3 is primed alongside buffer 2 even though
nothing reports on it: the feature block reads SenkouSpanB every bar exactly as it reads SenkouSpanA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 11:06:37 -04:00
AnimateDread
ddb9fa0897 fix(features): the Ichimoku failure branch is PER BAR - a CopyBuffer there stalled the fleet
I put a raw CopyBuffer plus a ten-argument StringFormat on the Ichimoku EMPTY-read branch to
capture the one number that would have prevented an earlier misdiagnosis. That branch runs once per
FAILING BAR, and transient rejections are entirely normal during a cold pass-1 scan, so it became a
terminal API call per bar.

Measured cost, on a fleet that had been healthy minutes before:

    pass 1 throughput   ~15,000 bars/s  ->  ~1 bar/s
    EURUSD LSTM         bar 1536 of 179042 after 1180s
    USDJPY CONV         bar 1024 of 178952 after 1277s
    combined-vote eras scored in 20 minutes: ZERO

Every chart stopped completing eras and the ensemble never scored, so nothing could be certified.
The branch was invisible before this only because the feature had been disabled - re-enabling
Ichimoku is what exposed a cost that had been sitting on a path nobody was walking.

The raw-handle comparison it was meant to capture already lives in the one-shot init probe, which
answers the same question once per start instead of once per rejected bar.

THE RULE: a diagnostic belongs where the diagnosis is READ, not where the failure is DETECTED. A
rejection path in a per-bar loop is a hot path, and it does not stop being one because the feature
that walks it happens to be off today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:40:16 -04:00
AnimateDread
b724fc58a2 fix(ichimoku): back ON - the stdlib was never the problem, the double-shift was
I switched Ichimoku off earlier today on the reasoning that "MT5 will not reliably serve the Senkou
buffers", from the symptom that spanA/spanB read EMPTY at every index while tenkan/kijun read fine
on the same bar. That conclusion was wrong. The operator pushed back - it is a stdlib indicator like
every other - and an init-time probe settled it by measuring the thing I had only inferred:

    wrapper: BufferResize(39502)=ok BarsCalculated=39502
             at idx 5 -> tenkan=ok kijun=ok spanA=ok spanB=ok
    raw:     spanA CopyBuffer(start=0)=64      err=0
             spanA CopyBuffer(start=-26)=64    err=0    spanA[0]=29432.895

On all three charts. CIndicatorBuffer::Refresh reads shifted buffers with
`CopyBuffer(handle, num, -m_offset, m_size, m_data)`, and that NEGATIVE start_pos is deliberate and
works - it is how the forward-plotted cloud region is addressed. The wrapper is coherent and the
indicator serves data.

What was actually broken is the double-shift fixed in 5cd93bf: CiIchimoku::Initialize already applies
Offset(kijun_sen) to buffers 2 and 3, and the feature block added kijunShift a SECOND time, reading
raw index idx + 2*kijun. Re-enabled to test whether that alone accounted for it, and it does -
since the re-enable: ZERO Ichimoku rejections, zero feature rejections of any kind, zero stalls, and
both EURUSD and USDJPY completed era 0 with the block live. Width back to 180 (6 bars x 30),
FEATURE_MASK_VERSION 10 -> 11.

THE DIAGNOSTIC GAP THAT CAUSED THE WRONG CALL, now closed. CDoubleBuffer::At returns EMPTY_VALUE for
any index >= m_data_total, so "EMPTY at every index" is equally consistent with a bad handle and with
a short buffer - and I never measured which. The failure path now prints a raw CopyBuffer result and
the TUNED kijun beside the EMPTY flags, so the number that separates those two cases is in the log
rather than inferred a second time. A temporary one-shot init probe (WarriorProbeIchimokuBuffers,
marked for deletion) prints the same comparison at startup.

The lesson is not subtle: when a symptom is consistent with two causes and one of them is "the
standard library is broken", measure before choosing that one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:14:13 -04:00
AnimateDread
58982c8651 fix(measure): a zero-cost instrument has no cost axis to split on
Indices carry no commission, so costPx is 0, every row costR is 0, the median is 0, and
`costR <= median` is true for EVERY row - the whole population lands in the cheap half and the
other reports n=0. Measured on NAS100: "CHEAP n=595, DEAR n=0", with the cost multiple printing
0.00x because a book divided by a zero cost is undefined, not infinitely profitable.

Refusing to form the split is what makes the silence honest. A degenerate split that still prints
two halves invites the reading that one half "wins", when one half is the entire sample. The test
is the TOP of the distribution: if the dearest row still costs nothing, there is nothing to
compare.

Found because the fleet result came in and the NAS100 lines were unreadable next to EURUSD and
USDJPY. The measurement itself has now closed its own question - the two charts that CAN be split
disagree in sign - but the guard matters for every future cost-denominated statistic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 09:06:48 -04:00
AnimateDread
97e55c3879 fix(pool): the reader kept both defects the writer was repaired for
151f2bc fixed CTrainPool::Add() - it capped a reserve that was `16384 * m_width` and it checked the
ArrayResize return. CTrainPoolReader::Take(), forty lines away, still had both, and it is the side
that runs the moment a SECOND chart publishes a matching pool.

THE RESERVE. `16384 * m_width` is a reserve of 16384 ROWS, so a wider feature vector bought a
proportionally larger allocation for no benefit: 23.6 MB at the current width of 180, per reader,
per member, per chart. The reserve is an allocation hint and has no business being a function of the
row width. Capped in ELEMENTS at 65536, the same constant and the same reasoning as the writer.

THE UNCHECKED RESIZE, which is the serious half. ArrayResize returns -1 on failure and the loop then
wrote at m_rowCount * m_width regardless. An array-out-of-range is FATAL in MQL5 and REMOVES THE
EXPERT FROM THE CHART - which is exactly the failure that left five charts idle for 77 minutes
through two terminal restarts, invisible because fleet expansion only ever asked whether a chart
existed. The writer was hardened against it and the reader was left behind.

ON FAILURE THE ROW IS STILL CONSUMED, and this is the part worth reading twice. Adopt() advances the
file by exactly one record per iteration and relies on Take() having read m_width doubles; the
else-branch beside the call already reads them explicitly "to stay on the record boundary". Returning
early without reading would desynchronise every subsequent row in the file - turning an allocation
failure into silently corrupt training data adopted from a peer. Skipping a sample is affordable;
losing the record boundary is not.

Found while tracing why the terminal commits 32.6 GB of address space against a 2.0 GB working set.
This is NOT that 30 GB - the pool is capped at TRAINPOOL_MAX_ROWS 60000, which is 86 MB at width
180 - and the over-commit source is still open. It is a real crash path on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 08:45:31 -04:00
AnimateDread
827b7c59f7 feat(measure): state the cost-split book in money, exactly, by letting the ATR cancel
costAtr is built as costPx / atrBar, where costPx = WarriorRoundTurnCostPriceByClass() is a
per-SYMBOL CONSTANT (commission) and only atrBar varies. The ride is measured in that same
atrBar. So the per-row ratio ride / costR has the ATR cancel outright:

    sum(ride in money) / sum(cost in money)
  = costPx * sum(ride/costR) / (costPx * n)
  = mean(ride / costR)

which is exact, needs no per-row ATR array, and answers the most decision-relevant question this
gate can pose: HOW MANY TIMES ITS OWN COST DOES THE AVERAGE CALL EARN. Subtract one for the net
multiple.

It exists because the first cut of this report could be answered with "ATR is doing the work".
Comparing two ATR-normalised halves invites exactly that objection, since a quiet bar's +0.250 ATR
is a smaller cash move than a loud bar's. This statistic prices that difference instead of hiding
it, and it is not a normalisation of the money answer - it IS the money answer.

The same identity settles what the halves are. With costPx constant, costR varies ONLY through the
bar's ATR, so "cheap half" is EXACTLY "high-volatility half". That reading was an interpretation
when the split shipped; it is an identity now.

Guarded on costR > 0 rather than >= 0, because this term divides by it and a zero-cost row would
enter as an infinite multiple rather than as a free trade.

Report-only: no feature, width, or fingerprint change, so it forces no retrain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 08:34:40 -04:00
AnimateDread
5cd93bfb7b fix(ichimoku): the stdlib already applies the cloud shift - we were applying it twice
CiIchimoku::Initialize sets Offset(kijun_sen) on the two Senkou buffers (Include\Indicators\
Trend.mqh:678-680), and CIndicatorBuffer::At(i) returns CDoubleBuffer::At(i + m_offset). So
SenkouSpanA(idx) ALREADY means "the cloud as plotted at bar idx" - raw buffer index idx+kijun
holds (Tenkan+Kijun)/2 for a bar kijun back, which is exactly the cloud edge visible at idx.

The feature block passed idx + kijunShift on top of that, reading raw index idx + 2*kijun. Every
cloud column was therefore lagged one full kijun past what its own comment claimed, and the
oldest 2*kijun bars fell off the end of a buffer sized to barIndex + kijun. Not a lookahead - the
values were merely stale - but the block was not measuring the quantity it named, which means the
keep-screen's Ichimoku vote (7/8 and 3/8) was cast on a feature nobody had specified.

The projected cloud is now computed from its definition rather than read from a buffer. Senkou
Span A is (Tenkan + Kijun) / 2 by construction, so the span that will be drawn kijun bars ahead
is available from the two lines already read AT idx - no buffer access, no lookahead. That matters
because the buffer read it replaces would have been SenkouSpanA(idx - kijunShift), and a NEGATIVE
index is the trap on the other side of this one: only CDoubleBuffer::At guards
index >= m_data_total and returns EMPTY_VALUE, while a negative index falls through to
CArrayDouble::At, which returns DBL_MAX - straight past every == EMPTY_VALUE guard and into a
feature. Span B has no closed form from the values in hand, so the pair-thickness "twist" is not
reconstructed; the near edge's forward slope carries the same regime information at the same
width, and the block stays 8 columns wide.

FEATURE_MASK_VERSION 8 -> 9 with the kept set UNCHANGED. The bump is the point: the fingerprint is
what stops a resumed model from loading weights fitted to the old semantics, and column count
alone cannot tell the two masks apart - the exact case that knob's own comment was written for.

NOT the cause of today's fleet stall, and the log rules it out: USDJPY ran 291 eras today on this
same code. The stall was Ichimoku buffers never becoming readable on the two charts that had been
REVIVED via ChartApplyTemplate, and a clean terminal restart cleared it. Two separate problems
that happened to name the same indicator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 08:28:43 -04:00
AnimateDread
c7ed4df786 feat(measure): split the book by what the bar COSTS - the one asymmetry direction cannot close
Commission is fixed in PRICE. The book is measured in ATR. So cost-in-ATR is inversely
proportional to the bar's own volatility, and the same contractual fee is a smaller fraction of
the move on a high-sigma bar. If the ATR-normalised edge is roughly sigma-invariant, then
restricting the book to high-sigma bars raises the edge-to-cost ratio WITHOUT predicting
direction any better than we do now.

That matters because of what this project has already measured. Excursion SIZE is the one
quantity that is genuinely predictable here - RANGE clears at ~4x its null on three instruments
with a working positive control, and the LSTM member scores +8.6% to +11.5% disjoint Brier skill
at 3.46-4.58 sigma against a trailing-quantile incumbent. DIRECTION is closed
(project_direction_closed_verdict, best-of-999 p=1.0000). A lever that monetises predictable
sigma is therefore the only lever our own evidence supports, and this is the cheapest possible
test of it: a split of rows we already collect, costing one pre-pass and six arrays.

The rows are cut at the MEDIAN measured per-bar cost, not at a fixed ATR threshold - the cost
distribution spans three orders of magnitude across the asset classes this fleet trades, so any
absolute cut would put one instrument entirely on one side. Each half carries its own cost into
its own accumulator and is judged NET OF THE COST IT WOULD ACTUALLY HAVE PAID; comparing both
halves against one pooled cost is what would make the cheap half look profitable for free.
Unpriced rows (-1) are excluded from the median and from both halves, on the same reasoning as
the sweep's existing >= 0 guard: binning an unpriced bar as cheap is the error that guard exists
to prevent.

MEASURED AND NOT GATED, on the same discipline the split-half report was held to. It prints at
the certified rung on every era. A positive cheap-minus-dear gap sustained across the fleet makes
a sigma filter worth building; a gap that is noise around zero closes the question. Replacing an
unreachable bar with an unmeasured one is exactly how the precision gate closed, and this does
not repeat that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 08:13:49 -04:00
AnimateDread
08591e9c4e fix(fleet): three charts, because the terminal was committing 29.5 GB of address space
WHY VS CODE KEPT CRASHING, and why the terminal died at 03:51 - one cause:

  terminal64   working set 2,222 MB   COMMIT 29,509 MB

Against a commit limit of ~47 GB (32 GB RAM + a 15.4 GB pagefile) on a box also running
six 1 GB tester agents and fourteen VS Code processes. At roughly 76% committed, every new
allocation starts failing. That is the bad_alloc that killed the terminal inside
WarriorCPU.dll (70a3c9a), the "fork: Resource temporarily unavailable" in bash, and VS
Code's helper processes dying with 0xC0000142 DLL_INIT_FAILED. VS Code was never the
problem; it was downstream of this.

THE COMMIT IS FLAT AND RESERVED UP FRONT - measured 29,510 MB twice, sixty seconds apart,
delta 1 MB. So it is not a leak, it is ~3 GB per chart claimed at startup. And it was
measured AFTER the width had already been halved to 180 inputs, so per-chart commit is
close to fixed and cutting features does not reclaim it. Cutting CHARTS does.

WHICH THREE, decided on effN rather than on preference. Independent observations run
3,300-3,650 on the FX majors against 868-1,231 on the indices - three to four times the
evidence, and effN is the binding constraint on every gate here. EURUSD is the richest,
USDJPY is a different pair, and NAS100 is kept despite the thinnest sample because it
carries the best measured alpha on the fleet (+0.707) and is an index, so the training pool
is not three correlated copies of one trade.

The other seven charts are backed up whole under
Profiles\Charts\_Default_10chart_backup_20260904; restoring them is a file copy plus
putting their symbols back on FLEET_EXPANSION_SYMBOLS. The list is cut too, or the revive
sweep added in c6d001d would faithfully re-open every chart this removes.

The six tester agents are NOT strays - they are MT5's configured local agent pool and
respawn when killed. That ~6 GB is the operator's to reclaim in Tools > Options > Agents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 07:06:15 -04:00
AnimateDread
0853f09dd9 fix(fleet): the expansion list named the four charts it ADDED, not the fleet
FLEET_EXPANSION_SYMBOLS was "BTCUSD,GBPUSD,NAS100,DAX40" - the four the list was created
to open. Correct while opening was its only job, because the original six were already
there. It is now also the list the REVIVE sweep walks, and a symbol missing from it cannot
be revived.

Measured tonight, on the very first sweep: BTCUSD, GBPUSD and NAS100 came back; SP500,
XAUUSD and XTIUSD stayed dead, purely because they were not named. Same failure, same
minute, opposite outcome, decided by an omission nobody would have looked at.

The list now names all ten. It means "the fleet", which is what its own header already
implied - "adding an instrument is a reviewable list in source control".

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:18:50 -04:00
AnimateDread
c6d001d94f fix(fleet): a chart whose EA was killed is invisible to fleet expansion - it now revives it
Five of ten charts sat idle for over an hour tonight with no missing chart and no error to
see. Sequence: an unchecked ArrayResize overran (fixed in 151f2bc), MQL5 treats that as
FATAL and the terminal REMOVES the expert from the chart, and nothing ever put it back.

WHY NOTHING NOTICED. Fleet expansion's whole test was "does a chart on this symbol and
period exist" - and it did. A chart with a dead EA is indistinguishable from a healthy one
by that test, so all five counted as "already present" and were skipped. Two terminal
restarts changed nothing either: a restart restores what the chart CARRIES, and the chart
no longer carried an expert. The only visible symptom was silence in a log that is never
silent.

THE FIX IS A HEARTBEAT. Every EA stamps TimeCurrent() under its own symbol and period on
each timer tick; the sweep reads those stamps and re-applies the fleet template to any
chart that exists with a stale one. ChartSaveTemplate already captures the EA and its
inputs, so the revive path is the open path minus ChartOpen.

THIRTY MINUTES OF STALENESS, not five. Training runs on this same thread and a long era
genuinely starves the timer for minutes (project_era_slower_than_bar), so a short threshold
would re-template a chart that is merely busy and kill the training it exists to protect.
Thirty minutes is far longer than any era and far shorter than the hour-plus a dead chart
went unnoticed.

AND THE SWEEP HAD TO STOP BEING A ONE-SHOT. It ran once per EA instance, correct while its
only job was opening missing charts at start-up. Reviving is not a start-up task, so it now
runs on a 10-minute throttle. Both halves are idempotent - an existing chart is not
re-opened, a live EA is not re-templated.

THE LEASE IS NOW RE-ACQUIRABLE. It is still taken atomically with SetOnCondition, but it
expires: a permanent lock would mean the first instance to win it owned the revive duty
forever, including after that instance died - which is precisely the failure being fixed.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:15:04 -04:00
AnimateDread
151f2bc1eb fix(pool+vote): unchecked ArrayResize wrote out of range once the feature width tripled
Nine live 'array out of range' errors within minutes of the width going 72 -> 288:
TrainingPool.mqh (298,32) on five charts, ExpertSignalAIBase.mqh (3254,24) on a sixth.

BOTH ARE THE SAME DEFECT AND NEITHER IS NEW - the wider vector only made them reachable.
Each site calls ArrayResize and then writes at the index it asked for, without checking
that the resize succeeded. ArrayResize returns -1 on failure; the write then lands past
the end of an array that never grew.

TrainingPool also asked for an absurd reserve. The hint was 16384 * m_width, i.e. sixteen
thousand ROWS - 196k doubles at a width of 12, and 4.7 million (38 MB) at 288, per pool,
on a 2013 Xeon running forty members. A reserve proportional to row width turns a wider
feature vector into a quadratically larger allocation for nothing. It is now a flat 65536
ELEMENTS, and the result is checked: a pool that cannot grow refuses the row.

The ensemble vote buffer checks the two arrays that actually bound the write - they are
resized in one block, so any failure in it surfaces there - and drops the bar with one
throttled line rather than corrupting memory. Losing a bar reads as low coverage; the
alternative reads as anything at all.

Found because the restored Wyckoff groups took the width to 288, which is the point of a
measurement pass: it exercises the code at a size nothing had run at before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:10:24 -04:00
AnimateDread
8866ff7c3b feat(features): restore RSI, MACD, Ichimoku and the five Wyckoff groups for re-measurement
Reverts 6974fb0 (2026-08-24), which removed all eight on closed verdicts: the three
oscillators measured at chance as entries, and the Wyckoff family returned zero
out-of-sample on five independent instruments.

WHY REOPEN A CLOSED VERDICT. Those measurements were taken under the OLD label. The label
is now the leg-ride on H1, and the keep-screen can only ever report on columns that are
actually EMITTED - so the only way to find out whether they carry anything under the
current target is to emit them. Exactly the reasoning that put the alt block back in mask
v3. If they are dead under this label too, the screen will say so and they go again, this
time with a verdict that describes the label being traded.

THE REVERT DID NOT APPLY CLEANLY - 11 conflicts across 6 files, because the tree has moved
a long way since August (the ditch campaign, the layers cut, the leg-ride label,
FeatureBuilder becoming a real collaborator). Every conflict resolved as KEEP BOTH, with
three that needed care:

  * THE FINGERPRINT. HEAD's composition is kept ENTIRELY - the old side wanted to restore
    TGT:META1 and the fractal-target token, which would have re-keyed every model on disk
    to a target that no longer exists. Only the Wyckoff encoding-version token (WES:2) came
    back with its group.
  * THE SIX LEGACY SLOTS. Between 2026-08-24 and now, UseRSI and the five AD flags were
    hashed as literal ZEROS - correct while every flag shipped false, and wrong the moment
    they emit again. They carry the real flags once more, or two models differing only in
    which Wyckoff groups they feed could share a weights file whenever their column counts
    happened to coincide.
  * m_ADZigZag -> m_zigZag, the handle having been renamed since.

THE MASK HAD TO OPEN TOO, and this is the half that would have made the whole thing a
no-op. The keep-screen reads the feature CACHE, which sits downstream of
WarriorFeatureBlockKept - so restoring the builders while the mask still returned false
would have emitted all eight groups and discarded them before anything could measure them.
FEATURE_MASK_VERSION 7 lets them through.

THIS IS A MEASUREMENT CONFIGURATION, NOT A TRADING ONE. Width goes from 72 to several
hundred and the capacity ratio goes with it; the models trained during this pass are
expected to be poor and the deploy gate is expected to refuse them. That is the correct
outcome - the run exists to produce keep-screen votes and the PCA/collinearity report. The
survivors then come back as a normal mask and the width collapses again.

Block table checked entry-by-entry against its own ArrayResize: 19 names, 19 widths, 19
entries. A count mismatch there re-points the mask at the wrong columns silently, which is
the one failure in this change that would not announce itself.

RETRAIN-FORCING: mask version and input width both ride the fingerprint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:48:36 -04:00
AnimateDread
05401c9c64 fix(nn): linear output head + the missing chain factor - gradient check now agrees to 1.0000
Test_Backprop: SUMMARY 38 passed, 0 failed, 38 total.
    LINEAR head (shipped): all 18 output-layer ratios 1.0000

THE DEFECT WAS THE HEAD, NOT THE BACKPROP. The classification head ran
z -> sigmoid -> a -> logit = CLASS_LOGIT_SCALE * a -> softmax, and set the output gradient
to (target - p). The softmax+cross-entropy cancellation that makes (target - p) correct
holds only when the softmax acts on z DIRECTLY. With a sigmoid and a temperature in the
way the chain keeps both factors, so the true gradient was

    dL/da = CLASS_LOGIT_SCALE * sigmoid'(z) * (p - target)

and the code carried neither term. sigmoid'(z) = a(1-a) VARIES per neuron and per sample,
so Adam could not absorb it either - I claimed earlier that it could, and that was wrong.

MEASURED, IN THREE STEPS, EACH ONE PREDICTED BEFORE IT WAS RUN:

  shipped SIGMOID head   ratio 0.667-0.675, differing per neuron   missing 6*a(1-a) = 1.5 at a=0.5
  LINEAR head, no fix    ratio 0.1667 flat                          missing 6 exactly
  LINEAR head, fixed     ratio 1.0000 on all 18                     nothing missing

The middle row is what nails it: swapping the activation alone left a clean 1/6, proving
the two factors are separable and naming each.

THE FIX. Output activation SIGMOID -> NONE for every classification head, and the softmax
branches in BOTH backends multiply by CLASS_LOGIT_SCALE. With a linear head there is no
activation derivative to carry, so those two changes make the existing (target - p)
exactly right.

WHY KEEP THE TEMPERATURE AT ALL. A linear head could use scale 1 and drop the factor, but
ApplyLogitAdjustment expresses its cap as a fraction of CLASS_LOGIT_SCALE, so changing it
would silently retune the prior correction by 6x. Keeping the temperature and carrying it
in the gradient is the same maths with a smaller blast radius. The sigmoid was the only
reason the temperature had to exist in the first place - squashing z into (0,1) capped the
reachable softmax probability at 0.576, and the scale was there to win the range back.

THE TEST ALSO FOUND TWO BUGS IN ITSELF, both worth keeping in mind:
  * connections point FORWARD (prevLayer.At(n).Connections.At(m_myIndex)), so reading them
    off the output layer iterates an empty array. First run: 0 assertions, and TSummary
    called it ALL TESTS PASSED. A vacuous suite now fails loudly - that false green would
    have applied to every future test.
  * CNet::backProp does not only compute gradients, it calls updateInputWeights and MOVES
    the weights. Differencing around the moved point against a gradient measured at the
    original one left a systematic 0.3-0.5% error that Richardson extrapolation could not
    remove, because it was never truncation. Snapshot-and-restore around backProp took the
    agreement from 1.004 to 1.0000.

Numeric reference is Richardson-extrapolated to O(delta^4); tolerance is the standard
relative-error criterion at 1e-4; the seed is fixed so the operating point does not move
between runs. The old SIGMOID head is kept as a case in the suite and asserts DISAGREEMENT
at ratio ~4.02 = 1/sigmoid'(z), so the test stays honest about what it is measuring and
turns red if either head drifts.

CNet gains one read-only accessor, Layers(), used by the test and nothing else.

RETRAIN-FORCING: every model's output activation changes. EnforceOutputActivation corrects
a loaded net in place, so existing weights self-repair rather than silently running the old
head.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:39:03 -04:00
AnimateDread
ba8bc8e1c1 fix(training): stop at 100 eras - the run was picking the best of 1337 re-scorings of one window
Tag era-cap-1. MaxErasPerRun 10000 -> 100, and the era cap stops prompting.

THE ERA CAP WAS LABELLED A "runaway backstop, not a training control" and left at 10000
because "the plateau ladder decides when a run ends". The ladder does not end runs.
PLATEAU_PATIENCE_ERAS is 8, but ANY new best resets the counter, and on a noisy score a
new best arrives by luck often enough that the ladder wanders indefinitely. Measured live
today: SP500 era 1337, XTIUSD 1126, USDJPY 721, EURUSD 638.

WHY THAT IS NOT FREE, and it is the mechanism behind the overfitting the operator has been
pointing at all day. Successive eras RE-SCORE THE SAME OOS WINDOW - they add no
independent observations at all (project_window_cut_verdict: "effective n at a rung is
coverage x OOS_bars / lifespan, computed ONCE, not once per era"). So the checkpoint is
the MAXIMUM of N draws of a noisy statistic, and the winner's OOS score is inflated purely
by construction. It gets worse with every era the run survives:

  chart    current era   BEST era      selection family
  SP500    1337          1323          best-of-1337, book now NEGATIVE
  XTIUSD   1126          1057          best-of-1126
  USDJPY   721           666           best-of-721
  EURUSD   638           624           best-of-638, book now NEGATIVE

AND NOTHING IS GAINED PAST ~ERA 20. Measured 2026-08-27 across six charts and two runs:
every precision-on-era slope under 2.5pp per 100 eras, signs disagreeing across charts AND
across runs, and all six still holding their ERA-20 checkpoint at era 66-71. "In-sample
error keeps falling; OOS does not follow."

THE LIVE NATURAL EXPERIMENT ran itself today. The two charts that restarted sit at era 69
with their best checkpoints at era 20 (USDCAD) and era 24 (XAUUSD) - exactly where that
measurement said they would be. XAUUSD books +0.66, second best on the fleet.

100 keeps the burn-in (ENSEMBLE_CHECKPOINT_MIN_ERA 20) plus ~80 eras of candidates, ample
for a ladder needing ~24 eras of patience to reach PLATEAU_STAGE_DEPLOY, and cuts the
selection family by 13x. It also makes a full retrain roughly an order of magnitude
faster, which is what makes experimenting on topology or labels affordable at all.

THE PROMPT HAD TO GO WITH IT, and this was caught before deploying rather than after.
PromptContinuePastEraCap raises a MODAL MessageBox on any non-tester chart. Reaching a
10000-era cap was rare enough to be worth interrupting for; at 100 the cap is the NORMAL
path, so every chart would raise one. Ten charts, ten modal dialogs, terminal frozen until
each is dismissed - on a fleet that is not a prompt, it is an outage. It now prints the
same text and deploys the best checkpoint, which is what the plateau ladder does anyway.

NOT YET DEPLOYED. Every live chart is already past era 100, so a restart on this build
would have them all hit the cap at once and ship their existing late-era checkpoints -
the very ones this commit argues are noise-selected. The change only pays on FRESH runs,
so it wants a fleet reset to go with it.

Not retrain-forcing by itself; worthless without a retrain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:37:18 -04:00
AnimateDread
1c329004c7 feat(gate): positive expectancy is the whole bar - stop charging spread, stop requiring alpha
Tag expectancy-1. Two operator decisions, both recorded with the reasoning so neither gets
quietly "fixed" back.

SPREAD IS NO LONGER CHARGED. The book being judged is a ZIGZAG LEG RIDE, and a leg is
dozens of times the bid-ask difference. Measured on this fleet, the round-turn spread is
0.008 ATR on BTCUSD against books of +0.17 to +0.52 - about 4%. The operator reports the
same result in SQX, where setting 0, 6 or 60 pips does not move the outcome, and that
their broker does not bill it as a separate line. Spread is still SAMPLED once per bar and
still PRINTED, so the number stays visible; it simply stops gating.

  THE CONSEQUENCE, STATED UP FRONT RATHER THAN DISCOVERED LATER: indices pay no commission
  either, so SP500, DAX40 and NAS100 now have a cost of EXACTLY ZERO and their test reduces
  to "book > 0". That is literally positive expectancy, which is the stated objective.

  Per-chart, what stops being charged (from the live init lines):
    SP500 0.68 | DAX40 1.73 | NAS100 2.20   -> all become zero cost
    XAUUSD 0.55 of 0.60 | XTIUSD 0.09 of 0.11 | BTCUSD 3.58 of 28.00
    USDJPY 0.003 of 0.009 | EUR/GBP/CAD ~0.00003 of ~0.00007

ALPHA IS REPORTED, NOT GATED. It gated for roughly an hour. In the operator's words: "We
do not require beating buy and hold either. I already explained the goal : positive
expectancy, simple as that."

  THE OBJECTION WAS RAISED AND OVERRULED, which is the right order and is recorded rather
  than re-argued: a book paying less than the drift carries the DRAWDOWN PROFILE of the
  underlying trend, and a prop account fails on drawdown. Against that - buy-and-hold is
  not a strategy a prop account can run, nobody pays a prop trader for alpha, and DAX40
  booking +0.17 against an always-long +0.234 still MAKES +0.17. Operator's account,
  operator's call.

  The refusal branch became unreachable (tradeableOK is now ratesOK AND bookPays, so a
  paying book implies deployable) and was converted into a DRIFT WARNING that prints on the
  deployable era instead of withholding it. Same information, no longer a block.

THE GATE IS NOW: measurable, covers the base rate, fires both ways, and the book beats
commission. Four conditions, down from six this morning, and every one of them is a thing
an operator asked for.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:03:54 -04:00
AnimateDread
50f71f42a3 feat(gate): measure the IN-SAMPLE book and compare it to the out-of-sample one
Tag is-oos-2. The operator's ask, in their words: "the goal is to train neural networks
to recognize the patterns as best they can, deploying once we are not making progress for
x number of eras. performance of is and oos should be similar. just like SQX does."

The plateau ladder already IS "deploy once no progress for X eras". The IS-vs-OOS
comparison did NOT exist: this project scored the out-of-sample slice only. There is an
in-sample MSE as a training diagnostic, but no in-sample BOOK to compare the OOS book
against, so nothing in the gate could see a model that had memorised its training span.
This adds it.

SAME RUNG, SAME LABEL, SAME DECISION RULE - the only difference is which bars. The
derivation is copied from the OOS branch deliberately (softmax with the as-of leg side,
then the prior-corrected adjusted signal) so the two books differ by the DATA and not by
the decision rule, which is the whole point of comparing them.

ITS OWN ARRAYS, NOT A FLAG ON THE OOS ONES. The vote rows feed coverage, precision, the
chance rate, the cost mean and the exact binomial bar - they ARE the deploy gate. Letting
in-sample rows into them would corrupt every one of those silently. Seven arrays instead
of twenty-two, because only the book is wanted.

THE FIRST CUT OF THIS WAS DEAD CODE, and it shipped as is-oos-1 before the fault was
found. The in-sample scoring was placed inside the pass-3 loop, which starts at
oosCutoff-1 and counts DOWN to 2 - so it walks the OUT-OF-SAMPLE bars only, and the
in-sample ones are the higher indices it never reaches. The guard could never be true and
the measurement never printed. It now rides its own descending cursor, stepped once per
OOS bar, which also means it INHERITS this loop's yielding and can never become the
blocking pass that would livelock an era (project_era_slower_than_bar).

SAFETY CHECKED BEFORE WIRING, not after: ApplyClassificationSoftmax and
AdjustedSignalFromSoftmax were both read for member side effects. Both mutate TempData and
nothing else, so scoring an in-sample bar cannot reach the operating-point fit or any OOS
tally. The in-sample step runs BEFORE the OOS bar rebuilds its own window, since TempData
is shared. No backprop anywhere in it - this is a scorer, and training on these bars again
inside it would be a second unshuffled epoch.

Also caught before it could mislead: the OOS path adds signedVote RAW, because
LiveVoteContribution already carries m_weight x the tier's pattern weight and voteWeight
only builds the divisor. The first draft multiplied them again, which would have squared
the weight and made the two books measure different things - a false overfitting signal
from the very comparison built to detect one.

MEASURED AND NOT GATED, same discipline that let the split-half test be rejected on
evidence within the hour rather than becoming another closed door. Bounded cost: at most
one extra feedForward per OOS bar, strided to ~ENS_IS_TARGET_BARS samples.

WHAT IT CANNOT SEE, printed in the line itself so the number is not overread: our other
overfitting route is SELECTING the best era and rung ON the OOS slice, and this comparison
is blind to it. Only a third slice that neither trains nor selects would speak to that.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:27:52 -04:00
AnimateDread
f1268a16d9 feat(cost): the broker charges by ASSET CLASS - commission schedule, detected not guessed
Tag commission-1. Cost_CommissionPerLotPerSide (one number, shipped at 0.0, never set)
is replaced by the operator's actual contract, applied per class:

    indices  none              forex   4 USD per lot
    crypto   0.03% notional    metals  0.001% notional    energy  0.03% notional

CLASS IS DETECTED FROM SYMBOL_PATH, which is what the broker itself organises its tree
by, with symbol-name and SYMBOL_TRADE_CALC_MODE as fallbacks for a flat Market Watch.
Verified in situ on all ten live charts rather than asserted - every one resolved
correctly from its own path (Indices\, Forex\, Crypto\, Energy\, Precious_Metals\).

A PERCENTAGE OF NOTIONAL NEEDS NO CONTRACT SIZE AND NO FX RATE. Commission in money is
pct x contractSize x price x (quote->account rate); the price-equivalent divides by
money-per-price-unit, which is tickValue/tickSize - and tickValue already carries the
same contractSize and the same rate. They cancel exactly, leaving

    price_equiv = pct x price      for ANY quote currency

so nothing stale or missing can be read. Worth stating because it looks too easy.

THE FACTOR-OF-TWO, and the two branches need it OPPOSITE ways round. The percentage
branch builds the round turn itself, so a per-side quote is MULTIPLIED by 2. The flat
branch hands a per-side figure to WarriorCommissionRoundTurnPrice, which does its own
doubling, so a round-turn quote is HALVED on the way in. Writing them the same way round
would have been a factor-of-four error between asset classes. The operator's figures are
read as the FULL ROUND TURN (Cost_CommissionIsRoundTurn, default true), which is how a
prop contract quotes it; false doubles every figure without editing any of them.

MEASURED EFFECT - COMMISSION DOMINATES SPREAD ON HALF THE FLEET, and the gate has been
charging spread alone until now:

    BTCUSD  comm 24.42 + spread 3.58  = 28.00   cost was UNDERSTATED 7.8x
    USDJPY  comm 0.006 + spread 0.003 = 0.009   3.0x
    EURUSD  comm 0.00004 + 0.00002    = 0.00006 3.0x
    GBPUSD  comm 0.00004 + 0.00003    = 0.00007 2.3x
    XAUUSD  comm 0.04  + spread 0.55  = 0.60    1.1x
    indices comm 0.00                          unchanged

In ATR terms BTCUSD goes 0.008 -> ~0.063 and still clears on a +0.52 book, but GBPUSD
goes ~0.014 -> ~0.033 against a +0.03 book and should now FAIL. That is the correct
outcome: it was the thinnest book on the fleet and it was being charged a third of its
true cost.

The resolved class, both cost halves and the spread sample count are PRINTED at init, so
a symbol filed in an unexpected folder shows up as a wrong class rather than as a
silently wrong number.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:03:46 -04:00
AnimateDread
728bc647ba feat(gate): the book must beat the DRIFT, not just its cost - and the split-half test is rejected on measurement
Tag alpha-gate-1. Second condition added to the ensemble deploy gate:

    tradeableOK = voteGate.ratesOK && bookPays && alphaPays

WHY book > cost WAS NOT ENOUGH. book-gate-1 admitted DAX40, which books +0.20 ATR per
call against a 0.042 cost - a healthy 4.8x - while the always-long book over the same
window pays +0.234. The vote earns LESS than passively holding: its alpha is NEGATIVE,
measured -0.057 +- 0.073 over 18 consecutive eras. That is beta sold as signal, and it
stops paying the day the trend turns, which is exactly when a prop account's drawdown
limit is being tested. Neither more training nor a higher rung fixes it - the model has
found the trend rather than the turns - so the refusal now says that in those words
rather than leaving an operator to conclude the model is merely undertrained.

AND THE SPLIT-HALF CONSISTENCY TEST IS REJECTED, ON ITS OWN EVIDENCE. It shipped in
7606517 as the candidate second condition, printed and not gated, precisely so this
question could be settled by measurement instead of taste. The fleet settled it, over
9-18 consecutive eras per chart:

  chart    n    alpha mean +- sd      [min, max]         split-half YES rate
  BTCUSD   17   +0.348 +- 0.057   [+0.237, +0.467]        17/17 = 100%
  GBPUSD    9   +0.090 +- 0.026   [+0.060, +0.150]         0/9  =   0%
  DAX40    18   -0.057 +- 0.073   [-0.174, +0.086]         7/18 =  39%
  EURUSD    8   -0.091 +- 0.039   [-0.177, -0.057]         0/8  =   0%
  SP500    17   -0.374 +- 0.038   [-0.438, -0.298]         0/17 =   0%

IT LACKS THE POWER TO BE A GATE, and the arithmetic says why: halving the window halves
effN, so each half carries SE ~ 3.1/sqrt(94) = 0.32 ATR - LARGER than every book value
it is asked to compare. GBPUSD's two halves (-0.040 / +0.169) differ by less than half
a standard error, and DAX40's flag flipped on 7 of 18 consecutive eras on a chart whose
alpha never once turned positive. It would also have REFUSED GBPUSD, whose alpha was
positive in 9 eras out of 9.

Alpha over the FULL window is the stable statistic: era-to-era sd of 0.026-0.073 against
a between-chart spread of 0.7. It is kept as a REPORT - a book carried entirely by one
stretch is still worth an operator's eye - it just cannot carry a decision.

ONE HONEST LIMIT ON THAT STABILITY, since it would be easy to overread: successive eras
share the same OOS bars, so their alphas are strongly correlated. The small era-to-era sd
shows alpha is a REPRODUCIBLE property of the model and window, not that its sampling
error is 0.05. The sampling error is still the ~0.46 ATR recorded in 7606517, and this
remains a point-estimate gate.

BASELINE POPULATION, stated rather than hidden: baseMean2 is the always-long book over
EVERY shared bar, not over the fired ones - it is accumulated above the voter and
direction filters on purpose, because a baseline restricted to the bars the vote chose is
a baseline the vote selected. So this asks whether each call beat what holding through
the window paid per bar. An unmeasurable drift SKIPS the test rather than waiving it;
inventing a 0.0 baseline would silently turn it back into book > 0.

EXPECTED EFFECT: DAX40 refused (drift). BTCUSD and GBPUSD keep clearing. NAS100, XTIUSD,
XAUUSD and USDJPY measured alpha +0.707/+0.503/+0.404/+0.166 before the restart and
should clear once they score an era.

Ships the ArrayCopy fix from ead2b12, which was committed but not yet deployed.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:46:39 -04:00
AnimateDread
ead2b12838 fix(persistence): ArrayCopy grows a destination but never shrinks it
LoadNetWithRetry copied op.indicatorParams into the caller's array without resizing it
first. ArrayCopy GROWS a dynamic destination and never SHRINKS it (mql5book.pdf p1151),
and this particular destination is REUSED: BuildOrLoadTopology calls LoadNetWithRetry
twice into the same loadedIndicatorParams - once on the pure-MQL5 path, then again on
the DLL fallback when the first fails.

So a second load carrying FEWER params would leave the first load's values sitting in
the tail, and the reader's guard is a SIZE check -

    if(netLoaded && ArraySize(loadedIndicatorParams) == AD_TUNE_PARAM_COUNT)
       AdoptIndicatorParams(loadedIndicatorParams, indicators);

- which a stale tail can satisfy while holding the wrong numbers.

STATED HONESTLY: this is the contract being honoured, not a fault observed. Both calls
read the same file, so in practice the two counts agree and the tail never diverges.
One ArrayResize closes it anyway.

From the API-contract list in the full read of docs/mql5book.pdf. The other items on
that list came back clean: OrderSend is never called directly (CTrade owns retcode
handling, p1208), StringToCharArray is never used (p1780), and the one CopyBuffer read
already guards with != span rather than treating a short read as success (p1473).

Not retrain-forcing. Compiled clean; NOT yet deployed - the fleet is running book-gate-1
and a redeploy would cost the era measurements it is currently gathering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:24:05 -04:00
AnimateDread
7606517dcd feat(gate): deploy on the book, not on the significance of the win rate
Tag book-gate-1. The ensemble deploy condition was

    tradeableOK = voteGate.tradeable && bookPays

and voteGate.tradeable ANDs in `precision > chance + EDGE_MIN_SIGMAS x SE`. That test is
now REPORTED instead of GATING. Measured on the live fleet, one era per chart:

  chart    verdict   book    cost    zero-skill  alpha    precision vs its bar
  NAS100   DEPLOY   +0.870   n/a      +0.163    +0.707    37.8% vs 33.6%
  XTIUSD   DEPLOY   +0.590   n/a      +0.087    +0.503    31.7% vs 30.4%
  XAUUSD   refused  +0.660   n/a      +0.256    +0.404    34.9% vs 36.4%
  BTCUSD   refused  +0.370   0.008    +0.032    +0.338    24.6% vs 30.7%
  USDJPY   refused  +0.290   0.014    +0.124    +0.166    29.9% vs 30.7%
  GBPUSD   refused  -0.010   0.012    -0.030    +0.020    27.2% vs 27.1%
  DAX40    refused  +0.180   0.042    +0.234    -0.054    29.3% vs 37.5%
  EURUSD   refused  -0.130   0.018    -0.053    -0.077    23.1% vs 24.1%
  USDCAD   refused  -0.040   n/a      +0.070    -0.110    24.4% vs 27.6%
  SP500    refused  -0.020   0.074    +0.248    -0.268    25.1% vs 32.2%

THREE BOOKS PAYING 20-50x THEIR OWN COST WERE BEING REFUSED, and not for any reason to do
with the model. effN deflates by the 34.5-bar label overlap, so the bar is chance +4.3pp at
USDJPY's 14,584 calls and chance +15.0pp at DAX40's 1,385. BTCUSD was asked for a 14pp
precision edge over chance on roughly 46 independent observations. Nothing produces that.
This was not a strict gate, it was a closed one, and it was closed by call COUNT rather than
by edge.

IT ALSO CONTRADICTED THE GATE NEXT TO IT. WarriorRungBookProfitable's own header argues that
demanding significance "would deploy NOTHING, ever, which is the prove-your-edge trap this
codebase has backed off twice", and gates the book on a POINT ESTIMATE for that reason.
Then tradeableOK demanded significance anyway and overrode it. Two gates, opposite
philosophies, and the closed one won every time.

AND PRECISION IS THE WRONG QUESTION, which this same file already said twelve lines away:
precision "says a turn was CALLED, not that the leg after it paid". BTCUSD calls 24.6% of
turns right and earns +0.37 ATR per call, because its winners are much larger than its
losers. A gate on the win rate cannot see that; a gate on the book can.

NEITHER SUPPLIED BOOK RECOMMENDS A SIGNIFICANCE GATE. mql5book.pdf pp. 1482-9 optimises and
then FORWARD-TESTS, ranking on a criterion built to reward a smooth equity curve - signed by
slope, weighted by sample size. It never asks a win rate to clear a sigma bar, and it is
honest about the yield (of its top 1000 in-sample passes, 323 were profitable forward).
neuronetworksbook.pdf has no cost model in 690 pages. We invented this bar ourselves.

WHAT CHANGED

  * SDeployVerdict splits its verdict into ratesOK ("is this a strategy at all" - measurable,
    covers the base rate, fires both ways; none of which needs statistical power to read) and
    edgeOK (the significance test). tradeable = ratesOK && edgeOK, UNCHANGED, so the member
    gate and its ranking are untouched. Only the ensemble reads ratesOK.
  * The rung selection reads ratesOK too. Selecting the certified operating point through a
    test the verdict no longer applies would have picked the rung by the wrong criterion -
    and on a low-coverage chart would have picked none.
  * The Sidak family-wise test no longer gates. It corrects the PRECISION z, and precision is
    no longer the decision; leaving it in the conjunction would have kept the closed gate
    closed through a side door. It is still computed and printed, because a best-of-N maximum
    is still a maximum and an operator should see how selected the number is.

WHAT STILL GATES: ratesOK, and the book beating its own measured per-rung cost.

WHAT IS NOW MEASURED AND DELIBERATELY NOT GATED: the SPLIT-HALF BOOK. The window is split at
its median timestamp and the ride book reported for each half at the certified rung. This is
the CONSISTENCY question - did the book hold up across the window, or is the total one good
stretch carrying a bad one - and it is what the coding book ranks on instead of significance.
It prints on every era. When there is fleet evidence that it discriminates a real book from a
selected one, it becomes the deploy condition. Replacing an unreachable bar with an unmeasured
one is precisely how the precision gate closed, so it is not being done blind.

THE HONEST CAVEAT, stated rather than buried: the book is still a maximum taken over
eras x rungs, so it is still a selected number, and at effN ~46 with ~3.1 ATR per-call SD its
own standard error is near 0.46 ATR - BTCUSD's +0.338 is under one sigma. This is a
point-estimate gate, knowingly. The controls that remain are the plateau ladder (this branch
is not reached until the best vote survives PLATEAU_STAGE_DEPLOY-1 warm restarts with no
improvement), the coverage and both-sides checks, and the live expectancy stop - which is the
only one of the three that judges the book the account actually experiences.

Three stale comments corrected to match (the sweep's [DERIVED] explanation, the stage-3
refusal attribution note, and the refusal branch's own condition).

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:17:39 -04:00
AnimateDread
cc3b0a219a fix(cost): measure the round turn from live Ask-Bid and an input, not from bar history
Corrects d70efc0, shipped an hour earlier, on the operator's report that historical
spread is not accessible from this terminal.

THE SOURCE WAS WRONG. d70efc0 read MqlRates.spread out of the bar history via
CopySpread, reasoning that a historical book should be charged the historical
spread. The reasoning is appealing and the data does not support it: that field is
only populated for bars the broker's own feed recorded, and anything backfilled,
imported or synthesised carries a constant or the spread at download time. A cost
that is silently constant is WORSE than no cost at all, because it looks measured -
it would have passed every review while charging a made-up number.

SPREAD IS NOW SAMPLED LIVE, Ask - Bid, ONCE PER BAR. Per bar and not per tick
deliberately: per-tick sampling weights busy hours, and busy hours are the
tight-spread ones, so the mean would be biased low - understating exactly the cost
this gate exists to charge. The running mean PERSISTS across restarts in a terminal
GlobalVariable, so it accumulates a real distribution over however long the fleet
has been up instead of resetting to a single tick on every deploy; past
WARRIOR_COST_SAMPLE_CAP samples it becomes an EMA so a genuine regime change can
still move it.

COMMISSION IS NOT A SYMBOL PROPERTY IN THIS BUILD, and this was put to the compiler
rather than assumed. A probe file compiled against this terminal returns:

    error 256: undeclared identifier 'SYMBOL_COMMISSION'
    error 256: undeclared identifier 'SYMBOL_TRADE_FEE'
    error 256: undeclared identifier 'SYMBOL_COMMISSION_TYPE'

while SYMBOL_ASK / SYMBOL_BID / SYMBOL_SWAP_LONG / SYMBOL_TRADE_TICK_VALUE in the
same file compile clean, so the probe itself was sound. The Symbol Specification
DIALOG shows a commission for many brokers; MQL5 is not given it. The only
programmatic ground truth is DEAL_COMMISSION on an executed deal
(Database\TradeJournalManager.mqh already reads it), which is useless before the
book has traded.

So it is an INPUT - Cost_CommissionPerLotPerSide, account currency per lot per
side, the way brokers quote it. Doubled for the round turn and converted to price
through tickSize/tickValue. ZERO IS A REAL ANSWER (spread-only accounts exist) and
means the gate charges spread alone; it is not a way to disable the test. It ships
at 0.0, so until an operator sets it the gate is charging spread only and says so.

THE PER-ROW ARRAY IS NOT REDUNDANT at a constant spread: cost is expressed in the
BAR'S OWN ATR, so a quiet bar is genuinely dearer to trade than a volatile one and
the number still varies row to row.

THE LIMITATION, STATED RATHER THAN HIDDEN: this charges a HISTORICAL book the
CURRENT spread regime. No data available to this terminal can remove that
assumption, so it is written into the module header and is the first thing an
operator should challenge.

CFeatureBuilder::SpreadPointsAt keeps its body and loses its claim: its comment
said it existed for the deploy gate, which stopped being true with this commit. It
still backs the spread FEATURE, where a constant column merely fails the keep-screen
- a safe failure, which as a cost it is not.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:35:59 -04:00
AnimateDread
d70efc0f74 fix(gate): the book had to beat zero, not beat what it costs to collect
WarriorRungBookProfitable tested (hLong+hShort)/n > 0.0. The round-turn spread was
computed a few lines away in the same function, printed in the payoff line, and
carried an explicit comment saying it gates NOTHING - "spreadR is a live snapshot
to be judged by hand".

That objection was correct about the QUANTITY and wrong about the CONCLUSION.
SymbolInfoInteger(SYMBOL_SPREAD) is whatever the book looks like at the instant an
era happens to end, which is not what the historical trades in that window would
have paid - so it should not gate. But this project has already measured that "4 of
5 die to spread and the survivor dies on commission"
(project_cost_boundary_equilibrium), and EXPECTANCY IS THE BAR. The answer is to
measure cost properly, not to leave it out of the gate.

MEASURED, NOT SNAPSHOTTED. g_ensVoteCostR carries the round-turn spread at each
vote row, taken from the broker's own per-bar history (CopySpread, already
maintained as m_spreadSeries for the feature block) and divided by that bar's ATR,
so it lands in the SAME units as the ride book it has to beat. Filled once per ROW
in the block that already measures the ride and the excursions, for the reason
stated there: cost is a property of the CHART at that bar, identical for every
member.

PER RUNG, over the bars that rung actually fires on - not a chart-wide average.
Signals cluster, and a cluster can sit in a wider-spread regime than the window
mean, so a window average would understate the cost of exactly the bars being
traded. sweepCost/sweepCostN accumulate alongside sweepFired.

AN UNKNOWN COST DOES NOT WAIVE THE TEST. g_ensVoteCostR is -1 where the spread
series or the ATR was unavailable and those rows are DROPPED from the mean rather
than read as zero - a cost of zero is the one answer that can never be right. If a
rung has no measurable cost at all the gate falls back to the pre-existing `> 0`
bar, which is "cannot price this", not a free pass.

NOT a multiple of the cost. The "expected payoff should be at least double the
spread" rule is a common heuristic (and is what prompted this - MQL5 article 8410,
Ilin, sent by the operator) but it is not measured here, so it is not imposed.
Sampling error on the book is already carried by the exact binomial bar the same
verdict applies to precision.

WHAT IT CHANGES, on the fleet as it stands. Median book per call against the
census's measured spread/ATR:

    SP500   +0.02  vs ~0.065   -> now correctly REFUSED (was certified)
    GBPUSD  +0.05  vs ~0.020   -> marginal
    DAX40   +0.08  vs ~0.029   -> marginal
    BTCUSD  +0.23  vs ~0.006   -> clears
    XTIUSD  +0.57  vs ~0.083   -> clears
    NAS100  +0.75  vs ~0.025   -> clears

Both charts currently DEPLOYABLE clear it comfortably, so this closes a hole rather
than reversing a live decision - but SP500's book was being certified while sitting
below its own spread.

The sweep line now prints cost beside the book at every rung, so BOOK-FAIL names a
visible reason instead of an invisible one, and its legend says what the number is
and what "n/a" falls back to.

Three call sites updated, not one - the rung walk, the derived-rung verdict and the
sweep report all ask this question (feedback_rename_leaves_readers_behind).

Not retrain-forcing: no fingerprint member moves, and g_ensVoteCostR is an in-memory
per-era buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:25:48 -04:00
AnimateDread
b5d2bc1622 fix(ensemble): the vote's scale outgrew its rung grid and the fleet went silent
SYMPTOM: every chart's era verdict reported 0.0% coverage at EVERY rung of the
threshold sweep. Only DAX40 (6.1%) and USDJPY (0.9%) had any coverage at all. Seven
of ten charts could not fire a trade at any operating point the derivation was
allowed to choose.

IT IS NOT A DEFECT IN THE MODELS. LiveVoteContribution is
moduleWeight * (tierWinRate - measuredChancePct), so THE VOTE'S UNIT IS PERCENTAGE
POINTS OF EDGE OVER CHANCE. Measured from the terminal's own tier-ranking lines
("pooled X% raw -> Y% shrunk toward the Z% coin-flip rate"), medians:

    2026-09-01   win 38.6%   chance 13.4%   EDGE 25.2pp
    2026-09-02   win 20.5%   chance 10.6%   EDGE  9.9pp
    2026-09-03   win 22.4%   chance 19.6%   EDGE  2.8pp

Two separate steps, both legitimate:
  * 09-01 -> 09-02 the WIN RATE halved (38.6 -> 20.5) - the leg-ride label (980f10b)
    is an honestly harder target.
  * 09-02 -> 09-03 the CHANCE RATE doubled (10.6 -> 19.6) while the win rate held -
    the zero-skill floor correction (8fd2756/5ec8902) doing exactly its job.

Corroborated independently by "Strongest vote X% against a Y% threshold": 20.3-54.2%
on 09-01, 0.6-52.2% on 09-02, 0.0-14.7% today, with only the 5% rung ever selected.

THE ACTUAL DEFECT: g_ensThresholdSweep was {5,10,15,20,25,30} - floor 5.0. With a
median edge of 2.8pp the median chart cannot reach even the lowest candidate. THE
DERIVED THRESHOLD IS ONLY AS ADAPTIVE AS ITS GRID: the derivation's own comment
claims it "re-picks the rung by itself" when the currency moves, and it can only do
that within these six values. The currency's scale changed under it twice in two
days and nothing said so, because an empty sweep reports as 0.0% coverage rather
than as an out-of-range grid.

Extended to {1,2,3,4,5,10,15,20,25,30}.

THIS IS NOT LOWERING THE BAR. Every gate still runs at every rung: the derived
coverage floor, the exact binomial bar on the EFFECTIVE call count, both sides
live, and WarriorRungBookProfitable requiring both sides to PAY. The change only
lets the derivation evaluate operating points it could not reach; it can still
refuse all of them, and a chart whose vote never leaves zero still fires nothing.

THE MULTIPLICITY PRICE IS PAID AUTOMATICALLY, which is what makes widening safe:
EnsembleSurvivesSelection sets nTried = eras * ENS_THRESHOLD_SWEEP_N and applies
Sidak, so adding rungs TIGHTENS the significance bar in the right proportion -
6 -> 10 costs a factor of 1.67 on the family size. Nothing else needed touching:
every consumer is sized by the constant, and the persisted rung is the VALUE not an
index, so no stored threshold is remapped.

NOTED, NOT CHANGED: the no-clearance fallback (derivedIdx = 0) now means rung 1.0
rather than 5.0. That is consistent with its documented intent ("take the lowest:
maximum evidence is the only thing left worth having") and it cannot trade, because
it is only published while g_ensBestEra < 0 - i.e. before any checkpoint exists, when
no member has demonstrated edge, so VoteCapableWeight leaves every member out of the
divisor and `fires` sees voters == 0.

IT ALSO EXPLAINS THE SELL-SIDE REPORT, and better than a per-side rung would. A rung
at the extreme top of the vote distribution admits only what the members AGREE on,
and agreement is highest where the drift is - long, on this fleet. NAS100's vote
DIRECTION is 2654 buy / 2345 sell (1.13:1) but after the 5% rung it fired 160 long /
5 short (32:1); XAUUSD's members are 2161 Buy / 1692 Sell post-operating-point
(1.28:1). Neither the member operating point nor the vote direction is one-sided -
the single rung at the tail does all the amplifying. A per-side rung was the obvious
next move and is deliberately NOT taken: selecting a rung per side on an outcome
metric is what 5a7d96c retired, and with the grid bottoming out at 5.0 while the
whole distribution sat below it there was only ONE reachable rung, so there was
nothing to choose between anyway.

DO NOT READ THIS AS "the fleet will now make money". Precision is ~22.4% against a
~19.6% chance rate; break-even is 30-38% (project_breakeven_margin_measured). The
fleet is 8-15pp SHORT of break-even. This makes the machinery able to express an
operating point at all - it is not an edge. Expect the gate to keep refusing most
charts, and expect the refusal to now name PRECISION instead of reporting an empty
sweep.

Not retrain-forcing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:59:06 -04:00
AnimateDread
e13e317e54 feat(signal): one operating point per side - a single global cut had silenced the sell book
User report: "some charts are not showing sell signals at all, only buys". It is
real. By-side ride book and n on the fleet's most recent era per chart - the
drift-free test the era log already prints:

    NAS100  160 long vs      5 short   -    32 : 1
    DAX40    30 long vs      0 short   -   no short book at all

(Earlier eras from the same charts read 744:1, 260:1 and 41:1, but those lines
predate today's restart and are quoted nowhere as current - the two above are the
post-restart measurement.)

THE LABEL IS NOT THE CAUSE. P(ride pays | up-leg) vs P(| down-leg) is 0.99x to
1.55x across the fleet and up-legs are ~50% of bars. A 1.2x rate asymmetry was
being amplified into 744:1.

THE MECHANISM is the operating point's depth. FitDirConfThreshold places one cut
where coverage matches the pooled directional label rate, and at 3-25% coverage
that cut sits deep in the tail. There, a small shift in one side's margin
distribution moves nearly ALL of that side across it: the threshold hits its
target POOLED and misses it per side by two orders of magnitude. The model was not
failing to see sells - BTCUSD's pre-threshold sell recall is 84% and its raw argmax
is B45/S40 - the single cut was discarding them.

Each side now clears the margin ITS OWN measured label rate asks for.
m_labelPrebuildBuyCount/SellCount already existed, so the two targets are
measurements, not choices.

THE PROPERTY THAT MAKES THIS SAFE, AND IT IS EXACT: the per-side targets are
100*buy/tot and 100*sell/tot against the same denominator the pooled rule uses, so
they SUM TO THE POOLED TARGET. Total coverage is preserved; only its split between
the books changes. This redistributes exposure rather than increasing it.

THE TRAP, NAMED: 5a7d96c (project_gate_book_not_sides) retired a per-side test
because it selected on noise. This is not that, and the difference is the whole
argument. That was a best-of-2 VERDICT on an outcome metric - selection. This fits
a RATE to a separately-MEASURED rate and maximises nothing, which is
FitDirConfThreshold's own stated rule ("call a direction as often as a direction
actually occurs") applied honestly per side instead of only in aggregate. The
precision at each fitted point is logged as REPORTED, not optimised, for the same
reason the pooled line already says so. And the deploy gate still judges the
POOLED book, so if the newly admitted calls do not pay, the gate closes -
restoring the sell side's COVERAGE is not a claim that it is profitable.

EACH SIDE MUST EARN ITS OWN FIT: DIR_CONF_MIN_FIT_CALLS per side, the same
evidence the pooled point needs, because a side's operating point deserves as much
as the pooled one did. A side that cannot reach it keeps DIR_CONF_SIDE_UNFITTED and
falls back to the pooled threshold - byte-for-byte today's behaviour - so this can
only change a decision where there is enough evidence to change it, and a model
whose RAW argmax really is one-sided has no sells manufactured for it. The
sentinel is negative rather than 0.0 because 0.0 would mean "call every bar on
this side", the trapdoor DIR_CONF_MIN_FIT_CALLS exists to keep the pooled point
away from.

ONE SWEEP, THREE FITS. FitDirConfBinFor() is shared by the pooled fit and both
per-side fits so the rule cannot differ between them - two copies of a sweep is
how one of them ends up with a different tie-break or a different denominator. The
coverage denominator stays m_dirConfPrimaryBars in all three: using a side's own
call count would make every side's target 100% by construction.

THE OPERATING POINT BELONGS TO THE WEIGHTS IT WAS FITTED FOR, so the per-side pair
travels through every one of the four checkpoint sites the pooled value already
uses - the ensemble joint commit (via the m_eraStatThresholdSide stash, because no
member "won" the era, the vote did), the solo capture at a new best, the mid-run
rollback, and the deploy-the-checkpoint restore. Leaving one behind would pair
checkpointed weights with cuts fitted for a rejected era.

Persisted in the .cfg, appended AFTER the variable-length pins so no existing
reader's offsets move, and length-guarded like every field above it: a .cfg from
before today ends early, both guards yield the UNFITTED sentinel, and the model
runs exactly the policy it was written under. This matters because a DEPLOYED
model runs no further eras - without it, every restart would drop both sides back
to the pooled point and re-silence the sell book on the charts this exists for.

WHAT THIS WILL AND WILL NOT FIX, measured before deploying rather than claimed
after. Fresh numbers taken at 11:45-11:47, after the restart, say the member layer
is NOT where the amplification happens on the charts that are currently firing:

    XAUUSD members, post-operating-point:  2161 Buy / 1692 Sell   = 1.28 : 1
    NAS100 combined VOTE direction:        2654 buy / 2345 sell   = 1.13 : 1
    NAS100 after the ENSEMBLE RUNG:         160 long /    5 short = 32   : 1
    DAX40  after the ENSEMBLE RUNG:          30 long /    0 short

So both the member operating point and the vote's DIRECTION are already balanced,
and the 32:1 appears at the single ensemble rung applied to |vote|. That rung sits
at the very top of the vote distribution - NAS100's strongest vote is 5.5% against
a 5.0% threshold, drawing 43 arrows on 4999 bars - and the top of that
distribution is where the members AGREE, which the era log's own note says is
where they agree WITH THE DRIFT. Cutting there selects drift-aligned calls, and on
this fleet the drift is long.

This commit therefore fixes the member instance of the defect, which is real and
which XAUUSD's 1.28:1 shows is already being held in check there, and it is NOT
expected to move the fleet's 32:1 on its own. The ensemble rung is the same defect
one layer up and is the next change; it is deliberately separate so that each
effect stays attributable, and because a per-side rung must be fitted to a
MEASURED RATE rather than selected on payoff - selecting a rung per side on an
outcome metric is exactly what 5a7d96c retired.

Not retrain-forcing: nothing here is a fingerprint member. Deployed ALONE rather
than bundled with mask v5 as project_next_three_tasks proposed - mask v5 is
retrain-forcing, and measuring the sell book's return needs the SAME mature models
this baseline was taken on (the method project_honest_floor_silenced_four_charts
used). Bundling would have made the effect unattributable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:49:57 -04:00
AnimateDread
3c0e2facb2 fix(chart): a signal mark's persisted time was the left edge of its line, not its bar
User report: "some labels are completly off (arrow not at the low/high, and entry
price way higher than candle body and much much away from what the spread would
add)". Not the training label, and not a fill - the only order attempt on the day
was refused by the client. Both halves are one display defect.

A mark is TWO objects since 2026-08-20: an OBJ_TREND segment from t-half to
t+half so it is wide enough to see, plus an OBJ_ARROW glyph. half is
PeriodSeconds * WARRIOR_SIG_LEVEL_HALF_SPAN = 4680 s on H1, so OBJPROP_TIME index
0 of the line is its LEFT EDGE. FIVE call sites read index 0 and every one of
them treated it as the bar time, because "which bar does this mark belong to" had
no single implementation and each site re-derived it from rendering geometry.

MEASURED on the live files, and it COMPOUNDS: Snapshot() wrote t-half, the restore
redrew a line centred on the stored value, and the next save read THAT line's left
edge. Predicted residues are the 10 multiples of 360 s, not the 60 possible
minutes - observed 704 of 704 persisted vote arrows across fifteen files on the
k*1080s ladder, ZERO off it, with k=1 where a mark had been saved once and k up to
6 where it had been carried through sessions. XAUUSD and EURUSD sat at k=6: 7.8 H1
bars adrift, which is why a mark's stored trigger price was nowhere near the candle
it was drawn over - the price was right for a bar 7.8 bars away. The arrow half
lost its low/high anchor for the same reason: a shifted time is not a bar open, so
iBarShift(exact) returned -1 and the glyph fell back to the trigger price, landing
inside the candle body.

WarriorSignalMarkBarTime() is now the one accessor, and it takes the MIDPOINT of
the two ends rather than t0+half: the midpoint recovers t exactly by construction
AND stays correct if the half-span is ever retuned, including for marks already
drawn under the old value. Its t1 <= t0 branch answers correctly for the
single-anchor arrow half too, which the rescan sweep needs. Routed through it:
CVoteArrowStore::Snapshot, CChartUI::SaveChartSignals,
WarriorReconcileVoteCooldown, WarriorLatestVoteArrowTime, and the rescan's
typed-blind scope sweep.

Two more defects the same read was hiding:

  * the cooldown reconciliation deleted by a RECONSTRUCTED name built from the
    shifted time it had just read, so it could not remove a freshly drawn arrow at
    all and its log over-reported kills. The name now travels with the time
    through an insertion sort over both arrays.
  * the rescan's scope sweep gave the two halves of ONE mark two different times,
    so at the window edge it deleted an arrow and left its line - exactly the
    split WarriorDeleteSignalMark exists to prevent.
  * WarriorLatestVoteArrowTime seeded the live cooldown clock 1.3 bars EARLY on
    every restart, so the first signal after a restart could fire inside the
    window the arrow on the chart was enforcing.

WarriorSignalMarkOnBarGrid() stops the drift surviving a restart. Arithmetic
rather than a history search, and the residue is READ off iTime(sym,period,1)
instead of assuming UTC alignment, because where the bar grid sits in epoch
seconds is the broker's day start. It KEEPS on an unknown - no history yet, or a
weekly/monthly frame that is not a modular grid - since deleting on an unknown is
the failure mode that cost this chart 272 of 273 arrows in 2ca32e9. Applied in
both sidecar loaders and in the reconciliation, which is the only layer that
reaches what MT5 restores from profiles\Charts\*.chr.

An off-grid mark is DROPPED, not snapped to the nearest bar. Its price names a bar
that cannot be recovered, so placing it anywhere would assert that a signal fired
on a bar where it did not. Verified in situ on the deployed fleet: nine charts
screened their files at init and dropped 236 of 236 records, every one off-grid,
matching the offline measurement exactly. The persisted arrow history is therefore
gone - it was already misinformation.

Also killed an IMMORTAL zero-price record measured in the USDCAD file.
WarriorPlotSignalLevel rejects price <= 0 so it was never drawn, but the save path
copied the undrawn restore queue straight back to disk every session, so one bad
record survived forever.

Not retrain-forcing: nothing here is a fingerprint member. Deployed as
mark-bartime-1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:25:31 -04:00
AnimateDread
8280a7cd40 feat(fleet): open the charts the census says are worth having
The census answers which instruments carry deep history at a spread the book can
cover; this is the half that acts on it, so adding an instrument is a reviewable
list in source control rather than six manual chart operations nobody can
reconstruct later.

BTCUSD, GBPUSD, NAS100, DAX40 - every one with more than twelve years of server
H1 and a sampled spread under 3% of ATR, against a measured ride book of +0.05 to
+0.63 ATR per call. BTCUSD is the cheapest deep instrument the broker offers
(0.63%) and the only one in a different asset class, which is worth the most to a
pooled certificate that declines the diversification credit. DAX40 trades a
different session, so its bars are not the same hours as everything else.

IDEMPOTENT: a chart is opened only when none exists on that symbol and period, so
a restart re-opens nothing and the list can be edited freely. That is what makes
it safe to leave armed.

THE TEMPLATE CARRIES THE EA. ChartSaveTemplate on the running chart captures this
Expert Advisor and its inputs, so a new member comes up configured exactly like
the one that spawned it - which is the point: a fleet whose members differ by
attach order is how four charts ended up on a 10-bar cooldown and two on 30.

Leased like the census and the alt-data fetch, because six instances would each
try to open the same four charts - and the charts this opens initialise an EA that
reaches this same code.

COST, STATED IN THE FILE: four added charts are sixteen more models on a six-core
box already at ~72% with six charts. Expect eras to slow across the whole fleet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:26:39 -04:00
AnimateDread
0a68a0511b fix(census): an unsynced history depth is UNKNOWN, not zero
The same defect as the spread-reads-0 bug, one field along, and found the same
way - by noticing a number that could not be true. SERIES_TERMINAL_FIRSTDATE
needs the symbol synced with the server, which does not happen for an unquoted
symbol within one sampling window of a terminal restart. So BTCUSD reported "0.0
years" of H1 history and would have been filtered out of the candidate list as
having none.

Measured across two runs twelve minutes apart: BTCUSD, NAS100, DAX40, UK100 and
US30 all read n/a on the freshly restarted terminal and all five reported real
first dates (2013, 2011, 2008, 2008, 2008) on the one that had been up longer.

Written as UNKNOWN now, so it cannot be sorted or filtered as though it were
shallow. Zero for an unmeasured quantity is the error this codebase already names
in its rate gates - "-1 IS NOT MEASURABLE, never 0, because a gate reading 0 would
treat an unmeasured quantity as a failure" - and a census is no different.

WHAT THE SAMPLING ALREADY CHANGED, for the record: ten readings thirty seconds
apart moved USDCAD from 4.76% of ATR to 6.23% (+31%), SP500 6.00 -> 6.49 and
XTIUSD 11.87 -> 11.95. The single snapshot understated the cost of the very
instruments whose books are thinnest, which is exactly where it mattered.

Ships with the next deploy; the fleet is mid-era and this is a report, not a gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 07:23:26 -04:00
AnimateDread
7a5dddc61a fix(census): one chart, not six - and sample the spread instead of snapshotting it
TWO DEFECTS IN MY OWN CENSUS, both found by reading its output rather than by the
compiler.

IT RAN ON ALL SIX CHARTS. The guard was a plain global, and MQL5 globals are per
PROGRAM INSTANCE - six charts are six instances, so every one of them walked all
64 symbols and overwrote the same file. Replaced with the atomic
GlobalVariableSetOnCondition lease that System\AltDataFetch.mqh already uses for
exactly this problem: six charts racing one shared file. Losing the lease is the
correct outcome, not an error.

THE SPREAD WAS ONE SNAPSHOT. Cost is the number this whole decision turns on - the
measured ride book runs +0.05 to +0.63 ATR per call, so an instrument costing a
tenth of an ATR a round turn has already spent most of what it could earn - and a
single reading is one moment of one session. Spreads widen at rollover and around
news. Now ten readings thirty seconds apart, reporting mean AND max, because the
max is what says whether an instrument is quietly untradeable at the wrong hour.

A reading only counts behind a real two-sided quote; a symbol with none reads
NO-QUOTE rather than 0, since 0 would rank it as the cheapest instrument on offer.
That is the same error the previous commit fixed one layer up, and the guard now
sits at the sample rather than only at the report.

WHAT THE FIRST GOOD RUN ALREADY SETTLED: 64 symbols openable both ways, and all 64
report a SERVER first-H1 date with server == local. So depth is real, not a sync
artifact - the broker genuinely offers deep H1 on twelve instruments and added the
other fifty-two in July/August 2026 with no history at all. The expansion universe
is six symbols, not fifty-eight.

Also promotes the derived-cooldown line out of PrintVerbose. It reports a change
to the TRADING POLICY, and this codebase's rule is that a line reporting a state
change never sits behind the verbosity gate - the tier-ladder restore learned that
the expensive way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 07:15:09 -04:00
AnimateDread
81efd64a7f feat(signal): derive the cooldown from the measured label lifespan, and census the broker's symbols
TWO CHANGES, ONE CAUSE: a per-chart input could not reach the fleet, and the fleet
had no data on which symbols were worth adding.

THE COOLDOWN IS NOW DERIVED, NOT SET. An input was the wrong shape for it twice:

  * It is a measurable property of the LABEL, not a preference. The leg-ride label
    resolves when the ZigZag leg flips, so the mean label lifespan IS the average
    leg duration in bars. Two calls closer together than that concern the SAME
    leg - the second pays a second spread for a move the first already owns. One
    lifespan apart is where consecutive trades concern DISTINCT legs, which is
    also what makes the deploy gate's independence assumption exact rather than
    approximate: EffectiveSampleSizeDeclustered's divisor becomes 1.
  * An input could not reach an attached EA. MT5 stores inputs per chart, so
    "raising Signal_CooldownBars from 10 to 30 changed nothing on six live
    charts" - and yesterday the fleet was found running 10 on four charts and 30
    on two, two different trading policies inherited from attach order rather
    than chosen. A derived value cannot drift that way.

The fraction is 1.0 because the argument picks it: less re-admits same-leg
duplicates, more declines distinct legs for no stated reason. At the measured
33-34.5 bar lifespan it lands within a few bars of the 30 the default intended.
Recomputed wherever the lifespan is measured - the label-cache build - so the
measurement and its consumer cannot drift apart. SignalCooldownOverrideBars still
wins, and switching declustering off entirely is still possible.

THE SYMBOL CENSUS answers "which symbols are worth adding" with data. The binding
constraint on this system is independent observations, and instruments are the
only lever that multiplies them, so it writes the three numbers that decide it:
history depth, spread against ATR, and whether both sides can be opened at all (a
close-only symbol can never satisfy the gate's two-sidedness test).

AND IT RUNS ON THE TIMER, NOT AT INIT, which the first version got wrong. At init
the terminal has just reconnected and nothing has a quote, so SYMBOL_SPREAD reads
0 everywhere - the first run duly ranked thirty untradeable-on-cost symbols as the
cheapest the broker offers. Caught by noticing GBPUSD reported a zero spread
beside EURUSD's 2 points. Now deferred three minutes, and a symbol with no tick is
written NO-QUOTE rather than a number, so the column cannot be sorted on by
mistake. It selects nothing: sixty symbols added to Market Watch is a change to
the operator's terminal, and this is a report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 07:05:49 -04:00
AnimateDread
8892c9f17e docs(gate): correct the effect size - the live cooldown is 10 bars, not the 30 the default declares
The previous commit computed the independence gain from Signal_CooldownBars =
SCB_30 and claimed a ~87%-independent traded stream. That is the DEFAULT, not the
runtime value. MT5 stores inputs per chart in profiles\Charts\*\chart*.chr and an
already-attached EA ignores a changed default - which is stated in this codebase's
own comment beside that input, and is exactly why "raising Signal_CooldownBars from
10 to 30 changed nothing on six live charts". The fleet runs 10. The stream is ~30%
independent, not ~87%.

Read out of the live log instead of estimated, on a USDCAD era:

  before   8,669 signal calls -> effN 261, precision 22%, bar 24.9%   FAIL
  after    1,385 traded calls -> effN 417, precision 23%, bar 23.7%   fails by 0.7pp

So the fix is worth about 1.6x the independent count and ~1pp of precision. It
closes most of the gap rather than clearing it. Real, and smaller than advertised.

AND RAISING THE COOLDOWN WOULD NOT HELP, written down so nobody tries it:
effN = traded/(lifespan/gap), and traded itself falls as 1/gap, so the gap cancels.
The independent observations in a window are bounded by window/lifespan whatever
the spacing. Spacing trades cannot manufacture independence - only a longer window,
more instruments, or a shorter label can.

Comment-only; no behaviour change, no redeploy needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 06:22:29 -04:00
AnimateDread
8fd2756aa5 fix(gate): the member gate certified a population the EA never trades
The deploy gate judged m_oos.DirCalls() - every bar the prior-corrected posterior
fired - and deflated it by the full 34.5-bar label lifespan. The EA does not trade
that set. It trades what survives declustering: live, a call the cooldown rejects
has its signal zeroed before the vote is published, so it produces no arrow, no
vote and no position.

The honest counts were already being computed, by the same CSignalDeclusterPolicy
object the live signal applies, on the same adjusted decision it feeds - printed
every era as "TRADED (declustered)". The gate simply never read them.

BOTH HALVES OF THE ERROR POINTED THE SAME WAY, which is what made it expensive:

  * the traded set is measurably CLEANER. Live USDJPY eras: judged 24% where
    traded was 26%, judged 26% where traded was 27%. The gate was reading a
    precision the EA would never have realised.
  * the traded set is far more INDEPENDENT. The cooldown is 30 bars against a
    34.5-bar label, so consecutive traded labels overlap by at most 4.5 bars - the
    stream is ~87% independent, not ~3%. Deflating it by the full lifespan applies
    a correction the cooldown has already made.

On a live USDJPY era that is the whole verdict: judged 24% against a 25.4% bar
FAILS; traded 27% against a 24.7% bar PASSES.

AND THE SELECTION IS UNBIASED, which is what makes the traded precision usable at
all: the declustering keeps the chronologically FIRST bar of each run, never the
highest-confidence one, so this is not cherry-picking winners.

COVERAGE STAYS ON THE SIGNAL POPULATION, and that split is now explicit in the
signature rather than implied. Coverage asks "did this model call often enough to
be a strategy", which is a question about the signal; precision asks "were the
calls it took right", which is a question about the account. Coverage cannot move
to the traded set for a structural reason: a 30-bar cooldown caps traded coverage
at 1/30 = 3.3% while the floor is a quarter of a ~20% base rate, so every chart
would fail it forever for reasons unrelated to the model.

THE ENSEMBLE GATE STILL HAS THIS DEFECT and is passed through unchanged, on
purpose. Its OOS rows carry each member's raw adjusted vote - the decluster replay
is per-member state that never reaches the shared vote buffer - so there is no
traded population there to read yet. Fixing it needs the per-member decluster
decision carried on the vote row. One gate at a time, so the effect of this one
stays attributable.

Not retrain-forcing: gate arithmetic only, no fingerprint field moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 06:14:28 -04:00
AnimateDread
6d266d4e62 feat(features): every price column here is memoryless - fractional differentiation
AFML ch.5. Every price-derived feature in this set is a FULL difference:
(close-open)/atr, (close-MA)/atr, the 20-bar return, the leg extension.
Differencing is what makes a series learnable - a model fitted on 2019 EURUSD
levels cannot read 2026 ones - but a first difference is memoryless BY
CONSTRUCTION. It keeps the last step and throws away the series. That is the
trade this feature set has been making silently at every column.

Fractional differentiation is the observation that the exponent need not be an
integer. (1-B)^d for 0<d<1 interpolates between the raw level (all memory, not
stationary, useless to a learner) and the return (stationary, no memory). Three
orders are emitted - d = 0.3/0.5/0.7 - in ATR-relative units.

A LADDER, NOT A CHOSEN d. AFML picks the minimum d passing an ADF test; there is
no ADF test here, and adding one to tune a feature's parameter per instrument per
era would fit the feature to the data before the model saw it. Three fixed orders
go in and the FLEET KEEP-SCREEN votes on them, exactly as it has already deleted
five ZigZag geometry columns and demoted the alt-data block. Kept by construction
in mask v4 for one outing only: the screen reports only on columns that are
EMITTED, so a block masked off at birth can never be measured.

THE BUG THAT WOULD HAVE SHIPPED THREE DEAD COLUMNS, caught before deploy by
checking the arithmetic at realistic price levels rather than on a series
starting at zero:

The weights of (1-B)^d sum to zero in the LIMIT - that is what makes it a
differencing operator. TRUNCATED at 64 bars they do not: the residual is 0.222 at
d=0.3. That residual multiplies the LOG PRICE LEVEL, so the raw sum carries
0.222 x log(2400) = 1.73 on XAUUSD against 0.222 x log(1.08) = 0.017 on EURUSD -
an instrument-identity constant hundreds of times larger than the signal. Divided
by the relative ATR it pins every bar to the clamp: measured 100% of XAUUSD and
SP500 bars clipped, 68% of EURUSD. Three constant columns that FEATURE HEALTH
would have reported only after a full retrain had been spent on them.

Anchoring every term at this bar's log price removes exactly the level component
and leaves a fracdiff-weighted combination of the multi-horizon RETURNS ending
here - stationary, scale-free, same meaning on every instrument, and the long
memory intact. Same series after: mean ~0, sd ~1.1-1.3, nothing clipped.

The weight recursion is checked against two known values: at d=1 it terminates to
[1,-1,0,...], the plain first difference, and at d=0.5 it reproduces the standard
expansion of (1-B)^0.5 to six terms.

REJECTS RATHER THAN DEGRADES at the oldest edge, unlike the swing and volume
windows beside it. A 30-bar Donchian range is still a Donchian range; a
fractional difference over a short window is a DIFFERENT OPERATOR - a different
effective d - reported in the same column, which is a silently wrong number
rather than a degraded one. Costs ~64 bars of 130,000.

RETRAIN-FORCING twice over: the input width is field 4 of the fingerprint and
FEATURE_MASK_VERSION rides it too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 23:12:02 -04:00
AnimateDread
f063cfd6ba fix(calibration): the confidence rescale was blended with a per-SAMPLE time constant, once per era
m_confidenceCalScale has been frozen at its 1.0 constructor default for the life
of the mechanism, and the units error that froze it is one line:

    m_confidenceCalScale += (eraScale - m_confidenceCalScale)
                            / Net.recentAverageSmoothingFactor;

recentAverageSmoothingFactor is 10000. It is a PER-TRAINING-SAMPLE constant - it
exists to average the network error over ten thousand samples inside backprop.
Applied ONCE PER ERA it moves the value by one hundredth of one percent, so after
a hundred eras the scale has travelled 1% of the way to its target and after a
thousand it is still not halfway.

That reframes what was already known. ConfidenceBridge.mqh records that the
confidence is miscalibrated and that five trade-management modes were deleted
because of it. The mechanism meant to fix it was not merely shape-blind, as the
isotonic curve's commit message argued - it was NUMERICALLY INERT, and never had
the chance to correct anything at all. A quantity blended per era needs a per-era
time constant; borrowing one from a per-sample loop reads as a working mechanism
in every review, because the line is shaped exactly like a working EMA.

The new curve inherited the same divisor when it was written yesterday, which
would have frozen it at its first fit - visible in the log as a "carried" column
identical to "refit" to four decimals on every chart. Both now use CAL_BLEND_ERAS
(5 eras): slow enough that one noisy band cannot swing the live number, fast
enough to track a model whose output distribution moves every era.

VERIFY, DON'T ASSERT: the curve's log line now prints the scalar's current value,
so the first line after this deploy reports the number restored from .stats - the
value it reached over that model's entire history. 1.000 is the claim above,
measured rather than argued from the arithmetic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:57:43 -04:00
AnimateDread
5ec8902020 fix(gate): the zero-skill floor was still halved one level down - and the measurement sizing actually needs
THREE FINDINGS, ALL FROM READING THE FLEET'S OWN LOG RATHER THAN THE COMPILER.

1. THE MEMBER DEPLOY GATE STILL USED THE 3-CLASS ZERO-SKILL FLOOR.

The ensemble gate's chance rate was corrected on 2026-09-03 when the head stopped
choosing sides. SOosTally::ChancePrecPct - one level down - was not, and it feeds
more than the ensemble copy does: the MEMBER deploy gate, the certified pair
HasDemonstratedEdge() reads to decide whether a member may vote at all, and the
cross-instrument pooled certificate. On a live H1 window it read 12.5% where the
honest always-ride floor is 21.1%, so every member showed "+13pp edge, PASSES"
and was admitted to the vote. A model with no skill whatsoever cleared it.

The same halved floor was in BaselineComparator's Alglib comparison, which scored
the forest and the linear baseline against a bar half the height of the one the
net is judged by.

ChancePrecPct now TAKES THE POLICY AS A PARAMETER WITH NO DEFAULT, so the next
head change is a compile error at every reader instead of a silent wrong answer
at some of them. That is the whole lesson of finding this one a week late: the
first fix was applied where the bug was noticed, not everywhere the assumption
lived.

The era line said "the gate ranks on the LARGER of the two" for that entire week,
on six charts, every era. It now states the policy actually in force.

2. THE ERA LINE PRINTED +/-1.79e308 IN THE MIDDLE OF EVERY RECORD. The binary
head has no third output neuron, so slot 2's min/max kept their DBL_MAX ctor
values and were formatted anyway. Width-aware now, and the slots are labelled
PAYS/DOESNT rather than B/S/N, which is what they hold.

3. THE SIZING QUESTION NEEDED A DIFFERENT MEASUREMENT THAN THE ONE I BUILT.

The reliability curve says the confidence is now HONEST - carried, out-of-sample,
ECE 38pp -> 1.3-2.4pp. It says nothing about whether it RANKS, and ranking is
what a bet size needs. The existing conviction curve cannot answer it either:
coverage collapses above the lowest rung, so every fired call sits in one bucket
and there is no curve to read.

So the ensemble now carries the calibrated confidence per OOS row - the mean over
members that actually called, which is exactly what LiveSignedConfidence()
publishes - and the era verdict splits the certified rung's fired calls at their
median confidence and compares what the two halves earned, in ATR per call.

A MEDIAN SPLIT, NOT A DECILE CURVE, and the reason is power: per-call SD is ~3
ATR and the labels overlap ~34 bars, so a decile of a few hundred raw calls holds
under ten INDEPENDENT ones and its error bar is wider than the whole book. Ten
noisy points would invite the best-of-N reading this project has already crowned
four times. Two halves is the most the data can be asked for, and it is reported
with its standard error on INDEPENDENT counts plus the size of difference this
window could ever resolve - so "not measurable" is distinguishable from "no
effect", which is a statement about the data rather than a verdict on the idea.

Nothing here sizes anything. This is the evidence ConfidenceBridge.mqh's standing
rule demands before anything may.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:50:19 -04:00
AnimateDread
18ada65d85 feat(calibration): the refit column is not evidence - carry the curve and measure the live range
Two corrections to what the first commit reported, both found by reading its own
first live output rather than by the compiler.

THE "AFTER" NUMBERS WERE IN-SAMPLE. ECE 44.7pp -> 0.5pp was the curve scored on
the very band it had just been fitted on, and isotonic regression is expressive
enough to drive that near zero whatever the data says. It is the attainable
floor, not a result. Quoting it would be the calibration-slice error - a
threshold fitted on memorised bars - one layer along.

The fix is free, because the previous era's curve is already sitting there when
the new one is fitted: score it too. That curve was fitted on EARLIER bands and
has never seen these bars, so it is a genuine out-of-sample calibration
measurement. The log now reads raw -> carried -> refit, and says in the line
itself that the middle column is the one to read.

THE SPREAD, NOT THE ERROR, IS THE SIZING QUESTION. The first fit showed claimed
0.75 and claimed 0.99 mapping to the SAME calibrated 0.273 - the curve is flat
at the top, meaning higher confidence there does not mean higher accuracy. A
well-calibrated constant is still a constant: no amount of calibration makes a
flat map rank anything, and a bet scaled by it would be a bet scaled by noise -
the precise error that killed the five confidence-scaled modes on 2026-08-25.

So the report now restricts to the calls the operating point actually ADMITTED -
the only ones that can become a trade - and states the calibrated probability at
the lowest and highest admitted bin, their spread, and the map's mean against the
realised rate on that same set. Carried as its own counts rather than derived by
cutting the histogram at a magnitude, because the operating point is a MARGIN and
this histogram is keyed on a MAGNITUDE; those are monotone in each other on the
binary head and not on the 3-class one, and a reparameterisation guess is exactly
the kind of thing that reads as a measurement.

This is the number the sizing decision will be made on, and it is deliberately
being gathered BEFORE anything is built on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:36:08 -04:00
AnimateDread
92ccf20385 feat(calibration): a scalar has no shape - the isotonic reliability curve
The model's confidence was corrected by ONE number: accuracy divided by mean
claimed confidence, EMA-blended per era. That can move the reliability curve up
or down and can do nothing else. A model that is honest at 0.55 and wildly
over-confident at 0.95 has a SHAPE problem, and no scalar has a shape.

The scaffolding for the fix already existed and was better than expected.
RunCalibrationPass already walks a PURGED, held-out band with batch norm frozen,
visiting each bar exactly once, and fills a 50-bin (margin, hit-rate) histogram.
That is a reliability diagram on out-of-fold predictions. Isotonic regression is
a fit over data already being collected, on a band already purged - no new walk,
no new holdout, no new cost.

WHAT THIS ADDS

  * A SECOND histogram in the same walk, keyed on |dPrevSignal| - the magnitude
    the consumer actually holds - not on the winner-vs-rival margin. Deliberately
    not a re-key of the existing one: on the 3-class head the two statistics are
    not the same quantity, and FitDirConfThreshold's own note records what
    happened the last time one curve served two fits.
  * FitCalibrationCurve: pool-adjacent-violators over the occupied bins, weighted
    by call count. The least-squares monotone fit (Ayer et al. 1955); the
    monotonicity constraint IS the regularisation, so there is no smoothing
    parameter to tune and it cannot fit a shape the data does not show.
  * EMA-blended across eras with the same smoothing the scalar used. A convex
    combination of monotone sequences is monotone, so blending costs nothing the
    fit exists to impose.
  * Brier, ECE and MCE reported before and after the map, on the band the map was
    fitted on. REPORTED, NOT OPTIMISED - nothing selects on them. A model that was
    already calibrated shows all three pairs unchanged, which is the outcome that
    says this map is not needed.
  * Persisted (.stats WSTE), because the curve is produced only by a completed
    calibration band and a DEPLOYED model runs no more eras - the same failure the
    tier ladder had. The bin count leads the block so a future
    DIR_CONF_THRESHOLD_BINS change is a mismatch the reader detects rather than
    fifty doubles landing in the wrong slots.

THE SCALAR STAYS as the unfitted-model answer, and the two are never applied
together: they answer the identical question, the curve per magnitude and the
scalar on average, so stacking them would correct the same error twice. Same
discipline as the logit adjustment being backward-pass only.

NOT RETRAIN-FORCING. The fingerprint is untouched, the .nnw is untouched, and a
WSTD file still loads - it simply comes back with no curve and keeps the scalar.
The fleet resumes exactly where it was.

STILL TELEMETRY. Variables\ConfidenceBridge.mqh carries a standing rule that
nothing there may steer a trade, and CalibratedConfidenceMagnitude has exactly
one consumer: the trade journal's aiConfidence bucket. Five confidence-scaled
trade-management modes were deleted on 2026-08-25 for one stated reason - the
confidence was known to be miscalibrated, so they scaled money by a quantity
whose units were never established. This is the measurement that establishes
them. Sizing is deliberately NOT in this commit: shipped alongside its own
calibration it would be untestable, because if the book moves nothing says which
half did it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:30:51 -04:00
AnimateDread
84789ea992 fix(head): the binary logit branch printed the 3-class banner
ApplyLogitAdjustment's meta-head branch sat BELOW the 3-class tau computation
and its log block. The offsets installed were correct - the branch returns
before the 3-class ones are written - but the 3-class banner printed first and
latched m_logitAdjustLogged, so every era log claimed "APPLIED across all
three" while the head had two logits, and the binary line was unreachable.
A log line that misdescribes a live gate-adjacent path is the same class of
defect as a stale comment.

Moved above the 3-class work. Also corrects this file's own claim about the
imbalance against the measurement it now has: the H1 fleet's shares are
9.4/9.0/81.6, so the binary split is ~4.4:1 rather than the ~3:1 estimated
before the run - and tau goes from 0.57 capped on three classes to ~0.86 on
two, which is the point: most of the correction gets through where most of it
was being clipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:50:05 -04:00
AnimateDread
8d6cff7fcb feat(head): the side was never a prediction - the binary meta-label head
The leg-ride target's SIDE is the direction of the ZigZag leg in progress as
of that bar, readable from bars t and older with no lookahead. The 3-class
head made the network re-derive it anyway: that is Lopez de Prado's PRIMARY
MODEL being learned instead of used, and it cost on four axes at once -
chance at 33% instead of 50%, an 8:1 imbalance instead of 3:1 against a tau
already pinned at its cap, every Buy row spent as evidence about "is this a
long leg" rather than about payoff, and a directional error scored the same
as a payoff error though only one of them was a question we asked.

The head is now binary: slot 0 "riding this leg to the flip pays at least
LEG_LABEL_MIN_RIDE_ATR", slot 1 "it does not". The side comes from
LegDirAsOf() at read time.

TWO OUTPUT NEURONS, NOT ONE, and that is what made this small. Both backward
passes in AI\Impl\NetForward.mqh already carry a `total == 2` softmax+CE arm,
left in deliberately when the old meta head was removed on 2026-08-25 - and a
2-class softmax IS a logistic/BCE head, the logit difference being the
log-odds. So this reuses the exact gradient path the 3-class head uses instead
of growing a second one.

NOTHING DOWNSTREAM CHANGED. ApplyClassificationSoftmax() expands (pGo, side)
back into the [pBuy, pSell, pNeutral] triple every reader already consumes, so
Argmax3, the operating-point histogram, per-class recall, confidence tiers,
the vote currency and the chart arrows are untouched. Neutral stops being a
class the net competes for and becomes what it always meant: pGo below the
operating point. The margin the threshold is expressed in becomes 2*pGo-1,
monotone in pGo, so the calibration walk fits the same statistic.

THE HEAD STAYS BOUNDED, deliberately, and the 2026-07-27 unbinding retry is
NOT bundled here. Reading the gradient showed why it need not be: the softmax
arm OVERWRITES the output neuron's gradient with (target - softmax), so the
sigmoid derivative and MIN_ACTIVATION_DERIVATIVE are already bypassed at the
head. What SIGMOID x CLASS_LOGIT_SCALE actually costs is p in [0.0025,0.9975]
- three orders of magnitude wider than the band where the live question is
0.35 versus 0.60. Unbinding has its own failure history and deserves its own
measurement; batch norm before the head, its precondition, already ships.

THE ZERO-SKILL FLOOR HAD TO MOVE WITH IT. The rate gate's chance was the
better of always-Buy and always-Sell. This head cannot choose a side, so its
no-skill policy is ALWAYS-RIDE - every bar taken in its own leg's direction -
which is right on EVERY directional-label bar, not the better half. Left
alone it would have handed a model with no skill whatsoever a ~+11pp edge.
The book gate already measured against always-ride; the rate gate now agrees
with it about what zero skill means. Same correction in the module-weight
shrinkage prior (Lifecycle.mqh's 50.0 side coin-flip).

RETRAIN-FORCING twice over: |MHEAD:1 joins the fingerprint and the output
count is field 6 of the .nnw filename, so no existing model or pool row can
be adopted. Verified live - all six charts rejected every peer file by
fingerprint and restarted from era 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:46:50 -04:00
AnimateDread
8d7cbdbf9d feat(training): sample the queue by uniqueness instead of weighting it - the update itself was the cost
Weighting by uniqueness (8351c39) redistributed gradient mass but still spent an
update on every redundant label. Lopez de Prado's actual prescription for
redundancy is the other one: bag size = average uniqueness (AFML ch.4). A label
now enters an era's training queue with probability proportional to its own
uniqueness, and carries no uniqueness weight once it is in.

UNIQUE_WEIGHT_VERSION becomes a MODE so the two can never stack - one integer
cannot be two values, which is the same class of mistake as the focal-loss and
logit-adjustment stacking this codebase already removed. 0 off, 1 weight, 2
sample. Mode 2 is the default.

- The draw is STRATIFIED BY CLASS. Uniqueness is correlated with the label here,
  since a label's span IS its ride and whether a ride clears 1 ATR is what
  separates Buy/Sell from Neutral. An unstratified draw would shift the training
  set's class balance away from the priors the logit-adjusted loss corrects
  against - the exact failure the queue site's own comment warns about. Dividing
  by the class mean equalises the expected acceptance rate across classes.
- It also decorrelates the ensemble: each member draws its own subset each era,
  so the four models no longer see one identical stream. That is the diversity
  lever the era report has asked for every era (4 models worth 1.3-1.9).
- Measured: eras now queue 7.4-9.0% of eligible bars, matching each chart's mean
  uniqueness, and era time fell from ~107s to ~22-27s on the index and oil.

One bug shipped and was caught in the first run. The probability was
rate * u / classMean, whose mean inside a class is 1.0 by construction, so at
rate 1.0 it kept 73% of bars and the reduction never happened. The class mean is
the right stratifier but the wrong scale; the scale is the global mean. Fixed
before this commit, and the rate now rides in the fingerprint (UWGT:2:R100)
because a model trained on a twelfth of the bars is not interchangeable with one
trained on all of them.

RETRAIN-FORCING. Compiled 0/0. Deployed 20:15 as uniq-sample-2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:18:40 -04:00
AnimateDread
8351c3933e feat(training): weight every sample by its label's uniqueness - the loss was counting 34x redundancy
Every statistic in this program deflates overlapping labels. The training loop
did not. EffectiveSampleSize() is called in exactly two places, the feature
keep-screen and the deploy gate, and both are reporting paths; the gradient step
weighted each sample by |ride| alone, with no term for how many other labels are
made of the same bars.

Measured on the H1 fleet: mean label overlap 31.6-37.4 bars. USDJPY carries
125,214 labels that this same binary reports as ~3,344 independent ones. The
optimiser was told it had thirty-four times more evidence than it has, which is
the textbook cause of a strong in-sample fit with a thin out-of-sample book -
the pattern every era log has shown since this label shipped.

The correction is Lopez de Prado, Advances in Financial Machine Learning ch.4:
average uniqueness is 1/concurrency averaged over the bars a label spans, and
the prescribed weight is uniqueness x attributed return, i.e. the product of the
new Variables\UniqueWeight.mqh and the existing MoneyWeight.mqh.

- ComputeLabelUniqueness() runs once when the label cache pre-build completes,
  O(bars) via a difference array for concurrency plus a prefix sum over 1/c, so
  a 125k-label cache costs two sweeps rather than millions of span walks.
- On the leg-ride target the two weights are anti-correlated: a label's span IS
  its ride, so a long leg earns a big money weight and sits where concurrency is
  highest. Money weight alone concentrated gradient on exactly the least
  independent evidence in the set. The product is re-clamped to [0.25, 4.00]
  because two clamped factors multiply to a 16x tail.
- Measured even when the knob is off, so the report can show the spread this
  would apply without training on it. First run: mean uniqueness 0.076-0.085.

This REDISTRIBUTES gradient mass; it does not reduce the update count. Training
on fewer, more independent samples is the sequential bootstrap (ch.4 s.4.5) and
gets its own commit and its own measurement.

UNIQUE_WEIGHT_VERSION 0 restores the prior behaviour and the prior fingerprint
exactly. RETRAIN-FORCING (|UWGT:1). Compiled 0/0. Deployed 19:40 as
uniq-weight-1; all six H1 charts from era 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:42:43 -04:00
AnimateDread
3aa15c8b4b feat(altdata): publication stamps by series, alt block back in, one pool for all six charts
Operator asked for the alt data to be properly mapped on H1. Three findings:

1. The as-of join was already whole-day: any H1 bar of day D reads row D, and the
   window layout (ALTW:2) carries that reading once per window at the anchor.
   What was wrong was the ROW DATE. Every FRED series was stamped "knowable next
   day", which is right for a market close and five to six weeks early for a
   monthly print: July CPI (dated 07-01) entered the export on 07-02 and was
   released 08-12. Unemployment the same; the H.10 dollar index (weekly, posted
   the following Monday) a week early; the effective funds rate a day early. On
   H1 that is ~1,000 bars of a value nobody had, in three of the twelve fleet
   columns. FredPublishLagDays() stamps by series (CPI +48d, UNRATE +40d,
   DTWEXBGS +8d, DFF +2d, closes +1d), cached rows are re-stamped on load, and
   ALTFETCH_EXPORT_VERSION (a .ver sidecar beside each export) forces one rebuild
   at the next init so every chart reads the corrected export immediately.
   Verified on XAUUSD_D1.csv: mac_cpi now changes on 08-18, mac_unemp on 08-10.

2. The alt block was not reaching the model at all. Keep mask v2 dropped all
   twelve alt columns on a screen measured under the pivot label on H4, and the
   screen only reports on emitted columns. v3 emits them again (28 of 47 columns,
   input width 168); the H1 keep-screen will say which of them clear.
   The window dedupe now uses the EMITTED alt width, not the panel's: under v2
   it placed a 12-wide block over the last twelve of sixteen emitted columns.

3. The training pool ran as two groups because the cross-asset block encoded
   index mode (base == quote: SP500, and this broker's XAUUSD/XTIUSD) with a
   different meaning per slot than FX mode, and the fingerprint tagged it
   ":IDX2". U3 gives both modes one layout (proxy fast/slow in 0/2, denomination
   fast/slow in 1/3, own move minus the proxy-vs-denomination cross in 4), so all
   six charts print the same fingerprint and pool together.

RETRAIN-FORCING (XA:6:U3, ALTV:2, FMASK:3). Deployed 15:00 as alt-stamps-1; all
six H1 charts from era 0, one shared fingerprint, exports rebuilt at v2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:02:27 -04:00
AnimateDread
8c37869c63 fix(init): a warm re-init kept the H4 run's globals; the training pool adopted other timeframes
Operator report 2026-09-02: the six charts were switched from H4 to H1 and the
status label kept showing the H4 run. MetaTrader does not unload the program on a
timeframe, symbol or input change - it runs OnDeinit and OnInit inside the same
instance and every file-scope global survives the pair. The members' converged
flags, the live vote line, the panel rows, the shared best-era/plateau state and
the once-per-chart report flags all belonged to the models just torn down.

- Warrior_EA.mq5: WarriorResetWarmReinitState() runs first in OnInit and puts
  every such global back to its cold-start default. Kept on purpose: the chart's
  book magic (positions opened before the switch stay owned), the alt-data fetch
  throttle (rate-limited APIs), the tester profile, the RNG, the OpenCL flags.
- TrainingPool.mqh: peers must be the caller's own timeframe. The fingerprint
  does not carry the period, so the first H1 census adopted 39,754 H4 rows from
  three peer files and credited them to the capacity budget. Files are named
  SYMBOL_PERIOD.bin; the suffix decides, and the reader names the rejection.

Build tag warm-reinit-1. Compiled 0 errors / 0 warnings. RETRAIN-NEUTRAL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 14:25:20 -04:00
AnimateDread
980f10b60c feat(label): ride the leg, exit on the flip - the leg-ride target replaces the pivot-event target
The pivot label paid +1.6 ATR on a hit and -1.7 on a miss at 50-65% precision:
a zero book by arithmetic, because a "bottom" that is not one is a move that
kept going, and the model called it because a big move had just happened.
This target never asks for a turn. Direction is the leg in progress, known on
the bar; the label is whether riding it from here until the leg flips pays at
least LEG_LABEL_MIN_RIDE_ATR (1.0). The flip is the exit the EA now places.

- Labeling/LegState.mqh: a line-for-line replica of ZigZag.mq5 (12/5/3) run
  over bars <= t only, so "which leg am I in" is what the chart showed on t,
  never the final buffer. Verified against the real indicator at every init
  (LEG STATE REPLICA line) and by Tests/Test_LegState.mq5 on hand-built bars.
- LegRideLabel (Labels.mqh): ride = legDir * (close[flip] - close[t]) / ATR,
  the flip found by asking the same as-of function of each newer bar in turn.
  Unresolved until the leg has flipped inside loaded history. Lifespan = the
  leg, so EffectiveSampleSize deflates honestly (~3x fewer than the window
  constant claimed); the topology's overlap is the median leg again.
- Money weight = |ride|. Online step's exit bar = the flip.
- Three as-of leg features in the swing block (direction, extension in ATR,
  age); FMASK:2 keeps them. Fingerprint TGT:LEG1:10 - RETRAIN-FORCING.
- Era verdict: the book WarriorRungBookProfitable gates is the RIDE (entry at
  the call, exit at the flip), printed per rung and at the certified rung
  beside ALWAYS-RIDE (zero skill) and the ORACLE RIDE (the ceiling). Fixed-
  horizon payoff stays as the signal diagnostic. The by-distance profile,
  its slot arithmetic and the reversal-exit pass are deleted (measured:
  the vote's reversals land 33-137 bars late; dead).
- Live: Exit_On_Leg_Flip (default on, not in the fingerprint) closes when
  the as-of leg flips against the position and places no take-profit; the
  measured stop stays. Build tag leg-ride-1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:02:47 -04:00
AnimateDread
635e4396ab feat(training): measure the reversal-exit book - what each call earns held until the vote flips
A perfect pivot caller buys the bottom and sells the top and collects the whole leg: median 4.5-5.0
ATR on these charts (SWING LEG SIZE line at init). The era report's fixed 18-bar exit collects ~35%
of that and a mean-excursion target about half, so the exit - not the label - is where the leg is
left. Nothing measured the one exit that can keep it.

Every era now walks the certified rung's fired calls in time order and scores each one held until
the vote next fires the other way - no stop, no target - in ATR at entry, by side, with the mean hold
in bars, beside the fixed-horizon exit on the SAME calls. Calls with no reversal before the window
ends are dropped like the leading edge; same-side re-fires are ignored. Two per-row fields (close,
ATR) join the OOS vote store to make an arbitrary row-to-row payoff measurable. Reported only, gates
nothing, retrain-neutral.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 10:11:58 -04:00