Commit graph Warrior_EA/System
Author SHA1 Message Date
AnimateDread
47a5ef338b Refactor Warrior EA: Integrate custom signal modules, enhance voting mechanism, and improve management features
- Replaced standard library signal modules with custom implementations to allow for named patterns and improved voting.
- Added new input parameters for module weights, allowing for optimization of individual signal contributions.
- Enhanced the management of trades with new options for breakeven and management cut.
- Introduced a mechanism for dynamic ranking of signal weights based on historical performance.
- Improved initialization logic to ensure proper registration of filters and handling of trading conditions.
- Added detailed logging for trading permissions and account status during initialization.
2026-09-13 14:32:40 -04:00
AnimateDread
5f8a2b1df8 Remove obsolete log and data files: deleted cpu_directml.log, opencl.log, and profiling.csv to clean up the repository. 2026-09-13 14:32:28 -04:00
AnimateDread
4e5e6fcae3 refactor(ea): delete on-chart training - the EA stops learning and starts executing
Operator, 2026-09-07: "we don't want to train on the legs anymore." That removes the reason
the whole training stack existed, so it goes.

77 files deleted, 68,078 -> 29,113 lines: 57% of the codebase. A full build drops from 66s to
24s. Compiles 0 errors / 0 warnings against a 0/0 baseline taken before the first cut.

THE SEAM. The four AI signal modules (PAI/CONV/LSTM/HYBRID) were the only consumers of
ExpertSignalAIBase -> AI/Network -> AI/Impl/*, AIBase/*, Training/*, Persistence/*, Topology/*,
Labeling/*, Features/*, OnlineLearning/*, ConfigLock/* and the training half of Chart/*. Cutting
those four dropped all of it. Nothing else reached in.

WHAT SURVIVES, and it is the part that matters: System\NNFilter.mqh - 225 self-contained lines
with their own forward pass, reading a plain ASCII model written by research/export_nn_filter.py,
with the feature-name contract that REFUSES a file whose feature list does not match rather than
approximating it. The offline meta-label net's entire runtime already existed; it never needed
any of what was deleted.

THE LEG-RIDE LABEL AND ITS EXIT. LiveLegDirection() replicated the stock ZigZag so the exit could
fire on the same event the label's ride ended on. No label, nothing to agree with - the replica,
the exit and Expert\Labeling\LegState.mqh are gone, and the take-profit is unconditional again
(it was suppressed only to avoid capping the tail the leg label selected for).

ONE THING THIS NEARLY DID SILENTLY. ClassicVotesMoveMoney() returned "no while an AI member is
present, yes when none is registered". Deleting the networks made the second clause true
everywhere - which would have reinstated the worst defect this codebase has had: fifteen classic
modules at weight 1.0 as the live money vote, which is what "the EA is not profitable" turned out
to mean. It now returns false unconditionally. The only thing that can open a trade is an ARMED
BOOK SETUP through the +/-100 override in Direction(). The classics stay wired because they are
silent and free, and because they are the raw material for the agreement count.

Also extracted System\ChartObjects.mqh - the chart-object namespace list and its sweep, which had
lived inside ExpertSignalAIBase.mqh and were never about training.

NOTE THE CONSEQUENCE, PLAINLY: the only book setup that exists in MQL5 is CSignalInsideBarGap and
it is still disabled, so this EA now trades nothing until the book triggers are wired. That is
slice 3 in REFACTOR_PLAN.md and it is a deliberate state, not an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 17:23:36 -04:00
AnimateDread
0686410b42 refactor(logs): two verbosity levels, and a throttle that works in the tester
The operator's complaint that "the logs get filled" is measurable, so it was measured. The
2026-09-06 tester log is 1,570,535 lines and FOUR print statements are 86.6% of it:

   556,732  35.4%  "Starting direction calculation with total filters: N"
   556,731  35.5%  "Final directional result: N"
   247,146  15.7%  the two "open rejected" traces

None of the four is a decision. Direction() runs on every tick and recurses into every filter -
about eight calls a tick on this fleet - and emits two lines carrying a filter count and a
number. The rejection traces fire on every tick a side is blocked, which on a one-sided chart
is forever.

They were all on the same switch as everything else, so turning VerboseMode on to diagnose one
thing produced a journal too large to search. Added a second level, TraceMode, off by default,
and moved exactly those four sites to it. PrintVerbose() keeps its meaning and none of its ~40
call sites changed.

TCLog's throttle was near-inert where it mattered most. It measured its window with
TimeCurrent(), which in the Strategy Tester is SIMULATED time: a pass over years of history
crosses sixty simulated seconds many times a second, so the throttle admitted nearly every call.
That is why 247,146 lines got through a function whose whole purpose is collapsing them. Now
GetTickCount64(), which is real elapsed milliseconds and behaves identically in both worlds -
unchanged in live trading, genuinely one line per key per minute of run time in a tester pass.
Suppressed calls are counted and reported on the next line through, so nothing is hidden.

Compiled clean in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline taken first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:44:31 -04:00
AnimateDread
84d0af6e91 fix(vote): the classic modules were the live book - inputs, not voters; certify with the live stop
The sweep after "still not profitable" found the EA was not trading the strategy it certifies.

THE VOTE. Direction() aggregated all children in one accumulator: 15 classic modules at the
stdlib weight of 1.0 each, the four AI members at trust weights of 0.17-0.22, and the derived
threshold (1% on most charts) had been certified on the AI members' vote alone. Four unanimous
networks netted 1.01; two classic modules confirming a state (10 + 10) netted 1.27 and opened
the trade with the networks silent. Proven in the tester on the old build: EURUSD H1 from
2025-09-01, threshold 1% published at 00:05, market buy 3.5 lots on the first bar, then vote
magnitudes of 4-6 that the networks cannot produce. The book that traded was the classic
consensus this project measured at chance; the certified book could barely open.
- ClassicVotesMoveMoney(): classic modules stay in pass 1 (journal, raw arrows, the vote vector
  the networks read) and leave the money sum and the overlay whenever an AI member exists. With
  no AI member they remain the book. Announced once.
- CExpertSignalAIBase::LiveVote(): the parent sums the AI member's certified contribution
  (module weight x (tier - chance), clamped) instead of its raw tier weight.
- VoteCapableWeight() uses LongCondition's readiness test, m_deployedLive included: a deployed
  member had numerator and no divisor share, which with the classics gone would have been a
  division by zero live while the inference-only tester looked fine.

THE BOOK. The ride book the gate judged carried no stop; every live position carries one at the
published mean adverse excursion. The verdict now gates the ride under that stop (sideG*: a ride
whose adverse excursion reached it pays -stop) and prints both. Sweep line: [book|stopped|cost].

THE REST OF THE SWEEP.
- Scheduled close-all: a +-1 minute window with no catch-up, and Processing() ran on the same
  tick with the cached vote, so it could re-open five minutes before the weekend. Now a per-day
  latch from target-1 min, retried every tick and timer, and OpenPosition refuses while latched.
- News filter: an empty CalendarCountries() answer (the base still synchronising) was cached for
  the session, leaving the filter inert with EnableNewsFilter true. Not cached any more.
- NF_MinImpact default HOLIDAYS vetoed 50% of EURUSD weekday hours (36% GBPUSD, 38% USDJPY,
  27% USD-only), measured on the terminal's own calendar export; HIGH vetoes 14/12/11/9%.
  Default -> HIGH. Charts attached before keep their stored value.
- One m_tradeOwner for both books delegated the short book's exit to a long-only setup, whose
  default CheckCloseShort fell into the base vote exit on the child (m_direction EMPTY_VALUE =
  DBL_MAX >= any threshold): every short died the tick after the inside bar armed. Per-side
  owners, and the stdlib's EMPTY_VALUE guard restored in CheckClosePosition.
- One expiry clock for two books -> per-book m_bookExpiration[2].
- The inside bar's time stop selected the lowest-ticket position on the symbol -> by book magic.

Build tag inputs-not-voters-1. Compiles 0 errors, 0 warnings. Not deployed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:07:53 -04:00
AnimateDread
a765e0f61f fix(news): weight by scheduled importance, not the post-release verdict
Operator's report, and it is correct: ImpactWeightedProximity weighted by
MqlCalendarValue.impact_type, which is MT5's ACTUAL-versus-FORECAST verdict
(ENUM_CALENDAR_EVENT_IMPACT: 0 NA, 1 POSITIVE, 2 NEGATIVE) and is only
knowable after the release. With searchForward=true on a historical bar it
read the post-hoc outcome of an event that had not yet happened - the exact
thing the comment above it claimed was "deliberately not exposed anywhere
here". The comment stated the right principle and the code contradicted it.

Verified against the terminal's own 200,510-release export rather than
argued from the docs:

  - impact_type is 0/1/2 and NEVER 3, so the /3.0 divisor capped the feature
    at 0.667 and 1.0 was unreachable. Exactly as reported.
  - of 16,103 rows where nothing had been released (no actual value), 16,079
    - 99.85% - carry impact_type 0. It is a function of the outcome.
  - it is near-orthogonal to importance: 8,783 of the 13,662 HIGH-importance
    releases (64%) carry impact_type 0. The feature scored ZERO on two thirds
    of the biggest events on the calendar, while scoring its maximum on a
    trivial event that happened to surprise.

THE SAME FIELD WAS ALSO DRIVING THE LIVE NEWS FILTER, and there it is worse.
NF_IMPACT_PRESETS is plainly the importance ladder - HOLIDAYS=0, LOW=1,
MEDIUM=2, HIGH=3, which is ENUM_CALENDAR_EVENT_IMPORTANCE exactly - and it
was being compared against impact_type. Since impact_type never reaches 3,
selecting "High Impact News", the obvious choice for anyone wanting to avoid
major news, made the test unsatisfiable and SILENTLY DISABLED THE FILTER: the
EA would trade straight through NFP with the news filter on and set to its
strictest setting.

Both now read the event's scheduled importance via CalendarEventById, which
is published in advance, takes the full 0..3 the divisor was written for, and
makes the forward-looking half honest - a training bar may know NFP is due in
twenty minutes, because everyone did. Lookups are cached in a sorted array;
a failed lookup is NOT cached, because an unsynchronised calendar base fails
transiently and would otherwise pin an event to 0 for the session.

Topology gains |NEWSV:2 so a model trained on the old leaky feature can never
load against the new one - UseNews and NewsFeatureWindowMinutes could not
catch it, because neither of them changed. Conditional append, per the rule
the MACD/Ichimoku block states, so a fingerprint with news OFF stays
byte-identical.

BEHAVIOUR UNDER THE SHIPPED DEFAULTS IS UNCHANGED, which is why no retrain is
triggered: EnableNews is false, so the feature is off and NEWSV appends
nothing; and NF_MinImpact defaults to HOLIDAYS=0, where ">= 0" vetoed every
relevant event before and does so now. What changes is that every other
preset now means what its label says.

Compiled clean in the _claude_s2build scratch copy: Result: 0 errors, 0
warnings. NOT DEPLOYED - feedback_no_compiling authorises compiling in a
scratch copy and forbids touching the deployed build; _claude_stage's .ex5 is
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:27:33 -04:00
AnimateDread
50fad051d4 fix(news): the calendar was not missing, it was still synchronising
The export now works: 200,507 releases of 1,494 distinct events, 2010.01.01 to
2026.09.06, written on all eight charts and loading clean through
research/newsdata.py.

What it actually was: none of the four query variants. The identical
(NULL, NULL) call that returned zero rows at 16:12 returned everything at
16:22 - the calendar base had simply not finished synchronising, which the
original failure message offered as a possibility and which I then talked
myself out of, twice: first into "this broker has no calendar feed" (wrong -
the Calendar tab comes from MetaQuotes, only the News tab is the broker's, and
the empty news.dat I reasoned from was the wrong subsystem), then into "the
query is being refused" (also wrong).

So the variants stay - a terminal that genuinely refuses the query is still
something this can meet, and a silent zero is precisely the failure that cost
hours - but the normal path must not pay for them in log noise. The plain call
now runs SILENTLY, and only when it comes back empty does anything print, at
which point it prints everything: CalendarCountries(), then each variant with
its n and error. The failure message leads with synchronisation, since that is
what it actually was.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 17:03:56 -04:00
AnimateDread
0dcad40f38 feat(news): export the terminal's own economic calendar - the two news setups become buildable
The operator found a paid MQL5 product that exports historical news, and drew the right
conclusion: MT5 carries the calendar itself and exposes it to MQL5, so the EA can export it the
same way it already maintains alt data. Verified before writing anything - a probe script
compiled clean against CalendarValueHistory / CalendarEventById / CalendarCountryById and the
full MqlCalendarValue field set on this terminal.

THE CONSTRAINT THAT SHAPES IT: the calendar API returns nothing inside the Strategy Tester. So
this follows the alt-data contract exactly - a LIVE chart writes
Common\Files\Warrior_EA\News\calendar.csv, and backtests, research scripts and models all read
the FILE. In the tester it says so once and leaves the existing file alone, because a backtest
must never truncate what a live session wrote.

WHAT IT UNBLOCKS, and it is three things rather than one:
  1. The two NEWS SETUPS - two of the Forex book's six, unbuildable until now. The News Straddle
     carries the only explicit R-multiple in the whole book (~8:1).
  2. The news AVOIDANCE rule both Walsh books state and we have never honoured: flat five minutes
     before a release, nothing new until fifteen after (forex s13). Every backtest so far has
     been holding through releases it should have been flat for.
  3. News as FEATURES for the networks - time to the next release, its importance, and the
     surprise itself (actual minus forecast), which is an axis price data does not carry.

SCALING, STATED HONESTLY. MqlCalendarValue carries actual/forecast/previous as longs and
MetaQuotes documents them as the real value times a million, with LONG_MIN meaning "no value".
That is documentation, not something this code has observed, so every row carries BOTH the
scaled reading and the raw long, plus digits/unit/multiplier - the first live export settles the
convention and nothing downstream has to trust it in the meantime.

Written atomically through AtomicFile, refreshed hourly beside the alt-data fetch.
Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 15:52:25 -04:00
AnimateDread
92fa576224 feat(setup): the setup owns its trade - delegation, network confluence, feature port, expiry fallback
TESTER-FOUND, NOT YET TESTER-CLEAN. Compile-verified (0 errors) in _claude_stage; run 7
(SP500 H1, 2022-2026) shows buy stops shaped 1.25R/8R/60s, the 15-minute time stop
closing them, and the network gate active. Two defects remain open - see the handoff
memory project_handoff_20260906_opus.

WHAT WAS WRONG. The parent CExpertSignalCustom never called a child's OpenLongParams
or CheckCloseLong: once the inside-bar setup voted, the parent shaped a MARKET order
from its own ATR rule (2 ATR stop, no target) and closed it on the vote 9-35 hours
later, or on the very next tick through the protective path. And with the fifteen
classic voters on, the setup's 100 netted to ~11 against a threshold of 15.

Expert/ExpertSignalCustom.mqh
  OwnsTrade() / SetupArmed(isLong) virtuals; ArmedSetupOwner() scans m_filters.
  OpenParams() asks the armed owner for price/sl/tp/expiration first and records
  m_tradeOwner + m_shapedExpiration; the ATR path clears them.
  CheckClosePosition() with an owner returns the owner's CheckCloseLong/Short only.
  Direction(): an armed setup is decisive (+/-100), never diluted by the classics.
Expert/ExpertCustom.mqh
  OpenPosition(): on TRADE_RETCODE_INVALID_EXPIRATION resend GTC and set
  CExpert::m_expiration to the requested expiry. KNOWN DEFECT: that clock is only
  read on the new-bar path (Expert_EveryTick=false) so orders lived 30 min, not 60 s.
Signals/SignalInsideBarGap.mqh
  OwnsTrade/SetupArmed overrides, m_armedBar; CheckCloseLong no longer chains to the
  vote exit; CNNFilter + CInsideBarFeatures wired as the confluence gate (file
  Common\Files\Warrior_EA\Adapt\_ALL__inside_bar_u.nn, feature names checked one by
  one); nn_gate/nn_a/nn_b/nn_agree in the journal context; verbose-mode feature dump
  to Adapt\features_{SYMBOL}_{PERIOD}.csv for research/check_feature_parity.py.
Features/InsideBarFeatures.mqh (new)
  The 48 network inputs computed on a chart from closed bars, in the file's order.
  First parity pass on 13 bars: 30/48 agree, 18 drift - NOT trusted yet.
System/NNFilter.mqh (new)  two groups of dense nets, agreement = both >= threshold.
System/AltData*.mqh
  cot_am_idx3y / cot_lm_idx3y / vix_rank500 appended to the catalog (export v3),
  raw COT cache widened to asset-manager long/short (old caches refetch), HasColumn().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 12:16:41 -04:00
AnimateDread
abda14856f refactor(features): delete the cross-asset panel - 564 lines and 117 touch points of dead code
IT HAS BEEN INERT SINCE BEFORE THIS SESSION. `EnableCrossAsset` is `const bool ... = false`, the only
thing that ever calls `UseCrossAsset(...)` is Warrior_EA.mq5:819 passing exactly that constant, and
`BuildCrossAssetPanel()` returned at its first line. Its 6 features were never added to the 92, and
its fingerprint tag `|XA:` was never appended - so REMOVING IT CHANGES NO FINGERPRINT AND NO FEATURE
VECTOR. Nothing retrains because of this commit.

WHAT WENT:
  System\CrossAsset.mqh                     564 lines, deleted outright
  the panel build, the per-bar emit block, the feature-table entry (FeatureBuilder)
  the member, the accessors, the persistence fields (ExpertSignalAIBase)
  three view interfaces + their two implementations
  the topology feature-count contribution and the fingerprint tag
  the blocking warm-up in OnInit, the sweep-guard rule, the input constant

THE PERSISTENCE FORMAT LOSES A FIELD, and that is safe ONLY because every model was wiped an hour
ago. The .cfg carried a length-prefixed cross-asset pin between the direction-confidence threshold
and the alt-data name list. Both the writer and the two readers (the loader and the cfg walker) drop
it together, so the remaining fields stay aligned - but a .cfg written by the PREVIOUS build would
now have its alt-data pin read from the cross-asset slot. There are no such files.

WHY DELETE RATHER THAN LEAVE IT SWITCHED OFF: it was 117 references across 22 files that every future
reader had to understand before concluding it did nothing - I spent part of tonight doing exactly
that, twice, and got the reasoning wrong the first time (I claimed UseCrossAsset(true) was never
called anywhere; it is called, with a const false). Dead code that takes two passes to prove dead is
not free.

NOT A MEMORY FIX. It was proposed as one and it is not: the panel allocated nothing at runtime. The
27 GB was the indicator tuner (b31f462).

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 15:02:27 -04:00
AnimateDread
c0a13a7715 fix(altdata): one chart per terminal fetches; the rest subscribe
Every chart runs its own CAltDataFetch with its own m_lastAttempt, so six
charts each believed they were the only one: six identical requests for one
shared cache file left within ~270 ms of each other (measured 2026-09-01,
12:40:15.831 -> 16.097), and six writers raced on the same file.

THIS DOES NOT FIX THE EIA EDGE BLOCK, and the burst theory that motivated
looking here is DISPROVEN. Six concurrent requests from this machine return
200, twice over. So does every request shape MT5 could plausibly send
(duplicate User-Agent, no User-Agent, Content-Type/Content-Length on a GET,
HTTP/1.0, keep-alive), every user-agent string, both resolved IPs, and the
Schannel stack. There is no proxy at the MT5, WinINet or WinHTTP layer. The
403 carries Server: awselb/2.0 with an empty WWW-Authenticate and an HTML
body rather than EIA's JSON error, so it is blocked at the edge before the
API ever sees it - seven mechanisms ruled out, cause still unknown.

What is committed here stands on its own: six redundant requests and a
six-way write race on one file are worth removing regardless.

- ClaimFleetFetchLease: terminal-wide lease via GlobalVariableSetOnCondition,
  a real atomic compare-and-set, so of six charts arriving in the same
  millisecond exactly one wins. A get-then-set would let all six read stale
  and all six proceed - the same bug, just faster.
- KEYED ON THE CACHE FILE, NOT THE THROTTLE SLOT. Slots 2 (COT) and 4
  (implied vol) are per-symbol - SP500 reads raw_COT_SP500.csv where EURUSD
  reads raw_COT_EURUSD.csv. A slot-keyed lease would have let one chart
  suppress another chart's fetch of a completely different file, leaving it
  waiting an hour for data nobody would download for it.
- A LEASE ALONE WOULD HAVE BEEN A DATA REGRESSION. AltRawLoad only ever ran
  on rows == 0, so a chart that skipped its fetch would serve startup data
  for as long as the terminal stayed up. PublishFleetRefresh bumps a
  generation counter only where a fetch actually persisted rows, and
  EnsureSeriesLoaded reloads from disk when another chart has published
  since. Global keyed by file (shared); local memory indexed by slot (within
  one chart a slot maps to exactly one file).
- The attempt stamp is written whether or not the lease is won, so a loser
  does not re-enter every tick to lose the race again.
- Leases are temporary globals: one persisted across a restart would
  suppress the first fetch after every launch.

Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 08:21:22 -04:00
AnimateDread
4ae4cda6b7 fix(altdata): print who sent the non-200 - the 401's body and headers were discarded
The 2026-09-01 fleet-wide EIA 401s could not be diagnosed from the journal:
EIA answers 403 to ANY key problem (including no key at all), the stored key
fetched 200 from the same machine over three TLS stacks and both resolved IPs,
and the one thing that names the refusing server - the response itself - was
thrown away at the non-200 branch.

- non-200 now prints the Server and WWW-Authenticate headers (RFC 7235 requires
  the latter on a genuine 401 - it names the party demanding credentials) plus
  the first 200 chars of the body.
- MaskUrl prints '(EMPTY)' when there is nothing between 'api_key=' and '&';
  the unconditional '***' made a blank key indistinguishable from a present one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 15:01:14 -04:00
AnimateDread
04cc345661 perf(tester): split the per-tick profile so the slow bucket names itself
Three turns of reasoning about where a 6.3-minute pass goes have produced
three hypotheses and no measurement. The profiler that would answer it
already existed and printed nothing: across 129 optimization passes on 12
agents the "tester pass profile" line appeared ZERO times, while OnInit's
output appeared on every one.

Two fixes, both aimed at ending the guessing rather than at being right.

1. The line is now built by WarriorTesterProfileLine() and printed from
   OnTester() as well as OnDeinit(). OnTester runs on the agent at the end of
   the pass, BEFORE OnDeinit. If the line appears there and not in OnDeinit,
   an optimization agent is discarding OnDeinit's Print; if it appears in
   neither, g_tpTicks is genuinely 0 and the instrumentation never ran. Those
   need different fixes. The zero-tick case now prints its own explicit line
   instead of staying silent, because a profiler that says nothing when it
   fails is indistinguishable from a fast pass.

2. "Expert.OnTick" was one bucket containing both halves of the question.
   CExpertCustom::Refresh() runs on EVERY tick whatever Expert_EveryTick says
   - correctly, since an open position must be manageable on any quote - and
   under a 1-minute-OHLC model that body executes millions of times per pass.
   Its three steps are now timed separately: TCHasEnoughHistory(),
   RefreshRates(), and m_indicators.Refresh(). They are reported as a SUBSET
   of Expert.OnTick, not as siblings, because double-counting a bucket is how
   a profile lies. ProtectOpenPosition()'s copy of the same refresh counts
   into the same bucket rather than hiding on the declined-tick path.

The accumulators move to System\TesterProfile.mqh: ExpertCustom.mqh has to
see them and is included long before Warrior_EA.mq5's own globals. Every
bracket is guarded on g_tpActive, which OnInit sets only for
MQL_TESTER/OPTIMIZATION/FORWARD - a live chart never reads the clock for it.

No behavioural change to any path. Compiled 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 12:07:09 -04:00
AnimateDread
a970405042 feat(pool,mi): one feature layout fleet-wide, and the keep-screen stops self-disabling on a cold start
TWO CHANGES, BOTH RETRAIN-FORCING BY INTENT.

1. SP500 was training alone, and one alt-data column was the reason.

   The alt block's width joins the model fingerprint, and the pool reader only
   adopts peer rows whose fingerprint and width match. The exporter gives each
   instrument the series that apply to it - FX 15 columns, metals/oil 14, SP500
   13 - so the fleet ran as three incompatible pools:

     EURUSD/USDJPY/USDCAD  adopt ~57-60k peer rows each
     XAUUSD/XTIUSD         adopt 6.4k / 20.3k
     SP500                 "EVERY peer file was REJECTED, so this chart is
                            training alone" - 0 rows

   SP500 therefore trained on 2279 independent observations against a 600-wide
   input with its first layer floored at 16, printing its own "expect
   overfitting" warning. It is the one chart with no pool and the worst
   capacity ratio in the fleet by a factor of three.

   Fresh models now pin ALTDATA_FLEET_COLUMNS - the 12-column intersection -
   instead of their own file header. An existing model still adopts its .cfg
   pin, so this re-keys nothing that is already trained.

   Intersection rather than union: filling an absent series with its median
   makes that column constant per instrument, which lets a pooled model
   identify the source instrument and stop learning the shared mechanism. It
   is also 6 columns narrower. Cost is six columns whose retained information
   is UNMEASURED - the keep-screen reports a bitmask nothing has mapped back
   to names.

2. The MI keep-screen disabled itself for the whole run on any cold start.

   ReportFeatureLabelInformation set m_miReportDone on ENTRY. On a cold start
   the label cache is allocated before it is filled, so BuildMiSample finds no
   row carrying a resolved label and returns 0 - a sixth exit, and the only
   one the 8c1266d instrumentation did not cover, which is why it printed
   nothing. observed then stayed -1, the permutation loop never iterated, and
   the report emitted "-1.00000 nats over 0 permutations" beside a plausible
   "strongest single feature 0.05979" that was a STALE m_miBestColumn from an
   earlier scoring call. The first ensemble member propagated the latch to
   g_ensembleChartMiReportDone and silenced every member on the chart.

   The flag now latches only once a measurement exists. A short sample is
   reported as a deferral naming the two numbers that identify it (cached bars
   vs bars carrying a resolved label) and retried, up to
   MI_REPORT_MAX_ATTEMPTS.

Build tag -> fleet-pool-v1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:41:14 -04:00
AnimateDread
d059780c22 fix(io): stage atomic writes to a PER-CHART temp, not a shared one
A DATA-INTEGRITY BUG, pre-existing, surfaced by the clearer failure message in
869cd1b putting two identical timestamps next to each other:

    13:42:36.584 (EURUSD) AltRawSave: atomic rename raw_EIA_WPSR.csv.savetmp -> ... failed
    13:42:36.584 (XTIUSD) AltRawSave: atomic rename raw_EIA_WPSR.csv.savetmp -> ... failed

Same file, same millisecond, two charts, a third winning the race. That is not
reader/writer contention - it is THREE WRITERS on one destination, and
AtomicWriteBegin derived the staging name from the destination alone:

    tmpName = finalName + ".savetmp"

So all three opened the SAME temp with FILE_WRITE and wrote it from offset 0 at
once. The published file could be an interleaved mixture of two charts' output,
and the atomic rename publishes that mixture faithfully - the swap guarantees a
reader never sees a HALF-WRITTEN file, and does nothing about a HALF-CORRECT one.
Alt-data is the exposed case: several charts fetch the same series and write the
same Common file.

Keying the temp on symbol+period makes staging private. The rename stays the only
contended operation, and a rename IS atomic, so a loser now publishes nothing
rather than half of itself. It also makes deferred promotion sound for the first
time: the temp promoted later is THIS chart's complete content, never a fragment
of someone else's.

SharedFileCopy.mqh uses the same shape but its destination is agent/terminal-local
and keyed by symbol+fingerprint, so charts cannot collide there. Left alone.

Note the two bugs are independent and both fixes are real. Confirmed in situ at
13:45:45, on the reader/writer one:
    CTrainPoolWriter::Publish: atomic rename TrainPool\USDCAD_16388.bin failed (5004)
    Warrior: deferred promotion of TrainPool\USDCAD_16388.bin succeeded - the peer
    chart that held it has closed it, and the content written earlier is now live
    without rewriting the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:48:43 -04:00
AnimateDread
869cd1b40c fix(pool): defer the atomic promotion to the timer instead of spinning on the tick
REPLACES the in-line retry from ad4ae58, which was the wrong shape and did not
work. Measured after deploying it:

    atomic rename ... failed (error 5004) after 4 attempts

MQL5 exposes no FILE_SHARE_DELETE, so a rename CANNOT succeed while any reader
holds the destination open - it is not a lock that waiting longer wins. The
retry assumed a peer holds a pool file for "tens of ms"; USDCAD_16388.bin is
134 MB and a peer reading it holds the handle for SECONDS. The loop lost every
time and bought nothing but 75ms of tick latency on the failure path.

The content is already written and correct - only the SWAP is blocked. So try the
rename once, and on failure remember the temp and promote it from OnTimer, where
I/O belongs. Once the reader closes, a single FileMove lands it. That beats the
old fallback of waiting for the next full publish, which rewrites all 134 MB and
may be an era away.

  * pending list is bounded (8) and deduplicated - AtomicWriteBegin reuses one
    temp name per file, so a second failure for the same file must not take a
    second slot. A full list falls back to the previous next-publish behaviour.
  * a successful write FORGETS any queued promotion for that name, so a stale
    temp can never overwrite fresher content.
  * a vanished temp (a later publish succeeded outright) is dropped, not retried.
  * a landed promotion is LOGGED. Silence is what made me misread the last
    attempt as working when there had simply been no contention in the window.

Compiled clean; NOT yet run - and note that verification needs a collision to
occur, which happened ~27 times across a whole day. Absence of the message in any
one window is not evidence either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:41:35 -04:00
AnimateDread
ad4ae58814 feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.

It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.

Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.

THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.

THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.

Retrain-neutral. Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00
AnimateDread
6adb710a79 fix(binomial): correct tail calculation in BinomialUpperTailP and add tests for accuracy 2026-08-25 23:37:22 -04:00
AnimateDread
0fddaeea12 fix: correct edge floor percentage calculation and logging for model training 2026-08-25 23:16:05 -04:00
AnimateDread
b2784b5a4d Enhance Feature and Topology Interfaces with Bulk Operations and Cache Management
- Added bulk read/write methods for feature caches in IFeaturesView and its implementations to optimize performance.
- Introduced LabelCacheInvalidateAll method to manage label cache invalidation alongside feature cache.
- Implemented PooledIndependentBars method in topology interfaces to account for additional independent observations.
- Enhanced risk budget management with throttling for peak-equity updates to reduce unnecessary file operations.
- Improved error handling and logging for ATR trailing stops to ensure better visibility of issues.
- Updated alt-data handling to prevent unnecessary operations during testing and optimization phases.
2026-08-25 22:51:50 -04:00
AnimateDread
484a9d8b0f fix(panel,arrows): one deploy predicate, a deployed-only readout, and persist the vote arrows
Four reported symptoms, three of them one root cause: the ensemble's
certified record was session-scoped and written ONLY at pass-3
completion. A deployed ensemble runs no further eras, so every restart
lost the aggregate win rate, the aggregate panel line and the overlay
snapshots - and could never regenerate them, because regeneration only
happens at an era end that will never come.

THE SELF-CONTRADICTION. Member rows read "Live - learning from new bars"
(from m_trainingComplete) while the line under them read "training, not
tradable yet" (from `prospective`, which means "this number came from
ProspectiveVote() rather than a real Direction() call" - what happens on
any bar where every member abstains, and which says nothing whatever
about training state). Both now resolve through one predicate:
WarriorChartModelsDeployed(), fed by members publishing their own state
on the same slot and cadence as their vote. Adds a third verdict word,
"armed (bar still open)", for a deployed model on a prospective
recompute - the case that used to claim it was training.

DEPLOYED PANEL. Once every published model is converged the per-member
rows are dropped: what ships is the aggregate vote win rate, the live
vote, and the verdict. While training the rows stay - they are the only
way a collapsed or lagging member is visible, since a collapsed member
abstains and so is invisible in the aggregate by construction.

ACCURACY NOW RESPECTS THE ENTRY THRESHOLD. The panel's "precision 65%"
came from m_cumOosCorrect/m_cumOosTotal, which counts every bar a model
called Buy or Sell - threshold-blind, and per-model rather than
per-vote. The correct number already existed (votePrecPct: bars where
|vote| >= threshold and the direction policy allows) and is now what the
panel shows, with the threshold named in the text because the number is
meaningless without it.

VOTE ARROWS PERSIST. With DrawUnfilteredSignals off - the default - the
chart shows SIG_VOTE_PREFIX arrows, and nothing saved them:
CChartUI's .arrows sidecar is member-scoped and never saw that layer.
New CVoteArrowStore mirrors them to a chart-keyed sidecar and restores
them progressively at init, on the same budgeted non-blocking path.
The header stores the open/close thresholds; a mismatch on load DISCARDS
the arrows rather than redrawing a picture of a strategy no longer
configured - stale arrows are worse than none, because none is visibly
empty and stale is confidently wrong.

Also: .stats bumped to WST7 carrying the ensemble record (guarded on
threshold match, most-complete-copy-wins), and the loader's version
tests collapsed from an or-chain to ">=" - the magics are ASCII 'WST1'..
'WST7' so they are already ordered, and a missed arm in that chain reads
the NEXT field's bytes into this one, which fails as plausible numbers
rather than as an error.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:00:15 -04:00
AnimateDread
6d48fdb4cd perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.

1. OnDeinit gets a tester/optimizer fast path.

   Everything in the live teardown exists to leave a CHART clean and a live
   model's state on disk. An optimization agent has neither. It was still
   running, on EVERY pass: a per-signal arrow-sidecar WRITE
   (ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
   chart-object scans plus a ChartRedraw. At optimization scale that is
   hundreds of thousands of pointless file writes per agent, against a
   ~4,500 ms budget MetaTrader force-terminates on - the shape of thing
   that stalls an agent rather than failing it.

   The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
   never leaves a half-written era) and still calls dbm.Deinit() and
   Expert.Deinit() - leaking the signal tree or a handle across passes is
   its own way to accumulate into a stall. The two now-unreachable
   !isTesterRun guards further down are folded away.

2. All four tester handlers are present and documented by WHERE THEY RUN.

   OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
   once per session; only OnTester runs on the agent, per pass. OnTesterPass
   was missing entirely - added empty and deliberately so: it only fires for
   passes that shipped FrameAdd() data, which this EA never sends, and
   reading frames there would put per-pass work on the terminal's critical
   path. Declared so that adding frame-sending later fails loudly instead of
   silently dropping every frame.

3. Expert_EveryTick is now actually enforced.

   It was passed to Expert.Init() and only ever reached StartIndex() - which
   bar a signal READS. The whole pipeline still ran on every quote. It now
   gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
   drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
   chart arrows, one-shot vote state), and re-running it on every tick of a
   4-hour bar repeats all of it.

   Scoped deliberately. Everything after that line still runs per tick -
   CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
   are risk management, and a stop that only trails at bar boundaries is a
   different strategy, not a faster one. The scheduled close-all in OnTick()
   matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
   over the thing 100% of label timeouts already resolve against.
   g_riskBudget.Update() also stays at quote frequency, by design.

   System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
   had zero callers and kept its watermark in a `static`: ONE watermark
   shared by every caller, so the first caller each tick consumed the
   transition and every other caller was told "no new bar" for a bar that
   had just opened. Per-instance state fixes that; first observation counts
   as new, so a fresh attach acts immediately instead of idling up to a full
   bar.

Compile-verified in _claude_stage: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00
AnimateDread
b5d34a82b4 feat(panel): one live vote line, no stale era count, no per-model HUD
Two chart-display fixes reported after watching a converged 4-model
ensemble: the ensemble panel's trailing "(era 69, 4 models,
DEPLOYING)" was frozen at whatever era the ensemble happened to
deploy on, and the separate top-right HUD (one line per model, raw
B/S/N + weight + era + error) was clutter once the vote itself is
what matters.

Root cause of the freeze: g_ensembleVoteLine is written once per era,
at pass-3 completion. A deployed/converged ensemble runs no further
eras (ScheduleTrainingIfNeeded's trainingComplete branch skips
Train() entirely), so that line could never update again - the era
count and "DEPLOYING" marker were permanent set-dressing from the
deploying era, not a live reading.

- EnsembleScoreCombinedVote() drops the era/DEPLOYING tail once
  g_ensDeployApproved - nothing left there worth freezing.
- UpdateVoteReadout() (the aggregate "VOTE ..." line, previously its
  own top-right chart object) now writes g_liveVoteLine instead of
  drawing anything. Both status-label builders - PublishEnsembleStatus
  for the ensemble panel, PublishStatus's choke point for the solo
  panel - append it as one line, refreshed every tick/timer exactly
  as the old HUD was, so the live vote replaces the frozen era tail
  in the same visual slot.
- RefreshVoteReadout()'s per-member loop (DisplayHudLine, one
  ObjectLabel per model) is deleted outright rather than folded in -
  the operator asked for the aggregate only, "without telling me each
  individual network".

Follow-on dead-code removal, since DisplayHudLine was the only
caller: the DispProb/DispSignal/MetaGateArmedNow/MetaHasScore/
MetaLastP/MetaLastBe/MetaApproved/MetaVetoed leg of IChartView (and
its AIBaseChartView/AIBaseChartViewImpl/ExpertSignalAIBase forwards)
had no other reader. The underlying data survives untouched -
m_metaTelemetry is still populated live by SignalMETA.mqh,
m_dispSignal still feeds ProspectiveVote - only the chart-view
forwarding that existed solely to reach the deleted HUD is gone.

Compile: 0 errors, 0 warnings (stage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:27:41 -04:00
AnimateDread
0e74ec88ed refactor(mi): dedupe the six hand-written permutation p-value formulas
FeatureScreen.mqh's MI/permutation-null diagnostics (mean/best-col
report, excursion report, lag-profile family-wise test, barrier-
geometry scan) and AutoTune.mqh's TuneIndicatorsByFilter install gate
each spelled out the add-one-smoothed Monte-Carlo p-value
(1+atLeast)/(draws+1) independently. Added PermutationPValue(atLeast,
draws) to System/BinomialStats.mqh (returns 1.0 for draws<=0, matching
every existing call site's own guard) and replaced all six inline
expressions with a call to it. Pure arithmetic substitution, no
control-flow change.
2026-08-24 04:08:47 -04:00
AnimateDread
53de361e9c refactor(altdata): split CAltDataFetch's HTTP client and symbol catalog into collaborators
CAltDataFetch (System/AltDataFetch.mqh) mixed six unrelated concerns in one
1406-line class. This is plain composition, not the Expert/AIBase view+adapter
pattern - CAltDataFetch has no single-inheritance parent forcing an adapter,
same shape as Database/DatabaseManager.mqh composing its four Database*
managers.

Extracted, grep-confirmed zero external callers of any moved method (only
Warrior_EA.mq5's already-public Update/NeedsMapping/CatalogCount/CatalogName/
CatalogLabel/SaveUserMapping surface, unchanged):

- CAltDataHttpClient (System/AltDataHttpClient.mqh): HttpGet/HostOf/MaskUrl/
  BlockedIndex (4014 per-host backoff), JsonField, UrlEncodePart. Owns the
  m_blockedHost/m_blockedUntil/m_urlAlerted state - STATEFUL, moved verbatim.
- CAltDataCatalog (System/AltDataCatalog.mqh): the SAltSymbolSpec catalog
  (AddSpec/BuildCatalog/AliasMatches/FindSpec) plus symbol_map.cfg user-mapping
  persistence (LoadUserMap/SaveUserMapping) and the public
  NeedsMapping/CatalogCount/CatalogName/CatalogLabel surface. Owns
  m_specs/m_userFrom/m_userTo/m_userMapLoaded - STATEFUL, moved verbatim. Added
  one new Spec(i) getter (by value - 4 strings + 1 double, cheap) so
  CAltDataFetch::Update()/RebuildFeatures() can read a resolved catalog row
  without reaching into the collaborator's array.
- LoadRaw/SaveRaw (raw-series CSV <-> file mapping) had zero member-state
  dependency - turned into free functions AltRawLoad/AltRawSave, matching the
  file's own existing AltSeriesAppend precedent, instead of a needless class.

CAltDataFetch itself keeps three concerns as a deliberate partial, same
judgment already applied to Topology's boot sequence / Features' shared-
indicator lifecycle elsewhere in this campaign: the four per-source fetch
pipelines (UpdateFred/UpdateCot/UpdateEia/UpdateGex, including FredKey/EiaKey/
LoadCommonKey/ShouldAttemptFetch/the GEX CBOE helpers) and the feature-CSV
builder (RebuildFeatures/FeatureValue/RollingPctRank/AsOf) both read/write the
9 SAltRawSeries caches kept resident on the orchestrator between timer ticks -
splitting them out would mean either relocating that cache's ownership or a
9-13 parameter signature per method, a larger design decision better made as
its own pass rather than forced through unattended given the finding's own
"high risk" estimate.

Every moved method body is copied verbatim (statement-by-statement diffed
against git show HEAD~1:System/AltDataFetch.mqh) with only the mechanical
substitution HttpGet/JsonField/UrlEncodePart -> m_http.*,
LoadRaw/SaveRaw -> AltRawLoad/AltRawSave, FindSpec -> m_catalog.FindSpec, and
m_specs[si].X -> spec.X (spec = m_catalog.Spec(si), resolved once per Update()
call instead of re-indexing). ALTFETCH_TIMEOUT_MS/ALTFETCH_GEX_TIMEOUT_MS
macros moved into AltDataHttpClient.mqh (their logical owner); ALTFETCH_DIR
stays in AltDataFetch.mqh, defined before both new #includes since
CAltDataCatalog's ctor path (symbol_map.cfg) and CAltDataHttpClient's HttpGet
default param reference it.

Self-compiled 0 errors, 0 warnings (_claude_stage, MetaEditor64 /compile),
twice - once before and once after a stale doc-comment fix (a leftover
"same reasoning as SaveRaw" mention updated to AltRawSave).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 03:32:02 -04:00
AnimateDread
1b077eeee4 refactor(persistence): dedupe the exponential-backoff retry loop into RetryWithBackoff
CopyFileWithRetry (System/SharedFileCopy.mqh) and CModelPersistence::
LoadNetWithRetry independently implemented the identical 5-attempt
Sleep-doubled-and-capped retry shape around a different single
operation, with a comment on the latter pointing at the former as the
"same reasoning" instead of sharing code. Added System/RetryWithBackoff.mqh:
an IRetryableOp interface (one bool TryOnce(bool quiet) method, MQL5 has
no closures/function pointers that bind per-call-site arguments) plus the
RetryWithBackoff(op, attempts, initialDelayMs, delayCapMs) loop. Each call
site now defines a tiny local operand class (CCopySharedFileOp,
CLoadNetOnceOp) and keeps its own tuning constants (150ms/1000ms cap vs
200ms/2000ms cap) unchanged - pure mechanical relocation, no behavior
change. CModelPersistence stays stateless (grep-verified in the prior
Persistence extraction): CLoadNetOnceOp is a separate local class, not a
new member on CModelPersistence itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 02:55:04 -04:00
AnimateDread
a8e6572d1d refactor(altdata): route AltDataFetch's two temp+FileMove writers through AtomicFile.mqh
SaveRaw() and RebuildFeatures() hand-rolled the same FileOpen(tmp)->write
->FileClose->FileMove(FILE_REWRITE) swap AtomicWriteBegin/AtomicWriteEnd
already generalize. AtomicWriteBegin hardcoded FILE_BIN (every prior
caller wrote binary payloads); these two write plain ANSI CSV lines via
FileWriteString, so AtomicWriteBegin now takes an optional modeFlags
param (default FILE_BIN, unchanged for the 4 existing callers) and the
two AltDataFetch sites pass FILE_TXT|FILE_ANSI.
2026-08-24 02:47:57 -04:00
AnimateDread
8dd840673b refactor(chart): dedupe StatusLabel's line-deletion loop
SetStatusLabel's trailing-line trim and ClearStatusLabel's full wipe
had byte-identical bg/txt ObjectFind+ObjectDelete loop bodies, differing
only in start bound. Extract DeleteStatusLines(fromIdx, toIdx); both
call sites keep their own counter reset/redraw. Pure UI, no behavior
change.
2026-08-24 02:13:55 -04:00
AnimateDread
d1d5719be0 refactor(trade): dedupe ResolveOrderType into TCResolveOrderType
CExpertCustom and CExpertSignalCustom each defined their own
stops-level order-type decision against the same TCStopsLevel()
helper, identical except CExpertSignalCustom's guard skipped the
EMPTY_VALUE check (a huge finite double would fall through into the
ask/bid comparison and misclassify as a pending order instead of a
market order). Moved the logic into TCResolveOrderType() in
System/TradeChecks.mqh, keeping the more defensive guard; both
classes now delegate. ask/bid are still passed in from each class's
own m_symbol so routing keeps using whatever RefreshRates() snapshot
that class already had - only the duplicated arithmetic moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 01:39:19 -04:00
AnimateDread
73b6ab9724 refactor(system): TunedPeriods reuses AltDataFileSymbol instead of re-implementing it
TunedPeriodsFileName() inlined the same 9-line StringReplace sanitizer
AltData.mqh already defines once as AltDataFileSymbol(), whose own
comment said it was meant for exactly this. Gave AltData.mqh a proper
include guard (it had none) and included it directly from
TunedPeriods.mqh so the call is safe regardless of include order.

Compile-verified 0 errors/0 warnings.
2026-08-24 01:31:14 -04:00
AnimateDread
d6e3236f69 refactor(crossasset): dedupe the pinned-pair-set resolution in Warm/WarmBlocking
CCrossAssetPanel::Warm() and WarmBlocking() opened with the identical
pinned-vs-discovered pair-set resolution block. Extracted a private
ResolvePairSet(pairs) returning whether a usable set was resolved; each
caller keeps its own early-return semantics (void vs bool-true) around
the call. Pure mechanical relocation, no arithmetic/behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 01:15:44 -04:00
AnimateDread
cc974683ab refactor(altdata): dedupe the throttle/staleness gate across Fred/Cot/Eia updaters
Added private CAltDataFetch::ShouldAttemptFetch(s, staleDays, throttleSlot, now)
combining the staleness check (parameterized by staleDays) and the byte-for-byte
identical throttle-check-and-stamp that opened UpdateFred/UpdateCot/UpdateEia.
Each call site drops from 5-6 lines to one guard call; UpdateGex is untouched
(its day/hour gate is unrelated). Same dedup doctrine as c214d3e (FredKey/EiaKey)
and defab3b (identifier-validation guard), just one level up the call chain.
2026-08-24 01:11:36 -04:00
AnimateDread
c214d3e1c6 refactor(altdata): dedupe FredKey/EiaKey into one CAltDataFetch::LoadCommonKey
FredKey() and EiaKey() implemented the same contract (input, then cache,
then a keys.txt "<prefix>=" line, cache + warn once) differing only in
which input/cache/warned-flag/prefix/messages they used. Both are now
one-line callers of a shared private helper taking those as parameters
(cache and warned-flag by reference). Pure relocation, no behavior
change - verified the warning text is character-identical at both call
sites and that the one flag-timing difference (LoadCommonKey only sets
warnedFlag inside the branch that actually prints, matching EiaKey's
original shape, vs FredKey's unconditional set) is unobservable since
a found key always short-circuits on the cache check before the flag
is ever read again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 01:04:34 -04:00
AnimateDread
a61a47e2d3 refactor(persistence): CopyFileWithRetry/CopySharedFile are pure functions, not signal methods
Neither ever touched a CExpertSignalAIBase member - both take everything as
parameters. Moved to System\SharedFileCopy.mqh (free functions, same doctrine as
TradeChecks.mqh's TC*), matching what they actually are instead of carrying them as
methods on a class they don't depend on. Topology.mqh's call sites are unchanged -
unqualified calls from within a class method resolve to the free function exactly
the same way. Compiled clean.

Correction to project_oop_module_pattern's Persistence(696) note: LoadNetWithRetry
is NOT a third pure utility alongside these two - it touches Net/dError/dUndefine/
dForecast/dtStudied/m_activeFileName/m_activeFileCommon/m_eraCount/m_trainingComplete.
The rest of Persistence.mqh (EnforceTopologyContract, Save/LoadModelStats,
ValidateCpuInference, Save/LoadAndCompareTopologyConfiguration, ReadAltDataPinFromCfg)
is heavily coupled to signal state - a real view+adapter extraction on the scale of
ChartUI's, not attempted here.
2026-08-23 21:18:32 -04:00
AnimateDread
04d70fb748 refactor(dry): one writer for the era-resume context, one for an alt-data row
Two literal duplications the scan found, both of the kind where a divergence is
silent:

- Training.mqh stashed the era-loop resume context at FOUR yield points, seven
  identical assignments each (pass 1 differing only in i-1). A field missed at
  one of them resumes the next chunk against a different era than the one that
  yielded, and nothing reports it until the numbers drift. Now StashEraResume().

- AltDataFetch grew its five parallel arrays inline in three places. They are
  one record split across five buffers, so a resize missed on any one reads out
  of range on the NEXT append, not at the site of the mistake. Now
  AltSeriesAppend(), which returns the new index and zero-fills; callers set
  only the columns their source has.

Braces balance across every in-scope file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:36:36 -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
81ad276859 docs(altdata): the as-of comparison is naive-vs-broker-time, and the publication buffer is what makes that safe
Features() binary-searches m_rowTime (StringToTime of a naive "2026.08.21" = 00:00) against
m_Time.GetData(idx), which is BROKER time. A row dated D carries values public from D 00:00 UTC,
so every bar in [D 00:00 broker, D 00:00 UTC) - the first H4 bar of each broker day - reads that
row 2-3 hours before it was public.

Not a leak today: every collector stamps with a conservative buffer on top of the real release,
and the overshoot fits inside all of them. Tightest is the FRED daily series - VIX prints 16:15 ET
and is stamped D+1 00:00 UTC, leaving >=1h45 in the worst DST alignment. COT (Friday 15:30 ET ->
Saturday 00:00 UTC) has no bars to read it before Monday at all; EIA has ~8h.

Recording it because nothing in the file said so, and the safety lives in a number nobody would
think to check before shortening a stamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:01:50 -04:00
AnimateDread
90c6e26e94 feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it
MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and
the lattice structure that shape of generator has. Two places here
actually lean on randomness and both were hurt by it:

WEIGHT INIT. Six He/LeCun-uniform sites drew
((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of
~250k weights had only 32768 possible values and thousands of
connections started byte-identical. Breaking that symmetry is the whole
job of random init.

SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand()
draws to reach 30 bits, and its own comment documented the residual
modulo bias it still carried. HQRndUniformI() is rejection-sampled and
exactly uniform, so the splice and the bias note both go.

CHighQualityRand is L'Ecuyer's combined multiplicative congruential
generator - two differenced streams, 31-bit output, period ~2.3e18 -
and it ships with the terminal.

AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount())
calls sit immediately before "build a fresh topology", once per model.
GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every
member inside one OnInit, so members could be handed the SAME seed and
draw the SAME weights wherever their shapes coincide - and members that
start identical are not an ensemble. WarriorRandSeed() takes a salt (the
model id) plus a never-reset call counter, so a collision is impossible
rather than merely unlikely, while the tick keeps the run itself
genuinely unrepeatable the way those call sites asked for.

Seeds are masked positive rather than trusted: HQRndSeed computes
s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the
generator in a state its own assertions reject. GetTickCount() is a uint
and goes negative as an int after ~24 days of uptime - a fault that
would surface as "training is broken" on a long-running terminal and
nowhere else.

The indicator tuner's 52 draws move across too: its random search is
where sample quality earns its keep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
AnimateDread
786aa76083 refactor(dry): one shrinkage estimator for classic ladders and AI tiers
The Beta-prior arithmetic that turns counts into a ranking weight was
written twice, term for term: WinRateFromCounts() for the classic
pattern ladders and RankTiersFromOos() for the AI confidence tiers.
Same formula, two transcriptions, and the same class of duplication the
binomial SE consolidation removed a few commits ago.

ShrunkRatePct() in System\BinomialStats.mqh is now the only copy. The
two call sites keep what genuinely differs - the classic path passes RAW
trade counts with a prior of MIN_TRADES_FOR_WIN_RATE, the AI path passes
OVERLAP-CORRECTED effective counts with TIER_PRIOR_EFF_N, which is far
smaller precisely because effective counts are - and that contract is
now stated once, in the function, instead of being implied by two
comments that could drift apart.

Also fixes a difference the consolidation exposed: with an empty sample
and a prior present, the posterior mean IS the prior, and returning 0
there would have handed a tier a vote weight of zero on no evidence.
The AI path could reach that (effN can round to 0 when labels overlap
heavily); the classic path cannot, since it returns NO_DATA_WIN_RATE
first.

Corrects a stale note of my own in passing: this ranking was recorded as
a "raw win rate behind a MIN_TRADES cutoff heuristic". It is not, and
has not been for some time - it is already a proper empirical-Bayes
estimator with a per-filter pooled prior. Replacing it with a
significance test, as that note implied, would have swapped the
estimator the weight needs for a gate answering a different question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:47:06 -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
0826f8b900 refactor(dry): one definition of "usable quote" in the pre-trade checks
Six checks each fetched bid/ask and rejected a non-positive pair with their
own wording. TCLiveQuote() now owns that rule, so what counts as a usable
quote is defined once and every rejection reads the same way. The one
message that said only "no live quote for <symbol>" now names what it was
about to do, like the other five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:33:19 -04:00
AnimateDread
ea2552efe2 refactor(stdlib): adopt Math\Stat for the deploy gate's normal tail; retire the b1/b2/lr/momentum macros
The gate's NormalUpperTail was a hand-rolled Abramowitz & Stegun 26.2.17
approximation. Its own comment gave the reason - "drags a chain of headers
behind it" - and that turned out to be one file: Math\Stat\Normal.mqh
includes only Math.mqh, which includes nothing. Swapped for Cody's rational
approximation in the library (~18 significant digits vs |error| < 7.5e-8).
No past verdict changes: at the z the gate operates on, the difference is
orders of magnitude below DEPLOY_FAMILY_WISE_ALPHA.

Adopting it needed the four bare macros in AI\Network.mqh gone first.
"#define b1 AdamBeta1" collides with an identifier in Math.mqh, so the
include would have macro-expanded the library's own local and failed to
compile - the same landmine that made the original author rename the
approximation's coefficients to ntB1..ntB5 rather than use the reference's
b1..b5. lr, b2 and momentum are the same class of hazard: single-token
global macros in a 52k-line codebase. All four now resolve to the input
names they always aliased, which is a pure textual identity - verified zero
bare occurrences remain.

Also:
- SelectionSort over the buffered signals was O(n^2) with an O(n^2) count of
  StructToTime calls, because the comparison rebuilt both datetimes from the
  six int date fields every time. Now materialises the keys once and does an
  insertion sort; ArraySort cannot permute a struct array. IsEarlier goes
  with it, MakeDateTime becomes SignalTime.
- Seven FileOpen sites lacked FILE_SHARE_READ|FILE_SHARE_WRITE, including
  AtomicWriteBegin, which stages every model save. All 43 sites now carry
  them - an exclusive open fails outright when another process holds the
  path, which here has meant a silently skipped save.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 19:31:36 -04:00
AnimateDread
552edb5fb1 refactor(yagni): drop 13 accessors nothing called; unify the ATR trailing pair
Verified dead by grep across all first-party sources (references/, Scripts/,
research/ excluded): EraCount, HiddenLayersCount, LstmHiddenSize, ConvFilterCount,
HistoryBars and MinTrainYear setters, PendingBatchSamples, getPrevOutIndex,
BaseCurrency, QuoteCurrency, CurrencyCount, IsLoaded, LastFiredDirection,
DBConfidence, SpecIndex, and the conv Step/WindowOut shape accessors. Every
backing member stays - each is still read internally and several are pinned by
the positional .cfg layout - so this removes surface, not behaviour.

Two comments were asserting the opposite of the code and are now true: the
"No setter: the taper's endpoints are derived" note was directly above three
setters, and the conv shape block claimed EnforceTopologyContract reads all
three accessors when CNet::FirstConvWindow only ever calls Window().

CTrailingATR::CheckTrailingStopLong/Short were byte-identical but for Bid vs Ask
and the isLong flag; both now delegate to one CheckTrailingStop body.

Deliberately NOT removed: the fractal-target branch (TrainTargetFractal,
IsFractalTarget and their label machinery). It reads as dead because the
TrainingTarget input was withdrawn, but Warrior_EA.mq5:836 documents it as a
parked option with a three-line restore path - that is a product call, not a
refactor.

Not compiled - MetaEditor compile pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 18:55:36 -04:00
AnimateDread
f64e0f8b67 feat(ensemble): per-NN inputs replace the preset selector - the meta head becomes the vote's gate
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>
2026-08-19 13:01:02 -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
fca610fea0 fix(indicators+panel): the dead handle is MEASURED now - recreate it; and order the ensemble panel by member, not by who published first
THE ANSWER, off the instrumentation added in be39674, first run:

  ConvLSTM [HYB-2484]: TUNABLE INDICATOR REPORTS NO CALCULATED BARS - 1 tunable
  indicator(s) enabled and the least-ready answers BarsCalculated()=-1 ...
  Per-indicator depth: price=33982 MA=-1 ZigZag=33982 ATR=33982

MA=-1 with price, ZigZag and ATR all at full depth. **The handle is INVALID, not
short.** Same line on USDJPY (price=50179 MA=-1). Depth was never the problem;
the previous session's five theories were all answering the wrong question.

And it is per-member, not per-chart: LSTM-2484 ran the 34-candidate auto-tune on
that same chart at 15:24:18 and went on to train normally (feature health, 51
features, excursion head) reading the same indicator. Only ConvLSTM's handle -
the last member constructed - was dead. WHY is still not established. All four
members request ADMovingAverage with identical params, so MT5 hands them the SAME
refcounted handle, and the tuner's inner loop is Create-then-IndicatorRelease over
exactly that shared handle; that is the obvious suspect and it is NOT yet proven,
so this commit does not act on it.

1. IndicatorDepthReport() NOW PRINTS HANDLE NUMBERS, not just depths.

"MA=-1" says the handle is dead. "MA=-1(h12)" against another member's "MA=33982
(h12)" says it is the SAME handle and someone released it; "(h-1)" says it was
never created. That is the difference between a refcount bug and a creation
failure and it is one field. This is the measurement the shared-handle suspicion
needs before anyone acts on it.

2. RECREATE A DEAD HANDLE INSTEAD OF SWEEPING AGAINST IT.

A member that cannot read its own indicator must rebuild it. RepairDeadIndicatorHandles()
re-Creates only the ENABLED tunables reporting BarsCalculated() < 0 - a merely COLD
indicator (valid handle, 0 bars) is left alone to warm up the normal way. It does
NOT release first: -1 means the terminal no longer knows the handle, so there is
nothing to give back, and MT5 recycles handle VALUES so releasing a stale one could
decrement whatever now owns that number. 30s cooldown, because every ServableBars()
consumer reaches it including live inference on every tick. The feature cache is
dropped with it, and the log names before/after depths.

Cause-agnostic on purpose. Whatever is killing the handle, sweeping 50,163 bars
against a buffer that answers EMPTY_VALUE at every index - then discarding the era
and doing it again - is not a recovery.

3. THE SWEEP NOW HOLDS ON A DEAD HANDLE.

ServableBars() keeps answering `want` (its contract; live inference and online
learning have their own refusal paths and a 0 there reads as "no history at all").
SettledBars() - the training sweep's entry, the one caller that can afford to wait -
returns 0 instead, so Train() holds and reports rather than burning a full-history
pass it is guaranteed to throw away. A recreated handle is cold, so it primes
through the existing settle path on the next call. If the repair fails the member
holds indefinitely and says so every minute, and be39674's barrier liveness escape
releases the rest of the ensemble after 12 minutes - which is the correct
degradation and is exactly what the log shows happening.

4. THE PANEL ROWS WERE ORDERED BY WHO PUBLISHED FIRST.

Reported on XAUUSD: LSTM, ConvLSTM, Perceptron, Convolutional instead of
Perceptron, Convolutional, LSTM, ConvLSTM. ClaimEnsemblePanelSlot() handed out the
next free row on each member's FIRST PublishStatus() call, so the order was a race -
the members busy sweeping published before the ones sitting idle at the era barrier,
and be39674 sharpened it by (correctly) making a held member stop writing the terse
line. Rows are now keyed to m_ensembleIndex, the registration/construction order,
which is fixed for the life of the chart. Claimed on every publish rather than once,
so it is idempotent and refreshes the tag for a member whose ID was not final when it
first published (the config-tag suffix is appended during InitIndicators, after
EnsembleMember() registers). Unclaimed rows are skipped by the render and excluded
from the model count, so a member that has not published yet leaves no gap and shifts
nobody.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:40:00 -04:00
AnimateDread
1cf4c57d57 fix(altdata): median-fill instead of zero-fill, and a one-shot feature-vector autopsy
ALT-DATA AUDIT. The files themselves are healthy - all six symbols, 6,073 daily
rows, 2010-01-01 to 2026-08-17, no constant or degenerate columns, sane tails
(mac_cpi/mac_unemp flat ~47d is monthly data behaving correctly). The problem is
not the data, it is what happens where the data ISN'T.

CAltDataPanel::Features() returned an all-ZERO vector for any bar older than the
file's first row, and left blank cells at 0 too. Both were deliberate ('the block
is additive context and must degrade, never reject the bar') and that reasoning
holds for the CHANGE columns - but half these features are LEVELS: vix, ivol,
mac_y10, mac_cpi, mac_unemp, eia_util. For a level, 0 is not a missing reading,
it is an impossible one far outside the series' range. VIX does not visit zero.

And the spike lands in exactly the wrong place. Every alt file starts 2010-01-01
while the charts run far deeper - USDJPY H4 reaches ~1994, roughly HALF its
history - so 'alt block is all zeros' is precisely the predicate 'this bar is
older than 2010'. The IS/OOS split is chronological, so that predicate covers
~half of IS and none of OOS: an in-sample feature guaranteed to be useless
out-of-sample, and a bimodal input for the first BatchNorm to normalise. Not a
lookahead leak - a distribution corruption, which is quieter and was never
reported anywhere.

Now filled with the column MEDIAN over the covered range. A constant cannot leak
whatever its source - it takes the same value on every pre-coverage bar, so it
carries no information about which of those bars won - which is what makes a
median computed over later data legitimate here. Median not mean because the
series are skewed. Blank cells get the same treatment (eia_stk_idx1y alone has
181 blanks in 6,073 rows) and the count is now logged at load.

THE BACKOFF WAS ALREADY THERE AND WAS DEAD. Training.mqh arms m_coldSweepTick on
m_featureFailTransient, but only the open/ATR guards ever set that flag, so
f0cf659's cold ADMovingAverage looked PERMANENT and the sweep re-ran at full
speed forever. Setting the flag in the indicator guards revives the mechanism
that was already designed for this; no second backoff was needed and the one I
first wrote has been removed in favour of it.

SELF-HEALING, as asked. ReportFeatureHealth() runs once, the first time pass 1
produces usable windows, samples ~400 bars spread across the whole training range
and names every feature slot that is CONSTANT or mostly-zero, tagging alt-block
slots as alt[i]. Both of today's failures were the same shape - a block silently
produces nothing while every downstream number stays plausible - and neither an
accuracy figure nor a model can tell 'this feature is always 0' from 'this
feature is genuinely 0 here'. Evenly spaced sampling so a block that dies only in
deep history is caught as surely as one dead everywhere. A report, not a gate:
a rare-flag feature can be legitimately constant, and refusing to train would
turn a diagnostic into an outage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 12:53:51 -04:00