Warrior_EA/Warrior_EA.mq5
AnimateDread 47a5ef338b Refactor Warrior EA: Integrate custom signal modules, enhance voting mechanism, and improve management features
- Replaced standard library signal modules with custom implementations to allow for named patterns and improved voting.
- Added new input parameters for module weights, allowing for optimization of individual signal contributions.
- Enhanced the management of trades with new options for breakeven and management cut.
- Introduced a mechanism for dynamic ranking of signal weights based on historical performance.
- Improved initialization logic to ensure proper registration of filters and handling of trading conditions.
- Added detailed logging for trading permissions and account status during initialization.
2026-09-13 14:32:40 -04:00

436 行
24 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <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"
//+------------------------------------------------------------------+
//| 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();
}
//+------------------------------------------------------------------+