//+------------------------------------------------------------------+ //| Warrior_EA.mq5 | //| AnimateDread | //| | //| THE WHOLE EA, in the shape MetaEditor's wizard generates. | //| | //| 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 //--- 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" //+------------------------------------------------------------------+ //| 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) { 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; } CWarriorMoney *money = new CWarriorMoney; if(money == NULL) return INIT_FAILED; if(MoneyMode == MONEY_FIXED_LOT) money.FixedLot(FixedLots); 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) { CExpertTrailing *none = new CExpertTrailing; if(none == NULL || !ExtExpert.InitTrailing(none)) return INIT_FAILED; } 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")); } 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(); } //+------------------------------------------------------------------+