Commit graph Warrior_EA/Expert/AIBase/Labels.mqh
Author SHA1 Message Date
AnimateDread
fec4d47eb2 refactor(labeling): CTripleBarrier - one copy of the fill/barrier arithmetic
Session B of the feature-selection/labeling refactor track. Extracts the two
pieces of triple-barrier arithmetic that were genuinely duplicated or
scattered, taking price/ATR/geometry as plain arguments - no chart, no
indicator handle - so it is testable with synthetic numbers.

CTripleBarrier::ComputeLevels() replaces the fill/barrier level arithmetic
that TripleBarrierLabel() and SimulateTradeOutcome() each spelled out by
hand; their own comments already called it "IDENTICAL... deliberately and by
copy." One caller resolves both sides at once (the both-won tie-break needs
both); the other selects the side its isLong argument names. Same for
ApplyMinStopWidening(), the broker-minimum-stop floor both walks applied.
Fuzzed 200k random (entry, spread, risk, reward, minStop, isLong) tuples
against both original hand-written forms: 0 mismatches.

CLabelOverlap replaces m_labelLifespanSum/m_labelLifespanCount - two members
reset from three separate call sites (constructor, label-cache rebuild), the
exact "N loose members cleared in more than one place" shape a candidate-
geometry incident (7452bd1) turned into a live bug. One object, one Reset(),
default-constructed like every other object member. MeanLabelLifespan() and
EffectiveSampleSize() on the signal become thin forwarders with an unchanged
signature - every one of their ~15 existing callers, direct and through the
CAIBaseTrainingData adapter, is unaffected.

SnapHorizonToLadder() forwards to CTripleBarrier::SnapToLadder(), the ladder
array's one remaining copy; EffectiveHorizonMax() (the close-all cache) stays
on the signal since that state has no clean argument form.

NOT extracted: TripleBarrierLabel()'s ~200-line walk itself. It resolves both
sides simultaneously, tracks the first-passage ladder, and feeds the label
every live order is sized from; a rewrite of it cannot be checked without a
compiler, so only the two pieces provably identical to their originals moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 18:57:17 -04:00
AnimateDread
2c351a02ca refactor(barriers): the ladder is an object, and its snap rule is one rule
CFirstPassageLadder owns the three caches (per-rung up/down first-touch ages
plus the terminal travel) and every question asked of them. The signal keeps
one member where it kept three arrays and a lifespan scalar.

WHAT THIS ENDS. The log-space rung snap existed THREE times: once as
LadderRungFor, twice written out inline inside LadderWinShare - and
LadderRungFor's own header said "Same rule LadderWinShare snaps with, so a
rung chosen here and a rung chosen there are the same rung". A comment asking
a reader to keep three copies equal by hand is the arrangement CMetaFamilies
was built to end. It is now one static RungFor(), so the two rungs agree by
construction.

The bounds test was spelled out at four sites and the "0 means never, tie
goes to the stop" comparison at three. Now Has() and FirstTouch(), once.
The four-site bounds test was also subtly weak: it computed
`idx * COUNT` and tested only the upper end, so a negative index slipped
through into a negative array read. Row() rejects it.

Spread and horizon are ARGUMENTS, not state. The ladder is pure travel in ATR
multiples; what a spread costs and how long the walk ran are facts the caller
supplies. Every answer is now a function of its inputs alone - which is the
point, because this is the barrier arithmetic that failed its own acceptance
test in b5e22a1 and it has never been runnable without a chart, a net and a
broker attached.

BEHAVIOUR UNCHANGED. Each moved body was checked statement-multiset against
its predecessor with the rename map reversed; the only differences are the
substitutions named above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:02:50 -04:00
AnimateDread
7452bd1ba9 fix(geometry): a shutdown abort cleared one tally of ten
Found by grouping, not by looking for it.

The candidate-geometry scan kept ten accumulators as ten separate
members. Era start cleared all ten in a ten-line block. The
shutdown-abort path inside the exit-policy simulation cleared
m_geoTrades and nothing else, so nine partial sums - diffSum,
diffSumSq, incSum, candSum, candSl, candTp, incOpen, candOpen,
startTick - survived the abort with the aborted era's values.

The next era then accumulated onto those sums while counting from
zero, so the paired mean is sum/trades with a numerator carrying an
extra era's worth of difference. The paired sigma is worse: diffSumSq
inherits the same contamination, so the scan reports a tighter or wider
spread than it measured depending on what the abort happened to be
holding.

That is the SAME arithmetic that failed its own acceptance test in
b5e22a1, where the reported gain turned out to be monotone in timeout
share. This is not that bug - it needs a shutdown mid-era to fire - but
it lands on the same number, and any geometry reading taken from a
session that was stopped and restarted is suspect.

SGeometryScan now owns all ten with one Reset(). Both sites call it.
A partial reset is no longer something that can be written: there is
one door, and it clears everything behind it.

The struct initialises itself, so the ten constructor-initialiser
entries in Lifecycle.mqh are gone too - MQL5 cannot list struct fields
there, which is a second reason ten loose members was the wrong shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:49:09 -04:00
AnimateDread
042f20bdb9 fix(barriers): cap the horizon at what the close-all actually grants
The diagnostic shipped in de382bb came back off both live charts and
confirmed the arithmetic exactly:

  CLOSE-ALL BUDGET - flattens every position every 29 bars ... an entry
  landing anywhere in the cycle gets 15 bars on average. The horizon
  ladder just granted 128.

So the ceiling the ladder was rejecting rungs against - BARRIER_HORIZON_MAX,
384 - never bound anything, while the one that does bind was invisible to
it. SnapHorizonToLadder and the scale ladder's fitsH test now both read
EffectiveHorizonMax(), which is the measured close-all cycle. One
function, so the ceiling cannot be lowered in the snap and left high in
the rejection test.

The CYCLE, not the 15-bar mean: a Monday entry really does get the whole
cycle, and rejecting on the mean would invent a second criterion where
the design deliberately has one ceiling and reports the milder snap-down
truncation instead of rejecting on it.

Expect the ladder to pick a NARROWER pair, which is what the MEASURE
objective already asks for - min provable EV grows as width squared, and
USDJPY's 6.00*ATR target was being asked of a trade that lives ~11 bars.

"Schedule off" is cached; "not enough bars loaded yet" is not. Caching
the latter would restore the 384-bar ceiling for the whole process
because one early call landed before history arrived.

RE-KEYS EVERY FINGERPRINT - the horizon is a label parameter, so this is
a full retrain on both charts. Done now because both are at era 0 after
a fresh deploy, which is the cheapest this change will ever be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 08:32:13 -04:00
AnimateDread
de382bbf88 diag(barriers): the horizon the geometry is sized for does not exist
Every label timeout on both live charts was the scheduled close-all and
none was the horizon. Not "mostly" - all of them:

  USDJPY  14417 of 14417 timeouts ended by the close-all
  SP500    2434 of 2434

targetDayOfWeek is CLOSE_FRIDAY, so every position is flattened weekly.
A trading week is ~30 H4 bars and an entry lands uniformly inside it, so
the average bar is labelled under ~15 bars of runway. The horizon ladder
granted USDJPY 96 and SP500 32, and the SCALE ladder rejects rungs
against BARRIER_HORIZON_MAX (384) - a ceiling that never binds while the
one that does is invisible to it. USDJPY's chosen target is 6.00*ATR,
asked of a trade that lives ~11 bars: 78.6% of labels come back Neutral,
the base rate collapses to 14.0%, and no model can clear a 33.4%
break-even against a label that mostly cannot resolve.

The close-all itself is correct and must stay - it is what the account
actually does, and 3e467f9 put it into the labels for that reason. What
is wrong is that the geometry deriver has never been told about it.

This commit only MEASURES it. MeasureCloseAllBudget() walks the real bar
series (session- and DST-correct, not arithmetic on a nominal week) and
returns the cycle length plus the mean an entry gets; a CLOSE-ALL BUDGET
line prints both next to what the ladder granted. No geometry changes:
the horizon is a label parameter, so capping it re-keys every
fingerprint and costs a full retrain on both charts. That is the
operator's call, and it should be made against this line rather than
against my arithmetic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 16:24:14 -04:00
AnimateDread
b5e22a1e34 fix(geometry): a free zero made "never resolve" the winning geometry
The CANDIDATE GEOMETRY line shipped in 05f1a53 said per-candidate
geometry beats the global pair on every SP500 member at 2-3 sigma. It
does not. It said so because a bar that reached neither barrier scored
0 R, and the incumbent's mean is NEGATIVE (-0.07 to -0.21 R). Against a
losing baseline a free zero is a win, so the widest candidate always
came out ahead - and the reported gain ordered itself by timeout share,
not by skill:

  PAI  95.1% timed out -> +0.189 R   (head measured -2.42 sigma, HARMFUL)
  HYB  73.8%           -> +0.182 R   (head at chance, +0.68 sigma)
  CONV 61.8%           -> +0.163 R   (head measured -2.47 sigma, HARMFUL)
  LSTM 27.1%           -> +0.158 R   (head +1.67 sigma)

Monotone in the timeout share and inverted against the sigma gate. The
acceptance test written when this was built - "the sigma gate predicts
LSTM helps and CONV hurts; if the R difference does not reproduce that
ordering, something is wrong" - is what caught it.

A trade that reaches neither barrier is not worth zero. It is closed at
the horizon, which is what the scheduled close-all does live and what
SimulateTradeOutcome's timeout path already charges. So mark it there:
TripleBarrierLabel now publishes the signed close-to-close travel at the
last bar it actually visited (m_termTravelCache, same validity flag as
the excursion and ladder caches), and LadderOutcomeR prices a timeout
off it instead of returning false. A bar that cannot be evaluated under
BOTH pairs is now dropped whole - scoring one leg and defaulting the
other is the same bug in a smaller costume.

Second defect, same function: CandidateGeometryFor applied neither of
the floors the global derivation applies, so on USDJPY it chose stop
2.00 / target 1.00 - a 67% break-even, forbidden by the 1:2 policy
floor. c3daded in miniature: a selector optimising its own criterion
with no reference to the decision criterion. Both floors now apply, and
the ratio is re-checked AFTER the per-leg rung snap, which can lose it.

Also: the module weight was an unshrunk pooled win rate. USDJPY ConvLSTM
fired 19 times (2.0 effective), won 36.8%, and took module weight 0.37 -
41% of the ensemble's capable weight and the loudest voice on the chart,
off two effective observations. It also lifted the computed vote ceiling
to 26.3 against a 25 threshold, which is why THRESHOLD UNREACHABLE never
printed on a chart whose peak vote is 14 and whose practical ceiling
without that member is 18.8. The pooled rate is now shrunk toward the
coin-flip rate on the era's own OOS bars over 30 prior-equivalent calls,
and the tiers shrink toward the shrunk value rather than the raw one. A
member with ~300 effective calls moves by ~0.4pp; the 19-fire member
goes 0.37 -> ~0.15.

MEASUREMENT ONLY still - no order reads any of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 11:30:54 -04:00
AnimateDread
05f1a539c4 feat(geometry): measure per-candidate barriers against the one global pair
Stage 2a of the candidate-conditional geometry the record has named as next and
never built. MEASUREMENT ONLY - no order uses it yet.

WHY THIS AND NOT META-LABELING. Meta-labeling asks take-it-or-skip-it at fixed
geometry, and its verdict stands: real skill, 0 operating points clearing
break-even, and the 4070c5c retraction only moved that bar 1.4pp. The excursion
head, by contrast, just cleared at 3.5-4.6 sigma on LSTM across four eras and
beat the trailing-quantile incumbent. What is learnable here is MAGNITUDE, so
the lever is the geometry, not the veto. A per-candidate rung means a
per-candidate break-even, which a binary gate cannot express.

HOW IT IS SCORED. At each OOS call the same bar is resolved under the incumbent
pair AND under the pair this bar's excursion head would choose, and the paired
difference is accumulated in R with a 2-sigma test. Both legs come from the SAME
first-passage ladder - four array reads, no re-walk, exact even on the ~28% of
bars where both barriers were touched. Mixing the ladder with the price walk
here would measure the discrepancy between two of our own evaluators rather than
the effect of the geometry, which is precisely what f8ac10c had to unpick one
layer over.

The candidate pair applies the GLOBAL derivation's own rule per bar: stop at
BARRIER_SL_QUANTILE of adverse travel, target at the median of favourable.
Neither creates expectancy; what moves is the break-even, which is why the
report quotes R and never a win rate.

FREE VALIDATION. The sigma gate predicts LSTM helps and CONV hurts. If the R
difference reproduces that ordering across members, the head's usefulness is
confirmed by a second, independent measurement. If it does not, something is
wrong and this must not be wired to orders.

Two things caught while writing it, both silent if missed:
- The ladder stores TRAVEL FROM ENTRY, and the scan's mapping is
  risk = ladder + spread but reward = ladder - spread, so the two legs convert
  with OPPOSITE signs. The stop leg had the sign backwards.
- A GEOMETRY_BUDGET_MS wall clock, because this adds a feature-window build and
  a head forward per OOS call to a walk that already runs unchunked at era end
  on a single-threaded EA. That is the shape that got the process force-
  terminated on 2026-08-21. It stops scoring, never the replay, and the report
  prints how many calls it covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 08:29:59 -04:00
AnimateDread
b91c7b1f7a refactor(comments): box headers to stdlib length
The //| box blocks were excluded from 0b06f8e and 5efdb48 and were what
remained: 160 of them ran to 10+ lines, the longest to 88. Compressed to their
leading topic sentences - 5 lines for a function header, 8 for a file header -
keeping the box format and the standard MQL5 name/author lines verbatim.

Verified at the BYTE level this time, across every in-scope file: the list of
non-comment lines is byte-identical to HEAD and braces balance. The first check
compared a locale-decoded 'git show' against a UTF-8 read and flagged 25 files
that had not changed at all - every BOM and every non-ASCII line mismatched.

47,696 -> 40,665 lines in scope; comment share 38% -> 26%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:30:14 -04:00
AnimateDread
5efdb48de4 refactor(comments): stdlib comment style across the remaining in-scope files
Same pass as 0b06f8e, applied file by file: comment runs of 4+ lines compressed
to their leading topic sentences, capped at 4 lines, whole sentences only.
Warning sentences (NEVER / MUST / trap / would-have) survive the budget.

Every file was checked the same way before committing: the list of non-comment
lines is byte-identical to HEAD, and braces balance. No code was touched.

Panel/, Enumerations/ and the already-terse System headers needed little or
nothing - PooledGate, TradeChecks, BinomialStats and Random came through with
no blocks over the threshold at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:25:52 -04:00
AnimateDread
f8ac10c808 fix(replay): the exit replay held trades through the Friday flat that the label and the live EA both close
The EXIT-POLICY REPLAY line reported an expectancy from SimulateTradeOutcome
beside a win rate read out of the label cache, and called them "the SAME
calls". Same calls, two different walks - and the walks did not agree.

TripleBarrierLabel stops at NextScheduledCloseAll (3e467f9); SimulateTradeOutcome
never called it, so the replay kept holding positions the live EA is flattened
out of and collected targets the label had already scored as cut. On SP500 H4
the simulation's implied win rate ran 2.2-3.4pp above the label's on identical
calls, and the timeout share read 0.8-1.3% because nothing was truncating the
horizon it walked.

That gap, plus 1.4pp of spread charged twice in CostAdjustedBreakEvenPct, is
the whole of the ~5pp the replay looked "off" by. It was not horizon timeouts,
which is what 4070c5c argued and this log disproved: solving E[R] = 3.008w - 1
+ t(1+m) on each row puts the simulation's zero-crossing at an implied 33.3%
against a frictionless 33.24% - it was internally consistent all along.

- SimulateTradeOutcome takes the close-all cutoff, same expression and same
  placement as the label's, falling through to the existing close-at-last-bar
  branch. Expect the timeout share to rise and expectancy to fall: the replay
  was optimistic.
- m_simTpHits counts this walk's own target-before-stop, printed next to the
  label's with the delta, so a future divergence is visible rather than
  inferable.
- The line prints all three break-evens and names the R convention. The
  frictionless figure is the one this expectancy crosses zero at, because both
  walks place the barriers off the spread-shifted fill.
- CostAdjustedBreakEvenPct is left alone: it still feeds the rung selector's
  BarrierMinReachPct, and moving that relabels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:08:26 -04:00
AnimateDread
20b7d9967e chore(test-run): defaults for the horizon-break-even measurement, and give the replay line a cadence
Two diagnostics on, neither in the fingerprint, so no model is re-keyed and the run resumes:

  VerboseMode          false -> true   per-era journal instead of every 25th
  Run_Alglib_Baselines false -> true   verifies 3d81fed and finally reaches the OOS scoring

And the fix that makes the run worth doing. ReportExitPolicyDivergence printed ONCE per run while
vote exits are off, justified by "then it is arithmetically guaranteed to agree with the
certificate". That is the claim 4070c5c disproved: the certificate scores win rate against a
GEOMETRIC break-even while this line replays the real payoff including horizon timeouts, and the
two disagree by ~6.5pp. The timeout share is a MEASURED quantity that moves with geometry and
volatility - one print per run is the wrong cadence for it. Now on TrainLogDue(), the same density
as every other era line, with m_exitReplayReported still guaranteeing at least one.

What to read: BREAK-EVEN geometric X% vs horizon-aware Y%, and the timeout share and mean R beside
it. Those two numbers decide whether LiveMetaGate and BarrierMinReachPct get repointed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 20:13:03 -04:00
AnimateDread
4070c5c9bd feat(breakeven): the break-even every layer scores against prices a trade that always resolves
CostAdjustedBreakEvenPct is risk/(risk+reward) and has no horizon term. It is the win rate a trade
needs when it is CERTAIN to end at one barrier or the other. SimulateTradeOutcome has an explicit
branch for the case where it does not - runs out of horizon, closes at the last bar seen for
whatever P&L that is - so on this label geometry the figure describes a different trade than the
one being replayed.

The gap is measurable and large. Across 21 exit replays today on SP500 H4 the geometric figure read
34.5% while the EA's own R simulation crossed zero between 27.4% (lowest positive) and 28.9%
(highest non-positive). Independent corroboration: the zero-skill reference, computed empirically
over every scored bar as max(winLong,winShort)/bars, reads 25.4% - add cost and it lands on the
same ~28%. The geometric number is the outlier, and every edge printed against it was ~6.5pp too
pessimistic: LSTM's 30.6%-win era reported -4.0pp while its replay returned +0.075 R on the same
trades.

With a timeout share t paying a mean m R apiece, expectancy is w(1+RR) + t(1+m) - 1, so

    w* = (1 - t(1+m)) / (1 + RR) = CostAdjustedBreakEvenPct x (1 - t(1+m))

which needs no new geometry - the existing figure already carries 1/(1+RR).

This commit MEASURES ONLY. The replay now separates timeout exits from barrier exits and latches
t and m for the next era to read (the accumulators are zeroed at era start and filled at era end,
so a mid-era reader sees zero trades and would fall back forever). Both break-evens print side by
side on the replay line with t and m beside them, and the threshold line's REPORTED edge - which
selects nothing - switches to the horizon-aware figure so the operator stops reading a wrong sign.

DELIBERATELY NOT CHANGED: LiveMetaGate's veto and the rung selector's BarrierMinReachPct still read
the geometric value. Both are decisions - the second re-derives geometry and therefore relabels -
and t and m have so far only been inferred from a zero-crossing, never seen on a log. One era of
this instrumentation settles that.

The file already contained the argument, one branch away, in the vote-exit comment: a vote exit
produces a CONTINUOUS payoff, not a win or a loss, and that is why an exit-aware gate cannot go on
scoring win-rate against a fixed break-even. A horizon timeout is the same thing, and unlike vote
exits it is on by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:26:25 -04:00
AnimateDread
667f2bcb6b revert(labels): drop the one-sided exit target; measure the calibration drift instead
Reverts a863796 on the operator's call - "unnecessary complexity". It was
right about the mechanism and wrong about the priority: it re-cut the classes
for a case the measured verdict never reaches (SP500 H4 reads "both sides" at
the derived geometry), while the drift that IS happening affects every chart
and every era. Recoverable from a863796 if a one-sided book ever becomes real.

Two pieces of it survive, both independent of the exit idea:

The drift verdict keeps reading m_winLongCache/m_winShortCache rather than the
collapsed label pair. That line reports always-long vs always-short win rates,
which is what the win caches hold - each side scored on its own barriers,
published before the collapse. The label pair carries only the side touched
first, so it undercounted long wins by the both-won-goes-to-short share. There
are zero both-won bars at any geometry with target >= stop, so this changes no
number today; it changes the wrong number to the right one.

And the .cfg gains nothing and loses nothing: the two appended ints go away
again, and they were the last fields, so a .cfg written by yesterday's build
still reads correctly - the loader simply stops before them.

WHAT THE REVERT MAKES ROOM FOR. The operator's actual requirement is that the
model reproduce the label distribution the scan measured, and nothing in the
pipeline ties it to that. The loss trains on a rebalanced sample and the
abstain rate is owned by a margin threshold fitted on EDGE, so the call rate
and the label prior can drift arbitrarily far apart - and did, invisibly:
at era 1350 the models call Buy on 20-28% and Sell on 22-32% of bars against
a scan-measured 2.1% and 4.8%. Roughly a 10x over-call, and not one line in
the journal said so.

The era line now carries it:

  CALIBRATION calls vs true rate Buy 28% vs 2% (14.0x) Sell 32% vs 5% (6.4x)
  Neutral 40% vs 93% (0.4x)

Reported as a ratio because that is the readable number - 1.0x is calibrated.
This is deliberately a measurement and not yet a correction: matching the
label rate would put coverage near 7%, below the ensemble gate's own 12.4%
coverage floor, so calibration and the gate are in direct conflict and which
one yields is the operator's call, not mine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 10:14:26 -04:00
AnimateDread
a86379621c feat(labels): on a one-sided book the blocked side's class is retargeted from an entry it can never take to the EXIT of the one it holds
User request: "when an asymmetry is noticed in a market (like sp500 upward
drift) ... it does not need to predict shorts, but exit points. a sell signal
needs to be preceded by a buy so that it can say I predict we must close that
long."

Until now a LONG_ONLY verdict only BLOCKED short entries. The network went on
being trained to predict them - a third of its output capacity spent learning
an answer the direction policy guarantees it can never act on, while the
question the book actually faces (when to get out of the long) was never
asked. The two are not the same event: "a short pays" needs price to travel
the SHORT's target before the SHORT's stop, and at any geometry where reward
!= risk that is a different bar from "this long hits its stop first". The exit
is the second one.

So on a one-sided book TripleBarrierLabel re-cuts all three classes around the
only position the book can hold: Buy = it reaches its target, Sell = it
reaches its STOP first, Neutral = the horizon expired with it still open. Both
come off the allowed side's own barriers, which the walk already computed -
this reads longLost where it used to read shortWon, so it costs nothing.
Label lifespan and the timeout flag follow the allowed side too, so the
overlap correction is sized on the window this label actually spans.

DECIDED ONCE, AT ERA 0, AND PINNED. m_exitTargetSide goes in the .cfg beside
the derived geometry under the same doctrine and for the same reason: it
decides what Buy and Sell MEAN, and a target that moved mid-run would retrain
a fitted model against something it never saw. A .cfg from before this ends
early and reads 0/0 - "not decided, symmetric" - which is exactly what every
existing model was trained as, so nothing needs migrating. The weights
fingerprint keys on the INPUT only (explicit Long only / Short only); under
Intelligent the measured verdict must never reach a filename, or the model is
orphaned the moment more history downloads.

THE DRIFT VERDICT HAD TO MOVE OFF THE LABELS FIRST, and it turns out it was
measuring the wrong thing anyway. It counted m_labelCacheBuy/Sell and called
them "always-long vs always-short win rate", but the label pair is the
COLLAPSED first-touch verdict: a bar where both sides reached their target
carries only the side touched first, so long wins were undercounted by the
both-won-goes-to-short share. m_winLongCache/m_winShortCache are the actual
per-side win rates, published before the collapse, and that is what it reads
now. Necessary as well as more correct - deriving the verdict from labels the
verdict shapes is a feedback loop, since Sell-as-exit is near complementary
to Buy and would close the very gap that produced it. The gap's SE now leans
conservative rather than anti-conservative for the same reason.

LIVE. The retargeted class is wired to close the position, or training it
would be pointless: CheckClosePosition's "never vote-exit a certified
position" rule keeps governing symmetric books and gains a one-sided
exception, and the replay reads the identical rule through one
LiveVoteExitThreshold() so certified and traded cannot describe different
policies. Armed only when the operator picks a close threshold
(Signal_ThresholdClose ships Disabled) AND the model's own pin says its
blocked-side class means "close" - a model trained symmetric never fires it,
whatever the verdict has since become. This does trade a different game from
the one the win-rate certificate grades; the era's EXIT-POLICY REPLAY line
already reports expectancy in R for exactly this case and says so in words.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:02:50 -04:00
AnimateDread
4346dd3c24 refactor(stdlib): the vote thresholds are ints on the library's scale, not "confidence %"
The MECHANISM was already stdlib and is untouched: ThresholdOpen() ->
m_threshold_open, tested as `m_direction >= m_threshold_open` exactly as
CExpertSignal does it. What was wrong was the presentation. Both inputs
were preset ENUMS labelled "Min confidence to open/close (%)", which
names the wrong quantity - m_direction is a WEIGHTED MEAN OF PATTERN
WEIGHTS, not a probability, and nothing in this path is a confidence.

They are now plain ints named the way the MQL5 wizard names them:

  input int Signal_ThresholdOpen  = 25;   // [0...100]
  input int Signal_ThresholdClose = 101;  // [0...100, 101 = never]

Values are exactly what shipped, so behaviour is unchanged. 101 rather
than the library's default of 100 for close: a weighted mean of pattern
weights cannot REACH 101, which is how the shipped config disables the
vote exit, and quietly lowering it to 100 would re-arm a live exit route
as a side effect of a naming change.

VOTE_CLOSE_PRESETS is deleted (its only user is gone). PERCENTAGE_PRESETS
stays - MinRecall genuinely is a percentage.

** ACTION NEEDED ON DEPLOYED CHARTS: the inputs are RENAMED, so saved
.set files no longer match and charts fall back to the defaults above.
Those defaults are the current shipped values, so a chart on 25/Disabled
needs nothing; a tuned one does.

Comment cleanup in the same pass, and this part was not cosmetic - three
blocks documented mechanisms that no longer exist:
- the AI early-exit route (deleted in 38a12a2) described as live and
  still firing every bar;
- the m_lastNonNeutralSignal alternation gate (removed 2026-08-01)
  described as consuming the AI's vote;
- 16 lines of VOTE_CLOSE_PRESETS documentation orphaned by that enum's
  deletion, ending with "see that enum's note directly above" pointing
  at nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:39:01 -04:00
AnimateDread
77594ef5fb refactor(stdlib): one quantile definition, from Math\Stat
The codebase had THREE conventions for the same statistic. AltData took a
true median; the barrier horizon and the derived input window took the
upper of the two middle values; the MI terciles and the barrier stop
ladder used nearest-rank indexing. All four now go through MathMedian /
MathQuantile, which is R's type 7 and the library's one answer.

  System\AltData.mqh          column median  -> MathMedian (exact, no change)
  AIBase\Labels.mqh           swing median   -> MathMedian
                              leg-range med  -> MathMedian
                              stop ladder    -> MathQuantile, read in one call
  AIBase\Topology.mqh         window median  -> MathMedian
  AIBase\AutoTune.mqh         MI terciles    -> MathQuantile + MathMin/MathMax
  Signals\SignalSessionFilter DST last Sunday-> CDateTime::DaysInMonth()

gaps[]/legs[] change from int to double so MathMedian can read them; the
values are bar counts either way.

VALUES MOVE. Even-sample medians shift by half a bin and the quantile
reads interpolate, so the barrier geometry and the derived input window
can land on different rungs - re-keying fingerprints and forcing a
retrain. Accepted deliberately: stdlib consistency was the ask, and three
private conventions for one statistic is what it buys out.

Two YAGNI finds fell out of the ladder rewrite. MathQuantile sorts its own
copy, so DeriveBarrierGeometry no longer sorts up[]/dn[] in place - which
means upUnsorted[], a full array copy kept only to undo that sort, is
gone. ArraySort(up) had no consumer needing order at all; it was pure
work. The library call also gets a failure guard the hand-rolled indexing
never needed but the ladder read does.

Verified while here: Math\Stat\Math.mqh's MathAbs/MathMax/MathSqrt/MathPow
and friends are ARRAY overloads, not scalar redefinitions, so pulling it
into the translation unit shadows no builtin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:16:03 -04:00
AnimateDread
29c82ad50b refactor(dry): one binomial arithmetic for every "is this edge real" test
The formula p(1-p)/n was transcribed nine times across six files - the two
deploy gates, the two edge floors, the collapse recall floor, the barrier
rung ladder, the inference bin SE, the pooled inverse-variance weights and
both detectability reports. System\BinomialStats.mqh now holds it once, as
free functions with no class dependency, so the god-class declaration does
not grow to host pure math.

  BinomialVar(p, n)                      p(1-p)/n
  BinomialSEPct(p, n)                    100*sqrt(p(1-p)/n)
  BinomialCallsForEdge(p, edge, sigmas)  the same, solved for n
  NormalUpperTailQ(z)                    Q(z), via Math\Stat\Normal.mqh
  SidakFamilyP(z, N)                     1-(1-Q(z))^N

Value-preserving by construction: rates go in as probabilities so no call
site gained a *100/100 round-trip, and BinomialSEPct is written through
BinomialVar so the multiply order is the one it replaced. Every degenerate
guard each site carried (p<=0, p>=1, n<=0) now lives in one place and
returns the 0 those sites already treated as "no bar to clear".

CExpertSignalAIBase::NormalUpperTail is gone; NormalUpperTailQ replaces it.

What consolidating SURFACED, and is deliberately NOT changed here: the two
Sidak selection gates compute their SE on the RAW call count, while every
other SE in the project deflates by EffectiveSampleSize() for triple-
barrier label overlap. That makes them the most permissive test in the
codebase, by ~sqrt(mean label lifespan). Correcting it tightens a live
deploy bar, which is a policy decision, not a refactor - flagged in the
code at both sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:50:24 -04:00
AnimateDread
2be2970434 fix(topology): the capacity budget counted overlapping bars as independent examples
EstimatedInSampleBars() returned raw bars (11372 on SP500 H4) and every derived
capacity decision spent that: first-layer width, conv filters, LSTM hidden size.
But triple-barrier labels overlap - mean lifespan 9.4 bars - so the label cache line
on the same run already reports those bars are worth ~1210 independent observations.
Sizing a network against RAW bars while grading it against EFFECTIVE ones is two
subsystems disagreeing about one sample, and it disagreed in the dangerous direction
because the capacity side was the optimistic one: the warning's "roughly 1.1 weights
per training bar" is nearer 11 per independent observation.

EffectiveSampleSize() has existed since 2026-08-17 and is applied at eight sites, all
of them statistics. This adds the ninth, in the one place that decides how many
parameters get fitted. Applied inside EstimatedInSampleBars() rather than at the call
sites, because that function exists precisely so the three stages spend one budget.

SELF-ENABLING AND THEREFORE INERT WHERE IT MATTERS MOST, which is why this is two
changes and not one. MeanLabelLifespan() is 1.0 until a label cache has measured
something, so on a model's first build - before any label exists - the deflation is
correctly the identity: an unmeasured overlap must not invent a shrink. A fresh
attach constructs a fresh object, so its counters are zero too; only a mid-session
weights reset carries real evidence into a rebuild. That is deliberately safe (no
attach can now re-derive a narrower topology and discard trained weights) but it
would have left the first build - the case you most want the truth for - quoting the
flattering figure. So ReportDetectability now restates capacity against the effective
sample at the first moment L is real, for the topology already pinned. It re-sizes
nothing; it reports what was bought. Placed ABOVE that function's break-even guard on
purpose - a degenerate geometry is exactly when you want to know the net is
over-parameterised, and "it only fires for sane configs" is how the 2026-08-18
IS-error stop managed never to fire at all.

The warning also names its basis now (independent observations and L, or an explicit
"overlap NOT YET MEASURED, this is an UPPER BOUND"), so a flattering number can never
again read as a measured one.

Also factors FirstLayerFanIn() out of ComputeFirstLayerWidth so the capacity REPORT
charges for exactly what the capacity DECISION charged for - same reason
RequiredHorizonBars was factored out after the 2026-08-17 divergence - and makes
MeanLabelLifespan()/EffectiveSampleSize() const so the const budget path can call them.

Verified: no recursion (EstimatedInSampleBars -> EffectiveSampleSize ->
EstimatedInSampleBarsRaw, which computes from Bars() alone); both new StringFormat
sites hand-counted (basis 3/3 and 1/1, CAPACITY 10 specifiers / 10 arguments).

NOT COMPILED - user compiles in MetaEditor.
2026-08-19 18:43:48 -04:00
AnimateDread
caad156464 fix(geometry): the ratio raise was reported against reachability, not bounded by it
First clean derivation after 6f2def0 produced stop 1.22 / target 4.86 = 1:4 on
SP500 H4, and the labels came out Buy 4.3% / Sell 6.1% / Neutral 89.6% - a 20.7:1
imbalance, against 3.4:1 at 1:2 and 1.5:1 at 1:1 on the identical 10601 excursions.
That is the class-collapse regime, not a geometry.

c3daded proposed ONE leg-implied ratio per rung and left the reachability floor to
REJECT the rung, with a comment claiming the raise was "bounded" by it. Rejection is
not a bound. The 5.09*ATR median leg proposed 1:4 at every stop, all seven rungs then
missed their own floor (0.7%-9.6% against 12-20%), the no-rung-clears fallback fired
and re-proposed the same 1:4 at the tightest rung - and printed a WARNING predicting
exactly the rare positive class that followed. The system diagnosed itself correctly
and had no authority to act on it: the same shape as the scan-vs-deriver split
c3daded fixed one layer up, reintroduced one layer down.

Why the full leg overshoots: a ZigZag leg is pivot-to-pivot travel and an entry is
not a pivot - it lands inside the leg, with roughly half of it left on average.
Sizing the target at the whole median leg asks the market to deliver, from an
arbitrary bar, the entire move it usually makes between extremes.

Rather than assume the half and hard-code a factor, the raise now steps DOWN the snap
ladder (5 -> 4 -> 3 -> 2.5 -> 2) until the first-passage ladder says the target is
actually reached, stopping at the 1:2 policy floor because that is risk policy and
not a measurement. The legs still raise the ratio wherever the travel supports it;
the market decides how far. Applied in BOTH the rung loop and the fallback branch -
omitting the fallback is what actually shipped the 20.7:1 labels, since that branch
runs precisely when no rung was reachable. Rung rows now print
"[legs proposed 1:R, unreached]" so a walked-back raise is visible as one.

Verified: BarrierStepDownRr is strictly decreasing and bottoms at the floor, so both
loops terminate; rung-row StringFormat re-counted at 18 specifiers / 18 arguments.

NOT COMPILED - user compiles in MetaEditor.
2026-08-19 17:04:32 -04:00
AnimateDread
c3daded397 feat(geometry): 1:2 becomes a FLOOR the swing legs may raise, and the scan can no longer undercut it
Two coupled changes, both from measurements in today's SP500 H4 log.

1. THE SCAN WAS OVERRIDING THE DERIVER ON THE WRONG OBJECTIVE.
   At 13:37:47 DeriveBarrierGeometry produced stop 1.21*ATR / target 2.41*ATR - break-even
   33.3%. Thirty-seven seconds later the barrier-geometry scan adopted 2:2 - break-even
   50.9% - because it carried 0.0143 nats of entry-time information against the configured
   pair's 0.0075, and cleared its family-wise gate. Information is not expectancy, and the
   scan says so itself; nothing checked what the adoption did to the operating point. It
   did this: the fitted thresholds immediately after read 38.8% win vs 50.9% break-even
   (-12.2pp) and 48.3% vs 50.9% (-2.6pp), where the earlier model on this instrument at a
   1:2 geometry had fitted +1.8pp. The deriver applies the ratio as user RISK POLICY; a
   scan that can crown 1:1 makes two subsystems disagree about one geometry - the same
   split this file already fixed once for the clamped-horizon rule. The scan now enrols and
   crowns only pairings at or above the floor; sub-floor pairs are still scored and printed
   (marked 'r') so the choice stays auditable. This is NOT the min-RR rule removed on
   2026-08-09 - that one guarded a rejection filter that no longer exists.

2. THE RATIO IS A FLOOR, NOT A CAP (user: "the ratio of 1:2 is a minimum that I want, but
   it should not cap to that if the average zigzag moves gives more room").
   BARRIER_TARGET_RR -> BARRIER_TARGET_RR_MIN. ComputeBarrierHorizonBars already scanned
   ZigZag pivots for leg DURATION; it now harvests leg RANGE in the same pass - two
   properties of one object, so the horizon and the target describe the same legs instead
   of two windows. The per-rung ratio is the floor raised toward median-leg/stop, snapped
   DOWN to a coarse ladder (2.0/2.5/3.0/4.0/5.0). The ladder is coarse on purpose:
   PooledGate pools only instruments whose structural break-even matches, and continuous
   per-instrument ratios would never match and would silently empty the pool.

   A leg is the right yardstick precisely because it owes NOTHING to the barrier - sizing a
   target off travel measured over the barrier's own horizon is the circular loop that ran
   EURUSD/USDCAD away to 14-31*ATR in 2026-08-07. The raise stays bounded by the three
   tests already in the ladder: reachability, the horizon ceiling (first-passage time grows
   with stop x target), and the cost fraction.

   Consequential fixes: the reachability floor was a macro keyed to the fixed ratio and is
   now BarrierMinReachPct(rr) evaluated per rung (a raised ratio has a lower break-even, so
   a fixed floor would be the wrong strictness); the detectability break-even likewise;
   PooledGate now writes and matches the ACTUAL ratio (TargetRR()) rather than the floor.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:16:33 -04:00
AnimateDread
6717509c8b fix(build): four compile faults - one was a SILENT enum collision that inverted the direction policy
Reported by the user's MetaEditor compile of f64e0f8 (26 errors, 4 warnings). The four
warnings mattered more than the errors.

1. INTELLIGENT WAS TWO ENUMS. MONEY_MANAGEMENT_STRATEGY::INTELLIGENT (=1) is declared
   BEFORE TRADING_DIRECTION::INTELLIGENT (=3) in InputEnums.mqh, so MQL5 resolved every
   'tradingdirection == INTELLIGENT' to the MM member and converted it to value 1 =
   TRADING_DIRECTION::LONG_ONLY. Wrong in both directions at once: selecting Intelligent
   (3) matched NOTHING and silently traded both sides, while selecting Long only (1)
   matched and handed the decision to the measured drift verdict - which can answer
   SHORT_ONLY, so the one setting that must never go short could have. Reported by the
   compiler as a WARNING only, never an error. Renamed to DIRECTION_INTELLIGENT; the
   VALUE stays 3, so saved .set files are unaffected. Swept every enum in the repo for
   sibling collisions (38 enums, detector validated against the pre-fix source, which it
   flags): none remain.
2. g_warriorMetaGate sits above the class it points at - added the forward declaration,
   the same pattern g_warriorEnsemble already uses in ExpertSignalAIBase.mqh.
3. The broker-time rename (b63e39f) never reached BufferNewTickSignal's PARAMETER or its
   two call sites: the local became brokerTime, the parameter stayed gmtTime, and the
   body was rewritten to read brokerTime. All five sites now agree.
4. ConfigureAISignal calls IsMetaTarget() from a free function - moved it to the public
   section (identity, not an implementation seam); the other meta seams stay protected.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:16:45 -04:00
AnimateDread
1445f175ce feat(consistency): the five review flaws fixed - training wears the live constraints, the gate wears the policy
1. Labels and the exit simulator go through the broker's stop-distance
   check: risk/reward widen to SYMBOL_TRADE_STOPS_LEVEL exactly as
   TCAdjustStops does at order time - the M5/tight-ATR case where live
   trades ran wider geometry than training measured. Current stops level
   stands in for history (like the spread); measured quantity, so it
   does not key the fingerprint.
2. The Intelligent drift verdict moved into RefreshDriftVerdict(), which
   RESCANS the label cache and now runs at every era end beside
   RankTiersFromOos - era-cadence instead of waiting for rare full
   rebuilds. Prints only on change.
3. Session filter is any-broker: sessions defined on their financial
   centres' civil clocks (London 08-16 Europe/London, NY 08-17
   America/New_York, Tokyo 09-18 Asia/Tokyo), converted to UTC by each
   centre's own computed DST rule (EU last-Sun-Mar/Oct, US
   2nd-Sun-Mar/1st-Sun-Nov), then to broker time by the MEASURED
   server-vs-GMT offset (half-hour brokers included). Windows may wrap
   midnight in broker time - the interval test handles it. Replaces the
   EET-hardcoded anchors, which were correct on exactly one broker and
   got Tokyo wrong by an hour each European summer.
4. The current-session-table-for-history caveat resolved by analysis:
   the bars bound the error - a too-late assumed close meets no bars
   (zero error), a too-early one truncates conservatively (<=1h, never
   optimistic, cannot manufacture edge). Documented at the site.
5. The ensemble deploy gate mirrors the direction policy: blocked-side
   fires are not fired bars (certified == traded), the zero-skill
   reference uses only ACHIEVABLE baselines (always-short is not a
   strategy a long-only book can run), and one-sidedness BY POLICY is
   not degeneracy - the two-sided requirement applies only when both
   sides are allowed. Sell predictions keep their other jobs (exit
   triggers, consensus dilution) untouched.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:14:01 -04:00
AnimateDread
1a46dfdad9 feat(sessions): market-hours entry gate + "Market close" close-all option, both live from the symbol's session table
Two user requests, one authority: SymbolInfoSessionTrade, read fresh on
every call so DST and per-symbol schedule changes track themselves.

- WarriorMarketOpenNow(): CheckOpenPosition refuses entries outside the
  symbol's trading sessions (Sunday reopen, index CFDs' daily breaks) -
  a vote can no longer fire into a closed book and collect a broker
  error. ENTRIES ONLY: exits, SL/TP and the scheduled close-all stay
  unguarded - closing risk must never be blocked by a session boundary.
- CH_MARKET_CLOSE = 24 (appended, .set-safe): the close-all fires
  "Close-all minute" minutes before that day's LAST session close.
  Friday + Market close + xxH05 = flatten 5 minutes before Friday's
  actual close. Resolved identically in three places: the live executor
  (CExpertCustom::OnTick), the label walk's vertical barrier
  (NextScheduledCloseAll - the symbol's CURRENT table stands in for
  history; MT5 keeps none, and a fixed hour is wrong by more), and the
  fingerprint (the |CUT: token already carries hour=24, so switching to
  the dynamic mode re-keys the model exactly like any schedule change).

Training itself is deliberately NOT gated on market hours: weekend
compute is free and labels only ever exist on real bars - what the
session table gates is order placement and, via the close-all barrier,
what the labels may count as holdable.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:34:27 -04:00
AnimateDread
3e467f92e9 feat(labels): the scheduled close-all is now a vertical barrier in the label walk
User report: "I exit everything on Friday close to avoid weekend swap...
if the NN training thinks I hold over the weekend it could produce
inaccurate results" - it thought exactly that. TripleBarrierLabel walked
its full horizon (64 bars, mean lifespan ~18 H4 bars ~ 3 days) straight
through the scheduled flat, scoring trades the deployed EA is guaranteed
to have closed on Friday 23:45. SQX applies this rule when building
strategies; the EA's own labels did not.

NextScheduledCloseAll() mirrors CExpertCustom::OnTick's live check
exactly (same three inputs, same -1 disabled sentinels, same
CLOSE_EVERYDAY semantics, same server clock). The walk stops at the
first bar that does not END by the cutoff - OHLC cannot order the
tradable fraction of a partial bar, and ties go to the refusal, as
everywhere in this file. An unresolved trade at the cutoff times out to
Neutral, exactly as live would flatten it. Excursions, the first-passage
ladder and the label lifespan truncate with the walk, so the DERIVED
geometry is automatically sized to the tradable window - a target the
flat rule never lets price reach stops counting as reachable.

The prebuild census now splits timeouts: "horizon too short?" vs "ended
by the scheduled close-all" - different questions, different fixes.
Schedule disabled = no cutoff, exactly like live.

Models trained under weekend-blind labels are fitted to a different
target; charts with the close-all enabled (the default) should be reset
to retrain under the honest labels.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:17:14 -04:00
AnimateDread
348492fb3b feat(direction): INTELLIGENT trade direction - the measured drift picks the side(s)
SQX EdgeFinder precedent (user request): adjust for the drift instead of
fighting it. The 2026-08-19 telemetry found the models leaning SHORT
(Buy recall 21% vs Sell 40%) against a long-favored market (always-long
34.3% vs always-short 29.5% at the adopted geometry).

TRADING_DIRECTION gains INTELLIGENT = 3 (appended, explicit value,
.set-safe). It resolves at runtime from the label cache's per-side win
rates - the Buy/Sell shares ARE the win rates of taking every bar
long/short at the REAL stop/target with spread charged. A side is
dropped only when BOTH hold: the drift gap clears 2 combined SEs on the
overlap-deflated effective sample (EffectiveSampleSize - labels overlap
~18x), AND the weaker side sits below cost-adjusted break-even (a side
that still clears costs is kept; drift tilt alone is not a reason to
refuse a profitable side). Fails open to BOTH: unmeasured, tiny
effective n (<30), insignificant gap, or classic-only charts (no label
cache).

One resolution point - WarriorEffectiveDirection() - feeds all three
gates so they cannot drift apart: CheckOpenLong/Short (live entries),
the filtered-view sweep (a blocked side falls into the delete branch,
mirroring live), and the vote HUD's "-> TRADE" verdict. The verdict
re-derives at every label-cache rebuild, prints only on change, and is
computed even when the input is not Intelligent (marked informational).

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:10:05 -04:00
AnimateDread
4858507146 feat(vote): thresholds become confidence percentages, on ONE scale everywhere
User request: "the entry/exit thresholds are manual numbers, I would like
them to be confidence percentages, so the current 20 would be only 20%
confidence in a profitable trade."

WHY 20 WAS EVER SENSIBLE. Under UseDatabaseRanking both factors of a filter's
contribution are win rates: the pattern weight is that pattern's measured win
rate (UpdateSignalsWeights -> ApplyPatternWeight) and m_weight is the filter's
average win rate over its patterns, /100. Dividing the sum by the VOTER COUNT
therefore produced a mean of PRODUCTS of two win rates - a genuinely
60%-accurate filter firing a 60% pattern scored 0.60 x 60 = 36. The number was
never on a probability scale, so its magnitude meant nothing on its own.

Dividing by Sum(m_weight) instead makes it a weighted MEAN of win rates, which
is a win rate: result = Sum(w_i*p_i)/Sum(w_i). Every voter at 60% now reads 60;
MACD's double-divergence pattern (weight 100) voting alone reads 100. m_weight
stops being a discount on the probability and becomes how much a filter's
opinion COUNTS - which is what a module weight should always have been.
Default Min_Vote_Open 20 -> 50: not a tightening, the same bar re-expressed.

ONE SCALE, EVERYWHERE - the part that made this bigger than a rescale. Three
other places compared against a 0..1 softmax confidence and would each have
become a fresh currency mismatch the moment the input changed meaning:
  * the AI early-exit route (LiveSignedConfidence vs m_ai_exit_threshold) now
    reads m_lastAiVote - the AI filters' own weighted mean, undiluted by the
    classic side, which is the only reason that route exists - against the
    same m_threshold_close the averaged vote uses. m_ai_exit_threshold is
    retired rather than left dangling.
  * m_oosDecisionSeries now carries the vote, not the confidence, so the exit
    SIMULATION stops modelling a close rule the EA does not run.
  * ExitPolicy() clamped anything > 1.0 to zero. Passing the unscaled input
    through that would have silently switched vote exits off in the
    simulation while live went on running them - found before it shipped;
    the bound now tracks the scale.
LiveSignedConfidence() is deliberately untouched and still 0..1: MM sizing,
SL/TP scaling and the intelligent trailing want a model confidence, not a win
rate.

CALIBRATION CAVEAT, stated in the code where the claim is made: this is only a
real probability to the extent the pattern weights are. A pattern with fewer
than MIN_TRADES_FOR_WIN_RATE journaled trades keeps its DEFAULT weight - a
designed prior (25/50/75/100 for the AI tiers), not a measurement. Until the
signal DB fills, "60" means "the designed conviction of the patterns that
fired". Closing that gap is the next commit.

Also corrects VOTE_CLOSE_PRESETS' comment, which documented the two scales
this removes.

NOT COMPILED - user compiles in MetaEditor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:52:08 -04:00
AnimateDread
65a3e4e877 fix(chart): a purge that reports "zero leftovers" was only ever checking its own list
2026-08-17 21:58: all three charts hit "Abnormal termination" ~5.3 s into
OnDeinit with NO cleanup-timings line - the teardown was starved again. The
22:00 init purge then removed 993 / 1373 / 1557 stranded objects and reported
ZERO by-name leftovers on every chart, and the charts still came up with
duplicated panels. "Nothing matching our prefixes remains" and "the chart is
clean" are different statements and only the first was being made.

Three changes, in the order they matter:

1. WHY the teardown starved, and it is a gap in ad80e0b. StartLabelCachePrebuild
   runs ResizeBuffers + RefreshData over the FULL study window (33,984 bars on
   XAUUSD), unchunked, and OnDeinit cannot begin until it returns. Normally a
   once-per-run cost. That night SP500 and XAUUSD LSTM were wedged in the "cache
   invalidated at era start" loop, which calls it on EVERY Train() call - two
   members re-preparing tens of thousands of bars indefinitely. The terminal
   closed into that. Guarded now, plus a resumable guard in the prebuild chunk
   loop (the tally pass after it is not chunked).

2. Catch-all "Warrior" prefix in WarriorChartPrefixes. Every family this EA
   creates is named Warrior* except the arrows (WarSig_), so one bare prefix
   covers the three named entries AND anything a rename or a stale .ex5 left
   under a name nobody remembers. Still a prefix delete, never
   ObjectsDeleteAll(chart) - the user's own drawings are not ours to remove. Does
   not defeat skipArrows: "WarSig_" does not start with "Warrior".

3. The init purge now REPORTS the residue it did not claim, by name (up to 12).
   Not deleted - an unmatched object may belong to the user or another indicator.
   If a Warrior panel is visible and appears in neither the removed count nor
   this list, the prefix list has drifted a third time and the name is in the
   journal instead of being inferred from a screenshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:07:09 -04:00
AnimateDread
ad80e0bb57 fix(shutdown): make ExitPolicy public, and stop every long loop the moment MT5 asks
Two things, one of which was a compile error.

1. ExitPolicy() was declared in the protected block but is pushed in from
   Warrior_EA.mq5:770. Moved to public beside the other EA-facing setters.

2. Chart objects surviving OnDeinit. The 4,500 ms teardown budget is measured
   from the STOP REQUEST, not from OnDeinit's first line, and OnDeinit cannot
   begin until whatever is in flight returns - so a scan still running after
   _StopFlag is raised does not delay the cleanup, it SPENDS it, and the purge
   never gets its turn.

   New CExpertSignalAIBase::ShutdownRequested() = IsStopped() || m_shutdownInProgress.
   Deliberately NOT m_trainingStopRequested: that latches, and a latched flag
   would permanently disable scans that must run again on the next Start.

   Guarded, longest first:
   - TuneIndicatorsByFilter - per candidate, restoring the OPERATOR's settings
     on the way out (best[] is mutated in place; the tuner otherwise keeps the
     last trial's parameters, which nothing chose).
   - ReportBarrierGeometryScan - per pairing, breaking to ONE restore point so
     m_barrierScanLiveLabels can never be left true (that makes ComputeLabelForBar
     read the last candidate's multiples as the configured geometry).
   - ReportFeatureLabelInformation / ReportExcursionInformation / lag profile -
     nulls ABANDON rather than truncate: fewer draws is not a smaller null, it
     is a wrong one, and p shifts toward significance. m_dirEvidence staying
     false is the safe direction.
   - SimulateExitPolicyOutcomes - zeroes its accumulators so the divergence line
     is dropped instead of latching a partial expectancy as the run's only report.
   - ReportGeometryExpectancyScan - per ladder rung.
   - HttpGet - one choke point for up to a dozen blocking WebRequests per
     first-pass Update(). An in-flight request cannot be cancelled; refusing to
     start another is the whole remedy.
   - PollTraining, OnChartEventHandler's study event, TuneIndicatorsAndTrain -
     entry points, so a queued event cannot open an era during teardown.
     TuneIndicatorsAndTrain's guard is the first statement, ahead of the
     m_tuneFilterDone / g_ensembleChartTuneDone latches.
   - OnTick / OnTimer / OnChartEvent.

   Training's own bar loops already honoured this (pass 1 per bar, passes 2/2.5/3
   yield on a 120 ms budget); the warm-up scans did not, and they are the longest
   uninterruptible stretches the EA has.

   StopTraining() is unchanged: the operator's Stop still finalises synchronously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:03:11 -04:00
AnimateDread
62a719f04c fix(consistency): one geometry authority, one exit authority, and the MI screen finally gets a veto
Consistency pass before a fresh deployment. Three places where two systems were
choosing the same thing and one of them silently lost.

1. THE GEOMETRY SCAN'S DECISION WAS INERT - measured, not suspected.

USDJPY, 2026-08-17:

  14:24:12.844  adopting barrier geometry 2:8 ... Relabelling and training on it.
  14:24:12.979  triple-barrier labels - stop 1.61*ATR, target 3.21*ATR ... this is what trains

It adopted 2:8 and trained on 1.61:3.21. ReportBarrierGeometryScan wrote only
m_sl_mode/m_tp_mode, and BarrierMultiples ranks the DERIVED pair ABOVE those ints -
so on any model carrying a derived pair (every model with a .cfg, including a fresh
one whose weights are gone but whose sidecar survived) the adoption changed nothing.
Worse, had it changed something it would have been undone immediately: the adoption
sets m_labelCachePrebuilt = false, and that prebuild re-runs DeriveBarrierGeometry at
era 0, which overwrites m_derivedSl/TpMult from the excursion quantiles.

ONE AUTHORITY: the derived pair, because it is what the labels read, what the deploy
gate certifies, what g_Derived*AtrMult places on the live order, and what the .cfg
pins across restarts. The scan now writes THAT (floored by MIN_SL_ATR_MULTIPLIER, the
same floor DeriveBarrierGeometry applies so the live stop can never be wider than the
labelled one), republishes to the bridge immediately rather than at the next era end,
and forces the sidecar to be rewritten. m_geometryAdopted latches it so the derive
pass the adoption itself triggers cannot overwrite it.

The scan outranks the derive for an evidential reason, not an architectural one: its
winner cleared a permutation test against the null of the MAXIMUM over every eligible
pairing, and it scores the incumbent derived pair as a peer in that same field. The
derive is a descriptive quantile read with no significance test attached.

BEHAVIOURAL CHANGE, and the reason to flag it before a fresh test: barrier geometry
will now actually move when the scan says so. Until today it never did.

2. ONE EXIT AUTHORITY, tied to whose certificate the trade was placed under.

CheckClosePosition had two routes. The AI early-exit reads the AI vote undiluted and
is exactly what the new exit replay reproduces. The blended route thresholds
m_direction, the average over EVERY filter including classic ones whose live votes
pass 3 never computes - so it can close a position the certificate never modelled,
and no replay can ever check it.

When g_DerivedSlAtrMult > 0 the AI's measured geometry is on the order, which means
the deploy gate's certificate is the reason the trade exists. In that state the AI now
governs the exit and the blended route is suppressed. Classic-only configurations are
untouched: there the blended route is the only exit opinion and stays exactly as it
was. Nothing moves at the shipped defaults either way (Min_Vote_Close = Disabled).

3. EDGEFINDER, SECOND HALF: THE MEASUREMENT NOW STEERS.

The MI suite has always printed its verdicts and then trained the direction target
regardless of what they said. That gap IS the difference between this and the
EdgeFinder discipline: measure what the market offers, THEN aim.

m_dirEvidence is set when EITHER the feature/label mutual information OR the
normalised excursion asymmetry clears its block-permuted null - an OR, because the two
look for the same thing by different routes and requiring both would reject on the
weaker of two independent measurements. Normalised asymmetry specifically, never the
raw one, which is the volatility confound.

Deploy - solo AND ensemble - now requires it. A run without it still trains, and keeps
its checkpoint: the research value is real and the measurement can be wrong. It simply
may not go live. Reported separately from the statistical gate because the remedy is
different: a failed selection test says train differently, this says look somewhere
else. Excursion SIZE keeps clearing where direction does not, and that is a
risk-control head rather than an entry signal.

For the ensemble the check is per-chart by construction - the MI suite runs once and
shares its outcome across members - which is the honest treatment: four models finding
nothing between them is not four chances at an edge, it is four fits to the same
absent information.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:43:20 -04:00
AnimateDread
94019f363e feat(gate): grade OOS calls on the exit policy actually in force, and move vote combining out of the members and into the orchestrator
Option (a) from the exit-policy question: the certified number must be the traded
number. Plus the modularity correction the user called for on 778b6c0.

1. VOTE COMBINING BELONGS TO THE ORCHESTRATOR, NOT TO A MEMBER.

778b6c0 fixed the last-writer-wins bug on g_LiveAISignedConfidence by having a
member average its siblings through g_warriorEnsemble. That trades a scheduling bug
for a coupling bug, and it is the wrong shape for this EA: every signal runs in its
own instance, minds its own state, and VOTES to the orchestrator, which is the only
thing allowed to combine opinions.

Replaced with a publish/aggregate pair in Variables\ConfidenceBridge.mqh whose split
is enforced by shape rather than by convention:
  - PublishAIVote(slot, conf)  - a member writes ONLY its own slot, reads nobody's;
  - AggregateAIVotes()         - called by CExpertSignalCustom::LiveSignedConfidence.
CExpertSignalAIBase::EnsembleLiveSignedConfidence is gone. The orchestrator also
republishes the aggregate into g_LiveAISignedConfidence, because the intelligent
trailing reads that global directly and must act on the same number the exit route
does rather than on a leftover from whichever member ticked last. A solo AI signal
owns slot 0, so the non-ensemble path is unchanged.

2. THE GATE NOW REPLAYS THE REAL EXIT RULE.

SimulateTradeOutcome() walks the same price series with the same fill/barrier/spread
convention as ComputeLabelForBar - deliberately by copy, so a disagreement between
the two can only be a policy effect and never a discrepancy between two pieces of our
own arithmetic - and terminates at the FIRST of stop / target / vote reversal /
horizon. Barriers are tested before the vote on the same bar: intrabar we cannot know
which came first, and the barrier is what the broker executes automatically, so
checking the vote first would credit the exit policy with escapes a real stop would
have taken out of its hands.

It runs AFTER pass 3, not inside it. A vote-flip exit for a trade entered at bar r is
decided by the model's output at bars r-1, r-2, ... - NEWER bars - and pass 3 walks
oldest-to-newest, so at the moment r is graded its own exit does not exist yet. Only
once m_oosDecisionSeries is complete over the whole OOS window can the replay run.
In ensemble mode that series carries the member's adjusted decision and the live exit
reads the ensemble aggregate, which is the coupling the user identified: an LSTM entry
really can be closed by the ensemble turning against it.

3. THE STATISTIC HAS TO CHANGE WITH THE POLICY, AND THAT IS THE REAL FINDING.

A barrier exit pays a fixed R. A vote exit pays whatever the close happens to be. So
the moment vote exits are enabled the payoff is CONTINUOUS, and "win rate vs
break-even" stops being a meaningful test - there is no fixed break-even for a
variable payoff. SimulateTradeOutcome therefore returns R rather than a bool, and the
replay reports expectancy in R with its SE taken from the R distribution (overlap-
deflated on the same EffectiveSampleSize doctrine as every other SE here), not from a
binomial.

This is the same class of error as win-based scoring in 2026-08-09: measuring a
variable-payoff process with a fixed-payoff statistic. Naming it now, while vote exits
are still off, is much cheaper than discovering it after they go on.

4. WHY THIS IS SAFE TO SHIP TODAY.

Min_Vote_Close ships Disabled (101, unreachable on both scales it drives) and reaches
the AI signal through the new ExitPolicy() setter as 1.01, which the setter turns into
0.0 = no vote exit. Under that policy every replayed trade resolves at a barrier and
the simulation is arithmetically the same trade the deploy gate already certifies -
they cannot drift. The report says so explicitly, and prints ONCE per run in that
state; when vote exits are on it prints every era, because then the divergence is the
thing to watch. Nothing about today's numbers moves.

The gate switchover is wired but dormant by construction: it becomes exit-aware the
moment the input is enabled, which is exactly what "the certified number is the traded
number" has to mean.

KNOWN LIMIT, stated rather than hidden: only the AI early-exit route is replayed. The
rule-based averaged-vote close (m_threshold_close) depends on every other filter's
live vote, which pass 3 does not reproduce, so a position the classic filters would
have closed is held to its barrier here. The replay therefore holds LONGER than live
and overstates barrier-reached outcomes. Faithful only while the AI is the dominant
vote - which is the configuration this is being built for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:25:57 -04:00
AnimateDread
d9f834d01d fix(buffers): revert the MA +1 - it asked for a bar that does not exist and stopped every chart
REGRESSION I INTRODUCED IN 1dda479, live for ~15 minutes.

CSeries::BufferResize -> CheckLoadHistory -> CheckTerminalHistory succeeds only when
Bars() >= size. Train() calls ResizeBuffers with barIndex == Bars(), so sizing the MA
buffer to barIndex + 1 asks for one bar more than the symbol has and fails the WHOLE
ResizeBuffers call. The log named it exactly:

  failed to get 50180 bars for USDJPY,PERIOD_H4     (Bars() = 50,179)
  failed to get 33983 bars for XAUUSD,PERIOD_H4     (Bars() = 33,982)

StartLabelCachePrebuild() then bailed on the false return and stayed silent, so the
only symptom was Train() reporting "arming the first label-cache prebuild" forever
with labelCacheBars=0 - the panel's "getting ready".

The premise was wrong, not just the arithmetic. The MA block reads GetData(idx) AND
GetData(idx + 1), and at the OLDEST bar that second read is SUPPOSED to fail - there
is no older bar to difference against. Rejecting that one bar is correct behaviour;
buying it cost the entire history.

Two more things, since the same defect had a second instance and no alarm:

- The Ichimoku pair (closeBars and m_Ichimoku, both barIndex + ichiKijun) is the same
  bug with a far larger constant, latent only because the feature is off. Both are now
  clamped to Bars(). The oldest ichiKijun bars then have no cloud, which that block's
  EMPTY_VALUE guard already handles per-bar - the right outcome.
- The prebuild's bare `return` on a false ResizeBuffers now says so once, naming the
  depth and Bars(). MQL5's own "failed to get N bars" was in the log the entire time,
  from a stack frame nothing connected to the prebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 14:12:47 -04:00
AnimateDread
45c9e211b3 feat(depth): prime -> settle -> sweep, and name which handle is short
"Max bars in chart" is set to Unlimited, so the static-terminal-limit reading in
1dda479 was wrong. Two other candidate causes are falsified too: the price series
is fully downloaded and flat (USDJPY 50,162 -> 50,163 over 80 minutes, i.e. one new
H4 bar), and the handles have been stable since 13:07 with zero windows for the 17
minutes after, so it is not download-in-progress and not handle churn. The MA period
tops out at 200 (ADIndicatorTuner MA_PERIOD_PRESETS) against ~50k bars, so it is not
indicator cost either.

What IS verified stays verified: CopyBuffer past the calculated depth fails outright
rather than short-reading, so the buffer holds nothing and every index reads
EMPTY_VALUE; m_MA is the only CiCustom whose block REJECTS on that (m_ADZigZag
neutral-fills, RSI/MACD/Ichimoku/ATR are built-ins); the wall is therefore feature 25
of every bar, exactly as the "24 of 832" stall lines said. And it is depth-correlated:
16k-bar charts train, 34k/50k get zero windows forever.

So the WHY is still open, and this fix does not depend on it. Per the user's protocol:
the request itself is the primer, so prime at full depth, then poll TunableBarsCalculated()
every 3s and hold the sweep until it stops changing (3 steady probes), then use whatever
it settled at. Bounded at 10 min, and a give-up is logged as a give-up so an abandoned
depth is never mistaken for a settled one. This supersedes 1dda479's clamp on the two
training paths, which snapshotted a value that may still have been climbing; the clamp
remains for the paths that cannot wait (inference/online/rescan/export, see 7e63a8b).

The load-bearing part is what does NOT happen while waiting: no sweep. A 50k-bar feature
scan starves the indicator threads the request just woke, which is how the failure
sustained itself for 40 minutes at a time - discard era, re-sweep, discard, which is the
0->100% oscillation on the panel.

Also adds IndicatorDepthReport(): per-handle BarsCalculated() on the priming, cap and
stall lines. The logs proved WHICH FEATURE died but never WHICH HANDLE was short, so the
cause had to be inferred - and was guessed wrong twice. The next occurrence reads it off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:44:27 -04:00
AnimateDread
7e63a8be01 fix(depth): route EVERY ResizeBuffers call site through one indicator-depth gate
1dda479 clamped the training sweep. It left five other paths asking the indicators
for a depth they cannot serve, and on a live account the quiet ones are worse than
the stall was - a stalled chart is visible, a chart trading on a degraded feature
window is not.

ServableBars(want, context) is now the single gate, and all six go through it:

  training sweep    clamp, floored at TRAIN_MIN_CLAMPED_BARS (below that a small
                    positive BarsCalculated is warm-up, which m_coldSweepTick owns)
  label prebuild    clamp - labels come from price/ADZigZag and would survive a
                    capped MA, but ResizeBuffers sizes EVERY buffer and a failed
                    CopyBuffer leaves m_MA EMPTY for the next reader, so this path
                    could silently re-break the block Train()'s clamp just fixed
  live inference    HOLD. Below `need` the swing block takes its degraded path and
                    inference runs on a different feature distribution than the model
                    was fitted on. This EA sizes real positions off that output, so
                    no signal beats a mismatched one
  online learning   HOLD, same reason and worse - this path WRITES to a live trading
                    model, so a mismatched (features, label) pair is not a wrong arrow,
                    it is a wrong weight update that compounds every bar
  chart rescan      clamp - SIGNAL_RESCAN_LOOKBACK_BARS is 5000 and MT5's smallest
                    "Max bars in chart" is also 5000, so this one is genuinely
                    reachable; uncapped it repaints the window all-Neutral
  research export   clamp before the emptiness test, so a capped symbol exports the
                    depth it has rather than writing a CSV with a dead feature block -
                    an artefact that looks complete and is silently wrong

Both HOLDs are insurance, not expected states: `need` tops out near 1,152 bars
(16 + 750 + 384 + 2) against a 5,000 floor on the terminal setting. They exist so
the failure mode is unreachable rather than merely unlikely.

Not changed: a genuinely SHORT price history still takes the old degraded path at
every site. That is pre-existing behaviour and narrowing it would mute charts that
trade today, so it stays a separate decision rather than a side effect of this fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 13:33:16 -04:00
AnimateDread
2b5d0f8355 feat(measurement): fix zero-skill denominator, publish the deploy bar, measure lifespan per rung, add a MEASURE scale objective
The last run could not have demonstrated an edge either way, and nothing in the
log said so. Four changes so it does.

1. THE ZERO-SKILL LINE DIVIDED BY THE WRONG DENOMINATOR. m_oosWinLongTotal resets
   every era; m_oosSamples only resets on a full model reset. So 'always-long %'
   decayed as ~1/era: a run whose true rate is 37% printed 1.2% at era 33 and
   0.0% at era 2219. This is the SAME bug already found and fixed for
   logBuyPredPct thirty lines above ('era-15 Buy:2% that was really ~30%'), left
   in the one line whose whole job is to be the reference every other number is
   read against. Correct at era 1, wrong everywhere after - including the '62%
   zero-skill' figure in the 2026-08-16 notes. Now per-era, and always-short is
   finally readable.

2. THE DEPLOY GATE STATES ITS OWN BAR. 'edge -1pp' era after era cannot separate
   'short by a hair' from 'short by an amount no strategy could cover'. The era
   line now prints the required win rate, the SE, the effective n and the
   lifespan it was deflated by; above 100% it says UNREACHABLE. At 4,738 OOS bars
   and L=75.6 there are ~63 independent observations, putting the bar near 66% at
   typical coverage.

3. LIFESPAN MEASURED PER RUNG. The first-passage cache already stores touch ages
   at every ladder level, so each candidate geometry's resolution time is
   readable without training on it - L-vs-width becomes a measurement across the
   whole ladder in ONE run rather than a second chart. Each rung reports L,
   n_eff, min provable edge and min provable EV.

4. SCALE OBJECTIVE IS PHASE-AWARE, defaulting to MEASURE. Width and detectability
   are opposed: labels overlap by L, L grows like m*k = width^2 at fixed ratio,
   so min provable EV ~ width^2 while the cost saving from width is only linear.
   Doubling width quadruples the smallest EV you can prove. DEPLOY (widest that
   clears reachability) is right once an edge is known; MEASURE (narrowest that
   keeps round-trip spread under BARRIER_MAX_COST_FRACTION_PCT) is right while it
   still has to be shown. The direction does not depend on the exponent, and
   item 3 makes the exponent checkable.

Fixed in review: m_lastRungLifespan is cleared on every LadderWinShare entry or a
rejected rung reports the previous rung's lifespan as its own; per-rung
detectability is labelled IS-sample based (the deriver may not see the holdout),
so absolute figures are optimistic while the ranking is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:37:05 -04:00
AnimateDread
d1ac18ebdb fix(labels): correct EffectiveSampleSize clamp order, share the horizon ladder, retract a false justification
Self-review of 1540ba8 against the FULL 6,930-era log rather than the first
three minutes of it. Three corrections.

1. EffectiveSampleSize() clamped in the wrong order. MathMax(2, MathMin(eff,
   rawN)) returns 2 when rawN is 1 - an effective sample LARGER than the raw
   one, shrinking the SE in exactly the direction the function exists to
   prevent. Floor first, cap at rawN last.

2. The horizon cap rejected on the CEILING only, and said so as though that
   made the label untruncated. It does not: the horizon ladder also snaps DOWN,
   so a pair needing 317 bars is granted 256 and is silently truncated without
   ever being flagged CLAMPED. Added SnapHorizonToLadder() / GrantedHorizonBars()
   and the scale ladder now reports "needs N gets M" per rung. Rejection stays on
   the ceiling alone - matching ReportGeometryExpectancyScan's '!' exactly, which
   was the point - because rejecting on the snap-down would select rungs for
   landing just above a ladder point rather than for anything about the market.
   ComputeBarrierHorizonBars' private copy of the ladder is gone; there is now
   one copy, which is the whole reason RequiredHorizonBars was factored out.

3. RETRACTED THE JUSTIFICATION IN 1540ba8's COMMENTS. That commit claimed the
   overlap correction was needed because the operating point's null-of-the-
   maximum gate fired on 47/73 Perceptron eras (64%) where a family-wise test
   should fire on ~5%. Those 73 fits were the first three minutes of a
   six-and-a-half-hour run. Over the full run:

     PAI  47/3214 = 1.5%    HYB  30/1200 = 2.5%
     CONV  4/63   = 6.3%    LSTM 75/915  = 8.2%

   All at or below the null. The gate from 7414570 is working as designed and
   PAI's 47 clears were a cold-start transient never repeated in 3,141 later
   fits; its threshold over the run's second half has sd 0.01. The overlap
   correction is still right - sqrt(p(1-p)/n) on overlapping labels is the wrong
   formula - but it fixes no observed failure, and it costs nothing today
   because no model is near the deploy line.

WHAT THE FULL RUN DOES CONFIRM, unchanged: the geometry ran away exactly as
described (2.00/6.00 h128 -> 3.49/6.99 h256 -> 4.86/9.71 h384, three passes,
stopping at q90 because the quantile ladder ended), the label stayed long-skewed
at Buy 42.9% / Sell 22.6%, and no checkpoint on any of the four models ever
cleared the deployability floor. Pooled declustered win rates: PAI 31.70%,
HYB 31.02%, LSTM 32.69%, CONV 31.57% - every one 4-6pp below the 37% always-long
chance rate and 1-2.7pp below the 33.7% cost-adjusted break-even.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 07:30:19 -04:00
AnimateDread
1540ba8e64 fix(labels): overlapping-label sample correction + horizon cap on the scale ladder
Three defects, all surfaced by the 2026-08-17 SP500 H4 run that shipped
stop 4.86 / target 9.71 (width 14.57*ATR, horizon 384).

1. EVERY STANDARD ERROR ASSUMED INDEPENDENT SAMPLES. Triple-barrier labels
   started one per bar overlap by the label's lifespan, so n calls are worth
   ~n/L independent observations (Lopez de Prado, AFML ch. 4 - sample
   uniqueness). All three sqrt(p(1-p)/n) sites divided by the RAW count.

   The tell: the operating point's null-of-the-maximum gate is family-wise and
   should fire on ~5% of eras under the null. Measured fire rates - PAI 47/73
   (64%), ConvLSTM 9/24, LSTM 8/21 (38%), CONV 4/62 (6%). CONV, the only model
   whose margin distribution admits few bins, sat on the null; the rest cleared
   a bar that was too low by ~sqrt(L). PAI's deployed threshold consequently
   alternated between the ENDS of its own range era to era (0.10 -> 0.88 ->
   0.86 -> 0.66; coverage 16% <-> 73%).

   TripleBarrierLabel now records when each label became KNOWABLE - the first
   winning touch, or both stops, or the timeout - and the prebuild accumulates
   the mean. EffectiveSampleSize() feeds the operating point, the member deploy
   gate and the ensemble vote gate. Conservative by construction (n/L is an
   upper bound on the damage); gates get harder, never easier.

2. THE SCALE LADDER RAN AWAY, again. Horizon scales as swingMedian*sl*tp, and
   since 4d8cb08 reachability is measured OVER that horizon - so a wider rung
   buys itself the time that makes it look reachable. Same target -> horizon ->
   reach -> target loop the excursion window is kept short to avoid; fixing the
   window confusion reopened it through the other door. It walked 128 -> 256 ->
   384 bars and stopped at q90, the widest rung there is, with every rung
   reading 39-48% against a 20% floor. A floor nothing fails selects nothing.

   Rungs whose required horizon exceeds BARRIER_HORIZON_MAX are now rejected -
   the same rule ReportGeometryExpectancyScan already applied. It was printing
   the shipped pair as CLAMPED and disqualified ('h384!') two lines under the
   deriver that chose it: two subsystems, one geometry, opposite verdicts.

3. THE RUNG SNAP DESTROYED THE RATIO IT WAS COMPARING. Both legs snapped
   independently to the coarse first-passage grid, re-rating each candidate:
   q90 4.86/9.71 -> 5.00/10.00 (2.00), q85 4.07/8.14 -> 5.00/10.00 (IDENTICAL
   measurement), q75 3.07/6.13 -> 4.00/6.50 (1.63 - a nearer target). So the
   ladder compared win shares taken at ratios from 1.63 to 2.17 and read the
   differences as scale. It is why the reach column came out non-monotone in
   width (q75 48.5% above q90 42.9%). The stop now snaps to its nearest rung in
   log space and the target follows the ratio off it; the pair actually measured
   is returned and logged, so a collision reads as a collision.

Also: LadderWinShare guarded against the conditional (fractal) geometry path,
which fills n from m_fracLegCount while leaving idxList empty - a latent
out-of-bounds on a currently-dead path.

New log lines: mean label lifespan and effective n on the label-cache line, the
required-vs-available horizon per rung, and the grid pair the reconciliation
actually measured (its tolerance now scales with the grid skew instead of a flat
5pp).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 01:12:05 -04:00
AnimateDread
4d8cb08501 fix(geometry): the reachability floor measured the WRONG WINDOW - my bug from bc57aca, and it cost real width
RECONCILED: the derivation reported "target reached on 17.7% of bars" while the
label cache reported Buy on 35.9%. Nothing was broken. They measure different
windows, and both are correct:

  EXCURSION window   ~12 bars (the SWING MEDIAN) - what m_excUpCache accumulates
                     over. Deliberately short: sizing a barrier off travel
                     measured over a horizon that itself scales with the barrier
                     is circular, and it ran away to 14-31*ATR on EURUSD/USDCAD
                     in 2026-08-07. That guard is correct and stays.
  BARRIER horizon    64 bars - what the LABEL walk and the first-passage ladder
                     run over, and how long the EA actually holds the trade.

So `up >= target` is a 12-bar question and `label == Buy` is a 64-bar one, and
the second can freely exceed the first. TripleBarrierLabel gates the excursion
accumulation on `idx - t <= excWindow` while the barrier walk and the ladder run
the full horizon - the split is explicit and intentional.

THE BUG IS MINE. bc57aca's scale ladder tested reachability with `up[i] >= tp`,
i.e. it asked the 12-bar question about a 64-bar trade. That understates
reachability by ~2x, which is why EVERY wide rung was rejected and the geometry
fell back to the tightest rung at 1.61/3.21. The data supported considerably
wider; the test was just asking the wrong question.

FIX: LadderWinShare() reads the answer off the first-passage ladder - target
touched strictly before the stop, over the full horizon, tie to the stop. That
is the identical question the label walk asks, so the ladder share and the Buy
rate should now agree to within rung discretisation. Both legs snap to the
SMALLEST rung at or above the requested multiple (harder target, harder stop) so
the floor stays conservative.

Expect the scale ladder to select a WIDER rung on the next relabel. On this
data the excursion test read 17.7% at q50 where the true full-horizon share is
35.9%, so rungs that scored 8.1% and 2.8% were likely well above the floor.

ALSO:

- Window reconciliation now PRINTED every derivation: excursion travel share,
  ladder win share, and the label cache's Buy share side by side, with the
  ladder-vs-label gap flagged if it exceeds rung discretisation. Those two must
  agree; if they ever stop agreeing, one of them is wrong and the line says so.
- Renamed tpReach/slReach -> tpTravel/slTravel and relabelled the log line. They
  describe the EXCURSION window and are near-tautological there (a q50 stop is
  exceeded by ~50% of bars); calling them "reached within the horizon" is what
  made the two quantities look like one.
- BARRIER_MIN_TP_REACH_PCT is now BARRIER_MIN_REACH_FRACTION_OF_BE (0.60) x
  break-even instead of a hardcoded 20.0. Break-even for 1:RR is 100/(1+RR), so
  the absolute floor silently tightened as RR rose - 0.60x at RR=2 but 0.80x at
  RR=3, penalising the user for asking for a bigger target. Evaluates to exactly
  20.0% at the shipped RR=2, so this is a no-op today and correct if the knob
  moves.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:51:08 -04:00
AnimateDread
bc57aca15d fix(geometry): the target was small BY CONSTRUCTION - ratio is now policy, scale is measured, ladder ceiling removed
The derivation read the stop from q75 of ADVERSE travel and the target from q50
of FAVOURABLE travel. Over one horizon those distributions are broadly the same
shape, so q75 > q50 MECHANICALLY - the target came out smaller than the stop no
matter what the market did. SP500 H4 shipped stop 3.07 / target 1.70: a 0.55:1
payoff needing 64.3%. That was never a measurement, it was two mismatched
constants.

The reachability line printed beside it - "target on 50.0% of bars, stop on
25.0%" - is exactly 1-q50 and 1-q75. Tautological. It cannot disconfirm
anything, and it read as validation.

WIDTH AND RATIO ARE INDEPENDENT AND ONLY ONE PAYS. EV = edge x width;
ratio is EV-neutral (a driftless walk reaches +m before -k with probability
k/(k+m), which IS break-even). Width is what buys cost efficiency: the spread
is a fixed 0.047*ATR here, so the shipped 4.77*ATR width paid it 21 times per
unit of travel. So:

  RATIO  = policy. BARRIER_TARGET_RR = 2.0 (user's 1:2). Break-even 33.3%.
  SCALE  = measured. The stop quantile is chosen from a ladder, WIDEST FIRST,
           taking the first rung whose implied 2x target is still reached often
           enough to be a trainable class.

That last clause is the difference from the min-reward:risk raise removed in
2026-08-09, which forced target = 2 x stop with NO reachability test, landed on
6.66*ATR reachable on 3.3% of bars, and trained the model to predict something
that essentially never happened. Same ratio; the scale now retreats until the
data says the target is attainable. Every rung is logged.

LADDER CEILING REMOVED. BARRIER_LADDER stopped at 5.00 and the expectancy scan's
"best resolvable pair on width alone" came back as stop 5.05 / target 4.95 - it
pinned to the top rung. A recommendation landing exactly on the edge of its own
search space is a boundary, not a finding: it cannot tell "5 ATR is optimal"
from "5 ATR is all we allowed". Extended to 20*ATR (8 -> 14 rungs). Nothing else
needs editing - every consumer is parameterised by BARRIER_LADDER_COUNT - and
the horizon constraints (decided >= 60%, reachability floor) now bind instead of
a constant.

THE SCAN COULD NOT SEE THE SHIPPED GEOMETRY. ReportBarrierGeometryScan looked
the configured pair up in its integer grid, and DeriveBarrierGeometry produces
CONTINUOUS multiples (3.07/1.70) that can never equal a grid point - so
cfgExcess stayed at its -1.0 sentinel and the report printed "configured 3:2
scores -1.00000", which reads as a catastrophic score and actually means "never
evaluated". Worse, the grid skipped target<stop entirely because it "inverts the
trade's whole premise" - while the derivation was shipping exactly that. The
incumbent is now always scored as a peer (never crowned; it is already in force
and is not an enum pairing the scan could adopt).

BREAK-EVEN NOW INCLUDES THE SPREAD. Every report quoted the frictionless
SL/(SL+TP). On SP500 H4 that read 64.3% while the MEASURED zero-skill rate was
62.1% - a 2.2pp gap that IS the cost, and that made every model look 2.2pp
better than it was. CostAdjustedBreakEvenPct() prices a win at (TP - spread) and
a loss at (SL + spread), matching the expectancy scan's convention exactly so
the two reports cannot disagree.

It also feeds FitDirConfThreshold, which is the correctness half: the operating
point subtracts break-even from precision, so the frictionless figure made every
candidate threshold look better by the width of the spread - 2.2pp against a
measured edge of 2.3pp, i.e. very nearly all of it.

Era line now carries both: "break-even 64.3% frictionless, 66.6% AFTER SPREAD".

Forces a full relabel and retrain. Requested.

NOT COMPILED - user compiles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:34:32 -04:00
AnimateDread
430cdbe650 feat(ai): conditional barrier geometry for the fractal target - MFE/MAE measured leg-by-leg at labeled bars
The derived stop/target were quantiles of EVERY bar''s excursions over a
fixed horizon - q75 adverse gave a 2.6-3.5*ATR stop against a ~1.7*ATR
target (user: "looks limiting"). That pooled measurement was correct
when direction was dead (any subset of bars had the same distribution)
and is provably mis-sized now that the gate certifies the label carries
information: the bars the model trades are the labeled bars, and their
excursions differ from the pool.

FractalDirectionLabel now records, for every Buy/Sell-labeled IS bar
during the prebuild, the favourable and adverse travel in ATR units
over exactly the LEG the label points at - entry close through the next
fractal extreme (user request: "from a fractal to the next for maximum
accuracy"). DeriveBarrierGeometry reads the same q75-adverse/q50-
favourable quantiles off that conditional sample instead of the pool,
with a logged fallback to pooled when fewer than the minimum legs
exist. Quantiles kept over averages deliberately: a mean MFE is
dominated by runaway legs and would set an unreachable target.

No circularity: the fractal label does not depend on SL/TP (the barrier
label does - this path must never feed it). Recording stops the moment
geometry is derived and pinned, so pass 2 relabels and later bars
cannot silently re-shape a certified pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:00:40 -04:00
AnimateDread
61a8c42a9c feat(ai): TrainingTarget input - fractal-direction label for the direction models
User direction (2026-08-15): back to predicting swing turns, D1 charts,
fractals over ZigZag pivots (their call - balances classes, matches the
reference library target, and a 5-bar fractal confirms 2 bars after its
extreme so labels resolve nearly to the present with no repaint embargo).

- TRAINING_TARGET enum + TrainingTarget input: TARGET_BARRIER (Market
  default - existing models keep their meaning and fingerprints) or
  TARGET_FRACTAL (private default).
- FractalDirectionLabel (Labels.mqh): per-bar 3-class label = direction
  from the bar close to the next confirmed strict 5-bar fractal extreme,
  costs charged in the same bid-series convention as the barrier label,
  Neutral when the move cannot clear max(2 spreads, 0.10 ATR) or on an
  outside bar (both-extreme bars are unorderable within OHLC).
- The barrier walk still runs in full: measured SL/TP geometry, the
  expectancy scan, excursion caches and the era gate all keep scoring
  what a trade at the EA's own stop/target actually collected - only the
  TRAINING label changes. NOT the pre-b4a704d "is this bar the pivot"
  form; that target's 31:1 imbalance stays retired.
- Fingerprint token |TGT:FRA1 so switching targets trains a separate
  model; AI_META unaffected (guarded setter).
- Private defaults: AIType back to AI_HYBRID (direction topology needed)
  + TrainingTarget=TARGET_FRACTAL = drop-on-D1-chart workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 04:44:10 -04:00
AnimateDread
320f13253f feat: first-passage ladder + expectancy scan - price every geometry, not just the chosen one
Corrects the premise of the previous plan. Break-even is NOT a ceiling.
If the model shifts the win probability on the bars it selects from
p0 = m/(m+k) to p0 + d, then

  EV = (p0+d)*k - (1-p0-d)*m = d*(k+m)

because p0*k - (1-p0)*m is zero by construction. The stop:target RATIO
is expectancy-neutral - a punishing break-even is exactly repaid by the
payoff - and only the real edge d and the TOTAL WIDTH (k+m) move EV.
Width matters because the spread is charged once per trade however wide
the barriers are, so a narrow barrier spends much of its own range on
costs. DeriveBarrierGeometry's own comment already said the ratio buys
nothing; the objective just never followed from it.

Blocker this had to solve first: m_excUpCache/m_excDownCache hold only
MAXIMUM travel each way, and a maximum cannot say which side was
reached FIRST - so any geometry other than the walked one was
undecidable on precisely the bars where both barriers were touched,
~28% of the sample.

- BARRIER_LADDER: per bar, the first-touch AGE for 8 travel distances
  in each direction, filled during the walk the labels already run.
  Cursors keep it O(1) amortised per walked bar rather than 16
  comparisons. Levels are travel FROM ENTRY, not barrier prices, so one
  ladder serves both directions and the spread is applied analytically
  when a level converts back to an SL/TP multiple - storing prices
  would need four ladders and bake today's spread into the cache.
  Sized, invalidated and validity-gated with the label caches.
- ReportGeometryExpectancyScan: every ladder pair priced exactly off
  that cache - width in ATR and in SPREADS (cost efficiency, knowable
  without knowing d), break-even, both base rates, the share of bars
  resolved inside the horizon, and EV per unit of edge. Compares the
  widest resolvable pair against the quantile rule's pick.

MEASUREMENT ONLY - the quantile rule still chooses. Nothing here can
measure d, and width buys nothing if the wider target is less
predictable. Base rates are printed beside each break-even because a
persistent gap is DRIFT and must not be credited to the model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:59:18 -04:00
AnimateDread
0c85c54a5b fix: a restart no longer loses the measured geometry or the training window
Terminal restart, 22:25: all four resumed models sat on empty windows with
enum 2:6 barriers. Three interlocking causes, all visible in one log excerpt:

1) THE PRE-SCAN WINDOW WAS SIZED BY THE SAVED WATERMARK. A resumed model's
dtStudied sits at its last studied bar, so Bars(dtStudied, now) ~ 0 and the
resumed-model MI pre-scan built a zero-bar "complete" label cache - logged as
"Buy: 0 | Sell: 0 | Neutral: 0". Train()'s own era start RESETS dtStudied to
the training-window rule before computing its window; the pre-scan did not.
The rule is now factored into TrainWindowStart() and both use it. The scan
also refuses to arm before SERIES_SYNCHRONIZED (it ran in the same second as
OnInit), and deployed models keep their watermark - for them it gates
inference recency, not a training window.

2) THE HORIZON LATCHED ON AN INDICATOR WARM-UP. ComputeBarrierHorizonBars ran
against a ZigZag with 0 calculated legs, fell back, and EnsureBarrierHorizon
latched fallback(32) x slMult x tpMult = 384 for the process lifetime. A
leg-starved horizon is now PROVISIONAL: re-resolved on the next rebuild, the
label cache wiped if it moved (labels from two horizons answer different
questions), and the geometry deriver refuses to run from it - a pair derived
over a warm-up window would get PINNED.

3) THE DERIVED GEOMETRY WAS NEVER PERSISTED. The .cfg is written at model
creation and at weights-reset - both BEFORE era 0 derives - so the measured
pair lived only in memory: every restart read back zeros, adopted nothing,
fell back to the enum barriers, and the era-0-only gate meant a resumed model
could NEVER re-derive. A full day of training on 3.33/1.62 resumed as 2:6.
Now: the settled pair is pinned to the .cfg the moment derivation completes
(one-shot, atomic write), and the derive gate accepts any model with no
pinned pair, not just era 0 - mid-run stability is carried by
m_geometryDerived itself, which never allows a second derivation.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 22:40:43 -04:00
AnimateDread
9a7c37f334 fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.

1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.

2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.

Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
  transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
  frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
  0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
  USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
  indices and buffer bindings. Any disagreement resyncs from the good copy,
  latches all BN kernels off process-wide, and training continues host-side.
  A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
  authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
  read-only; restores/loads/resets push; a mid-batch handover drains the
  device gamma/beta accumulator into the host arrays so no sample is lost.

3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
AnimateDread
5cef0947f4 fix: the deploy gate was benchmarking a win rate against a label frequency
The gate rests on an invariant stated at ExpertSignalAIBase.mqh:199 - under a
driftless walk P(touch +k before -m) is m/(m+k), and break-even for a k:m trade
is ALSO m/(m+k), so "beats chance" and "is profitable" are the same test.

That invariant needs reward >= risk, and the measured geometry no longer
satisfies it. With target 1.62*ATR and stop 3.33*ATR, break-even is 67.3%, but
both-won bars were stripped out of Buy and Sell so the label base rate read
37.5%. chancePrecPct is max(BuyTotal,SellTotal)/bars, so the gate was clearing
models nearly 30pp short of break-even: 42% "directional precision" is +4 sigma
against 37.5% and loses money on every single trade. Live since 217b9bc.

Root cause is that label agreement stopped being the same question as trade
profitability. Buy implies winLong, but the converse fails on every both-won
bar, and the label can only name one of two directions that both pay.

So stop asking the model whether it matched a label and start asking whether
its trade paid:

- cache winLong/winShort per bar beside the label, under the same validity
  flag; published from the barrier walk before the collapse to 3 classes
- dirPrecPct now counts wins on the side actually called
- chancePrecPct is max(P(winLong), P(winShort)), MEASURED - the textbook
  m/(m+k) would credit SP500's drift to the model
- the NMS "what would I have made" pair, the live-fired precision, and the
  IS/OOS cumulative win rates all move to the same test. IS and OOS are read
  side by side as the overfitting signal, so measuring one in wins and the
  other in agreement would put a fixed gap between them that has nothing to do
  with generalization
- the confidence threshold is FITTED on wins too, so the operating point
  maximises what the gate grades
- per-class label-agreement precision is still computed and logged; it is the
  right diagnostic for class separation, just not for a deploy decision
- era line renamed dir-precision -> win-rate, chance -> chance=break-even

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:00:51 -04:00
AnimateDread
ce5265488e fix: both-won bars were labelled "do not trade" - resolve by first touch
Removing the min-reward:risk raise let the MEASURED geometry come back with
the target NEARER than the stop (SP500 H1: target 1.62*ATR at q50 of
favourable, stop 3.33*ATR at q75 of adverse). That reopened a branch the
code called unreachable: price can reach +target and -target inside one
horizon, winning in BOTH directions, and those bars fell through to Neutral.

Neutral has only three producers, both-lost is unreachable (you cannot touch
-3.33 without crossing -1.62 first, which wins the short), and timeouts logged
at 1.0% of Neutral - so ~27% of ALL bars were being handed to the model as the
abstain class when a trade either way would have collected its target. The
cleanest positives in the sample, labelled "do not trade", while the fitted
confidence threshold was being asked to find selectivity in what was left.

Resolved by FIRST TOUCH: the target reached earlier is the trade that would
have closed first. Same forward window, no extra lookahead. Same-bar ties stay
Neutral - OHLC cannot order two touches, and unlike an intrabar stop tie there
is no pessimistic side to fall to, so a guess would inject a coin-flip
direction into the target.

Also:
- count both-won and its same-bar tie subset in the prebuild line, so the
  share is measured rather than inferred from arithmetic on a log line
- scope the timeout counter to IS, matching the tally it is reported as a
  percentage OF; it was incremented over the whole scan and divided by an
  in-sample denominator
- clear m_lastBarrierTimedOut at the top of the walk with the excursions, not
  at the bottom - the two early returns published the previous bar's verdict
- mark the pass-1 label line PROVISIONAL. It prints the enum fallback because
  geometry can only be derived from excursions that do not exist yet, and it
  reads exactly like a config change that failed to take effect

FORCES RETRAIN. Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:35:42 -04:00
AnimateDread
217b9bc9bf feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement
The barrier geometry is derived from the instrument's own excursion
distribution (stop at q75 of adverse travel, target at q50 of favourable),
and then a 1:2 floor was applied on top, raising the target to twice whatever
the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR,
reached on 3.3% of bars inside the horizon - so the label became "almost
never a win" and every topology was trained to predict an event that
essentially does not occur. A measured target has to stay measured.

The ratio never bought what it was believed to buy. A reward:risk floor does
not create expectancy; it trades hit rate against payoff at a break-even the
geometry already fixes - which this project has separately MEASURED (payoff
0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four
consecutive Market validation rejections for "no trading operations" when it
rejected 100% of setups, and the label corruption above.

Removed:
- the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a
  live enum with no input behind it is the shape of the stale-.set incident
  that trained ~250 eras on the wrong target)
- the forced target raise in the label geometry
- the rrOK eligibility gate in the barrier-geometry scan, so every unclamped
  pairing now competes on the measurement alone. Clamping stays disqualifying
  for its own unrelated reason.
- the reward < minRR*risk veto in OpenParams

Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing
in MoneyIntelligent - the ratio as a SIZING input was always the sound use.
Risk stays bounded where it actually is - account risk % and CRiskBudget.

The low-reachability warning survives but is re-aimed: with nothing inflating
the target, a target the market rarely reaches can only mean the horizon is
truncating the excursions the geometry is derived from.

Both build variants compile 0 errors / 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
AnimateDread
b3b7e7bceb fix: excursion window must not depend on the barrier it sizes
DIRECTION IS NOT THERE, and this run is what establishes it. Three symbols:

  raw ASYMMETRY   clears on all three (p=0.0199 / 0.0050 / 0.0050)
  norm ASYMMETRY  collapses on all three (p=0.3433 / 0.5075 / 0.2736),
                  USDCAD landing BELOW its own null
  RANGE control   strengthens to 3-5x its null everywhere

Divide sigma out and the apparent directional signal vanishes entirely. What
cleared was volatility leaking through an unnormalised difference. Note this
would have passed any replication test: three instruments at p=0.005 is exactly
the evidence one would accept before committing to a rebuild, and the confound
reproduces perfectly. Replication was never going to catch it - only the
normalisation could.

Two defects of mine, both surfaced by the same run.

1. THE GEOMETRY DERIVATION WAS DIVERGING, NOT CONVERGING. It produced a
   14.57*ATR stop and a 29.14*ATR target that only 5.7% of bars ever reach.
   Excursions were measured over the barrier horizon; the horizon scales with
   the target; the target is a quantile of the excursions - so target ->
   horizon -> excursions -> target ran away, and "settled" only because the
   horizon ladder caps at 384 bars. A saturated runaway, which the iteration
   guard could not catch because it watches for OSCILLATION.
   Fixed at the root: excursions now accumulate only over m_swingMedianBars -
   the UNSCALED median ZigZag leg, a property of the instrument that owes
   nothing to the barrier. The barrier walk still runs the full horizon,
   because that is how long the trade is held; only the MEASUREMENT used to
   size the barrier is confined to a geometry-independent window.
   (The Min_Risk_Reward_Ratio warning fired correctly and is what flagged it -
   the diagnostic worked while the derivation behind it did not.)

2. THE CONFOUND VERDICT WAS UNREACHABLE. `sizeCleared && !asymCleared` was
   tested first and is true whenever size clears - i.e. always - so the branch
   that NAMES the volatility confound never printed; all three symbols showed
   the generic size-not-direction message instead. Verdict chain rewritten with
   the specific case first, and the dangling elses my first patch introduced
   removed.

FORCES A FULL RETRAIN (the excursion window changes every derived barrier).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:57:23 -04:00
AnimateDread
32ffeb99f3 fix: normalise the asymmetry target - the raw one is confounded by volatility
Three symbols ran the excursion test. RANGE/UP/DOWN cleared on all three;
raw ASYMMETRY cleared on EURUSD and USDCAD at p=0.0050 and not on SP500
(p=0.1045). That looked like the first directional signal this project has
found. It probably is not, and the test as built could not tell.

(up-dn) IS NOT SCALE-FREE. If sigma is predictable - and RANGE clears at ~4x its
null on every instrument - and the directional part is symmetric noise eps, then
up-dn ~ sigma*eps, so a large sigma pushes the value into BOTH outer terciles. A
pure volatility predictor scores positive MI against a 3-bin (up-dn) while
carrying no directional information at all. Crucially that confound REPLICATES,
so reproducing on two instruments is not evidence against it - and the effect
sizes fit it: asymmetry runs 1.3-1.6x its null where RANGE runs ~4x, and carries
~0.1% of the target's entropy against RANGE's ~0.9%. That is the shape of a
leaked fraction of the volatility signal, not an independent one.

So add (up-dn)/(up+dn): bounded in [-1,+1], volatility divided out, and the only
target a directional claim may rest on. The verdict now separates the cases and
NAMES the confound when raw clears while normalised does not, instead of
reporting the raw line as a finding.

Two bugs of mine in the same block, both caught by output rather than review:

  - The derived-geometry line had a MISORDERED argument list: it printed
    "stop 25.00*ATR (q3 of adverse travel)" - the quantile percentage as the
    multiple and the multiple as the quantile. Real values were 2.61 stop /
    8.03 target. A 25*ATR stop is absurd on its face, which is why it was seen.
  - THE STOP QUANTILE WAS BACKWARDS, and this one changes labels. It was 0.25
    "so ordinary noise does not reach it", but q25 means 75% of bars EXCEED the
    stop - hit three times in four. The printed reachability said exactly that
    ("stop on 75.0% of bars"). Now 0.75. A quantile is a threshold, not a rate.
    This is the entire reason reachability is measured and printed rather than
    assumed.

Also raises BARRIER_DERIVE_MAX_PASSES 3 -> 5: SP500 did not settle in 3 (stop
still moving ~14% per pass) while EURUSD and USDCAD converged on pass 2. And
bounds both quantile indices with MathMin(..., n-1) so q=1.0 cannot run off the
end of the sorted array.

The geometry from the previous run is NOT usable and the asymmetry result is
unresolved, not established. Both are decided by the next run.

FORCES A FULL RETRAIN (the stop quantile changes every label).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:04:13 -04:00
AnimateDread
a7701f032b feat: derive the ATR multiples from measured excursions - no hardcoded geometry
The barrier was still two constants. SL_Mode/TP_Mode left the Inputs tab in
3482b6c, but the fallback was a hardcoded 2:6 and the geometry scan only ever
chose from a hardcoded grid {2,3} x {2,3,4,6,8,10}. Picking the least-bad of
eleven guesses is not deriving anything.

WHY THE SCAN WAS THE WRONG INSTRUMENT, now measurable rather than argued. It
ranks pairings by how predictable their OUTCOME is - a question about direction.
The excursion test (2c78f3b) ran on SP500 H1 and direction is the one thing
absent: ASYMMETRY p=0.0846, against RANGE/UP/DOWN all at p=0.0050, with RANGE
scoring 0.01345 vs a 0.00343 null - 4x, where the barrier label sits at 1.01x.
Hence the scan failing its own gate on every run, and its "winner" wandering
2:8 -> 3:8 -> 2:8 -> 2:4 across four runs of the same data. Excursion SIZE is
strongly measurable, so derive the geometry from that instead.

  stop   = q25 of measured ADVERSE travel   (ordinary noise does not reach it)
  target = q50 of measured FAVOURABLE travel (reached ~half the time, by
           construction, inside the horizon)

Continuous, in ATR units, superseding the enum multiples. Reachability ("target
on X% of bars, stop on Y%") and the implied break-even are printed so the choice
is auditable rather than trusted.

FIXED-POINT ITERATION, not one-shot. ComputeBarrierHorizonBars scales the
horizon with the target (first-passage time grows with the band) and the
excursions are measured OVER the horizon, so target -> horizon -> excursions ->
target is a real loop - deriving once sizes the target from travel measured
under the PREVIOUS horizon. Re-measures until the multiples move <5%, capped at
3 passes, and says so if it does not settle.

Does NOT create expectancy, and the log says as much: chance precision equals
break-even at every geometry (m/(m+k) on both sides). It buys a target the
market reaches and a stop that survives noise. Where Min_Risk_Reward_Ratio
forces a target the market rarely reaches, it WARNS rather than overriding -
the ratio is the user's risk policy, so the honest move is to state its cost.
That is the collision that once rejected 100% of setups.

Pinned in the .cfg as doubles appended AFTER this morning's two ints, so .cfg
files written earlier today still load (their length guard finds no doubles) and
a model that carries them was trained on them and never re-derives.

Also fixes a message from e5ceed6 that claimed "this model resumed from disk"
unconditionally - it printed above a "seeding era 0" line on a brand-new model,
because the branch fires whenever the cache is not built, which is equally true
before a fresh model's first prebuild. A diagnostic that misreports its own
trigger is worse than one that says nothing: it gets quoted back as evidence.

FORCES A FULL RETRAIN (labels change).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:06:25 -04:00
AnimateDread
2c78f3b90d diag: is "optimal SL/TP" learnable? Score the features against excursions
Proposed direction: train the net to predict entry/SL/TP that maximise return
and minimise drawdown, rather than to classify direction. Before rebuilding a
head, measure whether the target is learnable at all.

That question splits into two that behave nothing alike:
  HOW FAR price travels (MFE/MAE) - essentially volatility, and volatility
    clustering is about the most robust regularity in markets.
  WHICH WAY it goes first (the asymmetry) - direction, which is what every
    noise-floor verdict in this project has been about.
Expectancy comes ONLY from the second. The first buys position sizing and
drawdown control - worth having under prop-firm limits, but not an edge: exit
management on RANDOM entries already moved the payoff ratio 0.92 -> 5.72 with
expectancy FLAT.

Crucially this is NOT already answered. Every MI figure here scored the
triple-barrier label, i.e. one specific question at one fixed geometry. A
noise-floor result there says nothing about whether excursion MAGNITUDE is
learnable - different target, different answer.

Four targets, and the verdict is the CONTRAST, printed explicitly because the
dangerous misreading of "UP clears" is "we can predict profitable trades":
  RANGE (up+dn)  - realised volatility, included as a POSITIVE CONTROL that
                   SHOULD clear. Every prior verdict here lacked a control
                   expected to pass; a range target at the floor indicts the
                   measurement, not the market.
  UP / DOWN      - MFE / MAE.
  ASYMMETRY      - up-dn, the only one that can pay.

Collected inside the walk the label already does (one max, one min per bar).
The early-out when both barriers resolved is GONE: it would have truncated the
excursions at whichever bar tripped the last barrier, making the measurement a
function of the CURRENT SL/TP - the circularity this is trying to escape. The
loop was already bounded by the horizon, so only the average cost moves.

Discretised into 3 EQUAL-FREQUENCY bins, so every downstream piece (block
permutation, null, p-value) is reused unchanged. Equal-frequency because MFE is
fat-tailed and fixed-width bins would put nearly every row in bin 0; it also
pins H(Y) at ln(3)=1.099 for all four, making them comparable to each other and
to the barrier label's ~1.02 instead of confounded by class balance.

Two bugs fixed in this code before it ever ran, both of which would have
produced a plausible quiet wrong answer rather than an error:
  - TripleBarrierLabel early-returns on invalid ATR/close BEFORE the point the
    accumulators were reset, so one bar's excursions would be cached under
    another bar's index. Cleared at the top now, ahead of every return.
  - An unresolvable bar is still flagged as labelled but carries excursions of
    exactly 0. Under equal-frequency binning a block of identical zeros drags
    the lowest cut onto zero and a third of the sample lands in one
    uninformative bin - a depressed score that reads as "not predictable", a
    false negative in the direction that would wrongly kill the idea. Rows
    where both excursions are zero are dropped; price cannot travel zero both
    ways over a whole horizon.

Read-only diagnostic. No topology or label change: no retrain of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:22:41 -04:00