Last surviving compile-time feature switch in the codebase - the same pattern
already killed for the MARKET build and DirectML tier (02766b5): one build,
configured at runtime like every other module (inputs + getters/setters, set
in ConfigureAISignal during OnInit), not a second code path that only existed
if someone remembered to define a macro before compiling.
Replaced with `input bool ExportFeaturesOnly = false` (Variables/Inputs.mqh)
and a plain m_exportFeaturesOnly member + setter, matching AutoTuneIndicators'
exact shape. Four call sites converted from #ifdef to a runtime read of the
same variable:
- Warrior_EA.mq5 OnTick() - reads the input directly (this check has to
stand before any per-signal object exists)
- Topology.mqh's config-lock skip and ExportFeatureMatrix() call - read
m_exportFeaturesOnly, now set by ConfigureAISignal before InitIndicators()
runs (same init-order guarantee AutoTuneIndicators already relies on)
- ExportFeatureMatrix()/ExportRawRates() declarations - always compiled now,
called conditionally instead of not existing as symbols
No change to what the flag does when off (the state of every build that
exists today, since the macro was never defined anywhere in-repo) or when on;
only how it's set. Verified: WARRIOR_EXPORT_FEATURES fully gone from every
#ifdef/#endif in the tree; brace and ifdef/endif counts balance in every
touched file; ConfigureAISignal runs before StepInitIndicators in OnInit's
linear init chain, so the flag reaches InitNeuralNetwork() in time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Market rule IV forbids DLL calls, and the DLL compute tier plus the
WebRequest alt-data fetch are what make this bot work. If it is ever sold
it goes through its own channel with the DLLs intact, so a no-DLL build has
nothing left to be for (operator, 2026-08-23).
Removed:
- Warrior_EA.mq5 the //#define toggle and the #resource block behind it
- Variables/Inputs.mqh two #ifdef pairs whose market arms forced every
Use_* NN input and Meta_ExportDataset to false
- AI/NeuronDirectML.mqh the 62-line market stand-in CDirectMLMy whose every
method returned false so the chain fell through to
plain MQL5
- IndicatorResources.mqh WARRIOR_CI(name), which had already collapsed to
(name) - a switch with one position is not a switch.
Its five call sites in Features.mqh name the
indicators directly now.
Behaviour is the private build's, unchanged: bare indicator names loaded
from <MQL5>\Indicators\, all four NN votes defaulting on, dataset export on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Baselines was 951 lines of CExpertSignalAIBase method bodies in a file
that only looked like a module. It is now CBaselineComparator: a class
the signal OWNS, which reads a CTrainingDataView and prints. It does
not name the signal anywhere in its code.
What the seam forced out into the open:
- Thirty-odd ArraySize() bounds tests, each carried by its caller, are
now one test per accessor next to the data. The two `hasValueN` and
one `arrowN` locals are gone with them.
- The -2.0 "never scored" sentinel on the arrow cache was tested at the
call site. It is now inside DataDirectionalCall, where it cannot be
read as a small confidence.
- DoubleToSignal needs m_outputNeuronsCount, so a raw double could not
be turned into a side by any reader. The view answers
DirectionalCall(bar, isBuy, magnitude) instead - the conversion
happens where the head width lives, and the module no longer needs
ENUM_SIGNAL at all.
- m_baselineDone was a latch on the signal for a decision only this
module makes. It is m_done, private, where it belongs.
Correction to my own earlier claim: I said Baselines had nine exclusive
members "polluting the signal class". It had none. m_x, m_f, m_ngrad,
m_AvgCE and the rest are FIELDS OF ALGLIB REPORT OBJECTS (state.m_x,
mrep.m_AvgCE) that my `\bm_\w+` scan matched after the dot. The module
needs no private state but its view pointer and that latch - which is
why it came out this cleanly.
The include sits below the g_ens* vote globals and the Alglib headers
it reads, because unlike the AIBase\*.mqh partials this is a real class
declaration compiled where it stands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run_Alglib_Baselines = true. It answers the question the campaign is
actually stuck on - is a flat result the architecture or the matrix - by
fitting an Alglib forest, MLP and OLS on the net's OWN windows, labels,
split and gate arithmetic. Nothing trades on it and no model is saved.
Not in BuildModelFingerprint, so this does NOT re-key anything: no
retrain, it can go out with the next compile. One-shot per chart
(m_baselineDone latches first, and g_ensembleChartBaselinesDone stops
the other three members re-measuring the identical fit), bounded by
BASELINE_BUDGET_MS = 45 s, which it spends frozen because the EA is
single threaded - it says so when it stops early.
FIXED WHILE TURNING IT ON: ReportBaselineModel sized its SE on RAW call
count while the NN's own DEPLOY BAR deflates by the mean label lifespan.
The whole point of this line is a like-for-like comparison against the
net on the same windows, and it was holding the baseline to the more
permissive standard - which is how the last forest run's +8.4pp read as
significant when it was +1.0 SE after deflation. Now uses
EffectiveSampleSize and prints how many independent calls that leaves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
USDJPY has taken no trades in 66 eras and its highest vote ever seen is 13
against a 25% threshold. Not a bug and not undertrained models - arithmetic.
Direction() divides the summed contributions by the CAPABLE weight, so a
unanimous vote returns the capability-weighted mean of the tier weights, which
is roughly the pooled holdout win rate. USDJPY's members pool at 15.6-19.4%
(its label base rate is 14.0% against SP500's 25.4%, because its derived
geometry resolves far fewer bars directionally: Buy 10.3% Sell 11.2% Neutral
78.6%). So the ensemble's CEILING is ~19 and the threshold is 25. Coverage can
never leave 0, and no amount of training moves it, because the ceiling IS the
win rate.
The report now computes that ceiling - every member voting at its best tier -
and says so when the threshold sits above it, instead of printing "0 fired at
vote>=25%" which reads as "the models are unsure".
Same class as the excursion head's disjoint gate (ee4d459) and the reason
ReportDetectability exists: a configuration that cannot reach its own bar has
to say that, not report a number that looks like evidence.
Also: VerboseMode and Run_Alglib_Baselines back to false. The per-era cadence
was for reading the horizon break-even and the excursion sigmas; both are
settled, and TrainLogDue still prints them every 25 eras. The baselines cost a
45 s single-threaded freeze at every attach and their forest row turned out to
be one deterministic observation that does not survive overlap deflation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
Two UX changes the operator asked for.
THRESHOLDS. Signal_ThresholdOpen/Close were raw ints with the legal range
written in the label ("[0...100, 101 = never]") - the one input style this
codebase converted away from everywhere else. Open now takes the existing
PERCENTAGE_PRESETS, whose comment already declared itself to be "Signal_
ThresholdOpen's scale" but was never wired to it; Close takes a new
SIGNAL_CLOSE_PRESETS carrying the same rungs plus CLOSE_DISABLED = 101, which
is why it cannot just reuse the other enum. Member names are prefixed because
MQL5 enum members share ONE flat namespace - a bare PCT_25 in the second enum
would silently resolve to the first one's, warning only. Values are unchanged,
so existing .set files keep their settings. Both call sites now cast
explicitly at the CExpertSignal boundary rather than leaning on an implicit
enum-to-int conversion that only warns.
ARROWS. 2026-08-19 replaced the low/high arrows WITH trigger-price lines; that
was a swap where it should have been an addition, and it cost the zoomed-out
view. A mark is now both objects: the line is the precise entry/exit level,
the arrow off the candle's extreme is the finder that says there is something
here to zoom into. The arrow's name is the line's plus a suffix, so it stays
inside SIG_ARROW_PREFIX and every prefix-scoped purge already reaches it.
The two type-filtered sweeps had to widen or they would clear one half and
leave the other: the Hide/Show visibility loop and the pre-rescan scoped
delete both walked OBJ_TREND only. Both are typed-blind and prefix-scoped now
- the same widening this file's 2026-08-09 note describes, for the same reason
it gives. Deletes go through one WarriorDeleteSignalMark() so an arrow cannot
outlive the line it belongs to, and the sidecar deliberately still records one
row per mark off the line (the half carrying the price), with the restore
redrawing the pair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Box header per function/class, section banners, and 1-3 line notes for
variables - not the paragraphs that had accumulated. Both files verified
mechanically rather than by eye: every declaration in Inputs.mqh (159 of
them) and every non-comment line in ExpertSignalAIBase.mqh is byte-
identical before and after.
Variables/Inputs.mqh 881 -> 355 lines (704 -> 179 comment lines)
Expert/ExpertSignalAIBase.mqh 4795 -> 4388, first ~800 lines converted
What was cut is narration - the history of what a constant used to be,
paragraphs restating the line below them, and commentary about earlier
versions of the comment itself. What was kept is every constant, every
measured number, and the traps worth a warning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
targetHour CH_23 -> CH_MARKET_CLOSE, targetMinutes CM_45 -> CM_5.
A fixed 23:45 was a guess at one broker's server offset. It is silently
wrong on any other feed, and wrong twice a year on the same one.
"Market close" resolves per day from the symbol's own session table and
backs off the minute value, so it is correct on every symbol and on both
sides of a DST switch with no number for anyone to maintain.
THIS RE-KEYS EVERY FINGERPRINT and forces a fresh run from era 0. That
is correct rather than collateral: the close-all schedule is where the
label walk stops treating a trade as holdable
(Expert\AIBase\Labels.mqh), so changing it changes every label, and
models trained against the 23:45 barrier are confounded for this one.
The CUT: field in BuildModelFingerprint() catches it automatically -
verified present rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Every direction verdict so far was measured through one architecture
family, so "flat" has two readings that no topology tuning can separate:
the net is the wrong learner, or the matrix carries no directional
information.
Two learners with completely different inductive biases - Alglib's
random decision forest and an ordinary least-squares fit - now train on
the SAME feature windows (BuildFeatureWindow, the net's own function, so
there is no second feature implementation to drift), the SAME labels,
the SAME IS/OOS split with both purges, and are scored through the SAME
precision-against-always-one-direction comparison and the same Sidak
family-wise arithmetic the deploy gate uses. If both also land at
chance, the matrix is the limit.
Deliberate choices, each of which could have made the comparison a
different question wearing this one's name:
- LRBuild, not LRBuildZ: the intercept absorbs the class imbalance, and
a baseline handicapped by a forced zero intercept would flatter the
net for the wrong reason.
- Raw call counts in the SE, matching the live gate's known-permissive
test rather than correcting it here - both sides must face the same
bar.
- No threshold sweep on the linear fit: a threshold fitted on the slice
being scored is the calibration leak the purged band exists to avoid.
- Uniform stride when a cap bites, not the newest N rows, so a score
difference cannot be a regime difference. What was dropped is logged.
Ships off (Run_Alglib_Baselines = false): it is a measurement, not a
trading feature, nothing trades on the answer and no model is saved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First of the AI vote layers to go. CheckClosePosition had two exit routes:
the stock blended vote, and an AI-only one reading the AI members' sub-vote
undiluted. The second existed because an AI reversal averaged in with the
classic filters could be diluted below the threshold before it could close
a position.
It is gone, and with it m_lastAiVote and the aiResult/aiWeightSum pair
Direction() carried to feed it.
This CLOSES the certified-vs-traded gap rather than widening it. The
deploy gate certifies a win rate measured on hold-to-resolution outcomes,
and CheckClosePosition already gated the blended route off whenever an AI
model's derived geometry was on the order - so the AI route was the only
vote exit an AI-certified trade could take, and the exit replay existed to
reproduce it. With it removed, an AI-certified position holds to its
barrier by construction instead of by reconstruction, so Warrior_EA.mq5
now pushes ExitPolicy(0.0, true) unconditionally. Previously it forwarded
Min_Vote_Close and relied on Disabled arriving as 1.01 to switch the
simulated exit off by arithmetic - correct at the shipped default, and one
input change away from the simulation and the live path describing
different games.
Min_Vote_Close keeps its meaning for the classic route and is now
documented as inert wherever an AI certificate governs, rather than
appearing to drive an exit it can no longer reach.
Comment debt cleared while here: a tombstone block for m_ai_exit_threshold
(a member deleted 2026-08-18) still sat in the header, and four sites still
named LiveSignedConfidence's "two consumers" - it had one, the intelligent
trailing stop, since that same date.
NOT touched, and deliberately: NMS declustering is NOT a quality layer. It
gates the live signal at Inference.mqh:226 (NmsLiveAccept), and the
undeclustered population is ~8x what the EA trades. Removing it would
multiply live position count, not simplify a scoring path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MA: CustomIndicators\ADMovingAverage is replaced by the built-in iMA (CiMA) on
both consumers - the classic vote and the NN MA input feature. This drops the
five advanced types ALMA/DEMA/ZLEMA/T3/Kalman, which have no iMA equivalent;
MA_TYPE_PRESETS is now ENUM_MA_METHOD's own codes and the tuner searches all
four. It also removes a documented failure mode: a custom indicator's depth is
bounded by TERMINAL_MAXBARS, and m_MA was the one whose feature block REJECTS
the bar on a short read - the "feature 25 fails on every bar" incident of
2026-08-17. A built-in is served at any depth.
MIGRATION. SMA moves from code 5 to 0, so persisted type codes change meaning.
SanitizeMaType() is the single validity rule; TunedPeriods records now carry a
version field and a v1 record remaps 5..8 -> 0..3, falling back to SMA for a
stored advanced type (unrecoverable - old 0..4 are indistinguishable from valid
new codes). Existing .nnw files re-key on their own, because MA_Type is hashed
into the topology fingerprint, so models retrain rather than silently running
on different MA values. EXPECT A FULL RETRAIN.
ZigZag: ADZigZag was a byte-identical rename of MetaQuotes' Examples\ZigZag -
verified by normalising identifiers and stripping comments, 233 significant
lines each with only renamed symbols differing. It now loads the stock one, so
nothing is bundled and MetaQuotes' fixes arrive without a rebuild here. Both
#resource entries are gone.
Classic_Shift: a new input, the BAR the four classic votes evaluate on (0 =
forming, 1 = last closed, default 1). One implementation on CExpertSignalCustom,
inherited by all four rather than repeated per module. Defaults to a sentinel
meaning "unset", so the AI signals and the aggregate keep the stock every_tick
rule and their feature/label alignment is untouched. The META corpus sweep still
takes precedence. CExpertBase::StartIndex turns out to be virtual, so this is a
real override, not the name-hiding the old comment claimed.
Not compiled - MetaEditor compile pending.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Typo, and more wrong than it was: the group now holds four independent NN toggles rather
than one architecture selector.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User request after the first ensemble run under the per-NN build: the drift verdict was
measured and printed every era while the input sat at BOTH, so it gated nothing - the
worst of both, because an authoritative-looking log line described a policy that was not
in force.
Safe as a default: the verdict fails open to BOTH (it drops a side only when the gap
clears 2 combined SEs on the overlap-deflated sample AND the weaker side sits below
cost-adjusted break-even), so an instrument with no measurable drift behaves exactly as
before. Verified it appears in no fingerprint or DB key - trade policy, not label
definition - so this changes no model identity and resets no training.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User design (2026-08-19): 'remove the enum menu that selects neural networks... individual
inputs for every NN just like classic signals... the META NN should be integrated into the
voting decision pipeline when enabled... as a bonus meta labelling is applied to enabled NNs.'
- AI_CHOICE is GONE (tombstoned per the stale-.set doctrine). Use_MLP/Use_CONV/Use_LSTM/
Use_CONVLSTM are ordinary bools like the classic votes; the ensemble arithmetic adapts to
any subset because the consensus divisor is the enabled capable weight. Two or more
enabled = ensemble (|ENS1 token + joint gate, exactly the old AI_HYBRID fingerprints, so
existing weight files keep loading); one = the old solo preset; none = classic-only.
- Use_MetaLabeling un-couples META from the direction NNs (the old selector made them
mutually exclusive). S3 ships: CSignalMETA::LiveMetaGate scores each vote-cleared entry
(shared window at bar 1 + proposal descriptor: side, net vote, live geometry, spread/ATR;
pattern one-hot ZEROED - ranking, not calibrated probability, documented in the body) and
vetoes below the cost-adjusted break-even. Entries only; fail-open everywhere, loudly.
- COEXISTENCE HAZARDS closed: VoteCapableWeight()=0 and ProspectiveVote()=false for the
meta target - solo-only until today, a trained META would otherwise sit in the consensus
divisor as a permanent abstainer and shrink every vote by its module weight.
- CERTIFIED == TRADED: the ensemble era verdict replays the identical veto through the same
g_warriorMetaGate pointer over its OOS fired bars (bar re-resolved from the row's own
time; fail-open counted as fires and reported: 'metaGate: N approved, M vetoed, K
unscored'). The overlay deliberately does NOT replay it (veto-filter-in-replay class,
calendar-cliff precedent) - documented at the sweep site. Solo charts' own gate does not
model the veto - the standing solo-gate caveat, documented at the input.
- DB continuity: the pattern/journal DB fingerprint's first slot was (int)AIType;
DbLegacyAiSlot() maps every legacy-expressible config to its OLD value (new 2-3 member
subsets get 100+bitmask, outside the legacy range) so no existing database re-keys.
filterID becomes the enabled roster via one EnabledNNSummary().
- HUD: the meta line shows the gate (armed/(trn), last P vs BE, ok/veto tally); the
armed/disarmed announcement fires on state change via one latch (MetaGateArmedNow), not
only when an entry happens to be proposed.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Measured from the journal (2026-08-19): the era deep-dive line (~2KB) plus
the excursion verdict, tier re-rank, calibration move, barrier hold and
selection-regressed note each printed EVERY era for EVERY member - ~940
eras/member/day - long after the systems they watch were confirmed
working. Yesterday's file was 1.3GB (70% of it the news-filter calendar
spam the sweep fix already removed).
VerboseMode returns as an INPUT (demoted 2026-08-01 for the marketplace;
that track is dead since the 2026-08-16 pivot) and gains a second job:
false throttles each settled per-era print to eras 0-3 plus every
TRAIN_LOG_EVERY_ERAS-th (25 ~= one deep-dive per ~15min per member);
true restores the per-era firehose, flippable live.
Never throttled: anything that marks a CHANGE - new bests, restores +
eta decays, plateau stage transitions, deploy approvals, warnings,
errors, the label-cache/adoption one-shots, and the combined-vote gate
line (the active system's primary telemetry, still every era).
Semantic fixes over blanket gating:
- barrier hold now ARMS silently and prints only when the hold outlasts
the 2-min report interval - a brief hold every era is the design, the
long hold is the watchdog case the line exists for;
- the ensemble deploy REFUSAL prints immediately when its reason
changes (that is a finding), on cadence when unchanged;
- the filtered-view census prints when its RESULT moves (drawn count,
or strongest vote by >=2pp) and at least every 10th sweep.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under CONSENSUS arithmetic the vote is quantized by agreement: with four
members at pooled tier weights ~29, unanimity reads ~29, 3-of-4 ~22,
2-of-4 ~14.5. The 10-point dropdown straddled every one of those rungs -
20 admitted 3-of-4, 30 admitted nothing - so the thresholds an operator
actually wants (between rungs, e.g. 25 = "unanimity or a top-tier 3-of-4")
did not exist. PERCENTAGE_PRESETS and VOTE_CLOSE_PRESETS both gain
5/15/25/35/45; members are only ever ADDED (explicit values, .set-safe),
never removed - MT5 does not validate saved enum values.
Min_Vote_Open default 40 -> 25: the 40 was priced under union arithmetic
and now sits above the unanimity ceiling (~29), i.e. a fresh attach would
silently never trade - the same fired-on-0-bars defect the 50 -> 40 move
fixed once already.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THRESHOLD. 40 is a measured correction, not a preference. Once
RankTiersFromOos() replaced the designed tier priors with each model's real
held-out win rate, the vote converges on that win rate - logged 2026-08-18 as
pooled 23-36% across four members on three symbols - so a 50% bar could not be
reached by anything on offer and the ensemble gate fired on 0 of 4,865 OOS
bars. 40 clears the ~34% break-even those same lines report without being
unreachable. The comment says plainly not to copy the number: break-even is a
function of the barrier geometry, so read the gate's own "needs >N%" for the
config in front of you.
READOUT. One line, top-right:
VOTE SELL 37.2% peak 44.1% need 40% 3 voter(s) -> no trade
Every other number on the chart is downstream of the weighted mean the open
threshold is compared against, and that was the one quantity never displayed.
A chart with no arrows could mean the models abstained, the vote was diluted,
or the threshold is unreachable - and telling those apart meant waiting for an
era to end and reading the gate line, which is how the last two sessions went.
PEAK is the part that earns its space. A threshold above what the vote ever
attains can never fire, and that is not knowable from a single bar - it is
precisely the "unreachable gate vs merely unmet gate" confusion this project
has paid for twice. Colour carries the verdict rather than the direction:
green/red ONLY when the vote would actually place an order, grey otherwise.
Green-for-buy would make a below-threshold buy look like a trade, which is the
specific misreading the display exists to prevent.
Guarded on `total > 0` for the same reason the normalization is: Direction()
is inherited as-is by every leaf filter, so without it each filter would write
its own opinion into the one shared label and the last to run would win - the
reader would be looking at an arbitrary member's number believing it was the
vote. Drawn after the +-100 range check, so it shows what the threshold is
actually tested against.
CORNER_RIGHT_UPPER: the status lines, control panel and ensemble panel all
live on the left. Registered in WarriorChartPrefixes() explicitly even though
the "Warrior" catch-all already reaches it - that catch-all exists because the
list has drifted twice, not to make entries optional.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Adds DrawUnfilteredSignals (default OFF) and, with it off, replaces the
per-model arrow layer with the decision the EA would really have made.
THE FILTERED ARROW IS DRAWN AT THE ORDER, NOT AT THE THRESHOLD. Clearing
Min_Vote_Open is not the same as trading: a setup can pass the vote and still
never reach the broker (invalid SL/TP, stops-level, ATR warm-up, unsynced
swing history), and every one of those lands in OpenParams' failure branch.
So DrawVoteArrow() fires only after the order parameters validate, and the
failure branch withdraws any arrow already standing on that bar. One arrow is
one entry the EA would have placed - carrying the vote, the threshold it
cleared, and the SL/TP the order would have had.
Classic signals now draw too, under their own name and weight, so a chart
running MA/RSI/MACD/Ichimoku alongside the nets reads the same way an
ensemble chart does. They can only be drawn from the aggregate's once-per-bar
pass, because unlike the AI members they have no cached per-bar scan.
Two subtleties that would each have produced a quietly wrong chart:
- The raw classic draw sits AFTER filter.Direction(), not beside the
journaling block. GetActivePattern*() are CONSUMING reads holding the
PREVIOUS evaluation - "one tick later", which at Expert_EveryTick=false is
one BAR later. Keyed off those and placed at StartIndex(), every classic
arrow would have been drawn one bar early, which on a chart is
indistinguishable from a model that genuinely leads. Peek*() accessors
(non-consuming) let pattern, weight and bar come from one evaluation.
- CExpertSignalAIBase::DrawObject() early-returns instead of gating its five
call sites, so the switch cannot be honoured in three passes and missed in
the fourth. Its delete counterparts stay ungated so flipping the input off
and rescanning clears the raw layer rather than stranding it.
SIG_ARROW_PREFIX and g_signalsVisible move from ExpertSignalAIBase.mqh down
to ExpertSignalCustom.mqh - the nearest common ancestor - because the classic
signals cannot see the AI header (it is included later in Warrior_EA.mq5).
The vote layer gets SIG_VOTE_PREFIX under the same bare prefix, so
WarriorChartPrefixes()' purge still reaches every arrow without knowing they
exist.
NOT YET BUILT: the reconstructed history behind attach. Filtered arrows
currently start where the EA starts. See the next commit.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
NOT COMPILED - user compiles.
The private build still DEFAULTED to TARGET_FRACTAL, so every fresh attach was
training the target adjudicated dead that morning (5,700 model-eras flat at -2pp,
best-of-243 p=0.17). The campaign closed; the default was never flipped back.
Rather than re-default it, the input is withdrawn entirely (user: "remove the
option if there is only one choice for now"). An input offering a single live
choice is worse than no input - it presents a dead option as supported, and an
operator picking it silently trains a model already known to carry nothing.
Direction models are now unconditionally triple-barrier.
Removed: the input, the TrainTargetFractal() call in the signal setup, and the
HoldToBarrier() exit-policy block (which existed only because the fractal vote
flips at swing-marker cadence, ~3-5 bars, far inside the barrier's travel time -
barrier-target models keep vote exits and always did, their label IS the vote's
horizon). Verified no code reference to TrainingTarget survives; the four
remaining mentions are comments.
Kept deliberately, so a rerun is a re-enable and not a rebuild: the TRAINING_TARGET
enum, the fractal label itself, its |TGT:FRA1 fingerprint token, its conditional
barrier-geometry derivation, HoldToBarrier()/m_holdToBarrier, and the campaign's
trained models on disk. Three lines bring it back; Inputs.mqh names them.
ALSO CORRECTS THE RECORD from 1b5a412. I claimed the live run was on the barrier
target, "confirmed" by break-even 34.3% matching the 0.62/1.18 geometry. That
proved nothing - break-even comes from BarrierMultiples, which grades wins
identically under either target. Neutral's share is the real tell: ~31% would say
barrier, the measured 10.6% says fractal. So the imbalance finding stands and its
mechanism is unchanged, but the cause of Neutral being rare was the FRACTAL
target, not the triple-barrier relabel. Neutral fell twice - 94% under the old
exact-pivot ZigZag label, ~31% under triple-barrier, 10.6% under fractal - and
the correction was re-checked at neither step. The fractal campaign was chosen
FOR its balanced classes and did balance Buy vs Sell (48.3/41.1) while quietly
making Neutral the thin residual the correction then subsidised.
Under the barrier target the same geometry gives roughly 34/34/31, where Neutral
is neither rare nor dominant, so 1b5a412's fix should be close to a no-op there -
which is the right answer when there is nothing to correct. It stays: never
subsidising the abstain class is correct under both targets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Systematic audit of the alt-data stack against "any symbol, any timeframe",
prompted by the H4 surprise. Findings, each fixed:
CAPACITY: ALTDATA_MAX_FEATURES was 16 with FX symbols already at 15 - the next
added column would have been silently truncated by a MathMin. Raised to 32,
pin-chars 512 -> 1024.
SYMBOL NAMES: the panel reload path re-derived symbol/timeframe by splitting
the file path on its FIRST underscore - mis-parsing every symbol containing
one (OANDA-style EUR_USD and US_500 are in our own alias lists) and knowing
only three timeframes. It now stores the (symbol, period) Load() was called
with and reuses them verbatim. Path-hostile characters in broker symbols
("EUR/USD") are sanitized by a shared AltDataFileSymbol() used by the panel,
the fetcher and TunedPeriods, so a slash cannot route a write into an
unintended subfolder.
DOWNLOAD VALIDATION: every FRED-family fetch now enforces a per-series
plausibility band (VIX 1-200, yields -5..30, CPI index 20-1000, ...) -
StringToDouble on transport garbage returns 0.0, and one absurd value poisons
every change/percentile feature computed across it (the BatchNorm NaN-latch
incident came from exactly one huge-but-finite input). Rejected rows are
counted and reported, never dropped silently.
LOUD EMPTINESS: a successful response with zero observations on an empty
cache now says so - naming the series (wrong id / format drift) or the COT
predicate (the unverified like-clauses) instead of leaving 0-filled features
unexplained. GEX gains a truncation guard: a day-over-day contract-count
collapse >50% is the fingerprint of a partial 13 MB download, not of markets,
and is skipped rather than recorded as a plausible-but-wrong number.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EIA (user directive: "the NN might find patterns in it for both oil and regular
symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR,
field production, refinery utilization - three features (1y percentile, 4w
change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on
WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not
the screen, decides whether a model trained on them trades. Publication stamp
observed+6d mirrors research/altdata/eia.py.
Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24
instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC
Markets naming, with prefix matching for the broker suffix zoo (US500.cash,
XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are
named by CANONICAL so two brokers' names for one contract share a download.
Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog +
dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and
"No alternative data" is a recorded choice, not a nag. Non-blocking by design:
an unmapped symbol contributes 0 features and must never hold up a chart.
Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the
wildcard and an unescaped one corrupts the query; docs/ gains the whitelist
URLs, an API-key backup, and the catalog reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AltData folder in Common\Files gets wiped before every fresh test, and
keys.txt died with it (2026-08-16 silent-FRED incident). The credential now
travels with the EA: FredApiKey input, owner key as default; keys.txt demoted
to a fallback consulted only when the input is blanked. EiaApiKey stored the
same way - reserved, nothing consumes it since the WTI screen came back null.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PeriodMA/MA_Type/PeriodRSI: input -> const seeds (closing the set: every
indicator parameter is now tuner-owned)
- Variables\TunedPeriods.mqh: chart-level tuned-period state. A gated
install writes TunedPeriods_{SYM}_{TF}.cfg; next attach reads it BEFORE
the DB fingerprint and classic-signal config, so classic votes, DB key,
and tuner seeds always describe the same indicators regardless of
classic/AI/hybrid use. Restart-grained adoption by design (no mid-run
handle churn); new periods re-key the signal DB (semantics rule).
- EnableAltData input in AI Input Features (consumption gate only;
collection keeps running); |ALT DB-fingerprint token; opt-out on an
alt-trained model correctly starts fresh via the width compare.
- Defaults: all four classic votes OFF (AI-first; WARRIOR_MARKET_BUILD
branches collapsed with the marketplace pivot), order-flow/Wyckoff NN
features OFF (alt data is the default information diet; toggles stay).
Compiles 0 errors / 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 18 inputs added 2026-08-08 (when the tuner defaulted off and the
values needed an operator path) become compile-time aliases of their own
defaults - same names, zero consumer churn, byte-identical values. The
tuner is now the only path by which these values move: it defaults ON
(the 08-08 off-flip was measured against the direction target's flat
landscape; the objective is now RANGE, which has signal), searches from
the seeds under the Sidak family-wise gate, and persists winners in the
.nnw beside the weights. ADP fingerprint token retired (deviation now
impossible by construction; tuned values were never its job).
Menu shrinks 102 -> 84 inputs. Compiles 0 errors / 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user is right that no special combination logic is needed: the AI
signals are ordinary voting filters, and the aggregate already has
union semantics - abstaining filters do not dilute the average, so an
ensemble chart trades whenever ANY deployed member clears the vote
threshold and disagreeing members net out. What the ensemble preset
actually adds:
- AI_CHOICE value 4 renamed AI_CONVLSTM (the name says the front-end);
enum VALUES stable, CSignalHYBRID class and State\HYBRID\ folder kept,
so saved configs and trained models keep their identity.
- New AI_HYBRID = 6: enables PAI+CONV+LSTM+CONVLSTM together on one
chart - replaces four separate charts of the same symbol. Each member
trains and self-gates independently; only certified members ever vote.
- |ENS1 fingerprint token on every member, so an ensemble member's
weight files can never collide with a solo model of identical
settings on another chart of the same symbol (the duplicate-chart
guard would otherwise correctly fight over one .nnw).
- Private default AIType = AI_HYBRID: one D1 drop now yields every
topology's gate verdict for that symbol.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
User request: attaching a chart must need zero Inputs-tab edits. Private
(non-Market) build now defaults to AIType=META, all four classic families
ON (they are the sweep's candidate sources), Meta_ExportDataset=true.
Market-build defaults unchanged (AI_NONE, MA/RSI only, no export);
UseDatabaseRanking=true applies to both per the earlier request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user should not need a tester corpus run per symbol. Every pattern
condition in Signals\Signal{MA,RSI,MACD,Ichimoku}.mqh anchors its reads on
`int idx = StartIndex()` with zero hardcoded indices (verified), so a
name-hiding StartIndex override + EvalShift(i) on CExpertSignalCustom makes
the EXACT live ladder code answer "what would you have fired at bar i" -
the silent-divergence trap that justified the DB corpus does not exist on
this path, and neither do the GMT-offset ambiguity, the DB row caps, or
the wipe procedure.
- CExpertSignalCustom: m_evalShift + StartIndex()/EvalShift() +
SweepPrepare(bars) (deep-resizes the shared price series); the four
classic signal classes override SweepPrepare to deep-resize their own
indicator buffers.
- CSignalMETA::BuildCorpusBySweep: per bar x per source filter, run
Direction() shifted, harvest the per-side pattern slots + netVote into
the same corpus arrays the DB loader fills; entry=bar open so
MetaPrepareEra's resolution matches at offset +0 with zero price error.
DB corpus remains the fallback when classic filters are disabled.
- Warrior_EA.mq5: META gets the enabled classic filters as candidate
sources (family ids match the descriptor one-hot).
- UseDatabaseRanking default false -> true (user request): a META chart
journals + ranks out of the box.
Workflow per symbol is now: attach ONE chart with AIType=META (optionally
Meta_ExportDataset=true for the offline pool) - candidates, labels,
training and export all happen in place, ~10 seconds of sweep instead of a
tester run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Meta_ExportDataset input: with AIType=META the chart writes its complete
training set once per attach - every resolved+labeled candidate as
[barTime|family|pattern|side|won|NetInputWidth floats] using the SAME
window builder, descriptor and label caches pass 2 trains on, so offline
examples are byte-equivalent to the EA's own. Sidecar .meta.csv carries
layout + the geometry/BE the labels were computed at. Files land in
Common\Files\Warrior_EA\MetaExport\<sym>_<period>.f32.
This is the pooling architecture decision: multi-symbol training INSIDE the
per-chart God-class would be the riskiest surgery this codebase has seen;
instead each chart exports, the pooled head trains offline (small dense+BN
net, minutes on this box), is validated per-symbol under the same
chronological splits and coverage x (p - BE) gate, and only a WINNER gets
written back into a .nnw for the EA to load natively (format fully mapped).
Also turns every future meta experiment from a 20-minute tester cycle into
minutes of offline iteration.
Cost-model note for the record (user challenge, verified): spread is 0.099
ATR = ~2% of the 4.74 ATR trade width - tiny per bar, but expressed in
win-rate points it is 0.099/4.74 = 2.1pp, which is the measured base-vs-BE
gap and the size of the entire observed skill lift. Zero-spread relabeling
would put base == BE by construction. Multi-day holds additionally pay swap,
which the label does NOT charge - the true bar is higher, not lower.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The NN now has a target that is not per-bar direction (closed, best-of-999
p=1.0000): P(win | this journaled candidate, at the EA's own SL/TP, net of
cost). One net for all 52 pattern-sides, AIType=AI_META.
- NetForward.mqh: the host-side softmax+CE gradient generalized total==3 ->
2||3 on both backprop paths; a 2-class softmax IS a logistic head, and no
compute backend changes.
- SignalMETA.mqh (new): corpus loaded read-only from the LARGEST signal DB on
disk (decoupled from the config fingerprint that burned four S1 runs); the
GMT->server offset is measured PER ROW against entryPrice vs bar open
(DST-immune, histogram logged); a window-span regime filter drops the
pre-2017 daily-backfill rows; 31-feature setup descriptor appended at the
input (26 one-hot + side + tanh netVote + SL/TP ATR + spread/ATR).
- Training.mqh: candidate-queued pass 1, binary-target pass 2, per-candidate
calibration (2.5) and OOS (3) walks. Counter mapping win->Buy / loss->Sell
lets checkpoint selection, the edge floor, the plateau ladder and the
family-wise deploy gate run UNCHANGED: precision reads as win rate among
traded candidates, chance as the base win rate, recalls as sensitivity/
specificity. Era-end META line: coverage x (p - break-even) vs the null.
- Labels are the side-conditional triple-barrier win caches - never the DB's
stop-and-reverse outcome. Logit adjustment deliberately skipped (~40% base
rate). Live inference + online learning guarded off until S3.
- Fingerprint: conditional |TGT:META1; State\META\ folder + 2-output filename
slot keep meta models fully separate from direction models.
Compiles clean (0 errors, 0 warnings). S2 run = attach a chart with
AIType=AI_META; S3 wires the votes via the per-side hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements stage S1 of Meta_Labeling_Design.md, superseding the
original "training-time ladder sweep": the per-side journaling from
652bf81/195be20 already produces the exact candidate stream a sweep
would compute - every pattern instance the live ladders fire, both
sides, uncensored, with netVote and touchable entry price - so the
corpus is READ from the DB instead of re-implementing 26 ladder
conditions in training code. That eliminates the silent-divergence
trap outright: the corpus is by construction identical to live
behaviour. Accepted costs are documented in the module and the doc:
coverage equals the populating backtest, and sampling is one
candidate per fire-stretch (the right dedup for training anyway).
- Expert\AIBase\MetaCorpus.mqh: CMetaCorpus reader (52 tables ->
SMetaCandidate rows) + VerboseMode OnInit report: volume/closed/
S&R-win-rate per family, span, and the GMT->server bar-offset
match table (offsets +0..+3h) that S2''s label plumbing pins to -
measured, not assumed.
- DB_MaxRowsPerTable input (default 1000 = old MAX_TABLE_ROWS): a
corpus build raises it (e.g. 20000) so a 15-20 year backtest
isn''t pruned; wired through CExpertSignalCustom::MaxTableRows().
- Report-only stage: nothing downstream consumes the corpus yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DB system logs objectively; the decision layer reads it to compute
win rates and adjust weights. The journaling path still had one
decision-layer tendril: rows were only written when the root''s
OpenLongParams()/OpenShortParams() succeeded. Those calls validate
ORDER PLACEMENT (broker stops-level, ATR warm-up, entry-mode
rejection) and their failures cluster in volatility/spread conditions,
so the gate non-randomly censored exactly those bars out of every
pattern''s win-rate sample - the same censoring class 652bf81/c8ef478
removed, one layer down. The ledger never needed placement to be
possible: entries are marked at the touchable side of the spread and
exits are same-pattern reversals, not broker fills.
Also documents netVote for what it is: a record of the decision
layer''s state at log time (per-pattern weights inside it drift as
ranking updates land), not an objective measure - the objective part
of a row is pattern/direction/price/result.
SIGNAL_DB_SEMANTICS_VERSION 4 -> 5: row populations gain the
previously censored bars, so the database re-keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verification of 652bf81 on a fresh 7-month backtest DB surfaced the
last one-sided mechanism: ProcessSignal absorbed a reversing signal as
the exit of the opposite trade and skipped registering it. For pure
EVENT patterns that strictly alternate (MACD model 3, the zero-line
cross), every reversal was consumed and the whole ledger landed on
whichever side fired first - 60 Buy rows, 0 Sell rows - so the silent
side never earned a win rate and UpdateSignalsWeights() weighted the
pattern from one side only. State patterns escaped by re-firing one
bar later.
The reversal now closes the opposite trade AND registers its own row;
the existing duplicate/outdated/open-trade checks still bound the
table at one open trade per pattern+side. Row populations change
meaning, so SIGNAL_DB_SEMANTICS_VERSION 3 -> 4 re-keys the database
(the v3 file is orphaned, not wiped - schema is unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The labelMatchesVote gate compared a single last-writer-wins label
(LongCondition then ShortCondition) against the net vote sign, which
structurally censored the pattern tables: a long event co-occurring
with any short-side state model lost its label to the later writer and
was dropped, while the mirrored short event journaled fine. Ichimoku
models 0/3 and MA model 1 could not produce a row at all by
construction (MA model 1 was "revived" in 8710240 yet still could
never journal - its weight-10 vote is exactly cancelled by the
opposing Pattern_0 state), and every pattern's win rate was measured
on a with-trend-only subset - the exact statistic
UpdateSignalsWeights() feeds back into the weights, self-sealing:
no rows -> no win rate -> default weight -> still censored.
- Direction() now evaluates the two ladders separately and snapshots
each ladder's matched pattern into its own side slot; each side that
matched journals its own row. The flat-vote poisoning the old gate
fixed stays fixed: a label can no longer contradict its side.
- The filter's net vote (raw pattern-weight units) is stored as a new
netVote column - data, never a drop filter. Snapshot is keyed on the
ladder setting a label, not on its weight, so a 0%-win-rate pattern
keeps journaling and can recover.
- SIGNAL_DB_SEMANTICS_VERSION is folded unconditionally into the DB
filename fingerprint: pattern-definition changes (b2069bc, 8710240)
re-key the database instead of blending incompatible Pattern_N
populations under one key, which the input-hash fingerprint cannot
see. 7 months of mixed-semantics rows shared one file because of it.
- dbVersion 2.0 -> 3.0: schema changed, and inserts carry the new
column, so the version-mismatch folder wipe is the migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Direction is closed - normalised asymmetry fails on three instruments
with a working positive control, and the classifier's own best-of-999
era-cap test agrees (+0.9pp = 1.48 sigma, family-wise p=1.0000). SIZE is
a different question and RANGE clears at ~4x its null.
Checked the denomination before building on that, since the source memo
warns to: m_excUpCache holds (maxHigh - fill)/ATR, so "RANGE is
predictable" is a claim about travel RELATIVE to current ATR, not a
restatement of "ATR is autocorrelated". It is exactly the part a fixed
multiple (stop 3.31*ATR, target 1.64*ATR) discards.
A second small CNet, 760 -> 24 -> 32 sigmoid outputs = P(price reaches
ladder rung k) upward and downward. Survival parameterisation rather than
regressing the multiple, because it needs nothing new from CNet: sigmoid
outputs and the per-neuron delta the `total != 3` branch already applies
(a quantile head would need a linear activation and a pinball gradient in
Network.mqh, Network.cl and the DirectML path, on a class four topologies
share). Targets are free - m_ladderUpAt already records first-touch age
per rung with 0 meaning never reached.
Separate net, not extra outputs on the classifier: more outputs would
change m_outputNeuronsCount, the .nnw shape and the fingerprint, and push
the count off 3 - the exact condition backProp uses to select the joint
softmax gradient the 3-class head depends on. The classifier is
bit-for-bit unaffected and this is removable without trace.
STAGE 1 PLACES NO ORDERS. It reports a Brier skill score against the
constant per-rung base rate - the baseline a fixed ATR multiple already
assumes - with both predictors fitted IS and evaluated OOS, so neither
gets a look at the test set. Positive skill justifies Stage 2 (drive
SL/TP and sizing off ExcursionQuantile, which is defined and deliberately
uncalled). Zero or negative means ATR already carries everything and
Stage 2 must not be built.
Trains only on primary occurrences: the replay queue oversamples for
CLASS balance, and a direction-balanced sample is a biased SIZE sample.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SignalClusterWindow 3 -> 10 for all topologies. On H1 a 3-bar window
collapsed only the tightest runs and left visible clusters at every
turn; 10 bars is closer to the spacing of genuinely distinct setups.
ALTERNATION. Rule 1 only collapses a same-direction run INSIDE the
window; past it a second Buy is emitted with no Sell between, giving
Buy/Buy/Buy/Sell. With both directions tradeable that sequence is the
model re-entering a move it is already in rather than finding a new
one. The kept sequence must now alternate: the first signal passes,
and after that a direction passes only if the last KEPT signal was the
opposite one.
Added to ALL THREE consumers, with identical logic, because they must
agree:
- NmsLiveAccept -> the live trade
- pass 3's OOS replay -> the tally the deploy gate grades
- PruneDirectionalClusters -> the drawn history
A rule applied to only some of these certifies one strategy and trades
another - the same defect class as the geometry the gate certified
while OpenParams placed something else (9a7c37f) - and would draw the
user arrows the EA would never have taken.
Deliberately NOT applied to the LABEL. The barrier target has no "must
flip" invariant: consecutive Buy labels are routinely correct, and an
earlier alternation gate was removed with the triple-barrier relabel
for exactly that reason. This filters what is ACTED ON, which is what
"applies to training" can honestly mean here - pass 3's declustered
tally is the training-side number that decides deployment.
BothDirectionsTradeable() is the stated precondition (with one side
disabled there is no opposite to wait for, so alternation would
suppress everything after the first call). This build has no
long-only/short-only input, so it is constant true - kept as a named
predicate so a future direction restriction has one place to change
rather than three call sites silently assuming both sides.
Build tag -> nms-alternate-v4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>