Warrior_EA/Warrior_EA.mq5

436 行
24 KiB
MQL5

fix: the recall gate was unsatisfiable and the LR decay was a spiral Both made the run structurally unable to succeed, independently of any signal in the data. Found by reading the 13:01 log. RECALL GATE. m_objectiveMet required Buy, Sell AND Neutral OOS recall each >= 40%. First-touch resolution (ce52654) collapsed Neutral from the ~94% majority it was under exact-pivot labels to a same-bar-tie residue - 250 of 38,261 bars, 0.65% - so the floor was asking the model to identify 40% of coin-flip ties before it could converge. Measured: CONV, LSTM and HYBRID all logged "Neutral:0% (need >=40% each)" on every era. No model could ever satisfy it; every run was destined for the plateau ladder or the era cap. Only the DIRECTIONAL floors are load-bearing for the anti-collapse job the gate exists to do: an all-Neutral model shows Buy and Sell recall at 0% and is blocked by them. Neutral's own floor guarded the mirror bias (over-calling Buy/Sell at Neutral's expense), which was real at 94% prevalence and is not at 0.65% - there, almost never calling Neutral is correct rather than biased. Prevalence-guarded rather than hardcoded off, so it returns by itself if a future label rule makes Neutral substantial again. Deliberately NOT extended to Buy/Sell: exempting a thin directional class reopens the era-44-46 hole, which directionalRecallMeasured only half-covers - it checks those classes were MEASURED, not that they passed. ETA DECAY. A regressing era restored the checkpoint, reset the optimizer and cut eta - all on the FIRST regression. The next era then started from an identical state with a smaller step, regressed again, and got the same treatment. The loop is self-sustaining and cannot discover anything, because rolling the weights back is exactly what removes the exploration that would end it. Measured on PAI: eras 2-11 every one a regression against era 1, eta 0.000594 -> 0.000024, dW/W 0.000%/0.000% from era 2 onward. Ten eras, ~45s each, reproducing era 1 exactly and unable to do anything else. Now requires ETA_DECAY_PATIENCE_ERAS consecutive regressions - the standard ReduceLROnPlateau formulation. A single bad era is noise, and an improving era clears the counter so alternating runs never accumulate into a decay. Build tag -> gate-patience-v3. It had not moved in six commits, which is why the running binary could not be identified from its own log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:28:58 -04:00
//+------------------------------------------------------------------+
//| Warrior_EA.mq5 |
//| AnimateDread |
//| |
//| THE WHOLE EA, in the shape MetaEditor's wizard generates. |
refactor(trade-mgmt): remove all confidence-scaled trade management Five modes went, all of them staking real risk on the model's confidence: Intelligent entry (ENTRY_INTELLIGENT), stop (SL_INTELLIGENT), target (TP_INTELLIGENT), trailing (CTrailingIntelligent) and lot size (CMoneyIntelligent's quarter-Kelly). With them, the Confidence_Source input and the CONFIDENCE_SOURCE enum, whose only job was choosing which number those five read. The reason is calibration, not correctness: the confidence magnitude is known to be miscalibrated against the label prior, so every one of these modes multiplied money by a quantity whose units were never established. The DB arm had a second, independent defect - since the tester DB guard (SignalDatabaseActive) it reads 0 in tester and optimizer but non-zero live, so any backtest of CONF_DB/CONF_BLENDED could not reproduce live trading. And what the DB produces is a filter-RANKING win rate, not a per-trade win probability. Both confidence numbers are still recorded per trade (aiConfidence / dbConfidence) and still bucketed against outcome in TradeJournalReport. Recording is what keeps the question answerable; acting on it was the part with no evidence behind it. ConfidenceBridge.mqh now carries an explicit telemetry-only rule at the top. ENUM ORDINALS PINNED. Removing a member vacated a value in four enums at once and MT5 does not validate an enum input replayed from a saved .set or a stored optimization pass. TRAILING_STRATEGY and MONEY_MANAGEMENT_STRATEGY now carry explicit values so the survivors keep the numbers they were saved as, and ValidateBarrierInputs is widened into ValidateTradeManagementInputs covering SL_Mode, TP_Mode, Entry_Multiplier, TrailingStrategy and MM_STRATEGY. Without that gate a chart saved with the Intelligent stop would feed SL_Mode = -1 into a multiplier now used verbatim, placing the stop on the wrong side of entry. RETRAIN-NEUTRAL: neither SL_Mode nor TP_Mode appears in BuildModelFingerprint() or ComputeDbConfigFingerprint() since the swing-pivot target replaced the barrier labels. No .nnw, .cfg or .db re-keys. Also drops the now-dead g_TradeRewardRiskRatio bridge, the CMoneyRiskBase::AdjustRiskAmount hook and the unsigned AIConfidence(). Compile-verified in _claude_stage: 0 errors, 0 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:10:20 -04:00
//| |
//| CExpert |
//| CWarriorVote the root signal - holds the filters |
//| classic modules MA, RSI, MACD, Ichimoku, CCI, ... |
//| Wyckoff modules event stream, failed structure |
//| NN modules trained in-terminal, vote like the rest|
//| CWarriorMoney fixed lot or fixed risk % |
//| CTrailingATR ATR trailing stop |
//| |
//| Every filter answers LongCondition()/ShortCondition() with 0..100. |
//| The root sums them, divides by how many answered, and CExpert |
//| opens a trade when the result clears the threshold. That is the |
//| entire decision. There is no override, no quorum counter, no |
//| per-setup order shaping and no special case for the networks - a |
//| net that votes is indistinguishable from an RSI that votes, which |
//| is the property that makes the whole thing rankable. |
//| |
//| WHAT THE DATABASE IS FOR. Every filter evaluation is queued during |
//| the vote and written on the timer, so each module accumulates its |
//| own record. The ranking layer reads those win rates and sets each |
//| module's Weight(). A module that carries nothing earns a small |
//| weight and stops mattering - which is the alternative to deleting |
//| it, and the reason the classic modules can be left switched on. |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property version "1.00"
#property description "Warrior EA - standard-library vote, NN signals, self-ranking database."
#include <Expert\Expert.mqh>
//--- The database layer logs through System\PrintVerbose.mqh, which reads these two globals. They
//--- are consts, not inputs: this EA reports through the threshold ladder and the journal summary,
//--- both of which print unconditionally. The old per-tick trace was 35% of a 1.5M-line tester log
//--- and told nobody anything - if something here needs explaining, it should print a sentence, not
//--- a number per tick.
const bool VerboseMode = false;
const bool TraceMode = false;
#include "Enumerations\WarriorEnums.mqh"
//--- CExpert with a session-aware bar clock: a daily bar that opens during the CFD maintenance hour
//--- is acted on at the first tick inside a session, not discarded. See the header for the count.
#include "Expert\WarriorExpert.mqh"
#include "Expert\WarriorSignal.mqh"
#include "Database\WarriorJournal.mqh"
#include "Expert\WarriorVote.mqh"
#include "Money\WarriorMoney.mqh"
//--- OURS, NOT THE WIZARD'S. These are the repo's own classic modules, restored from 3ed053e /
//--- 095bd27. They are the MetaQuotes indicators with the one thing the stock modules lack: each
//--- NAMES the pattern that matched, so the journal can key a row to it and the ranking layer can
//--- give that pattern its own weight. 49 named patterns across the eleven, against 0 from the
//--- stdlib versions - a CExpertSignal cannot be dynamic_cast to CWarriorSignal, so every stock
//--- module voted invisibly and none of them could ever be ranked.
#include "Signals\SignalMA.mqh"
#include "Signals\SignalRSI.mqh"
#include "Signals\SignalMACD.mqh"
#include "Signals\SignalCCI.mqh"
#include "Signals\SignalStoch.mqh"
#include "Signals\SignalWPR.mqh"
#include "Signals\SignalRVI.mqh"
#include "Signals\SignalSAR.mqh"
#include "Signals\SignalAO.mqh"
#include "Signals\SignalAC.mqh"
#include "Signals\SignalIchimoku.mqh"
//--- Regime context: trending / consolidating / mean reverting, from closes alone.
#include "Signals\SignalRegime.mqh"
//--- The measured US-index dip-buy. Run ALONE on SP500/NAS100/US30 D1, Direction = DIR_LONG.
#include "Signals\SignalDipBuy.mqh"
//--- OURS. Wyckoff derives from CWarriorSignal, so it votes exactly like the modules above and is
//--- additionally JOURNALLED - the vote cannot tell them apart, the database can.
#include "Signals\Wyckoff\SignalWyckoffEvent.mqh"
#include "Signals\Wyckoff\SignalWyckoffStructure.mqh"
//--- The network. An ordinary signal module: it builds its own inputs, votes 0..100, and
//--- trains itself from this chart's history the first time it runs.
#include "Signals\SignalNeural.mqh"
//--- ATR trailing stop - a plain CExpertTrailing, self-contained.
#include "Trailing\TrailingATR.mqh"
perf(deinit): I/O-free chart cleanup, dead-panel purge, skip clean weight saves The 18:23 terminal close (20260825.log) killed two of six charts inside OnDeinit: they printed "shutting down" then nothing for 5.9 s until "Abnormal termination", stranding ~700 objects each - including the one family no prefix sweep can reach, the control panel (CAppDialog names its 15 objects <numeric instance id><control>, and a re-attach mints a new id, so a killed panel is a permanent ghost; XTIUSD carried one across sessions). The stall sat in the two file writes that preceded all visible cleanup while the four sibling charts flooded the same 2013-era disk - the ~4x18MB-per-chart shutdown weight saves. Three changes: 1. OnDeinit touches no file until the chart is clean. CVoteArrowStore splits Save() into Snapshot() (the chart scan, in memory) and WriteSnapshot() (the disk half, consuming). New order: status label, vote-arrow snapshot, prefix sweep, panel destroy - all object ops - then member sidecars, final sweep, timings, and only then the visibility file, the vote-arrow write and the weight saves. 2. PurgeOrphanedPanelObjects() at OnInit: deletes numeric-prefix CAppDialog ghosts by name (6 chrome + 9 buttons), qualifying a prefix only when >=4 of OUR button names carry it, so a foreign dialog sharing stock chrome names is never touched. 3. m_netDirty: set by every net mutation (both backProp sites, both RestoreWeights sites, online learning conservatively, panel reset), cleared only on a successful Net.Save. Shutdown AND the per-bar autosave now skip the ~18MB write when the net is provably unchanged - for converged ensembles that is every save - which removes the very flood that starved the sibling charts. .stats still writes every time (small; carries the vote record and calibration). A skipped save leaves the .nnw header dtStudied stale, which is the already-handled attach-after-offline-gap case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:46:51 -04:00
//+------------------------------------------------------------------+
//| Inputs - every one an enum, every value its own payload. |
//+------------------------------------------------------------------+
input group "=== General ==="
input ulong Magic = 20260911; // Magic number
input bool EveryTick = false; // Evaluate on every tick (else bar open)
input group "=== Vote ==="
input WARRIOR_THRESHOLD ThresholdOpen = THR_15; // Vote needed to open (% of ensemble)
//--- 101 = never close on a vote (the default: exits are the stop, the target or the trail). The
//--- dip-buy module's exit IS a vote - its ShortCondition fires when the dip has reverted - so on an
//--- index chart this is set to 50 and, with the module alone, its 100 clears it.
input int ThresholdClose = 101; // Vote needed to close (101 = never)
input WARRIOR_DIRECTION Direction = DIR_BOTH; // Allowed direction
input WARRIOR_FADE FadeCrowd = FADE_OFF; // Fade near-unanimous votes, else stand aside
input group "=== Risk ==="
input WARRIOR_MONEY_MODE MoneyMode = MONEY_FIXED_LOT; // Position sizing
input WARRIOR_RISK_PCT RiskPercent = RISK_1; // Risk per trade (when sizing by risk)
input double FixedLots = 0.01; // Lot size (when sizing fixed)
input WARRIOR_SL_MODE StopMode = SL_ATR_x2; // Stop loss
input WARRIOR_TP_MODE TargetMode = TP_ATR_x4; // Take profit
input WARRIOR_TRAIL_MODE TrailMode = TRAIL_NONE; // Trailing stop
input WARRIOR_BREAKEVEN BreakevenAt = BE_OFF; // Move stop to entry at this profit
input WARRIOR_MGMT_CUT ManagementCut = MGMT_OFF; // Exit at +0.5R when the model says retrace
//--- MODULE WEIGHTS - the optimiser's half of the ranking. The journal sets pattern weights from
//--- measured expectancy; these say how loudly each module's patterns are heard. WEIGHT_0 switches
//--- a module off entirely, which is the setting worth sweeping: it lets a pass answer "does this
//--- indicator contribute anything", a question the EA could never ask while every module voted at
//--- 1.0 by default.
input group "=== Module weights (optimise these) ==="
input WARRIOR_WEIGHT W_MA = WEIGHT_100; // MA
input WARRIOR_WEIGHT W_RSI = WEIGHT_100; // RSI
input WARRIOR_WEIGHT W_MACD = WEIGHT_100; // MACD
input WARRIOR_WEIGHT W_CCI = WEIGHT_100; // CCI
input WARRIOR_WEIGHT W_Stoch = WEIGHT_100; // Stochastic
input WARRIOR_WEIGHT W_WPR = WEIGHT_100; // Williams %R
input WARRIOR_WEIGHT W_RVI = WEIGHT_100; // RVI
input WARRIOR_WEIGHT W_SAR = WEIGHT_100; // Parabolic SAR
input WARRIOR_WEIGHT W_AO = WEIGHT_100; // Awesome Oscillator
input WARRIOR_WEIGHT W_AC = WEIGHT_100; // Accelerator
input WARRIOR_WEIGHT W_Ichimoku = WEIGHT_100; // Ichimoku
input WARRIOR_WEIGHT W_WyckoffEv = WEIGHT_100; // Wyckoff events
input WARRIOR_WEIGHT W_WyckoffSt = WEIGHT_100; // Wyckoff structure
input WARRIOR_WEIGHT W_Neural = WEIGHT_100; // Neural
input WARRIOR_WEIGHT W_Regime = WEIGHT_100; // Regime
input WARRIOR_WEIGHT W_DipBuy = WEIGHT_100; // Dip-buy
input group "=== Modules ==="
input bool UseClassics = true; // Classic indicator modules
input bool UseWyckoff = true; // Wyckoff modules
//--- OFF BY DEFAULT so every comparison made before it still holds. Registering a fourteenth voter
//--- changes the divisor in CExpertSignal::Direction(), which changes what EVERY threshold
//--- percentage means - a roster change and a threshold change cannot be measured in one run.
input bool UseRegime = false; // Regime module (trend/range/revert)
//--- THE DIP-BUY IS NOT A VOTE MEMBER. One filter at 100 scores 7.7% on a thirteen-voter roster and
//--- clears nothing; it must run ALONE (every other module off) with Direction = DIR_LONG, on a US
//--- index D1 chart, with a wide stop and no target (StopMode SL_ATR_x3, TargetMode TP_NONE). See the
//--- module header for what was measured and where it does NOT work (Europe, forex, gold).
input bool UseDipBuy = false; // US-index dip-buy (run alone, long only, D1)
//--- The dip's two measured forms (see the module header): RSI(2) on D1 with the 200-bar filter and
//--- the SMA5 exit, or the z-score on H4 with no filter and the SMA20 exit. All four are optimiser
//--- inputs; the module refuses values outside the measured neighbourhood.
input WARRIOR_DIP_ENTRY DipEntry = DIP_RSI2; // Dip-buy: entry form
input double DipZ = 1.5; // Dip-buy: z-score depth (DIP_ZSCORE)
input int DipExitMA = 5; // Dip-buy: exit when close crosses above this SMA
input int DipTrendMA = 200; // Dip-buy: only above this SMA (0 = no filter)
input int DipMaxBars = 10; // Dip-buy: time stop in bars
//--- THE META-LABEL (System\DipMeta.mqh): an ALGLIB forest + MLP, trained in the terminal on every
//--- past dip and what the EA's own exit made of it, says take/skip. CONF_50 = off. Walk-forward:
//--- refitted every DipMetaRefit bars from history-so-far, never saved.
input WARRIOR_CONFIDENCE DipMetaCut = CONF_50; // Dip-buy: skip when P(pays) is below this
input int DipMetaRefit = 500; // Dip-buy: refit the meta-label every N bars
input bool UseNeural = false; // Neural module (trains on first run)
input WARRIOR_CONFIDENCE NeuralCut = CONF_50; // Neural: probability cut
input bool UseDatabase = true; // Record every evaluation to the database
//--- 🛑 THE RANKING FEEDS THE WEIGHTS - AND MEASURED, IT LOSES MONEY. Default OFF.
//---
//--- EURUSD D1 2015-2026, identical settings, the ONLY difference being how much of the journal the
//--- ranker had to act on: with a cold database (patterns mostly keeping their AUTHORED weights,
//--- 1 module ranked at the start, 12 modules / 25 patterns by the end) the run made +150.67 at
//--- PF 1.33. With the same window but a journal pre-filled from 2000-2014 - fully ranked from the
//--- first bar, 12 modules / 27 patterns immediately - it made +27.53 at PF 1.05.
//---
//--- More evidence made it WORSE, which is the signature of a measurement that does not persist:
//--- a pattern's edge is real on the window it was measured on and gone on the next. Recording
//--- stays ON regardless - the journal found the give-back and the 46/54 continue-vs-retrace split,
//--- and neither of those was visible any other way. It is the CONTROL LOOP that is switched off,
//--- not the LOGGING. Left as an input so the optimiser can re-test it rather than trusting this.
input bool DB_RankingFeedsWeights = false; // Let measured expectancy set the weights
//--- ATR drives the stop, the target and the trail, so it is read once here and shared.
input int AtrPeriod = 14; // ATR period for stop/target/trail
//+------------------------------------------------------------------+
CWarriorExpert ExtExpert;
CWarriorVote *ExtSignal = NULL;
CWarriorJournal ExtJournal;
CDatabaseManager ExtDbm;
bool ExtDbReady = false;
//--- Registers one filter and says so if it cannot. A filter that fails to attach silently is a
//--- module missing from the vote, which changes what the threshold means without changing the log.
//--- Takes the STDLIB type, so a MetaQuotes module and one of ours register identically.
bool AddFilter(CExpertSignal *filter, const string name, const WARRIOR_WEIGHT weight)
{
if(filter == NULL)
{
PrintFormat("OnInit: could not create %s", name);
return false;
}
//--- A module at WEIGHT_0 is REGISTERED AND SILENT, not skipped. That matters: the vote divides
//--- by the number of filters that answered, so removing a module from the roster changes what
//--- every threshold percentage means, and a sweep over weights would then be comparing runs
//--- whose thresholds are not the same quantity. Registered-at-zero keeps the divisor fixed.
filter.Weight((double)weight / 100.0);
if(!ExtSignal.AddFilter(filter))
{
PrintFormat("OnInit: could not add %s to the vote", name);
return false;
}
return true;
}
//+------------------------------------------------------------------+
int OnInit()
{
if(!ExtExpert.Init(Symbol(), Period(), EveryTick, Magic))
{
Print("OnInit: ExtExpert.Init failed");
return INIT_FAILED;
}
ExtSignal = new CWarriorVote;
if(ExtSignal == NULL)
return INIT_FAILED;
ExtExpert.InitSignal(ExtSignal);
ExtSignal.ThresholdOpen((int)ThresholdOpen);
//--- CLOSED ONLY BY THE STOP, THE TARGET OR THE TRAIL. A vote-driven exit is a different strategy
//--- from the one the entry threshold was chosen for, and mixing them makes neither measurable.
ExtSignal.ThresholdClose(ThresholdClose);
//--- WHICH SIDE MAY OPEN. Declared as an input since the rebuild and applied nowhere until now:
//--- the gate lives on the vote's CheckOpenLong/Short, which the stdlib's reversal path also uses.
ExtSignal.Allowed(Direction);
//--- THE BARRIERS. StopMode and TargetMode are ATR MULTIPLES (the enum value IS the multiple), so
//--- the stop is the same size in volatility terms on every symbol and in every regime - which a
//--- fixed point distance is not. Set BEFORE InitIndicators, which is where the ATR is created.
ExtSignal.Barriers(AtrPeriod, (double)StopMode, (double)TargetMode);
ExtSignal.ManagementCut((double)ManagementCut / 100.0);
ExtSignal.FadeAt((int)FadeCrowd);
if(UseClassics)
{
if(!AddFilter(new CSignalMA, "MA", W_MA)) return INIT_FAILED;
if(!AddFilter(new CSignalRSI, "RSI", W_RSI)) return INIT_FAILED;
if(!AddFilter(new CSignalMACD, "MACD", W_MACD)) return INIT_FAILED;
if(!AddFilter(new CSignalCCI, "CCI", W_CCI)) return INIT_FAILED;
if(!AddFilter(new CSignalStoch, "Stoch", W_Stoch)) return INIT_FAILED;
if(!AddFilter(new CSignalWPR, "WPR", W_WPR)) return INIT_FAILED;
if(!AddFilter(new CSignalRVI, "RVI", W_RVI)) return INIT_FAILED;
if(!AddFilter(new CSignalSAR, "SAR", W_SAR)) return INIT_FAILED;
if(!AddFilter(new CSignalAO, "AO", W_AO)) return INIT_FAILED;
if(!AddFilter(new CSignalAC, "AC", W_AC)) return INIT_FAILED;
if(!AddFilter(new CSignalIchimoku, "Ichimoku", W_Ichimoku)) return INIT_FAILED;
}
if(UseRegime && !AddFilter(new CSignalRegime, "Regime", W_Regime))
return INIT_FAILED;
if(UseDipBuy)
{
CSignalDipBuy *dip = new CSignalDipBuy;
if(dip == NULL)
return INIT_FAILED;
dip.Entry(DipEntry);
dip.ZEntry(DipZ);
dip.ExitPeriod(DipExitMA);
dip.TrendPeriod(DipTrendMA);
dip.MaxBars(DipMaxBars);
dip.StopAtr((double)StopMode); // the label simulates the stop the EA places
dip.MetaCut((double)DipMetaCut / 100.0);
dip.MetaRefit(DipMetaRefit);
if(!AddFilter(dip, "DipBuy", W_DipBuy))
return INIT_FAILED;
}
if(UseWyckoff)
feat(cost): the broker charges by ASSET CLASS - commission schedule, detected not guessed Tag commission-1. Cost_CommissionPerLotPerSide (one number, shipped at 0.0, never set) is replaced by the operator's actual contract, applied per class: indices none forex 4 USD per lot crypto 0.03% notional metals 0.001% notional energy 0.03% notional CLASS IS DETECTED FROM SYMBOL_PATH, which is what the broker itself organises its tree by, with symbol-name and SYMBOL_TRADE_CALC_MODE as fallbacks for a flat Market Watch. Verified in situ on all ten live charts rather than asserted - every one resolved correctly from its own path (Indices\, Forex\, Crypto\, Energy\, Precious_Metals\). A PERCENTAGE OF NOTIONAL NEEDS NO CONTRACT SIZE AND NO FX RATE. Commission in money is pct x contractSize x price x (quote->account rate); the price-equivalent divides by money-per-price-unit, which is tickValue/tickSize - and tickValue already carries the same contractSize and the same rate. They cancel exactly, leaving price_equiv = pct x price for ANY quote currency so nothing stale or missing can be read. Worth stating because it looks too easy. THE FACTOR-OF-TWO, and the two branches need it OPPOSITE ways round. The percentage branch builds the round turn itself, so a per-side quote is MULTIPLIED by 2. The flat branch hands a per-side figure to WarriorCommissionRoundTurnPrice, which does its own doubling, so a round-turn quote is HALVED on the way in. Writing them the same way round would have been a factor-of-four error between asset classes. The operator's figures are read as the FULL ROUND TURN (Cost_CommissionIsRoundTurn, default true), which is how a prop contract quotes it; false doubles every figure without editing any of them. MEASURED EFFECT - COMMISSION DOMINATES SPREAD ON HALF THE FLEET, and the gate has been charging spread alone until now: BTCUSD comm 24.42 + spread 3.58 = 28.00 cost was UNDERSTATED 7.8x USDJPY comm 0.006 + spread 0.003 = 0.009 3.0x EURUSD comm 0.00004 + 0.00002 = 0.00006 3.0x GBPUSD comm 0.00004 + 0.00003 = 0.00007 2.3x XAUUSD comm 0.04 + spread 0.55 = 0.60 1.1x indices comm 0.00 unchanged In ATR terms BTCUSD goes 0.008 -> ~0.063 and still clears on a +0.52 book, but GBPUSD goes ~0.014 -> ~0.033 against a +0.03 book and should now FAIL. That is the correct outcome: it was the thinnest book on the fleet and it was being charged a third of its true cost. The resolved class, both cost halves and the spread sample count are PRINTED at init, so a symbol filed in an unexpected folder shows up as a wrong class rather than as a silently wrong number. Not retrain-forcing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:03:46 -04:00
{
if(!AddFilter(new CSignalWyckoffEvent, "WyckoffEvent", W_WyckoffEv)) return INIT_FAILED;
if(!AddFilter(new CSignalWyckoffStructure, "WyckoffStructure", W_WyckoffSt)) return INIT_FAILED;
}
if(UseNeural)
{
CSignalNeural *net = new CSignalNeural;
if(net == NULL)
return INIT_FAILED;
net.AtrPeriod(AtrPeriod);
net.Confidence((double)NeuralCut / 100.0);
if(!AddFilter(net, "Neural", W_Neural))
return INIT_FAILED;
feat(cost): the broker charges by ASSET CLASS - commission schedule, detected not guessed Tag commission-1. Cost_CommissionPerLotPerSide (one number, shipped at 0.0, never set) is replaced by the operator's actual contract, applied per class: indices none forex 4 USD per lot crypto 0.03% notional metals 0.001% notional energy 0.03% notional CLASS IS DETECTED FROM SYMBOL_PATH, which is what the broker itself organises its tree by, with symbol-name and SYMBOL_TRADE_CALC_MODE as fallbacks for a flat Market Watch. Verified in situ on all ten live charts rather than asserted - every one resolved correctly from its own path (Indices\, Forex\, Crypto\, Energy\, Precious_Metals\). A PERCENTAGE OF NOTIONAL NEEDS NO CONTRACT SIZE AND NO FX RATE. Commission in money is pct x contractSize x price x (quote->account rate); the price-equivalent divides by money-per-price-unit, which is tickValue/tickSize - and tickValue already carries the same contractSize and the same rate. They cancel exactly, leaving price_equiv = pct x price for ANY quote currency so nothing stale or missing can be read. Worth stating because it looks too easy. THE FACTOR-OF-TWO, and the two branches need it OPPOSITE ways round. The percentage branch builds the round turn itself, so a per-side quote is MULTIPLIED by 2. The flat branch hands a per-side figure to WarriorCommissionRoundTurnPrice, which does its own doubling, so a round-turn quote is HALVED on the way in. Writing them the same way round would have been a factor-of-four error between asset classes. The operator's figures are read as the FULL ROUND TURN (Cost_CommissionIsRoundTurn, default true), which is how a prop contract quotes it; false doubles every figure without editing any of them. MEASURED EFFECT - COMMISSION DOMINATES SPREAD ON HALF THE FLEET, and the gate has been charging spread alone until now: BTCUSD comm 24.42 + spread 3.58 = 28.00 cost was UNDERSTATED 7.8x USDJPY comm 0.006 + spread 0.003 = 0.009 3.0x EURUSD comm 0.00004 + 0.00002 = 0.00006 3.0x GBPUSD comm 0.00004 + 0.00003 = 0.00007 2.3x XAUUSD comm 0.04 + spread 0.55 = 0.60 1.1x indices comm 0.00 unchanged In ATR terms BTCUSD goes 0.008 -> ~0.063 and still clears on a +0.52 book, but GBPUSD goes ~0.014 -> ~0.033 against a +0.03 book and should now FAIL. That is the correct outcome: it was the thinnest book on the fleet and it was being charged a third of its true cost. The resolved class, both cost halves and the spread sample count are PRINTED at init, so a symbol filed in an unexpected folder shows up as a wrong class rather than as a silently wrong number. Not retrain-forcing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:03:46 -04:00
}
CWarriorMoney *money = new CWarriorMoney;
if(money == NULL)
return INIT_FAILED;
if(MoneyMode == MONEY_FIXED_LOT)
money.FixedLot(FixedLots);
feat(trade): two books per symbol, and delete the vote exit 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>
2026-08-26 09:20:35 -04:00
else
money.RiskPercent((double)RiskPercent);
if(!ExtExpert.InitMoney(money))
return INIT_FAILED;
//--- ONE OBJECT, TWO INDEPENDENT FEATURES. CTrailingATR carries both the ATR trail and the
//--- breakeven stop, and either may be off: the stdlib's plain CExpertTrailing has neither, so
//--- installing it whenever TrailMode is NONE would silently discard BreakevenAt.
if(TrailMode == TRAIL_NONE && BreakevenAt == BE_OFF)
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
{
CExpertTrailing *none = new CExpertTrailing;
if(none == NULL || !ExtExpert.InitTrailing(none))
return INIT_FAILED;
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
}
else
{
CTrailingATR *trail = new CTrailingATR;
if(trail == NULL)
return INIT_FAILED;
trail.Multiplier((double)TrailMode); // 0 = no ATR trail, breakeven only
trail.BreakevenAt((double)BreakevenAt / 10.0); // the enum is TENTHS of R
if(!ExtExpert.InitTrailing(trail))
return INIT_FAILED;
PrintFormat("OnInit: management - ATR trail %s, breakeven %s.",
(TrailMode == TRAIL_NONE ? "off" : "x" + IntegerToString((int)TrailMode)),
(BreakevenAt == BE_OFF ? "off" : "at +" +
DoubleToString((double)BreakevenAt / 10.0, 1) + "R"));
feat(altdata): EIA wired, 24-instrument symbol catalog, mapping dialog for unknown symbols EIA (user directive: "the NN might find patterns in it for both oil and regular symbols"). Weekly Petroleum Status Report via the v2 API - crude stocks ex-SPR, field production, refinery utilization - three features (1y percentile, 4w change, utilization) on EVERY catalog symbol, not just oil. EIA screened NULL on WTI's short 7y sample, so these ship as EXPLORATORY inputs: the deploy gate, not the screen, decides whether a model trained on them trades. Publication stamp observed+6d mirrors research/altdata/eia.py. Symbol handling was hardcoded to three if-blocks; it is now a catalog of 24 instruments x alias lists covering The5ers/FTMO/AvaTrade/Dukascopy/OANDA/IC Markets naming, with prefix matching for the broker suffix zoo (US500.cash, XAUUSDm, EURUSD.r). Adding an instrument is one AddSpec row. COT caches are named by CANONICAL so two brokers' names for one contract share a download. Unrecognised symbol -> a chart dialog (Panel\AltDataMapDialog.mqh, CAppDialog + dropdown) asks which instrument it is; the answer persists in symbol_map.cfg and "No alternative data" is a recorded choice, not a nag. Non-blocking by design: an unmapped symbol contributes 0 features and must never hold up a chart. Also: UrlEncodePart now escapes '%' - SoQL like-predicates use it as the wildcard and an unescaped one corrupts the query; docs/ gains the whitelist URLs, an API-key backup, and the catalog reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:18:29 -04:00
}
if(!ExtExpert.ValidationSettings() || !ExtExpert.InitIndicators())
{
Print("OnInit: validation or indicator init failed");
return INIT_FAILED;
}
//--- THE DATABASE. Off during optimisation and forward passes on purpose: those run many agents
//--- in parallel against one file, which is write contention, not a throttle problem.
if(UseDatabase && !MQLInfoInteger(MQL_OPTIMIZATION) && !MQLInfoInteger(MQL_FORWARD))
{
//--- One database per symbol AND per timeframe. Mixing timeframes in one file would pool a
//--- module's D1 record with its M5 record under the same table name, and they are not the
//--- same claim - the whole point of ranking is that a module can be good on one and not the
//--- other. FILE_COMMON, so the tester and the live terminal read the same corpus.
string folders[] = {"Warrior_EA", "Journal"};
const string dbName = Symbol() + "_" + IntegerToString(Period()) + ".db";
if(ExtDbm.Init("1.0", folders, dbName) && ExtDbm.OpenDatabase())
{
ExtDbReady = true;
ExtJournal.Bind(GetPointer(ExtDbm), (bool)MQLInfoInteger(MQL_TESTER));
ExtSignal.Journal(GetPointer(ExtJournal));
}
else
Print("OnInit: database unavailable - running without a record.");
}
//--- SAY OUT LOUD WHETHER THIS EA CAN TRADE AT ALL. Five switches can stop it dead and none of
//--- them raise an error - the EA simply places nothing, which is indistinguishable from a
//--- strategy that found no setups. This repo lost a session to exactly that (AlgoTrading off).
//--- Not fatal in the tester, where some of these read differently, so it reports rather than
//--- refuses - but it always reports.
string tradeWhy = "";
if(!TCTradingPermitted(tradeWhy))
PrintFormat("OnInit: ⚠ TRADING IS NOT CURRENTLY POSSIBLE - %s", tradeWhy);
ENUM_ORDER_TYPE_FILLING fill = ORDER_FILLING_FOK;
string fillWhy = "";
TCPickFilling(Symbol(), fill, fillWhy);
PrintFormat("OnInit: filling mode %s, %s account.", EnumToString(fill),
(TCIsHedging() ? "hedging" : "netting"));
//--- THE ACCOUNT'S KILL SWITCH, PRINTED. This broker is a prop firm and its account carries a
//--- money-mode stop-out at 95% of deposit; the tester inherits it and ENDS THE RUN the moment
//--- balance crosses it - "stop out occurred on 33% of testing interval", final balance 4747.48.
//--- Four symbols across three sweeps were written off as broken history or margin faults on
//--- that evidence. They had simply lost 5%. Every run that ended early sat just past -250 on a
//--- 5,000 deposit and every run that did not, did not.
const ENUM_ACCOUNT_STOPOUT_MODE soMode = (ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);
PrintFormat("OnInit: stop-out mode %s, margin call at %.2f, stop out at %.2f, balance %.2f - "
"%s",
EnumToString(soMode),
AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL), AccountInfoDouble(ACCOUNT_MARGIN_SO_SO),
AccountInfoDouble(ACCOUNT_BALANCE),
(soMode == ACCOUNT_STOPOUT_MODE_MONEY
? "a MONEY stop-out ends a backtest at that equity, whatever the margin level"
: "percent mode - a stop-out needs the margin level to fall this low"));
EventSetTimer(60);
//--- THE RESOLVED LADDER. The threshold is a percentage of the ensemble's weight, so what it
//--- demands depends on how many filters are registered - state it rather than let it be assumed.
const int members = ExtSignal.FilterCount();
if(members > 0)
PrintFormat("OnInit: %d filter(s) registered, so one unopposed voter at full weight scores"
" %.2f%%. ThresholdOpen = %d%% therefore needs about %d of them agreeing."
" ThresholdClose = %d%%, direction %s.",
members, 100.0 / members, (int)ThresholdOpen,
(int)MathCeil((double)ThresholdOpen * members / 100.0),
(int)ThresholdClose, EnumToString(Direction));
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
EventKillTimer();
if(ExtSignal != NULL)
ExtSignal.ReportLadder();
if(ExtDbReady)
{
ExtJournal.Flush();
ExtJournal.Report();
ExtDbm.CloseDatabase();
}
ExtExpert.Deinit();
}
//+------------------------------------------------------------------+
void OnTick()
{
ExtExpert.OnTick();
}
//+------------------------------------------------------------------+
//| The queue is drained here and nowhere else. Writing per |
//| evaluation would put SQLite in the tick path; the connection is |
//| opened once at init and held for the whole run. |
//+------------------------------------------------------------------+
//--- HOW OFTEN THE WEIGHTS ARE RE-DERIVED, in days of market time. The ranking pass is ~100 SQL
//--- aggregates; run every timer tick it would dominate a decade-long pass, and run once at init
//--- it would freeze the weights on whatever the first days happened to show. Daily is the natural
//--- cadence for a swing EA: often enough to track a module going bad, rare enough to be free.
#define WARRIOR_RERANK_DAYS 1
datetime ExtLastRerank = 0;
void OnTimer()
{
if(ExtDbReady)
{
//--- FLUSH BEFORE RANKING, always. The ranking reads the database, not the queue, so a
//--- rerank on an unflushed buffer silently scores the module on everything EXCEPT its most
//--- recent evidence - which is the evidence most likely to have changed the answer.
ExtJournal.Flush();
const datetime now = TimeCurrent();
if(now - ExtLastRerank >= WARRIOR_RERANK_DAYS * 86400)
{
ExtLastRerank = now;
if(DB_RankingFeedsWeights)
ExtSignal.Rerank(GetPointer(ExtDbm));
}
}
ExtExpert.OnTimer();
}
//+------------------------------------------------------------------+
void OnTrade()
{
ExtExpert.OnTrade();
}
//+------------------------------------------------------------------+