Warrior_EA/Warrior_EA.mq5
AnimateDread 75d7362161 feat(warrior): the vol-gated dip-buy book, ported into Warrior_EA
Warrior's defaults are now the validated book: DIP_ZSCORE alone, long only, H4,
risk 0.25%, one chart per index with a shared Magic.

- System/BarCache.mqh: whole-history closed bars, Wilder ATR, GK sigma and the
  expanding vol percentile (no 1024-bar stdlib ceiling)
- System/AccountGuard.mqh: open-risk cap, kill switch, cross-chart lock and
  Friday flat, shared through terminal globals by Magic
- CWarriorExpert: guard on every tick; a transient open failure retries the bar
- CWarriorSignal::SetupStop: the dip owns its 3 x Wilder ATR stop from the bid
- SignalDipBuy: no entry vote while holding (a still-dipping time exit never
  closed, and Processing re-entered on the exit bar); no entry on a stop bar
- WarriorMoney sizes on equity; WARRIOR_RISK allows fractional risk
- TradeLog + research/compare_ea.py: trade-for-trade check vs WarriorDipZ -
  SP500/US30/DAX40 identical to the cent, NAS100 96.9% (stale-quote timer fills)
- research/nn_cross_index.py: pre-registered cross-index NN meta-label - FAIL
  (AUC 0.564, CI [0.498, 0.630]); DipMetaCut stays off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:14:53 -04:00

478 行
27 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. |
//| |
//| THE DEFAULTS ARE THE VALIDATED BOOK (2026-09-23): the vol-gated |
//| dip-buy, alone, long only, on an H4 chart of SP500, NAS100, US30 |
//| or DAX40 - one chart per symbol, the SAME Magic on every chart. |
//| Tester 2022-26: 4 charts, +11.6%, equity DD 2.82%, ret/DD 4.11, |
//| every index and every year positive; unchanged on real ticks. See |
//| STRATEGY.md. The account layer (System\AccountGuard.mqh) shares |
//| an open-risk cap, a kill switch and a lock across those charts. |
//| Every other module is still here and still switchable; with them |
//| on, this is the old ensemble, which has never been profitable. |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property version "2.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"
#include "System\AccountGuard.mqh"
#include "System\TradeLog.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 = 50; // Vote needed to close (101 = never)
input WARRIOR_DIRECTION Direction = DIR_LONG; // 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_RISK; // Position sizing
input WARRIOR_RISK Risk = RISK_0_25; // Risk per trade, % of equity (when sizing by risk)
input double FixedLots = 0.01; // Lot size (when sizing fixed)
input WARRIOR_SL_MODE StopMode = SL_ATR_x3; // Stop loss (the dip-buy places its own 3 x Wilder ATR)
input WARRIOR_TP_MODE TargetMode = TP_NONE; // 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.
//--- THE ACCOUNT, SHARED BY EVERY CHART WITH THIS MAGIC. Attach one chart per symbol with the same
//--- Magic: the cap sums every such chart's open risk, the kill switch halts them all, and a
//--- cross-chart lock stops two charts rolling to the same bar from both reading the cap before
//--- either has opened. Different Magics = independent books with independent caps.
input group "=== Account (shared by every chart with this Magic) ==="
input double MaxOpenRiskPct = 0.75; // Open-risk cap: total risk to stops, % of equity (0 = off)
input double KillSwitchDD = 4.5; // Kill switch: equity DD from peak, % (0 = off)
input int FridayFlatMin = 170; // Friday flat: minutes before this symbol's Friday close (-1 = off)
input bool TradeLog = true; // Write warrior_trades_<SYMBOL>.csv (common folder) at deinit
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 = false; // Classic indicator modules
input bool UseWyckoff = false; // 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, a wide
//--- stop and no target. The production form is DIP_ZSCORE on H4 with the vol gate, on SP500, NAS100,
//--- US30 and DAX40 (the defaults); the RSI(2) form is the D1 US-index variant. It does NOT work on
//--- forex or metals - thirteen registered families found nothing there (research/FX_RESULTS.md).
input bool UseDipBuy = true; // Index dip-buy (run alone, long only)
//--- 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_ZSCORE; // Dip-buy: entry form
input double DipZ = 1.5; // Dip-buy: z-score depth (DIP_ZSCORE)
input int DipExitMA = 20; // Dip-buy: exit when close crosses above this SMA
input int DipTrendMA = 0; // Dip-buy: only above this SMA (0 = no filter)
input int DipMaxBars = 10; // Dip-buy: time stop in bars
//--- THE VOLATILITY GATE (z form only): trade the dip only when the signal bar's Garman-Klass
//--- sigma(30) sits at or above this percentile of its own whole history. Validated on H4; on D1
//--- it halves the cadence below the prop floor, so switch it off there.
input bool DipVolGate = true; // Dip-buy: volatility-regime gate (H4: on, D1: off)
input double DipVolPct = 0.50; // Dip-buy: gate - minimum vol percentile
//--- 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;
CAccountGuard ExtGuard;
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.AtrPeriod(AtrPeriod);
dip.VolGate(DipVolGate, DipVolPct);
if(DipEntry == DIP_ZSCORE && DipVolGate && Period() != PERIOD_H4)
PrintFormat("OnInit: ⚠ the dip-buy's vol gate was validated on H4 only; this chart is %s."
" On D1 switch DipVolGate off.", EnumToString(Period()));
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)Risk / 100.0);
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 ACCOUNT LAYER. A fixed-lot book has no risk unit, so an entry adds nothing the cap can
//--- price in advance; the cap still counts what is already open.
ExtGuard.Init(Symbol(), (long)Magic,
(MoneyMode == MONEY_FIXED_RISK ? (double)Risk / 100.0 : 0.0),
MaxOpenRiskPct, KillSwitchDD, FridayFlatMin);
ExtExpert.Guard(GetPointer(ExtGuard));
//--- 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();
if(TradeLog)
WriteTradeLog("warrior", Symbol(), (long)Magic);
}
//+------------------------------------------------------------------+
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();
}
//+------------------------------------------------------------------+