A generated strategy is not the shape edge.py measures. That screen asks whether direction is
callable on a SYMMETRIC barrier; this one is long-only with a target near twice its stop, so
it can pay at a win rate well under 50%. 'Direction is at chance' and 'this makes money' are
not in conflict - they are different measurements, and the way to settle which applies is to
replay the rules rather than argue from the screen.
Both readings of the entry are implemented because they are not the same strategy. Taken
literally, a BUY STOP at the previous day's LOW sits below the market, triggers at once and
fills a median 4.2 ATR above its own level - 91% of the time - so the stop loss is measured
from a level the trade never touched and realised reward:risk lands at 0.28 rather than the
~2.0 the coefficients imply. Read as a pullback (LIMIT), the geometry comes out at 2.1 as
designed and only 40-60% of orders ever fill. A trade export decides which one the generator
ran; nothing else can.
Also carries a correction. The docstring first claimed the omitted trailing stop could not
bind because activation sat far out. Measured, activation is at 0.54R - it arms before the
trade is one unit of risk in profit. The claim was wrong, the number is now printed every
run, and the omission is recorded as the largest deviation rather than a small one.
edge.save_rows writes to a temp file and renames. The first D1 run crashed mid-dump and left
a truncated JSON at the canonical path, which is worse than no file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every pooled statistic here was reported as a bracket: SE_INDEP, which claims 28 cells are 28
independent tests, and SE_CORR, which claims they are one. Neither is the number. The 15 feeds
are 7 markets - six PAIRS entries are one instrument quoted by two vendors at correlation
>= 0.999986, and ES_fut, SPY_d1, SP500_d and SP500_5 are four claims on the same index - so
market_t() collapses each market's cells by inverse variance and takes a plain t across the
market means. Its degrees of freedom are separate price series, which is the only n this
catalog can defend, and it is now the column the verdict is read from.
catalog.MARKET is where that collapse lives, next to PAIRS, because it is the same fact.
save_rows() writes both arms' scored cells, per-trade diff vectors included, to a JSON beside
the bar cache. The screens cost ~40 minutes and produced nothing but a printed table, so
re-pooling, collapsing feeds, or reweighting a threshold meant paying for every fit again -
which is why the H4 run's rows are gone and it has to be re-run to get them back.
Also folds two spellings of the Market Data root into catalog.ROOT; sqxbars had its own copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six bare literals across four functions all encoded the same judgement - below this many
rows, a fit or a score is noise - and all six were absolute counts tuned on H4. D1 has six
times fewer bars per year, so a D1 run would have dropped almost every instrument from the
sample without saying it had: build wanted 3,000 labelled rows and no D1 series but SPY has
that many, and evaluate wanted 5,000, which nothing has. The run would still have printed a
table, just a much emptier one, and the emptiness is exactly the kind of thing that reads as
a null result.
floors_for(tf) scales them by bars-per-year so the judgement stays 'this many YEARS', with
clamps so the coarse end cannot scale down into a sample no statistic survives. FLOORS is
set once at the entry point, after the timeframe is known, and printed with the run.
k and the barrier window join it as arguments. k was pinned at 2.0 in three places while the
banner claimed 'k=2' unconditionally; at D1 that same k resolves in ~11 days and leaves ~250
independent trades per instrument, which is a different experiment from the H4 one and needs
to be requested rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The screen now runs two arms that differ in exactly one thing - what the model was
trained on - and compares them cell by cell.
THREE CORRECTIONS, each of which biased toward finding an edge:
The pooled arm's STATIC BENCHMARK was fitted globally on all instruments' training
rows. Any instrument drifting against the pool got an "always long" benchmark while
it was actually falling, handing the model a win it had not earned. Now fitted per
instrument on that instrument's own purged rows.
The two arms used DIFFERENT FOLD BOUNDARIES - the pooled arm cut on calendar time
(it must: the instruments have different bar counts and start dates), the solo arm
on row index. So any difference between them mixed "pooled training helps" with
"the arms saw different years". Both now share calendar folds, and the solo purge
moved from bar index to exit TIME to match.
The pooled model was REFIT PER THRESHOLD. The fit does not depend on the threshold,
only the walk does, so this doubled the cost of the most expensive arm for an
identical model. One fit now serves all thresholds.
Dropped the EMBARGO constant: purging on exit time already keeps a training row only
if its trade had closed before the test window opened, and a bar-count embargo on top
is a second, weaker statement of the same rule.
RESULT, on identical columns, folds, purge, benchmark and scoring:
price pooled +1.18pp per-instrument -0.62pp
price+vol pooled +1.33pp per-instrument -0.83pp
Per-instrument training is NEGATIVE on every feature set. Nothing clears a defensible
bar in either arm - the best of 140 cells reads t=2.79 against a Sidak bar of 3.56,
which is what the maximum of a null grid looks like - but the GAP between the arms is
the largest effect in the run, and the EA trains a net per chart.
Also: alt data is negative in every alt-containing cell of both arms. Six macro
columns against ~1,900 independent trades buys overfitting, not information.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
features.py assembles one causal matrix per instrument - 18 price columns in ATR
units, 4 volume ratios, 5 time encodings and the alt block (COT positioning, VIX,
curve, dollar, breakevens, Fed funds, and the instrument's own implied vol where one
exists) - plus a symmetric k*ATR first-passage label. edge.py evaluates it.
Four things it does that the naive version does not, each of which has already cost
this project a retracted result:
BENCHMARK Gold's up-first rate is 53.49% over 23 years against a 51.00%
break-even, so "go long" alone looks profitable. Skill is the excess
over the best STATIC side fitted on the training fold, and its t is
PAIRED - the static walk takes the same trades, so drift, regime and
sample composition cancel. An unpaired t against break-even is the
drift's t, not the model's.
PURGING Barrier labels stay open for many bars, so training rows whose trade
exits after the test window opens are dropped, plus an embargo. In the
pooled arm the purge is on TIME, not bar index - the instruments have
different calendars and index-purging would align 2015 with 2021.
INDEPENDENCE The scorer walks each fold sequentially - take a signal, jump to that
trade's exit, look for the next - so it counts what an account could
have taken instead of counting the same swing once per bar.
NULL A rotation null is provided for the best-of-N problem: rotating the
fitted predictions against the labels keeps both series'
autocorrelation and destroys only their alignment.
Two design errors found and fixed while building it, both worth keeping visible:
The alt `_na` missing-flags were a DATE PROXY - each flips once at its series'
first publication, so a tree reads "before 2010" and fits that era separately.
`alt_mode='restrict'` (now the default) keeps only the published era and carries no
flags. It matters: gold's price-only cell went from t=3.55 to t=0.35 under it, so
that apparent edge lived entirely in the pre-2010 sample.
Coverage was reported as trades/bars, which reads 8% for a walk that is actually
taking ~90% of every slot available. Non-overlapping trades make the ceiling
bars/duration, and the honest number says there is nothing left to take.
Also fixes a regression this change introduced: `book._resample` divided the summed
spread by `v` to get the mean, which was correct only while `v` was a bar COUNT.
Volume is now real (carried from the .dat's sixth field rather than discarded), so
that divisor is now the bar count explicitly - USDJPY H4 reads 0.640 bp against the
catalogued 0.64. Frame.has_volume distinguishes real volume from a bar-count proxy,
because a "volume feature" built on the latter is measuring session length.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first version reported `required / floor` (the spread's win-rate hurdle over the
smallest uplift the sample can resolve) and called a LARGE ratio cost-bound,
concluding H1 was untradeable on all 14 instruments. That inverts the meaning. A
large ratio means the hurdle sits many standard errors away, so a break-even-sized
edge would be seen at overwhelming significance - USDJPY H1 read 5.89, which is a
break-even edge showing at ~12 sigma. That is a well-POWERED cell. The bad case is a
SMALL ratio: cost cheap, nothing measurable.
The tell was in the same row and went unchecked: it also said the spread was 4.5% of
a 1-ATR stop and break-even was 52.25%. Neither supports "cannot pay its spread".
When a derived ratio disagrees with the raw quantity it came from, the raw quantity
wins.
Rebuilt around the number that actually decides whether to act:
CONFIRM = 50 + 50*costR + 2*SE
the win rate a symmetric kR:kR setup must hit to be PROVABLY profitable. The two
terms pull opposite ways in trade size - widen the stop and the spread shrinks as a
share of the move, but each trade eats more history so SE rises - so CONFIRM is
U-shaped and its minimum is the cell worth testing first. This also makes explicit
that the horizon axis and the stop-multiple axis are the same axis: a 20-bar hold on
H1 is an H4 trade, and the table now prices both.
Durations are MEASURED by walking each trade to its first barrier touch rather than
assumed to follow the diffusive k^2 scaling, which is off by an instrument-dependent
factor. Trades open at the window cap are reported, since they make n_eff optimistic.
Result on the four longest histories: the best cell needs 51.9-52.9% and the surface
is FLAT from H1 k=2 to H4 k=2. There is no magic horizon. Add commission and the
working target is ~53% - against a deploy gate that asks ~66% at 18% coverage purely
because its OOS window holds ~63 independent observations. The binding constraint is
the gate's window, not the market.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It listed SignalStoch/SignalPB/SignalITF/SignalRVI and six other files as though
present, marked SignalSessionFilter '(Removed)' while its 207 lines were being
instantiated on every init, and omitted SignalMETA (578 lines), OscillatorDivergence,
SignalRiskGuard and SignalHYBRID entirely - so the one file meant to orient a reader
was wrong in both directions at once.
Rewritten against the actual roster, with the removal reasons recorded here rather
than in commits nobody re-reads, and with the two conventions that are easy to get
wrong on first contact: the voting machinery is stock CExpertSignal (so 'simplify
back to stdlib' removes nothing), and Direction() is a transaction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RSI, MACD, Ichimoku and the five AD/Wyckoff indicators (CumulativeDelta,
ShorteningOfThrust, WyckoffEventStream, WyckoffFailedStructure,
WyckoffSignificantBarInversion). All eight inputs shipped false and each carries a
closed verdict: the three oscillators are the same patterns that measured at chance
as entries, and the Wyckoff family returned zero out-of-sample on five independent
instruments - which is what closed the context score.
RETRAIN-NEUTRAL, and this one is worth stating precisely because the change looks
larger than it is. Every removed group contributed `flag ? N : 0` to the input
width, and every flag was false, so the width was ALREADY zero for all eight: no
.nnw's input layer changes. On the fingerprints, UseRSI and the five AD flags were
hashed unconditionally and become literal 0 legacy slots (the convention the
m_focalGamma slot above them already uses); UseMACD/UseIchimoku were appended only
when enabled, so their segments simply never appear - byte-identical to every
fingerprint ever produced, since neither ever shipped on.
CADIndicatorTuner IS DELIBERATELY NOT SHRUNK. Its flat parameter array is persisted
inside every .nnw, and Unflatten() rejects a size mismatch by falling back to
constructor defaults - so dropping the dead fields would silently revert the tuned
MA period of every model on disk while keeping its trained weights. That is the
feature/weight mismatch this project has already paid for twice, and it is not
worth 200 lines. AD_TUNE_PARAM_COUNT stays 42, the dead slots are still written and
read, and AutoTune's ParamOwner gate now matches only owner 5 (MA) so nothing
searches them. The class comment says all of this at the declaration.
Also renamed ReInitADIndicators -> ReInitTunableIndicators: it rebuilds exactly one
indicator now, and a name saying "AD" for the MA handle is the kind of stale label
that gets believed later. Its release-AFTER-recreate ordering is untouched - that
is a documented fix, not bookkeeping.
Compile-verified in the stage copy: 0 errors, 0 warnings, against the same 0/0
baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
research/classic.py transcribed all 26 shipped vote patterns (MA 4, RSI 4, MACD 6,
Ichimoku 12) with their constructor weights and tested them as entries on 178k-bar
histories, four instruments x three barrier geometries. Nothing separated from
chance - not one pattern, not the averaged vote at any threshold 10-70, not a
2/3/4-module quorum, not event-plus-confirmation. Residual E[R] everywhere was
-0.01 to -0.08 R, which is approximately the spread. The +4 sigma reading that had
once justified the set was two bars of lookahead: closing it took MACD_p4 on EURUSD
from +5.05pp to -0.02pp.
All four inputs have shipped false ever since, so this deletes dormant code rather
than changing behaviour.
RETRAIN-NEUTRAL, deliberately. EnableMA and EnableRSI were hashed UNCONDITIONALLY
into the DB config fingerprint, so they become literal 0 legacy slots - the same
treatment the ind_Periods slot two lines above already uses, and every existing
database keeps its key. EnableMACD/EnableIchimoku were appended only when enabled,
so with both gone the segment simply never appears, which is byte-identical to
today. No .nnw or .db is orphaned.
WHAT THIS COSTS, STATED PLAINLY: these four were CSignalMETA's only wired candidate
sources, so the on-chart ladder sweep (BuildCorpusBySweep) now has nothing to sweep
and a META chart is no longer self-contained. That is survivable rather than fatal
because MetaPrepareEra already falls back to CMetaCorpus::LoadLargestOnDisk, and its
own comment names this exact case - "charts whose classic filters are disabled".
Use_MetaLabeling ships false regardless. SignalMETA.mqh is otherwise UNTOUCHED, and
its 26-slot one-hot stays at 26: a tester-built corpus on disk still encodes those
pattern ids, and narrowing the descriptor would invalidate every stored corpus.
Signals/SignalMA.mqh SignalRSI.mqh SignalMACD.mqh SignalIchimoku.mqh deleted
Signals/OscillatorDivergence.mqh deleted - RSI and MACD were its only users
Classic_Shift deleted - the four votes were its only readers
Compile-verified in the stage copy: 0 errors, 0 warnings, against a 0/0 baseline
taken before any edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tick .dat files and the bidask/bars/sqxbars caches are gone, so every screen
that reached for fills.Book was dead and breadth.py's five symbol keys no longer
matched the SQX export (it has been rewritten with ONE underscore, turning every
lookup into FileNotFoundError). Rebuilt the data path from the only source left -
the SQX bar files - and pointed it at a local copy so research never reads the
live SQX install.
catalog.py is now the single place that says what an instrument is: path, asset
class, synthesised spread, and the price range that PINS the decimal scale. The
scale used to be fitted against an MT5 reference series that no longer exists, so
it is now asserted per file instead of inferred, on three checks that agree on 1e6
for every file - price level, round tick GCD, and the medians recorded when the
decoder was last validated at corr 1.000000 (FTSE 7246, WTI 65.4, USDCAD 1.26 all
reproduce exactly). breadth.py's two duplicated dicts are gone; catalog owns it.
Validation: five of the six dual-feed pairs agree at corr >= 0.999986 with a
sub-basis-point median difference. WTI is the exception at corr 0.9988 / -14.7 bp,
because the vendors roll the continuous contract on different days - so the WTI
pair is NOT a clean replication arm and must not be quoted as one.
detect.py answers the question the closed verdicts never did. "No edge" has two
opposite causes that look identical in a results table - the effect was smaller
than the spread, or the window could never have resolved it either way - and only
the second one is fixed by more data. So it computes both bounds per cell: the
win-rate uplift needed to pay the spread, and the uplift that is distinguishable
from chance on the trades the history actually holds.
Also fixed, all found by running the above:
book.py W1 buckets, with the 4-day offset the epoch's Thursday needs, or
every weekly bar would straddle a weekend
breadth.get serves each symbol's finest AVAILABLE base series and refuses to
resample upward rather than inventing intrabar highs and lows
catalog.keys filtered on M1, which silently dropped the futures tree (M60) and
the 33-year SPY series (D1) from every screen that iterated it
breadth.cells one generator, shared, that skips the unbuildable tick-derived arm
instead of dying on it - was duplicated in two test scripts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same doctrine as AltDataFetch's SaveRaw/RebuildFeatures (a8e6572): a plain
FileOpen(FILE_WRITE) truncates on open, and this file carries the daily/total halt
LATCHES - a crash mid-write left a truncated file that LoadState()'s magic/size guard
rejects, silently re-seeding from live equity and clearing a compliance halt. Staged
through a temp file + atomic rename; each FileWrite* call's return value is now
checked so a write failure sets ok=false instead of silently corrupting the state
alongside a successful-looking FileClose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AcquireConfigLock/ReleaseConfigLock moved off CExpertSignalAIBase into
Expert/ConfigLock/CConfigLock, same view+adapter shape as BarrierHorizon/ExcursionHead.
Stateful: m_configLockName is exclusive (grep-verified, nothing outside Lifecycle.mqh's
old body touched it). Pure relocation - same FNV-1a hash, same owner-liveness check,
same log wording. Left uncommitted mid-campaign; independently compile-verified in
isolation now (0 errors/0 warnings) before this commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CFirstPassageLadder (RungFor/StoreBar/FirstTouch/OutcomeR/WinShare),
CTripleBarrier+CLabelOverlap (ApplyMinStopWidening/ComputeLevels/SnapToLadder,
the label-overlap effective-sample-size correction), CMetaFamilies (the
classic-pattern taxonomy + table-naming rule), SGeometryScan::Reset() (guards
against 7452bd1's partial-reset shape recurring) and System/BinomialStats.mqh
(every deploy-gate/edge-floor formula this codebase shares). Each is a small
.mq5 Expert Advisor under Tests\ printing PASS/FAIL per assertion via
Print(), sharing Tests\TestHarness.mqh. All 5 self-compile-verified 0
errors/0 warnings. FirstPassageLadder.mqh/TripleBarrier.mqh expect
BARRIER_LADDER_COUNT/BARRIER_LADDER/BARRIER_HORIZON_LADDER_COUNT predefined
by their includer (normally ExpertSignalAIBase.mqh); the test EAs define
copies matching production values rather than including the whole AIBase
chain. SGeometryScan is reproduced verbatim from ExpertSignalAIBase.mqh for
the same reason, flagged in-file as needing to stay byte-identical.
Also adds Tests\convert_sample_data.py, which runs research/sqxbars.py's
decoder against a COPY of SP500_the5ers_H1.dat (never the SQX install
itself) so the operator has real sample data to point a manual tester run
at. Decodes structurally (52,542 H1 bars, monotonic, 0 high<low violations)
without calibrating a price scale - sqxbars.load()/sqx.calibrate_decimals()
both require a validated reference series to do that safely, which this
self-contained script does not have. Tests\sample_data\ (the raw copy +
decoded .npz) is gitignored, same policy as the existing Market Data/ rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
ApplyClassificationSoftmax/AdjustedSignalFromSoftmax/DirectionalMargin each
re-derived `pBuy > pSell && pBuy > pNeutral` (and the Sell mirror)
independently, one of them documenting the duplication by comment rather
than eliminating it. Added Argmax3() as the single derivation (ties to
Neutral); all three now branch on its ENUM_SIGNAL result instead of
re-testing the comparison. Pure relocation, statement-by-statement
equivalent - verified by diff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CTradeJournalManager::GenerateReport() mixed four jobs in one
211-line method: DB fetch, per-hour/day/confidence aggregation,
suggestion-derivation, and CSV formatting. Split into
FetchClosedTrades/AggregateJournalStats/DeriveSuggestions/
WriteJournalReportCsv, each independently testable/replaceable;
GenerateReport is now a 12-line orchestrator. AggregateJournalStats
touches no class member so it stays a free function alongside the
existing JournalBucket* helpers (moved next to SJournalStats, ahead
of the class, since the new method signatures reference it);
Fetch/Derive/Write stay private methods since Derive needs the
already-private AddSuggestion. Pure relocation - every quoted string
literal and if/for/return count verified identical (net of the
intentional new step-boundary guards/returns) against the pre-edit
file.
StateRSI/StateMain, ExtStateRSI/ExtState, and CompareMaps were duplicated
verbatim (~110 lines each, even the comments) between SignalRSI.mqh and
SignalMACD.mqh - the only real difference was the oscillator value source
(RSI(pos) vs Main(pos)). Neither method touches the pattern-weight fields
LongCondition/ShortCondition read, so unlike the previously-declined
ApplyPatternWeight dedup, this extraction needed no change to the live
voting logic itself - only the ExtState(idx)/CompareMaps(...) call sites
now go through the shared collaborator.
New Signals/OscillatorDivergence.mqh: IOscillatorDivergenceSource
(abstract - oscillator value + price low/high extremum lookup) and
CDivergenceDetector (owns m_extr_osc/pr/pos/map as real members, STATEFUL,
fully exclusive - grep-confirmed no reader outside the old per-class
copies). MQL5 has no multiple inheritance and both signals already extend
CExpertSignalCustom, so each gets a thin CSignalXxxDivergenceSource
adapter (owner pointer + 3 forwards) rather than implementing the view
directly, matching the Expert/* view+adapter pattern. Added 3 small
public DivergenceOscillatorValue/DivergencePriceLow/DivergencePriceHigh
wrappers per class since the adapter is a separate object, not a
subclass, and can't reach RSI()/Main()/m_low/m_high (protected) directly.
Every moved statement verified against the pre-edit body; the only
non-mechanical change is a stale comment ("ExtStateRSI's scan") updated
to the new method name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
LoadLargestOnDisk() and LoadFromConfigDb() each built an MqlDateTime
struct field-by-field then called StructToTime() to get an Add()
timestamp. Added a shared static BuildStamp(y,mo,d,h,mi); both loops
now call it. Pure relocation, no arithmetic/ordering change.
CloseAndDeleteAllForSymbol() had two structurally identical ~15-line
loops back to back (select-by-ticket, filter by symbol+magic, freeze
guard, act) differing only in the Position*/Order* API calls. Added
private CloseAllLoop(target, symbol, caller) dispatched by a small
CLOSEALL_POSITION/CLOSEALL_ORDER enum, matching the RetryFileSystemOp
precedent (b8f936f). caller is threaded through as __FUNCTION__ from
the two call sites so TCLog text is unchanged. Pure relocation - every
filter/freeze-check/act statement and log message verified unchanged.
InsertTradeRecord and UpdateTradeRecord each hand-rolled the same
"append, comma unless last" loop for building SQL column/placeholder
lists. Extract JoinWithCommas()/PlaceholderList() private helpers so
that shape exists once; pure text-building, no query semantics change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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.
Both selected on an enum via nested if/else-if chains 2-3 levels deep,
inconsistent with the switch-dispatch style the rest of the file uses
(HandleControlPanelAction, 2f1951e). InitializeTrailing's ATR
x1/x2/x3 multiplier also collapsed from 3 sequential equality checks
into its own small switch. Pure control-flow reshape: same branches,
same bodies, same fallthrough-to-true default - no behavioral change.
CSignalMETA::AppendCandidateFeatures (training corpus rows) and
AppendLiveDescriptor (ScoreProposal's live gate descriptor) duplicated the
identical one-hot/side/tanh-squash-netVote/SL-TP-multiple/spread-ATR sequence
byte-for-byte, with a comment admitting they had to be kept in sync by hand -
the exact "copies disagreeing" failure mode this campaign exists to close,
and here it feeds both the training corpus and a live trade-gating decision.
Added private AppendDescriptorTail(slot, side, netVote, idx): slot -1 zeroes
every one-hot column (AppendLiveDescriptor's case, since the vote is not a
classic-ladder fire), any other slot sets exactly that column to 1.0 (the
training case via OneHotSlot()). Both callers now delegate to it; each
statement verified equivalent to the original inline copy before the edit
(int/char side both narrow to the same (double)side write, m_metaCands.Bar()
vs the barIdx parameter both feed the same idx argument).
Self-compiled 0 errors, 0 warnings via the _claude_stage mirror.
ReadStoredDbVersion() already retries the FileOpen 5x with a 1s sleep
internally (OpenVersionFileWithRetry, b8f936f) before returning the
"ERROR" sentinel. Init()'s outer while(attempts<5){Sleep;retry} wrapped
that whole sequence again, so sustained lock contention could block
OnInit for ~25-30s across two stacked retry loops that read as one.
Treat a single exhausted ERROR as a hard Init() failure instead.
inTradingSession() had three near-identical if(session==...) branches
(London/NewYork/Tokyo) that each set an openUtcMin from a DST test and
called the same inTimeInterval() shape, differing only in the trade
toggle, DST function, winter-UTC open hour, and window length. Replaced
with one dispatch that fills 4 locals (winterOpenUtcHour, lengthHours,
tradeToggle, isSummer) per session name, then one shared computation +
inTimeInterval() call.
Verified branch-by-branch equivalence before compiling: London
(8,8,EuSummer), NewYork (13,9,UsSummer), Tokyo (0,9,none) reproduce the
exact original arithmetic per session, including Tokyo's already-UTC-
converted 0 (not 9, since Tokyo has no DST and JST is UTC+9). Unknown
session still returns false. This is live trade-veto logic, so no
structural reordering beyond the branch consolidation itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
All three MM_STRATEGY branches repeated new+null-check+Expert.InitMoney()+
null-check verbatim. Added CreateAndInitMoney<TMoney>(functionName), the
same template-helper shape as the existing CreateSignalWithRetry<TSignal>
a few hundred lines up. Pure relocation - same error text, same control
flow, only the branch-specific setter calls (Percent/Lots/UseAIConfidence...)
stay inline.
CExpertCustom's three Long/Short pairs (OpenLong/OpenShort,
TrailingStopLong/Short, TrailingOrderLong/Short) each duplicated the
same pre-send gate logic, differing only in the order-type constant
and which base CExpert::Xxx method to delegate to - the same shape
CExpertSignalCustom already fixed for CheckOpenPosition/CheckClosePosition.
Added OpenPosition/TrailingStopCommon/TrailingOrderCommon, each
isLong-parameterized with a caller string threaded through (passed as
__FUNCTION__ from each 1-line wrapper) so every TCLog message keeps
its original per-direction function name and topic tag. Pure
relocation - every log string, arithmetic expression and branch order
verified unchanged against the pre-edit file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both methods shared ~90% identical bodies differing only in ORDER_TYPE_BUY
vs ORDER_TYPE_SELL. Extracted a private CheckOpen(type, price, sl), mirroring
the CheckTrailingStop(pos, sl, tp, isLong) unification already used in
Trailing/TrailingATR.mqh. Pure relocation, no arithmetic/ordering change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ParamCandidates() repeated an identical resize+copy+return-count block
9x, once per discrete MA/RSI/MACD/Ichimoku preset list. Added a private
FillCandidatesFromPresets(src[], out[]) doing that once; all 9 sites
now a one-line call. Pure mechanical dedup, no arithmetic/ordering
change.
CTradeJournalManager::AddSuggestion() does the resize+assign+increment
once; the hour/dow/near-miss/sl-tight/confidence-tier suggestion sites
now each call it with their already-built StringFormat text. Pure
textual relocation, no arithmetic or ordering change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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.
CDatabaseFileSystemManager's CreateDirectory/CleanDirectory/DeleteFile had
three identical retry loops differing only in which FolderCreate/FolderClean/
FileDelete ran and the noun in the log line - collapsed into one
RetryFileSystemOp(enum, target, verb, caller) private helper, dispatched by
enum rather than a function pointer (MQL5 function pointers to a built-in
with default params is untested territory, not worth it for 3 one-liners).
CDatabaseVersionManager's ReadStoredDbVersion/UpdateStoredDbVersion had the
same retry shape around FileOpen, differing only in the open flags and the
reading/writing noun - collapsed into OpenVersionFileWithRetry(path, flags,
verb).
CDatabaseConnectionManager::OpenDatabase (the 6th instance the finding named)
is the only retry loop in its file - no in-file duplication to fix there, and
sharing it with the other two would need a cross-class free function bound to
DatabaseOpen/FileOpen as function pointers, an untested construct for a
20-line win. Left as-is.
Every Print() message text verified identical at each call site; public
method signatures unchanged, no external caller needed a rewrite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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.
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>
CreateTable/DeleteTable/InsertTradeRecord/FetchRecordCount/FetchOpenTradeEntry/
FetchNewestTimeKey/FetchWinLossCounts/FetchTradeRecords/UpdateTradeRecord/
DeleteOldestEntry each repeated the same three-line "if(!IsValidIdentifier)
{ Print(...); return false; }" block, differing only in the verb printed.
Replaced with one private RequireValidIdentifier(tableName, verb) helper that
does the check + Print + returns the bool; each call site is now a single
guard line. Pure mechanical dedup - every Print message text and control-flow
path is unchanged, no call-site signature changes since it's all internal to
this one class.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OnInit() orchestrated a dozen unrelated boot concerns (chart-object
purge/reporting, risk-budget config, alt-data/cross-asset blocking
warm-up, DB+journal init, creation/wiring of eleven signal objects,
filter registration, a DB-transaction retry loop, control-panel
setup) inline in one function. Extracted each into a free function
(PurgeStaleChartObjectsAndReport/ConfigureRiskBudget/WarmExternalData/
InitDatabaseAndJournal/CreateAndConfigureSignals/
VerifyDatabaseTransactionCycle/FinalizeStartupUI), called from OnInit
in the exact original order - the load-bearing ordering comments
("alt-data MUST be on disk before any model is built", "filters added
exactly once, before the DB retry loop") stay next to the calls they
govern. OnInit: ~430 -> ~100 lines.
Pure relocation, no logic changes: every INIT_FAILED return became a
plain false/true return at the new function boundary; __FUNCTION__/
functionName usages became an explicit `caller` parameter so logged
messages still read "OnInit: ..." rather than the helper's own name.
Verified via a diff script - quoted-string set identical (64/64), and
the only structural deltas (if(): +3, return: +6) are fully explained
by the 3 new call-site guards and the 3 new function-end `return
true;` lines a void-context call chain didn't need before.
Self-compiled 0 errors, 0 warnings.
The ~185-line switch mixed chart-UI toggling, AI training-lifecycle
dispatch, weight save/load/reset and DB/report admin in one function
with subtly different guard conditions per branch. Each CP_ACTION_*
case is now its own HandleCp*() free function (matching this file's
existing procedural style - ConfirmDestructiveAction, RefreshControlPanelLabels,
etc. are already standalone functions over the same globals); the
switch is now a one-line-per-case dispatch table. Every guard/confirm/
Alert/Print sequence is preserved verbatim (break -> return only
change; verified via quoted-string-set diff = empty and if/Alert/
Print/DispatchSignalCommand counts identical against the original).
Compiled 0 errors, 0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert/AIBase/Labels.mqh (1807 lines) exclusivity-grepped almost entirely
SHARED: the label/win/excursion/ladder caches and the geometry-derivation/
prebuild state are touched with real per-bar array logic by Training.mqh's
hot era loop (m_labelCacheBuy/Sell/HasValue at 22+ sites), by FeatureScreen.mqh's
geometry scan (direct writes to m_barrierScanSlMult/TpMult, m_geometryAdopted,
m_geometryCfgSaved), by AutoTune.mqh and by SignalMETA.mqh - moving that state
into a collaborator would mean wrapping dense hot-loop array indexing behind
method calls across 5 files for no coupling reduction (same judgment already
recorded for AutoTune.mqh's remainder / Inference.mqh).
One genuinely closed sub-cluster survived the grep: the scheduled close-all
budget and the horizon ladder snap (NextScheduledCloseAll, MeasureCloseAllBudget,
EffectiveHorizonMax, RequiredHorizonBars, SnapHorizonToLadder, GrantedHorizonBars).
Only 2 fields are exclusive (m_closeAllCycleBars/m_closeAllMeanBudget - grep-
verified, Lifecycle.mqh's touch was constructor-init-list only) and NONE of the
6 methods has any external caller outside Labels.mqh (grep-verified whole-repo),
so nothing needed rewiring. New Expert/BarrierHorizon/: IBarrierHorizonView.mqh
(abstract, 4 accessors, 3 reused from the signal's existing Chart* getters, 1
new HorizonSwingMedianBars() wrapper) + AIBaseBarrierHorizonView.mqh/
AIBaseBarrierHorizonViewImpl.mqh (the adapter) + BarrierHorizon.mqh (CBarrierHorizon,
STATEFUL - owns the 2 exclusive fields as real members). Every method body is a
verbatim relocation (diffed programmatically against git HEAD modulo the field->
view substitutions - identical except one comment-wording update). The original
6 declarations on CExpertSignalAIBase became one-line forwards at their existing
position; Labels.mqh's own callers of these six needed zero changes since they
call them unqualified, which now resolves through the forwards.
Labels.mqh: 1807 -> 1643 lines. The rest of the file (label-cache population,
TripleBarrierLabel, DeriveBarrierGeometry, StartLabelCachePrebuild/
AdvanceLabelCachePrebuild, exit-policy simulation) is deliberately left as a
raw-include partial - not separable without relocating Training.mqh's era-loop
coupling, not reducing it.
Self-compiled 0 errors, 0 warnings (_claude_stage, ~94s).
Expert/AIBase/Features.mqh (2017 lines, 38 methods) split by exclusivity grep
(whole-repo, not just Expert/): 30 methods -> Expert/Features/FeatureBuilder.mqh
(CFeatureBuilder + CFeaturesView/CAIBaseFeaturesView), 8 stay behind as a much
smaller raw partial.
CFeatureBuilder is STATEFUL, same shape as Excursion/OnlineLearning: owns the
10 feature-only indicator handles (m_Volumes/m_MA/m_RSI/m_MACDFeature/
m_Ichimoku/5 AD* CiCustom indicators - grep-verified touched nowhere else in
the repo, only their bare declarations) plus the depth-probe/handle-repair/
spread-series/detectability-latch scalars (exclusive, Lifecycle.mqh ctor-init
only elsewhere). m_Open/m_Close/m_High/m_Low/m_Time/m_ATR/m_ADZigZag stay
signal-owned - Labels.mqh/AutoTune.mqh/Training.mqh read them directly - and
are reached read-only through the view (FeatureOpenAt/FeatureHighAt/
FeatureLowAt/ChartBarClose/ChartBarTime/OnlineAtrMain, all reused where a
forward already existed).
Deliberately did NOT move InitOpen/InitClose/InitHigh/InitLow/InitTime/
InitADZigZag/ResizeBuffers/RefreshData: they manage the 7 shared indicators'
Create/BufferResize/Refresh lifecycle, which would need a pure-relay wrapper
per operation per indicator for zero coupling benefit - same judgment as
Topology's boot sequence. They stay in Expert/AIBase/Features.mqh and reach
CFeatureBuilder's 10 owned indicators through 20 new Feature*BufferResize()/
Feature*Refresh() forwards (signal calling into its own owned collaborator
directly, no view needed in that direction).
Whole-repo grep (not just Expert/) caught a real external miss the campaign's
own doctrine warns about: Signals/SignalMETA.mqh read m_spreadSeries/
m_spreadSeriesBars directly as an inherited protected field (a subclass, not
an AIBase/*.mqh partial) - fixed with two new FeatureSpreadSeriesBars()/
FeatureSpreadSeriesAt() forwards.
Verified: if(/for(/while( counts identical between the original file and the
new split (269/20/1); return-count delta (+12) fully accounted for by the 12
new trivial one-line forwards added (10 indicator BufferResize + 2 spread-
series getters); quoted-string-literal diff empty except two doc-comment
paraphrases. Self-compiled 0 errors, 0 warnings.
Expert/AIBase/Topology.mqh (1191 lines) held two genuinely different jobs: the
fingerprint/derived-shape/BuildFreshTopology math, and InitNeuralNetwork/
InitFeatureIndicators - the network boot sequence (config-lock, tester-cache
seeding, load/save the .cfg, net-load backend fallback, chart/persistence/
online-learning orchestration).
Extracted the first job to Expert/Topology/ as CTopology + CTopologyView/
CAIBaseTopologyView (20 methods: BuildModelFingerprint, the Estimated*/Compute*
budget math, the Conv*/Lstm* shape helpers, Add*Stage, BuildFreshTopology).
STATELESS, like ModelPersistence - grep-verified zero exclusive fields, every
member these methods touch is shared elsewhere in the signal. Reused ~15
existing Data*/Chart*/Persist*/Exc* getters per the established convention;
added ~20 new getter overloads next to their existing setters (UseVolumes(),
MinDirectionalRecall(), etc. - same pattern as SignalClusterWindow) and ~16
new Topology*() wrappers for fields with no prior accessor. The Net-pointer
swap in BuildFreshTopology is one consolidated view call
(TopologyReplaceNetFromTopology), same doctrine as Persistence's
RunCpuInferenceSelfCheck - irreducible pointer work, not signal state.
Deliberately did NOT extract InitNeuralNetwork/InitFeatureIndicators: they
orchestrate nearly every other collaborator (chart, persistence, online-
learning, cross-asset, config-lock) rather than deriving a shape, so moving
them would just relocate a hub, not reduce coupling - same judgment call as
Inference.mqh (assessed, not extracted). They stay in the AIBase/Topology.mqh
partial, byte-identical to before (diffed against git HEAD to confirm), and
now call the extracted math through the same public forwards every other
caller already used.
Verified: string- and numeric-literal diff of the old file's 20 method bodies
against the new CTopology methods (0 differences), InitNeuralNetwork/
InitFeatureIndicators byte-identical, self-compiled 0 errors/0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert\AIBase\OnlineLearning.mqh (595 lines) -> Expert\OnlineLearning\:
IOnlineLearningView.mqh (abstract, ~50 accessors) + AIBaseOnlineLearningView.mqh/
AIBaseOnlineLearningViewImpl.mqh (the adapter) + OnlineLearning.mqh (COnlineLearning).
STATEFUL, unlike CModelPersistence: grep-verified the shadow net, the OOS
continual-learning simulation state and the pattern-database backfill state are
genuinely exclusive to this file's own methods - Training.mqh/Topology.mqh/
Lifecycle.mqh/the signal's own header only ever CHECKED or RESET this state at
era/lifecycle boundaries, never owned it, so it moved onto the collaborator as
real members (same doctrine as Excursion). Those external touch points became
consolidated view/forward calls instead of raw field pokes - AbortSimIfActive()
replaces THREE separate copies of the same delete/null/false triple (Training.mqh's
stop path, FlushTrainRun, ResetWeights), matching the geometry-scan duplicate-reset
precedent in project memory. ResetForFreshTopology() replaces Topology.mqh's five-
field reset block, DeployNet() replaces the shadow-preferred net selection duplicated
in Inference.mqh and ChartScoreBarForRescan, and BlendTowardNet() replaces the era-end
blend Training.mqh used to poke m_shadowNet for directly.
Reused the signal's existing Data*()/Chart*()/Persist*() getters wherever one already
answered the question (labels/outcome/history/horizon/priors/servable-bars/etc.);
added ~30 new Online*() wrappers only for what nothing else exposed yet. The three
PersistOnline*() get/set pairs (WST3 .stats fields) now forward through the owning
member instead of touching the field directly - CModelPersistence is unaffected.
Every method body is a pure relocation of the original's statements in original
order; verified against `git show HEAD~1:Expert/AIBase/Excursion.mqh`-style diff
against the pre-extraction file kept in the working tree until this commit.
Compiled 0 errors, 0 warnings (stage mirror + MetaEditor64 /compile, ~91s).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert/AIBase/Excursion.mqh was 13 method bodies of CExpertSignalAIBase,
#include'd after its declaration - same "not a module" problem already
fixed for ChartUI (S2) and Persistence (S3). Extracted to
Expert/Excursion/CExcursionHead behind CExcursionHeadView/
CAIBaseExcursionHeadView, same view+adapter shape.
STATEFUL, unlike Persistence (0 exclusive fields): grep-verified 26
fields (m_excNet and every accumulator/trailing-ring field) touched
nowhere else in Expert\ except Lifecycle.mqh's old ctor-init-list
defaults and destructor deletes (now moved onto CExcursionHead's own
ctor/dtor). m_geo (SGeometryScan) and m_ladder (CFirstPassageLadder)
stay on the signal - both are genuinely shared with Labels.mqh/
Training.mqh at era boundaries - and are reached only through 15 new
Exc*() view wrappers, including one consolidated
ExcGeometryScanAccumulate() call (same doctrine as Persistence's
RunCpuInferenceSelfCheck) rather than field-by-field pokes.
All 13 original public methods stay at their same declaration point as
one-line forwards to m_excursionHead. Training.mqh's 2 raw m_excUs
reads now go through the new ExcursionMicroseconds() forward. Every
method body is a pure relocation, verified statement-by-statement
against the original (git show HEAD~1:Expert/AIBase/Excursion.mqh).
Compile-verified: 0 errors, 0 warnings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expert/AIBase/Persistence.mqh (598 lines, 8 methods) -> Expert/Persistence/:
IPersistenceView.mqh (abstract, 68 read+write accessors) + AIBasePersistenceView.mqh/
AIBasePersistenceViewImpl.mqh (the adapter) + ModelPersistence.mqh (CModelPersistence,
the real collaborator - signal owns m_modelPersistence and binds it to m_persistenceView,
same shape as ChartUI's S2).
Grep-verified before starting: every field these 8 methods touch is ALSO touched
elsewhere in the class (Training/Lifecycle/OnlineLearning/Topology/FeatureScreen/
Labels.mqh) or already exposed via ChartView. Zero exclusive state, unlike ChartUI's
arrow-restore/rescan queues - CModelPersistence is stateless, holding only the
borrowed view pointer, operating entirely through 68 Persist*/PersistSet*() accessors
on the signal.
ValidateCpuInference's Net-pointer/throwaway-clone core is ONE consolidated view call
(PersistRunCpuInferenceSelfCheck) rather than field-by-field - irreducible pointer/
object work, not signal state, same doctrine as ChartScoreBarForRescan.
LoadNetWithRetry keeps its original CheckPointer(Net)-free Net.Load() call unchanged
(no guard added - would change failure behaviour on what must be a pure relocation).
This code writes the actual on-disk .cfg/.stats binary layouts every deployed model
depends on (explicit "DO NOT REORDER" comment in the original), so beyond compiling
clean (0 errors, 0 warnings) this was verified with a positional field-order diff:
every FileWrite*/FileRead* call's target field, extracted and normalized from both
the original and the new file, matches 1:1 in the same order (43/43 on the write
side covering SaveModelStats+SaveTopologyConfiguration, 17/17 on LoadModelStats'
read side; LoadAndCompareTopologyConfiguration's local-variable read block was
copied verbatim, untouched, so nothing to diff there). The magic-version
conditionals (WST2-6, haveDerivedStages/haveBarrierGeometry/etc.) moved unchanged.
All 8 methods keep their exact original signatures as one-line forwards - zero
external call sites changed.
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.