- 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.
Everything from tonight, committed so the restructure that follows is
recoverable: the graded stdlib vote, the Wyckoff modules and feed, the
ALGLIB serializer workaround, the restored DB queue, and the Simple/
prototype that is about to be folded into the real filetree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shadow journal filled a stop entry AT its level, once a minute. Real
stop orders under the tester's 1-minute OHLC ticks filled 0.7-1.2 bp
past it. On the EURUSD decade the same agreement-3 firings priced +0.9
bp virtually and realised -1.8 bp traded, with identical exit rules;
the pairing of real trades with their own virtual twins put the whole
difference in the fill and in a label cost 0.8 bp too low.
- Unfilled firings are checked on every tick; a stop fills at the price
that crossed it, a limit at its own price; risk is measured from the
fill. The ladder still advances once per minute.
- fill_slip_bp and fill_spread_bp are appended to the row's context.
- The meta-label cost per row is the spread at that fill plus the class
commission; the chart's spread now is only the fallback.
- A real trade's notebook line is the OWNER's (the setup that shaped
the order) with the bar's agreement annotation, not whichever setup
published last - 343 of 632 rows named another setup's entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ManageTrailing derived risk from |open - CURRENT SL|. At +1R the stop
moved to breakeven, risk became zero, and the function returned false
for the rest of the trade: no trail ever ran, and a trade that had
reached +1R waited at breakeven to be stopped at 0.
Measured on the clean EURUSD decade (shadow journal): the agreement-3
firings re-priced under their own setup's exit pay +0.91 bp net
(n 2,031); the same cell traded realised -1.54 bp on 632 - 529 stopped,
94 time-stopped, 9 targets, mean MFE 1.06R, realised R piled at -1.0 and
0.0. Slippage against the setup's level: 0.00 bp. Same shape as the
m_entry defect: a quantity redefined by the code's own side effect.
Risk now comes from the journal's record of the stop the position was
opened with (TrackedRisk by POSITION_IDENTIFIER); the current stop is
the fallback only on a position the tracker has not seen yet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two consecutive fits on the same 53,745 firings gave +0.05 and +0.38 bp
at the same threshold: the replace-on-key rewrites rows in place, SQL
returned them in a different order, and the bootstrap followed. Sorted,
then three seeds summed into the same accumulators.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
First fill run: the uncompressed forest was 56 MB and RefreshIfDue
re-read it every server day; the threshold rule picked the highest mean
and kept 2% of firings on 474 rows. Now: 60 trees, DFBinaryCompression
before serialising, reload only on a changed modify time, and the
threshold (and take-all) scored as mean minus one standard error.
Also found: the abandoned fill runs had written Adapt files daily from
2015 samples, and the next run adopted their skip flags from day one -
sos_bar long fired 102 times in 2015 and never again. Artefacts wiped;
the tester now writes only at deinit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "could we use mql5's alglib random forest and mlp instead of
relying on python? very quick training could reopen the door to selling
the bot."
Database\MetaLabel.mqh. Trained from the Virtual:<setup> rows (every
firing, not the few the gate traded): 14 features parsed from the
notebook line by ONE parser shared with the live gate (setup ordinal,
side, agreement count, opposed, armed count, headroom, minutes to the
forced close, risk in bp, day, hour, order type, valid test, window,
target); label = the firing paid after cost under its own setup's exit,
re-priced from the ladder as research/pricing.py prices it; cost = the
symbol's spread plus the class's commission, printed with every run.
The honest number is walk-forward: for every year from the third, a
forest trained on the years before scores that year, and the take
threshold is the one whose out-of-sample rows paid best - adopted only
if it beats taking everything, else no gate is written and a stale one
is deleted. Final model on every row, ALGLIB CDecisionForest via the
builder (100 trees, 0.66 subsample, Gini importance), serialised to
Adapt\{SYM}_{PERIOD}_meta.rf with the feature contract in the header; a
file whose contract differs is refused.
Gate: in the agreement block, last, as the mean P(pay) over the setups
armed on that side; a refusal is counted as its own entry gate. Trained
at deinit in the tester (fill, then learn, then trade gated - the 2024
loop) and daily when live. Pure MQL5, no DLL, no Python.
Also: virtual firings advance once per minute, not per tick - the
per-tick walk made a decade run four times slower.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "meta labeling should drastically change the outcome anyways.
unless the journal logs every patterns independently (I think it should
and also voted trades outcomes for comparison)."
Until now the journal recorded POSITIONS - what the agreement gate let
through - so a meta-labeller would have learned from a few dozen trades
per setup. Every arming is now a virtual trade: a pending order at the
setup's own entry and stop, filled when price reaches it inside the
setup's own window, tracked through the same first-passage ladder as a
real position (one shared AdvanceTrack), closed at the ladder's last
horizon, written to the same table as filterID="Virtual:<setup>". The
parent annotates each bar's firings with agree=N, opposed and the armed
combination. Real trades stay filterID="Book", so the two populations
sit on one table. Closed virtual rows are written 200 per commit.
Context now also carries headroom, minutes to the forced close and the
order type. The `journal` global lives in the header so setups and the
signal base reach the one instance the expert feeds.
Also: SQL identifiers are quoted - "2WD_Pattern_0_Sell" starts with a
digit and every Second Wind pattern table failed to create the moment
the database was on in the tester; an index on the natural key so the
replace-on-key scales to a decade of firings; "no closed trades yet" is
verbose again.
Smoke, BTCUSD H1 one month: 52 firings, 26 filled, 26 rows, annotated,
no database errors.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "is there not a meta labeling neural network in the EA? why
did it keep taking a losing pattern? The journal is there for that."
There is no meta-label net (CNNFilter is a loader nobody instantiates;
the offline net was never built), the adaptive layer learns management
only by design, and it could not read anyway: SignalDatabaseActive()
switched the DB off for every tester run on the argument that "a tester
run's DB is written and never read" - a premise AdaptiveExitWriter had
already deleted. Every run ended with 93 verbose-level "journal read
failed - database not initialized" lines and learned nothing.
- The DB is ON in a single tester pass; optimisation and forward stay
off (12 agents on one FILE_COMMON SQLite file finished zero passes).
- A closed trade REPLACES its own row on the natural key (symbol, open
minute, side, entry price): tester tickets restart at 1 on every run,
so a re-run over the same history no longer double-counts.
- AdaptiveExitWriter writes skip_long / skip_short when a setup-side is
a categorical loser on this chart: n >= 60 and t <= -2.5 on realised
R x risk in bp, gross of commission. Not ranking - the measurement
that made selection anti-predictive ranked marginal cells against
each other; this is the shape Second Wind showed on forex (t -4.0).
A skipped side never arms, so it never counts toward agreement.
- The reader adopts the flags with the exit, once per server day, and
prints the verdict when it changes.
- "Could not read" is printed once at normal level with the reason.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Operator: "I just want it to keep learning during live trade. similar to
2024 versions of the ea where I would run a backtest to fill the database
and then it would keep filling in real time, averaging on the whole sample."
Most of this already existed. CBookSetupSignal::LoadAdaptiveExit() has
always been the READER - once per server day it re-reads
Common\Files\Warrior_EA\Adapt\{SYM}_{PERIOD}_{setup}.cfg and adopts the exit
in it, refusing anything outside the research grid. FILE_COMMON is the whole
point: the tester and the live chart share one file and one journal table,
so a backtest fills the sample and live trading carries on filling it.
Nothing ever WROTE that file.
And the journal already records what is needed to re-price an exit without
re-running anything: `passages` carries the first-touch minute of 9 stop
levels and 10 target levels plus the signed R at 6 horizons, in exactly the
field names research/pricing.py reads.
So this is the estimator: read every recorded trade for this symbol, group
by setup, re-price each one across the (target x horizon) grid with the same
first-passage rule pricing.py applies, and publish the winning cell.
WHY THIS IS NOT THE FEEDBACK LOOP THAT WAS DELETED. Two differences, both
measured rather than asserted:
1. IT LEARNS MANAGEMENT, NEVER SELECTION. Selection by a cell's own past
P&L measured ANTI-predictive on this journal - a cell gets WORSE as its
evidence accumulates - while management measured positive. So it may move
a target or a time stop. It may never decide which setups fire, what the
agreement threshold is, or what any vote weighs. DB_RankingFeedsWeights
stays false.
2. IT IS A CUMULATIVE MEAN OVER THE WHOLE SAMPLE, NOT A ROLLING RE-FIT. The
harm in the old loop was a MOVING RULER - a pattern's contribution changed
as the DB re-scored it, so the same setup voted differently at different
times and nothing could be evaluated against anything. An average over
everything ever recorded converges instead of chasing.
THE ASYMMETRY THAT WOULD OTHERWISE POISON IT, and TradeJournalManager's own
header stated it before this was written: a live trade is closed by its own
exit, so levels beyond the one it took are never touched and record as -1.
From live rows an exit can honestly be re-selected TIGHTER, never WIDER, and
a layer ignoring that would "learn" that wide targets never pay because it
never saw one reached. That is not hypothetical - today's journal sweep found
the best exit is the WIDEST (6R on a 1R stop is never touched, so it is
really "no target", with a long horizon: +2.311 bp at 3+ agreement, positive
in all three periods). So the writer is hard-capped at "never wider than what
produced the evidence".
Other guards: 120 trades before a setup is considered at all, 60 re-priced
rows before a cell is, a positive mean, and at least one neighbouring cell
also populated and positive - a plateau rather than a spike, because picking
the argmax off a grid is what this journal punishes. The stop is never tuned:
a book setup's stop is its structure and the books state it.
FetchClosedTrades is now public - it is the read side of this loop and the
writer only reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the training deletion. The whole "Neural Networks" input section survived the file
deletions and every one of its knobs was inert: Use_MLP / Use_CONV / Use_LSTM / Use_CONVLSTM,
Run_Alglib_Baselines, Use_Training_Pool and OOSSplit are inputs the operator sees in the dialog,
and after the cut none of them reached anything. Exit_On_Leg_Flip was the same - a switch for a
label that no longer exists. 132 lines, and Inputs.mqh drops 1,022 -> 891.
An inert switch in the Inputs tab is worse than a deleted one: it invites the operator to change
a setting and conclude the EA ignores them, which is close to what the complaint that started
this refactor actually was.
TWO SLOTS DELIBERATELY KEPT AS LITERALS so no existing database is re-keyed. DbLegacyAiSlot()
encoded which architectures were enabled and now returns the legacy AI_NONE value; the optimizer
slot is written as 1, ADAM's ordinal, which is what TrainingOptimizerDefault always carried
(ENUM_OPTIMIZATION pins SGD/ADAM at 0/1 precisely so this cannot drift). I first wrote 0 there,
which would have silently changed the fingerprint on every database this EA has written.
The trade journal's run label was the enabled NN roster ("MLP+LSTM"); it is now "Book".
Compiles 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The second half of the sweep. Everything the first pass reported and left.
THE ENSEMBLE GATE JUDGED A POPULATION THE ACCOUNT NEVER SEES. It scored every OOS bar whose
vote cleared the rung; live, a vote that clears the threshold still has to pass
VoteCooldownAccept, and a suppressed bar produces no arrow, no order and no position. On this
fleet that window is 30 bars against a mean ride of ~34, so the certificate counted roughly an
order of magnitude more trades than the account could take, each overlapping its neighbours.
The member gate was fixed for exactly this on 2026-09-03 and both sites carried a comment
saying the ensemble still had the defect. The sweep now replays the vote cooldown per rung:
rows arrive in time order, so one kept-timestamp per rung reproduces it exactly. sweepTraded[]
carries precision, the book, the cost and the by-side counts; sweepFired[] stays the signal
population and only coverage reads it, because a cooldown caps traded coverage by construction.
The effN uses the declustered helper - the cooldown has already spaced that stream.
BOTH FAMILY-WISE SELECTION GATES FORMED THEIR SE ON RAW CALL COUNTS, the last SEs in the
project still undeflated for label overlap, which made the correction guarding the deploy
decision the most permissive test here. Now effective. This TIGHTENS both bars, which is why
it was left standing until asked for; the ensemble gate's own note already recorded that every
chart clears it by 6.5-12 sigma on effective calls, so the measured cost is nothing.
THE RIDE WAS PRICED AT BAR CLOSES NO ORDER CAN FILL AT. Entry was the close of the bar the
vote was formed on. Direction() runs on the first tick of the NEXT bar and the market order
fills there, which is that bar's open; the exit is read from closed bars and acted on one bar
after the flip. So the book credited every ride with two bar gaps and called it the trade the
EA places. Entry is now the next bar's open and the exit the open after the flip - the same
event, at the price the account gets.
RIDES LONGER THAN THE 200-BAR CAP WERE DROPPED, not capped: one-way survivorship against the
longest winners a trend-riding label has. A ride whose bars existed and simply had not flipped
is now marked to market at the cap. Only one that ran off the leading edge of loaded history
stays unresolved, which is the one case genuinely not knowable.
Both label changes re-key: TGT:LEG1 -> TGT:LEG2. Every model retrains from era 0.
Also: the minimum-stop floor wrote an un-normalised price (TCAdjustStops normalises only what
it widens, so a legal floored stop reached the trade layer off-tick); and the journal's context
window was 180 seconds, sized for the dead sixty-second order expiry, while an entry window is
counted in BARS - so any fill later than three minutes silently lost the context column the
per-pattern adaptation is built from.
Compiles 0 errors, 0 warnings. Smoke-tested on EURUSD H1 over 2026-08-24..09-01: runs clean,
no runtime errors, fingerprint reads TGT:LEG2:10:D12 in situ, and nothing trades - which is
the re-keyed label refusing the stale models, as intended. Not deployed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE VOTER FIX, found in the strategy tester and worth stating plainly because it
will bite the next standalone setup too. CExpertSignalCustom weights each child
by VoteCapableWeight(), which is m_weight only when GetPatternCount() > 0 and
ZERO otherwise: a child with no patterns is a news/session-style VETO with no say
in the consensus. CSignalInsideBarGap declared none, so its first tester run
fired 192 times over 2019-2022 and placed nothing - it voted 100 with a capable
weight of 0 and the accumulator divided by zero into 0.00 on every bar. It now
carries one pattern at weight 100, an ID, and its used-series mask, exactly as
every classic module's constructor does. Tester validation of the fixed build is
in progress; this commit is compile-verified only (0 errors, 0 warnings, in a
scratch copy inside the MQL5 tree - the repo path cannot resolve Network.cl).
THE JOURNAL. Two TEXT columns, added IN PLACE with ALTER TABLE (new
EnsureColumn on the database managers) rather than a dbVersion bump, which wipes
the directory:
context key=value;... the setup's view at signal time, the same terms the
research conditioned on, named identically (PublishContext). Accepted
by the journal only when published within 180 s of the position
opening, so an expired order's line cannot be inherited by a later
position from another signal.
passages first-touch MINUTE of every stop and target level on the research
grid plus signed R at each time-stop horizon - the exact fields
research/pricing.py prices an exit from. A synthetic round-trip
proved the live format re-prices the traded exit identically to the
research (max |diff| 4e-7 R) once the horizons carry six decimals.
One honest limit is written into the file header: a live trade is closed by its
own barriers, so from live rows an exit can only be re-selected TIGHTER.
THE ADAPTIVE EXIT. CSignalInsideBarGap reads
Common\Files\Warrior_EA\Adapt\{SYMBOL}_{PERIOD}_inside_bar_u.cfg once per server
day, written by research/journal_adapt.py with the research's discipline (choose
on the older trades, confirm on the newer, placeable stop, stable after dropping
the best 5%). No file means the validated defaults; a file outside the research
grid is refused and logged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CTradeJournalManager keyed its whole open-trade tracker on the value PositionGetTicket()
returns, and passed that same value to HistorySelectByPosition(). That function matches
DEAL_POSITION_ID, which is the position IDENTIFIER. The two coincide for an ordinary
position that opens and closes once, which is why this has never been visible.
THEY STOP COINCIDING ON EXACTLY THE PATHS THAT RENUMBER A TICKET (mql5book.pdf pp. 1228,
1322): a netting reversal, clearing, and - the one that reaches this fleet - a symbol whose
SYMBOL_SWAP_MODE is one of the REOPEN_* variants, where the broker force-closes and reopens
the position at rollover. POSITION_IDENTIFIER never changes across any of them.
TWO DISTINCT FAILURES, both silent:
* IDENTIFIER != TICKET (netting reversal, clearing). HistorySelectByPosition(ticket)
selects an EMPTY history, ResolveClose returns false, and the caller is written to keep
tracking and retry next tick - by design, for the case where history has not caught up
yet. Here it never catches up. The trade is never journalled and the tracking slot is
never freed, so the retry runs every tick for the life of the chart.
* ROLLOVER REOPEN. The ticket changes while the position is genuinely still open, so the
old ticket vanishes from PositionsTotal() and the new one is unrecognised. One trade was
recorded as two: the first written at the reopen price with its MAE/MFE truncated at
rollover, the second opening at that same price with its excursions reset to zero.
This is not cosmetic, for the same reason the DEAL_FEE omission in 34649e2 was not: these
rows feed the expectancy stop (Variables\RiskBudget.mqh) and the tier ranking that sets each
member's vote weight. A trade silently split in two halves its apparent R and doubles its
apparent count, and a trade that never lands leaves the stop reading a book it cannot see.
Our mean hold is 17-19 H1 bars, ~0.75 of a day, so positions DO cross rollover - this is a
live path on this fleet, not a theoretical one.
THE FIX: SJournalOpenTrack carries positionId alongside ticket, FindTracked keys on the
identifier, and ResolveClose takes it. The ticket is still what the DB row records, because
that is the number an operator reads back off the platform. positionId falls back to the
ticket if a server leaves POSITION_IDENTIFIER at 0, which is precisely the case where the
two were identical anyway.
Found by working the API-contract list from the full read of docs/mql5book.pdf.
Not retrain-forcing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ResolveClose summed DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION. That is three of the
four components MQL5 charges.
DEAL_FEE IS A SEPARATE CHARGE, not a synonym for commission: MQL5 defines it as
"fee for the deal which is charged immediately after the deal", and a broker may
levy one, the other, or both. The canonical accounting is
DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION + DEAL_FEE.
So every closed trade's realised P&L was overstated by exactly the fee. That
matters more here than the size of the number suggests, because this figure is not
cosmetic: it feeds the EXPECTANCY STOP (Variables\RiskBudget.mqh) and the tier
ranking that sets each member's vote weight. An overstated result makes a losing
book look break-even to the one guard that is supposed to halt it.
Found by reading docs/mql5book.pdf end to end (p1338 for the property, pp. 1486 and
1535 for the book's own accounting). Two comments claiming ResolveClose "sums all
three" corrected to four - a stale comment counts as a guess.
STILL OPEN, from the same read, and deliberately not bundled:
* DEFERRED-COMMISSION BROKERS READ AS ZERO. If commission is charged at period
end rather than per deal, DEAL_COMMISSION is 0 on the trading deals and the
charge arrives as separate DEAL_TYPE_COMMISSION / _DAILY / _MONTHLY balance
deals. ACCOUNT_COMMISSION_BLOCKED != 0 detects such an account (it is
permanently 0 on a per-deal broker). Our journal would silently report zero
commission there.
* SWAP IS FORWARD-READABLE AND THE GATE IGNORES IT. Unlike commission,
SYMBOL_SWAP_LONG/SHORT are real symbol properties. The measured mean hold is
17-19 H1 bars, ~0.75 of a day, so positions cross rollover and pay it; the
book's own measurements put XAUUSD at -12.60 points long and AUDUSD at -14.80
short, the same order as some of our spreads. ENUM_SYMBOL_SWAP_MODE has nine
variants, so this needs the same "handle what is measurable, report the rest as
unknown" discipline the commission input already uses.
Not retrain-forcing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THREE FINDINGS, ALL FROM READING THE FLEET'S OWN LOG RATHER THAN THE COMPILER.
1. THE MEMBER DEPLOY GATE STILL USED THE 3-CLASS ZERO-SKILL FLOOR.
The ensemble gate's chance rate was corrected on 2026-09-03 when the head stopped
choosing sides. SOosTally::ChancePrecPct - one level down - was not, and it feeds
more than the ensemble copy does: the MEMBER deploy gate, the certified pair
HasDemonstratedEdge() reads to decide whether a member may vote at all, and the
cross-instrument pooled certificate. On a live H1 window it read 12.5% where the
honest always-ride floor is 21.1%, so every member showed "+13pp edge, PASSES"
and was admitted to the vote. A model with no skill whatsoever cleared it.
The same halved floor was in BaselineComparator's Alglib comparison, which scored
the forest and the linear baseline against a bar half the height of the one the
net is judged by.
ChancePrecPct now TAKES THE POLICY AS A PARAMETER WITH NO DEFAULT, so the next
head change is a compile error at every reader instead of a silent wrong answer
at some of them. That is the whole lesson of finding this one a week late: the
first fix was applied where the bug was noticed, not everywhere the assumption
lived.
The era line said "the gate ranks on the LARGER of the two" for that entire week,
on six charts, every era. It now states the policy actually in force.
2. THE ERA LINE PRINTED +/-1.79e308 IN THE MIDDLE OF EVERY RECORD. The binary
head has no third output neuron, so slot 2's min/max kept their DBL_MAX ctor
values and were formatted anyway. Width-aware now, and the slots are labelled
PAYS/DOESNT rather than B/S/N, which is what they hold.
3. THE SIZING QUESTION NEEDED A DIFFERENT MEASUREMENT THAN THE ONE I BUILT.
The reliability curve says the confidence is now HONEST - carried, out-of-sample,
ECE 38pp -> 1.3-2.4pp. It says nothing about whether it RANKS, and ranking is
what a bet size needs. The existing conviction curve cannot answer it either:
coverage collapses above the lowest rung, so every fired call sits in one bucket
and there is no curve to read.
So the ensemble now carries the calibrated confidence per OOS row - the mean over
members that actually called, which is exactly what LiveSignedConfidence()
publishes - and the era verdict splits the certified rung's fired calls at their
median confidence and compares what the two halves earned, in ATR per call.
A MEDIAN SPLIT, NOT A DECILE CURVE, and the reason is power: per-call SD is ~3
ATR and the labels overlap ~34 bars, so a decile of a few hundred raw calls holds
under ten INDEPENDENT ones and its error bar is wider than the whole book. Ten
noisy points would invite the best-of-N reading this project has already crowned
four times. Two halves is the most the data can be asked for, and it is reported
with its standard error on INDEPENDENT counts plus the size of difference this
window could ever resolve - so "not measurable" is distinguishable from "no
effect", which is a statement about the data rather than a verdict on the idea.
Nothing here sizes anything. This is the evidence ConfidenceBridge.mqh's standing
rule demands before anything may.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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.
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.
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>
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.
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>
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>
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>
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>
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>
The warning lived inside the VerboseMode-gated corpus report, so a
forgotten wipe silently voided an entire 18-year corpus run - the
outdated-row guard rejected the whole replay against leftover rows
and the run appended 35 rows instead of building a corpus. The check
now runs unconditionally at tester OnInit (MetaCorpusStaleCheck): 52
quiet one-row newest-key probes vs the test start, with a loud stop-
wipe-rerun instruction when the DB is newer than the test. Absent
tables probe quietly via FetchNewestTimeKey''s new quiet flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6819bb4 called dbm.FetchRecordCount() from ProcessSignal, but the
method only existed on CDatabaseOperationsManager - CDatabaseManager
never exposed it (nothing outside the DB layer had needed it before).
The 12 compile errors were the usual MQL cascade from one unknown
member.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The historical 1000-row cap existed for a real reason: ProcessSignal
pulled BOTH full tables into MQL struct arrays on every buffered
signal, and UpdateSignalsWeights pulled all 52 per cycle -
materializing thousands of string-bearing structs per event is the
practical limit the cap protected against (SQLite itself has none).
Raising the cap for an 18-year meta-label corpus build would have
made runs crawl; sharding across databases would re-read the same
rows and inherit the same cost.
Every question is now answered inside SQLite, one row or one number
per query, flat in table size:
- FetchOpenTradeEntry: the open (NA) trade''s entryPrice for
pattern+direction, LIMIT 1
- FetchNewestTimeKey: newest row''s yyyymmddhhmm via max ROWID
(rows insert chronologically) - the duplicate/outdated guard
- FetchWinLossCounts: COALESCE''d SUM aggregates with the
before-now bound applied in SQL, replacing the tester-only array
trim (now also active live, where it is harmless by construction)
ProcessSignal semantics preserved exactly: prune -> close opposite
(stop-and-reverse still registers its own row) -> duplicate/outdated
-> one-open-trade -> register. CalculatePatternWinRate''s array walk
becomes WinRateFromCounts; the private FetchTradeRecords wrapper and
ShouldDeleteOldestEntry are gone. DB_MaxRowsPerTable=20000 is now
cheap at any table size.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every INSERT/UPDATE a backtest journaled printed a phantom "Failed to
execute bound query (error 5126)" + "Failed to insert/update" pair -
11.7k error lines in one tester run - while every row landed
correctly (verified: v5 DB complete and identical in totals to v4,
results populated, zero non-5126 database errors in the whole log).
5126 is ERR_DATABASE_NO_MORE_DATA, SQLite''s DONE: DatabaseRead()
stepped the statement to completion and there is nothing to read back,
which for DML IS the success outcome. The tester agent reports 5126
where the live terminal reports 0 for the same completed step, and
PrepareAndExecuteBound() treated any nonzero code as failure. Success
is now 0 or 5126; genuine failures (busy, locked, constraint, misuse)
surface as other codes and still fail.
No schema or semantics change - the v5 database and its data are
valid as-is; this only stops the misreporting that would bury a real
error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. The expectancy stop was stone dead at shipped defaults. Its only feed -
RecordTradeResult inside CTradeJournalManager::Update() - ran solely under
UseDatabaseRanking, which ships false, so the da54639 halt was armed
(ExpectancyMinTrades=40) and never received a single closed trade. A risk
rule must not be a side effect of an analytics toggle: the journal gains
InitTrackingOnly(), Update() runs unconditionally from OnTick and skips
only the DB insert when no DB was initialized.
2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by
TCNormalizeVolume - correct for a user-entered fixed lot, but in the
risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/
step-0.01 symbols: double the intended risk, after CapRiskAmount already
clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario
the budget exists to close. CMoneyRiskBase now refuses the trade when the
risk-derived lot is below the broker minimum.
3. All trading was async fire-and-forget (SetAsyncMode(true)) with no
OnTradeTransaction handler and no retry: server retcodes were never
observed. Fail-safe for entries, not for closes - a silently rejected
close rode the position until the next bar (or next day for the timed
close window). Now synchronous, matching the risk-budget flatten's own
already-synchronous CTrade; on an H1 EA the latency is irrelevant.
4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no
OpenRiskAtStops) - pre-halt it could commit more than the remaining daily
allowance. A fixed lot cannot be scaled, so the rule is binary: its
loss-to-stop fits the remaining allowance whole or the trade is refused;
unpriceable risk (no SL) is refused while the budget is enabled.
Compile: 0 errors, 0 warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The daily (4%) and total (8%) rules bound how FAST an account can lose. Nothing
noticed WHETHER it was losing. A negative-expectancy signal traded at 1% inside
that envelope breaches no rule and still arrives at zero - it just takes longer,
with every limit green the whole way down. That is the realistic way this EA
destroys an account, and no existing guard could see it.
THE ARITHMETIC THIS ENFORCES. Expected value per trade is p*TP - (1-p)*SL - cost.
With no directional edge p equals SL/(SL+TP), which is also the break-even rate,
so the payoff terms cancel exactly and EV = -cost. Expected P&L is -(trades) x
cost: strictly negative, proportional to activity. Measured here: directional
precision 23-24% against a 25% break-even, flat across every confidence tier,
with 58 points of spread on SP500. Sizing, stop placement and trailing move
variance around that mean; none of them changes its sign.
So every closed position now reports its result in R (net profit over money
actually at risk) and the running mean is tested against zero. Above the
configured minimum sample, if mean + sigma*SE < 0, new entries stop.
- SIGNIFICANTLY below, not merely below. A run of losers is ordinary variance
even for a profitable system; halting on the raw mean would be the same
act-on-noise error the MI gates exist to prevent. Using the standard error
means a wide spread simply demands more trades before the rule can fire.
- NET of swap and commission (ResolveClose already sums all three). Deliberate
and load-bearing: when the edge is zero, cost IS the expectancy, so a gross
version would measure a strategy nobody can trade.
- Reported in R so symbols, lot sizes and balances share one scale and one
mean. Trades without a stop are not scored rather than assigned a guessed R.
- LATCHED across restarts, like the daily halt and for the same reason: a
latch a reattach clears is not a latch. Clearing it means deleting the risk
state file, deliberately, after looking at why.
State is appended to the risk file length-guarded, so files written before this
still load and start their sample at zero rather than misreading.
Defaults 40 trades / 2 sigma; ExpectancyMinTrades = 0 disables it.
This does not make the strategy profitable and is not meant to. It stops paying
tuition on one the results say is losing, and does it on measurement rather than
on a drawdown limit finally being reached.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Moves CLayer neuron construction to AI/Impl/Layer.mqh to keep Network.mqh clean
- Unifies four previously duplicated architecture initialisation blocks (MLP/CONV/LSTM/HYBRID) into a single shared function
- Eliminates risk of behavioural drift where one architecture missed a setter, causing mismatched feature sets or targets
Removes standalone AI confidence parameters (MinAIConfidence, MinAIExitConfidence) and replaces them with unified Min_Vote_Open and Min_Vote_Close thresholds that apply to both AI and classic engines. Updates all code comments, report suggestions, and market descriptions accordingly, simplifying configuration and ensuring consistent vote requirements across entry and exit logic.
- Added optional `weighScale` parameter (default -1.0) to `CNeuronBase::Init` and `CLayer::CreateElement`.
- Updated `CNeuronPool::Init` to use LeCun-uniform scaling (1/sqrt(window+1)) for its base initialization.
- Updated `CNet::CNet` to use He-scaled initialization (sqrt(2/neurons)) for dense layers.
- These changes enable more flexible and statistically sound weight initialization, matching the rationale used in OCL-based implementations, leading to better training stability and convergence.
- Define MAX_WEIGHT constant (1.0e6) for weight limits in clusters
- Remove redundant barrier from FeedForward kernel (prevents sync issues)
- Port FeedForwardProof and CalcInputGradientProof kernels for max-pooling (no weights, sliding max)
- Port FeedForwardConv kernel for convolution layers (shared weights, multiple output channels)
- Remove unused code and refactor signal condition logic (CSignalPAI)