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>
478 行
27 KiB
MQL5
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();
|
|
}
|
|
//+------------------------------------------------------------------+
|