- 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.
832 lines
44 KiB
MQL5
832 lines
44 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| WarriorVote.mqh |
|
|
//| AnimateDread |
|
|
//| |
|
|
//| THE ROOT SIGNAL. It holds the filters, runs the standard |
|
|
//| library's own vote arithmetic, and records what every filter said |
|
|
//| so the database can rank them later. |
|
|
//| |
|
|
//| WHY Direction() IS OVERRIDDEN AT ALL. CExpertSignal::Direction() |
|
|
//| already does exactly the right arithmetic and this class does not |
|
|
//| change one term of it. What the stdlib cannot do is tell anyone |
|
|
//| WHICH filter said what - it adds each contribution and discards |
|
|
//| the attribution. The database needs precisely that attribution, |
|
|
//| so the loop is written out here with a single extra line in it. |
|
|
//| The arithmetic below is the stdlib's, term for term: |
|
|
//| |
|
|
//| result = m_weight * (LongCondition() - ShortCondition()) |
|
|
//| number = (result == 0) ? 0 : 1 |
|
|
//| for each filter: result += filter.Direction(); number++ |
|
|
//| EMPTY_VALUE from any filter aborts the whole vote |
|
|
//| result /= number |
|
|
//| |
|
|
//| ⚠ ABSTAINERS SIT IN THE DIVISOR. `number` counts every filter |
|
|
//| that answered, including the ones that returned 0. That is the |
|
|
//| stdlib's behaviour and it is kept deliberately - a consensus of |
|
|
//| twenty in which three agree is not the same claim as a consensus |
|
|
//| of three in which three agree. It does mean the threshold means |
|
|
//| something different on every roster, which is why the resolved |
|
|
//| ladder is printed at init instead of being left to be assumed. |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef WARRIOR_SIMPLE_VOTE_MQH
|
|
#define WARRIOR_SIMPLE_VOTE_MQH
|
|
|
|
#include "WarriorSignal.mqh"
|
|
#include "..\Enumerations\WarriorEnums.mqh"
|
|
#include "..\Database\WarriorJournal.mqh"
|
|
#include "..\System\TradeChecks.mqh"
|
|
#include "..\System\ManagementNet.mqh"
|
|
|
|
class CWarriorVote : public CWarriorSignal
|
|
{
|
|
protected:
|
|
CWarriorJournal *m_journal; // not owned; may be NULL, in which case nothing is recorded
|
|
//--- OWNED AND REGISTERED HERE. An indicator that is merely Create()d is never refreshed by the
|
|
//--- expert - CIndicators does that, and only for what has been added to it. A standalone CiATR
|
|
//--- reads 0.0 forever, which made Params() refuse every entry and the EA place no trades at all
|
|
//--- while every other diagnostic looked healthy.
|
|
CiATR m_atr;
|
|
int m_atrPeriod;
|
|
double m_stopAtr; // stop distance, in ATR
|
|
double m_targetAtr; // target distance, in ATR (0 = no target)
|
|
int m_refusedNoAtr; // entries declined because ATR was not readable yet
|
|
int m_refusedClosed;// entries declined because the market was shut
|
|
//--- WHICH SIDE MAY OPEN. The stdlib has no such switch: CheckOpenShort() fires on any vote at
|
|
//--- or below -ThresholdOpen, and CheckReverseLong() flips a long into a short on the same test.
|
|
//--- The EA carried a Direction input for days that was applied NOWHERE - the dip-buy's exit
|
|
//--- vote (a "sell" so ThresholdClose can act) opened 36 real shorts on SP500, every one a loss.
|
|
WARRIOR_DIRECTION m_allowed;
|
|
int m_refusedSide; // opens declined because that side is switched off
|
|
int m_fadeAt; // fade the crowd at this many agreeing filters; 0 = off
|
|
int m_faded; // bars inverted this run
|
|
int m_fadeStood; // bars stood aside because the crowd was not big enough
|
|
//--- THE MANAGEMENT MODEL AND ITS PER-TICKET LATCH. See ManagementNet.mqh for the question.
|
|
CManagementNet m_mgmt;
|
|
double m_mgCut; // P(continue) below this = get out at the crossing
|
|
int m_mgMinBars; // history needed before the first fit
|
|
int m_mgRetrainBars; // refit every this many bars (0 = once)
|
|
int m_mgTrainedAtBars;
|
|
datetime m_mgLastTrain;
|
|
bool m_mgTrained;
|
|
//--- Latched when the ticket changes. R is the ORIGINAL risk and is never recomputed: reading
|
|
//--- it from the CURRENT stop is the bug that once made breakeven destroy the trail.
|
|
ulong m_mgTicket;
|
|
double m_mgEntry, m_mgRisk, m_mgMae;
|
|
int m_mgBars;
|
|
bool m_mgCrossed; // +0.5R has been touched, so the model has been asked
|
|
bool m_mgExit; // ...and it said get out
|
|
int m_mgAsked; // crossings the model was asked about, this run
|
|
int m_mgExited; // ...and how many it closed early
|
|
int m_voteHist[101];// |vote| in 1% buckets, sampled once per bar
|
|
int m_voteBars;
|
|
datetime m_voteBar;
|
|
|
|
public:
|
|
CWarriorVote(void) : m_journal(NULL), m_atrPeriod(14), m_stopAtr(2.0),
|
|
m_targetAtr(4.0), m_refusedNoAtr(0), m_refusedClosed(0),
|
|
m_allowed(DIR_BOTH), m_refusedSide(0),
|
|
m_fadeAt(0), m_faded(0), m_fadeStood(0),
|
|
m_mgCut(0.0), m_mgMinBars(750), m_mgRetrainBars(500), m_mgTrainedAtBars(0), m_mgLastTrain(0),
|
|
m_mgTrained(false), m_mgTicket(0), m_mgEntry(0.0),
|
|
m_mgRisk(0.0), m_mgMae(0.0), m_mgBars(0),
|
|
m_mgCrossed(false), m_mgExit(false),
|
|
m_mgAsked(0), m_mgExited(0),
|
|
m_voteBars(0), m_voteBar(0)
|
|
{
|
|
m_id = "VOTE";
|
|
ArrayInitialize(m_voteHist, 0);
|
|
}
|
|
~CWarriorVote(void) {}
|
|
|
|
void Journal(CWarriorJournal *j) { m_journal = j; }
|
|
//--- THE BARRIERS, IN ATR. The stdlib's own OpenLongParams takes m_stop_level/m_take_level as a
|
|
//--- fixed number of POINTS, which is the one thing a stop must not be: 200 points is a tight
|
|
//--- stop on one symbol and an absurd one on the next, and the same number changes meaning as
|
|
//--- volatility moves. Set the multiples here and the levels are computed per trade from ATR.
|
|
void Barriers(const int atrPeriod, const double stopAtr, const double targetAtr)
|
|
{ m_atrPeriod = atrPeriod; m_stopAtr = stopAtr; m_targetAtr = targetAtr; }
|
|
virtual bool InitIndicators(CIndicators *indicators) override;
|
|
virtual bool OpenLongParams(double &price, double &sl, double &tp, datetime &expiration) override
|
|
{ return Params(true, price, sl, tp, expiration); }
|
|
virtual bool OpenShortParams(double &price, double &sl, double &tp, datetime &expiration) override
|
|
{ return Params(false, price, sl, tp, expiration); }
|
|
//--- THE DIRECTION SWITCH, on the two entry checks and therefore - through the stdlib's own
|
|
//--- CheckReverseLong/Short, which call these - on reversals too. Closes are untouched: a side
|
|
//--- that is switched off can still be exited if a position somehow exists.
|
|
void Allowed(const WARRIOR_DIRECTION d) { m_allowed = d; }
|
|
WARRIOR_DIRECTION Allowed(void) const { return m_allowed; }
|
|
virtual bool CheckOpenLong(double &price, double &sl, double &tp, datetime &expiration) override
|
|
{
|
|
if(m_allowed == DIR_SHORT)
|
|
{ m_refusedSide++; return false; }
|
|
return CWarriorSignal::CheckOpenLong(price, sl, tp, expiration);
|
|
}
|
|
virtual bool CheckOpenShort(double &price, double &sl, double &tp, datetime &expiration) override
|
|
{
|
|
if(m_allowed == DIR_LONG)
|
|
{ m_refusedSide++; return false; }
|
|
return CWarriorSignal::CheckOpenShort(price, sl, tp, expiration);
|
|
}
|
|
//--- How many filters are registered. This is the divisor the vote is normalised by, so it is
|
|
//--- what turns a threshold percentage into "how many voters must agree" - the EA prints both.
|
|
int FilterCount(void) const { return m_filters.Total(); }
|
|
virtual double Direction(void) override;
|
|
//--- Bars whose |vote| reached `pct`. Buckets are 1% and truncate downward, which is how the
|
|
//--- comparison in CheckOpenLong behaves, so this counts the same population the threshold would.
|
|
int BarsAtOrAbove(const int pct) const
|
|
{
|
|
int n = 0;
|
|
for(int b = (pct < 0 ? 0 : (pct > 100 ? 100 : pct)); b <= 100; b++)
|
|
n += m_voteHist[b];
|
|
return n;
|
|
}
|
|
void ReportLadder(void) const;
|
|
//--- THE SELF-RANKING PASS. Restored from ac57a72, where it lived inside the 4,000-line
|
|
//--- CExpertSignalCustom; the arithmetic is unchanged, only its home is.
|
|
void Rerank(CDatabaseManager *dbm);
|
|
//--- 0 disables the management model entirely; otherwise P(continue) below this exits at +0.5R.
|
|
void ManagementCut(const double v) { m_mgCut = v; }
|
|
void FadeAt(const int n) { m_fadeAt = n; }
|
|
void TrainManagementIfDue(void);
|
|
virtual bool CheckCloseLong(double &price) override;
|
|
virtual bool CheckCloseShort(double &price) override;
|
|
|
|
protected:
|
|
bool Params(const bool isLong, double &price, double &sl, double &tp,
|
|
datetime &expiration);
|
|
//--- A rate shrunk toward a prior. hits/n is the module's own evidence; priorPct/priorN is the
|
|
//--- pool it belongs to, expressed as that many pseudo-observations. From System\BinomialStats
|
|
//--- at ac57a72, unchanged.
|
|
static double ShrunkRatePct(const double hits, const double n,
|
|
const double priorPct, const double priorN)
|
|
{
|
|
const bool havePrior = (priorN > 0.0 && MathIsValidNumber(priorPct) && priorPct >= 0.0);
|
|
if(!MathIsValidNumber(hits) || !MathIsValidNumber(n) || n <= 0.0)
|
|
return havePrior ? priorPct : 0.0; // no evidence => the prior IS the estimate
|
|
if(!havePrior)
|
|
return 100.0 * hits / n;
|
|
return (hits + priorN * (priorPct / 100.0)) * 100.0 / (n + priorN);
|
|
}
|
|
//--- Rounded to the nearest 10, as it always was: a weight is an ordinal here, and reporting a
|
|
//--- win rate to the percentage point claims a precision 100 samples cannot support.
|
|
static int NormalizeWinRate(const double pct) { return (int)MathRound(pct / 10.0) * 10; }
|
|
//--- The state AT A CROSSING BAR. `shift` is that bar; nothing after it is read.
|
|
bool MgmtFeatures(double &x[], const int shift, const int barsToCross,
|
|
const double maeBeforeR, const bool isLong);
|
|
bool MgmtDecide(const bool isLong);
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
//| Entry at market, stop and target a multiple of ATR from it. |
|
|
//+------------------------------------------------------------------+
|
|
bool CWarriorVote::Params(const bool isLong, double &price, double &sl, double &tp,
|
|
datetime &expiration)
|
|
{
|
|
if(m_symbol == NULL)
|
|
return false;
|
|
const double atr = m_atr.Main(1); // the CLOSED bar - bar 0 is still forming
|
|
//--- REFUSE RATHER THAN GUESS. An unreadable ATR during warm-up would otherwise produce a zero
|
|
//--- distance, and a zero-distance stop is how a position sizer is handed a division by nothing.
|
|
if(atr <= 0.0 || !MathIsValidNumber(atr))
|
|
{
|
|
m_refusedNoAtr++;
|
|
return false;
|
|
}
|
|
const int digits = m_symbol.Digits();
|
|
const double entry = isLong ? m_symbol.Ask() : m_symbol.Bid();
|
|
if(entry <= 0.0)
|
|
return false;
|
|
const double stopDist = m_stopAtr * atr;
|
|
const double targetDist = m_targetAtr * atr;
|
|
price = NormalizeDouble(entry, digits);
|
|
sl = NormalizeDouble(isLong ? entry - stopDist : entry + stopDist, digits);
|
|
tp = (m_targetAtr <= 0.0) ? 0.0
|
|
: NormalizeDouble(isLong ? entry + targetDist : entry - targetDist, digits);
|
|
expiration = 0; // market order: nothing to expire
|
|
|
|
//--- THE FULL PRE-TRADE CHECKLIST, on the one path that opens a trade.
|
|
//---
|
|
//--- Everything in System\\TradeChecks.mqh existed before today and NONE of it was called: the
|
|
//--- entry path reached the broker with nothing between it and the server. The visible symptom
|
|
//--- was "Market closed" rejections, but a rejection is not a harmless no-op - the firing is
|
|
//--- discarded, so the backtest quietly measures a strategy that skips whichever signals land
|
|
//--- in a session gap. That is a rule nobody chose and it was never in the report.
|
|
//---
|
|
//--- TCCanOpen() may ADJUST sl/tp (broker minimum distance, freeze band), which is why they are
|
|
//--- passed after they are computed rather than before.
|
|
//---
|
|
//--- THE VOLUME PROBE IS THE MINIMUM LOT, deliberately. Params() does not size the position -
|
|
//--- CWarriorMoney does - so the question asked here is the weaker but still decisive one: is
|
|
//--- ANY trade possible at this moment? If the smallest legal volume is refused, no size the
|
|
//--- money module chooses can succeed, and finding that out here names the reason.
|
|
double probe = m_symbol.LotsMin();
|
|
string why = "";
|
|
if(!TCCanOpen(m_symbol.Name(), (isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL),
|
|
probe, price, sl, tp, why))
|
|
{
|
|
m_refusedClosed++;
|
|
TCLog("entry-gate", "CWarriorVote: entry declined - " + why);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| The base registers the child filters; this adds the one indicator |
|
|
//| the root itself needs. Order matters: the base call first, so a |
|
|
//| filter that fails to initialise is reported as such. |
|
|
//+------------------------------------------------------------------+
|
|
bool CWarriorVote::InitIndicators(CIndicators *indicators)
|
|
{
|
|
if(indicators == NULL || !CWarriorSignal::InitIndicators(indicators))
|
|
return false;
|
|
if(!indicators.Add(GetPointer(m_atr)))
|
|
{
|
|
Print("CWarriorVote: could not add ATR to the indicator collection");
|
|
return false;
|
|
}
|
|
if(!m_atr.Create(m_symbol.Name(), m_period, m_atrPeriod))
|
|
{
|
|
Print("CWarriorVote: could not create ATR");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
double CWarriorVote::Direction(void)
|
|
{
|
|
double result = m_weight * (LongCondition() - ShortCondition());
|
|
int number = (result == 0.0) ? 0 : 1;
|
|
int longVotes = 0, shortVotes = 0; // how many filters took each side, for the journal
|
|
double fadedResult = EMPTY_VALUE; // EMPTY_VALUE = not faded; the vote returns `result`
|
|
const int total = m_filters.Total();
|
|
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
const long mask = ((long)1) << i;
|
|
if((m_ignore & mask) != 0)
|
|
continue;
|
|
//--- PLAIN CExpertSignal, so MQL5's OWN modules (CSignalMA, CSignalRSI, CSignalMACD, ...)
|
|
//--- drop straight in with no wrapper and no copy - which is how the wizard EA gets its
|
|
//--- indicators and there is no reason for this EA to do it differently.
|
|
CExpertSignal *filter = m_filters.At(i);
|
|
if(filter == NULL)
|
|
continue;
|
|
//--- Only OUR modules can be journalled, because only they name the pattern that matched.
|
|
//--- A stdlib module still votes; it simply records nothing, and that is a fair trade for
|
|
//--- getting eleven indicators for free.
|
|
CWarriorSignal *ours = dynamic_cast<CWarriorSignal *>(filter);
|
|
//--- Cleared BEFORE the call so a pattern left over from the previous bar can never be
|
|
//--- attributed to this one. A module that matches nothing this bar records nothing.
|
|
if(ours != NULL)
|
|
ours.ClearActive();
|
|
const double direction = filter.Direction();
|
|
//--- A veto. EMPTY_VALUE is the stdlib's prohibition signal and it ends the vote outright -
|
|
//--- no threshold, no arithmetic. Session, news and risk filters speak through this.
|
|
if(direction == EMPTY_VALUE)
|
|
return EMPTY_VALUE;
|
|
//--- 🛑 THE ONE ADDED LINE, and the reason this override exists. Every evaluation is recorded,
|
|
//--- not only the ones that become trades: a module's win rate is meaningless unless the
|
|
//--- firings that did NOT trade are in the same table as the ones that did.
|
|
//--- THE BARRIERS ARE THE TRADE'S OWN, and that is what makes the R-multiple meaningful.
|
|
//---
|
|
//--- A symmetric barrier was tried and is wrong for this: it forces every win to +1R and
|
|
//--- every loss to -1R, so expectancy becomes identical to win rate by construction and the
|
|
//--- pattern most worth finding - right 30% of the time, wins three times what it loses -
|
|
//--- cannot appear in the ranking at all. Scored against the stop and target the EA actually
|
|
//--- places, a firing resolves to +target/stop R or -1R, and mean R separates them.
|
|
//--- CONFIRMATION PATTERNS ARE NOT RECORDED. They are true on nearly every bar, so they would
|
|
//--- dominate the corpus by sheer count while carrying the least information of anything the
|
|
//--- module can say - and the ranking would then re-weight the one pattern that was
|
|
//--- deliberately built quiet. Skipping them here is also the single biggest efficiency win
|
|
//--- available: they were roughly two fifths of every row written.
|
|
if(m_journal != NULL && ours != NULL && ours.ActivePattern() != "" &&
|
|
!ours.IsConfirmation(CWarriorSignal::PatternIndex(ours.ActivePattern())))
|
|
{
|
|
const double atrNow = m_atr.Main(1);
|
|
if(atrNow > 0.0 && MathIsValidNumber(atrNow))
|
|
m_journal.Record(ours.FilterID(), ours.ActivePattern(), ours.ActiveDirection(),
|
|
m_symbol.Bid(), direction,
|
|
m_stopAtr * atrNow,
|
|
(m_targetAtr > 0.0 ? m_targetAtr : m_stopAtr) * atrNow);
|
|
}
|
|
const double signed_ = ((m_invert & mask) != 0) ? -direction : direction;
|
|
if(signed_ > 0.0) longVotes++;
|
|
else if(signed_ < 0.0) shortVotes++;
|
|
result += signed_;
|
|
number++;
|
|
}
|
|
if(number != 0)
|
|
result /= number;
|
|
|
|
//--- FADE THE CROWD - see WARRIOR_FADE for the measurement. The journal is stamped with the
|
|
//--- ORIGINAL vote below, so the record keeps describing what the modules said; only what the
|
|
//--- EA does with it is inverted. Stand aside when the crowd is not big enough: the trades this
|
|
//--- mode skips are the ones measured at -0.059R, and there is no reason to keep taking them.
|
|
if(m_fadeAt > 0)
|
|
{
|
|
const int crowd = MathMax(longVotes, shortVotes);
|
|
if(crowd < m_fadeAt)
|
|
{ m_fadeStood++; fadedResult = 0.0; }
|
|
else
|
|
{
|
|
//--- The SIDE is read from the count, not the sign of `result`: a weighted sum can in
|
|
//--- principle lean against the majority, and the measurement was on the majority.
|
|
fadedResult = (longVotes >= shortVotes) ? -MathAbs(result) : MathAbs(result);
|
|
m_faded++;
|
|
}
|
|
}
|
|
|
|
//--- STAMP CONFLUENCE ONTO THIS BAR'S FIRINGS, now that the loop has finished and the counts
|
|
//--- exist. Before the vote closes the answer is not merely unknown, it is undefined - which is
|
|
//--- why this cannot live inside Record().
|
|
if(m_journal != NULL)
|
|
m_journal.StampBar(TimeCurrent(), longVotes, shortVotes, result, RegimeCode(1));
|
|
|
|
//--- ONE SAMPLE PER BAR for the ladder. Per-tick sampling would weight quiet bars by how many
|
|
//--- ticks they happened to carry, which says more about the feed than about the strategy.
|
|
const datetime bar = iTime(m_symbol.Name(), m_period, 0);
|
|
if(bar != m_voteBar)
|
|
{
|
|
m_voteBar = bar;
|
|
//--- FEED THE MARKET'S ANSWER TO THE OPEN FIRINGS, once per bar, from the CLOSED bar - bar 0
|
|
//--- is still forming and its high/low would grow under the resolver, letting a firing
|
|
//--- "reach" a barrier the bar had not reached when the decision was made.
|
|
if(m_journal != NULL)
|
|
m_journal.AdvanceBar(High(1), Low(1));
|
|
//--- Deferred fit: at OnInit the tester has almost no history, so a model built
|
|
//--- there would be built on nothing. Here Bars() is history-so-far.
|
|
TrainManagementIfDue();
|
|
int b = (int)MathAbs(result);
|
|
if(b < 0) b = 0;
|
|
if(b > 100) b = 100;
|
|
m_voteHist[b]++;
|
|
m_voteBars++;
|
|
}
|
|
return (fadedResult == EMPTY_VALUE) ? result : fadedResult;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| What each selectable threshold would have admitted. Printed at |
|
|
//| deinit, so choosing a threshold costs one run instead of one run |
|
|
//| per candidate. |
|
|
//+------------------------------------------------------------------+
|
|
void CWarriorVote::ReportLadder(void) const
|
|
{
|
|
if(m_voteBars <= 0)
|
|
{
|
|
Print("CWarriorVote: no bars sampled - the vote never ran.");
|
|
return;
|
|
}
|
|
const int rungs[] = {1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 40, 50};
|
|
string line = "";
|
|
for(int k = 0; k < ArraySize(rungs); k++)
|
|
{
|
|
const int n = BarsAtOrAbove(rungs[k]);
|
|
if(n <= 0 && k > 0)
|
|
break;
|
|
line += StringFormat(" %d%%:%d(%.2f%%)", rungs[k], n, 100.0 * n / m_voteBars);
|
|
}
|
|
PrintFormat("CWarriorVote: THRESHOLD LADDER over %d bar(s) - bars at or above each rung:%s"
|
|
" | this run opened at >= %d%%, admitting %.2f%% of bars.",
|
|
m_voteBars, (line == "" ? " (no bar reached 1%)" : line), m_threshold_open,
|
|
100.0 * BarsAtOrAbove(m_threshold_open) / m_voteBars);
|
|
//--- ENTRY GATES, ALWAYS PRINTED. A guard that silently declines is indistinguishable from a
|
|
//--- strategy that found nothing, and this EA has already lost a day to exactly that (an ATR
|
|
//--- that read 0.0 forever refused every entry while every other diagnostic looked healthy).
|
|
//--- A number here turns "why so few trades" into a one-line answer.
|
|
PrintFormat("CWarriorVote: ENTRY GATES - %d declined (market shut or untradeable),"
|
|
" %d declined (ATR not readable yet), %d declined (side switched off: %s).",
|
|
m_refusedClosed, m_refusedNoAtr, m_refusedSide, EnumToString(m_allowed));
|
|
//--- HOW OFTEN THE MANAGEMENT MODEL ACTUALLY ACTED. A model with a held-out AUC of 0.68 that
|
|
//--- changes nothing in the P&L is either not being asked, or asked and always saying "hold",
|
|
//--- or acting where the payoff is symmetric - three different problems with three different
|
|
//--- fixes, and without these two numbers they are indistinguishable from each other.
|
|
if(m_fadeAt > 0)
|
|
PrintFormat("CWarriorVote: FADE at >= %d agreeing - inverted %d bar(s), stood aside on %d.",
|
|
m_fadeAt, m_faded, m_fadeStood);
|
|
if(m_mgCut > 0.0)
|
|
PrintFormat("CWarriorVote: MANAGEMENT - model %s, asked at %d crossing(s), closed %d early"
|
|
" (%.1f%%), cut %.2f.",
|
|
(m_mgmt.Ready() ? StringFormat("ready (AUC %.3f)", m_mgmt.AUC()) : "NOT ready"),
|
|
m_mgAsked, m_mgExited,
|
|
(m_mgAsked > 0 ? 100.0 * m_mgExited / m_mgAsked : 0.0), m_mgCut);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| THE SELF-RANKING PASS - what the database was built for. |
|
|
//| |
|
|
//| For every filter, for every pattern it can express, count how its |
|
|
//| Buy and Sell firings resolved and turn that into a weight. The |
|
|
//| arithmetic is restored from ac57a72 unchanged; what had been lost |
|
|
//| was not the formula but the CALL - ApplyPatternWeight() sat in |
|
|
//| this repo overridden by three modules and invoked by none, and |
|
|
//| the firing rows carried no outcome for it to read. |
|
|
//| |
|
|
//| WHY A POOL PRIOR. A pattern with 100 firings at 58% and one with |
|
|
//| 100,000 at 52% are not equally believable, and raw wins/total |
|
|
//| treats them identically. Each pattern is shrunk toward the mean |
|
|
//| of its OWN module by MIN_TRADES_FOR_WIN_RATE pseudo-firings, so a |
|
|
//| pattern measured at exactly the minimum lands half on the pool |
|
|
//| and half on itself, and the pull halves again with every doubling |
|
|
//| of its sample. A lucky rare pattern cannot out-rank a common one |
|
|
//| on noise alone. |
|
|
//| |
|
|
//| NO LOOKAHEAD, BY CONSTRUCTION - and it is worth being precise |
|
|
//| about why, because "the database knows the future" is the obvious |
|
|
//| objection. Two independent guards: |
|
|
//| * a firing row is INSERTED only once it has resolved, so every |
|
|
//| row that exists resolved before now; and |
|
|
//| * the count is cut off at nowKey, so a row stamped later than |
|
|
//| this moment cannot enter the sum even if one existed. |
|
|
//+------------------------------------------------------------------+
|
|
#define WARRIOR_MIN_FIRINGS 100 // below this a pattern has no win rate, only a coincidence
|
|
#define WARRIOR_NO_DATA -1
|
|
//--- HOW MUCH EXPECTANCY IS A FULL VOTE. weight = 50 + 50*(R / scale), so at 0.5 a pattern earning
|
|
//--- +0.5R per firing votes 100 and one losing 0.5R votes 0, with breakeven at 50. Set from the
|
|
//--- spread actually observed rather than from taste: on EURUSD D1 with an ATR2 stop and ATR4
|
|
//--- target, pattern mean R runs roughly -0.35..+0.25, so 0.5 keeps the useful range off both rails.
|
|
#define WARRIOR_R_SCALE 0.5
|
|
|
|
|
|
void CWarriorVote::Rerank(CDatabaseManager *dbm)
|
|
{
|
|
if(dbm == NULL)
|
|
return;
|
|
MqlDateTime nt;
|
|
TimeToStruct(TimeCurrent(), nt);
|
|
const long nowKey = ((((long)nt.year*100 + nt.mon)*100 + nt.day)*100 + nt.hour)*100 + nt.min;
|
|
|
|
const int total = m_filters.Total();
|
|
int patternsSet = 0, ranked = 0, regimeCells = 0;
|
|
//--- The regime AS OF THE CLOSED BAR, read once: every pattern is asked about the same present.
|
|
const int regimeNow = RegimeCode(1);
|
|
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
CExpertSignal *filter = m_filters.At(i);
|
|
if(filter == NULL)
|
|
continue;
|
|
//--- Only our modules can be ranked: a stdlib CExpertSignal names no pattern, so it wrote no
|
|
//--- rows and there is nothing to count. This is the whole reason the classics were restored.
|
|
CWarriorSignal *ours = dynamic_cast<CWarriorSignal *>(filter);
|
|
if(ours == NULL)
|
|
continue;
|
|
const string id = ours.FilterID();
|
|
const int pc = ours.PatternCount();
|
|
if(id == "" || id == "?" || pc <= 0)
|
|
continue;
|
|
|
|
//--- THE MODULE'S OWN POOL, in R. Each pattern is shrunk toward the module it belongs to, so
|
|
//--- a pattern with a thin sample inherits its module's behaviour instead of asserting its
|
|
//--- own noise. Two aggregates per table, computed inside SQLite - no rows materialise.
|
|
//--- FETCHED ONCE, USED TWICE. The pool and the per-pattern score need the same numbers, and
|
|
//--- querying them separately doubled the work: at ~400 aggregates a day over sixteen years
|
|
//--- that was 2.4M round trips a run, and it showed - 90s became 140s.
|
|
int nB[], nS[];
|
|
double rB[], rS[];
|
|
ArrayResize(nB, pc); ArrayResize(nS, pc);
|
|
ArrayResize(rB, pc); ArrayResize(rS, pc);
|
|
ArrayInitialize(nB, 0); ArrayInitialize(nS, 0);
|
|
ArrayInitialize(rB, 0.0); ArrayInitialize(rS, 0.0);
|
|
int poolN = 0;
|
|
double poolSum = 0.0;
|
|
for(int j = 0; j < pc; j++)
|
|
{
|
|
if(ours.IsConfirmation(j))
|
|
continue; // never recorded, so never ranked
|
|
const string pat = "Pattern_" + IntegerToString(j);
|
|
dbm.FetchExpectancy(id + "_" + pat + "_Buy", nowKey, nB[j], rB[j]);
|
|
dbm.FetchExpectancy(id + "_" + pat + "_Sell", nowKey, nS[j], rS[j]);
|
|
poolN += nB[j] + nS[j];
|
|
poolSum += rB[j] * nB[j] + rS[j] * nS[j];
|
|
}
|
|
if(poolN <= 0)
|
|
continue;
|
|
const double poolR = poolSum / poolN;
|
|
|
|
bool any = false;
|
|
for(int j = 0; j < pc; j++)
|
|
{
|
|
//--- KEEPS ITS AUTHORED WEIGHT. A confirmation pattern ships at 10 so it can add
|
|
//--- confluence without ever triggering alone; letting the journal lift it to ~50 is a
|
|
//--- five-fold amplification of exactly the wrong thing.
|
|
if(ours.IsConfirmation(j))
|
|
continue;
|
|
const string pat = "Pattern_" + IntegerToString(j);
|
|
//--- REGIME-CONDITIONAL FIRST, all-regime as the fallback.
|
|
//---
|
|
//--- A pattern's unconditional record mixes the conditions it was good in with the ones it
|
|
//--- was bad in. That is the most likely reason every module measured out at a coin flip
|
|
//--- with a 2.6-point spread: a trend-follower earns in a trend and gives it back in a
|
|
//--- range, and the average of the two is nothing. Asking "how did THIS pattern do when
|
|
//--- the market looked like it does NOW" is a different and much sharper question.
|
|
//---
|
|
//--- The cost is sample: three regimes divide every cell by roughly three, and a cell below
|
|
//--- the floor has no estimate at all. So this is a HIERARCHY, not a replacement - the
|
|
//--- regime cell is used when it has the evidence, and the pattern's whole record when it
|
|
//--- does not. Nothing is ever scored on fewer observations than before.
|
|
int nbR = 0, nsR = 0;
|
|
double rbR = 0.0, rsR = 0.0;
|
|
dbm.FetchExpectancy(id + "_" + pat + "_Buy", nowKey, nbR, rbR, regimeNow);
|
|
dbm.FetchExpectancy(id + "_" + pat + "_Sell", nowKey, nsR, rsR, regimeNow);
|
|
const bool haveRegime = ((nbR + nsR) >= WARRIOR_MIN_FIRINGS);
|
|
const int nb = haveRegime ? nbR : nB[j];
|
|
const int ns = haveRegime ? nsR : nS[j];
|
|
const double rb = haveRegime ? rbR : rB[j];
|
|
const double rs = haveRegime ? rsR : rS[j];
|
|
if(haveRegime)
|
|
regimeCells++;
|
|
const int n = nb + ns;
|
|
//--- A side with no evidence contributes nothing rather than a zero: averaging a sentinel
|
|
//--- would quietly halve any pattern that only ever fires one way.
|
|
if(n < WARRIOR_MIN_FIRINGS)
|
|
continue;
|
|
const double meanR = (rb * nb + rs * ns) / n;
|
|
//--- Empirical-Bayes toward the module pool, WARRIOR_MIN_FIRINGS pseudo-firings of it, so
|
|
//--- a pattern measured at exactly the minimum lands half on the pool and half on itself
|
|
//--- and the pull halves again with every doubling of its sample.
|
|
const double shrunk = (meanR * n + poolR * WARRIOR_MIN_FIRINGS) /
|
|
(n + (double)WARRIOR_MIN_FIRINGS);
|
|
//--- EXPECTANCY -> A 0..100 VOTE. Breakeven is R = 0, and that maps to 50 - the same place
|
|
//--- a coin flip sat under the win-rate map that survived measurement. WARRIOR_R_SCALE
|
|
//--- says how much R it takes to reach a full-throated vote.
|
|
//---
|
|
//--- Gentle on purpose. Centring the OLD win-rate map so a coin flip voted nothing took
|
|
//--- this run from +99.98 (55 trades, PF 1.18) to -89.28 (22 trades, PF 0.70): the
|
|
//--- ranking is walk-forward, so a pattern significant on the past is largely not
|
|
//--- significant on the future, and leaning hard on the measurement concentrates the
|
|
//--- portfolio into whatever most recently got lucky. Rank on expectancy - but lean
|
|
//--- gently on the ranking.
|
|
double w = 50.0 + 50.0 * (shrunk / WARRIOR_R_SCALE);
|
|
if(w < 0.0) w = 0.0;
|
|
if(w > 100.0) w = 100.0;
|
|
ours.ApplyPatternWeight(j, (int)MathRound(w));
|
|
patternsSet++;
|
|
any = true;
|
|
}
|
|
if(any)
|
|
ranked++;
|
|
}
|
|
//--- THE MODULE WEIGHT IS NOT SET HERE. It is an INPUT, swept by the MT5 optimiser alongside the
|
|
//--- threshold and the ATR multiples - see WeightOf() in the EA. Two reasons it belongs there
|
|
//--- and the pattern weights belong here: there are 49 patterns and only 12 modules, so the
|
|
//--- fine grain is impossible to optimise and the coarse grain is easy; and a module weight
|
|
//--- derived from the same journal as its own patterns would count the same evidence twice.
|
|
//--- Measured before this split: module weights from the journal moved every module to within
|
|
//--- 1.5 points of the same number, which rescaled the whole vote without reordering anything.
|
|
if(ranked > 0)
|
|
PrintFormat("CWarriorVote: reranked %d module(s), %d pattern weight(s) from expectancy;"
|
|
" regime %d, %d of them on regime-specific evidence.",
|
|
ranked, patternsSet, regimeNow, regimeCells);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| The crossing bar's state. Reads bar `shift` and older - never |
|
|
//| newer - so a row built here could have been built live. |
|
|
//+------------------------------------------------------------------+
|
|
bool CWarriorVote::MgmtFeatures(double &x[], const int shift, const int barsToCross,
|
|
const double maeBeforeR, const bool isLong)
|
|
{
|
|
ArrayResize(x, MGMT_FEATURES);
|
|
ArrayInitialize(x, 0.0);
|
|
const double atr = m_atr.Main(shift);
|
|
if(atr <= 0.0 || !MathIsValidNumber(atr) || barsToCross <= 0)
|
|
return false;
|
|
const double h = High(shift), l = Low(shift), c = Close(shift);
|
|
const double rng = h - l;
|
|
if(rng <= 0.0)
|
|
return false;
|
|
double v20 = 0.0;
|
|
for(int k = 0; k < 20; k++)
|
|
v20 += (double)TickVolume(shift + k);
|
|
v20 /= 20.0;
|
|
const double vNow = (double)TickVolume(shift);
|
|
|
|
int i = 0;
|
|
//--- Bounded, so one slow grind cannot dominate the input scale.
|
|
x[i++] = MathMin(barsToCross, MGMT_HORIZON) / (double)MGMT_HORIZON;
|
|
x[i++] = MathMin(maeBeforeR, 2.0) / 2.0;
|
|
x[i++] = MathMin(MGMT_TRIGGER_R / barsToCross, 1.0);
|
|
x[i++] = EfficiencyRatio(shift, 20);
|
|
x[i++] = MathMin(VarianceRatio(shift, 60, 5), 3.0) / 3.0;
|
|
x[i++] = RegimeCode(shift) / 2.0;
|
|
x[i++] = MathMin(rng / atr, 5.0) / 5.0;
|
|
//--- Where the bar closed IN ITS OWN RANGE, oriented to the trade: 1 means it closed at the
|
|
//--- extreme in our favour (extension), 0 means it gave the bar back (exhaustion). Orienting it
|
|
//--- is what lets one model serve both sides.
|
|
x[i++] = isLong ? ((c - l) / rng) : ((h - c) / rng);
|
|
x[i++] = (v20 > 0.0) ? MathMin(vNow / v20, 3.0) / 3.0 : 0.0;
|
|
x[i++] = isLong ? 1.0 : 0.0;
|
|
return (i == MGMT_FEATURES);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| TRAIN ON EVERY VIRTUAL TRADE HISTORY OFFERS. |
|
|
//| |
|
|
//| One trade is simulated from every bar, in BOTH directions, under |
|
|
//| the EA's own stop and target. A row is emitted only when the |
|
|
//| trade both CROSSED the trigger and later RESOLVED - a crossing |
|
|
//| with no answer is not a training example, and a trade that never |
|
|
//| crossed is not one either because the question was never asked. |
|
|
//| |
|
|
//| Both directions on purpose: it doubles the sample and the model |
|
|
//| carries `is_long`, so the asymmetry is something it can learn |
|
|
//| rather than something split across two half-sized models. |
|
|
//| |
|
|
//| Deferred like the neural module's fit, and for the same reason - |
|
|
//| Bars() at OnInit is near-zero in the tester, so training there |
|
|
//| can never work, while training partway through means every fit |
|
|
//| saw only its own past. |
|
|
//+------------------------------------------------------------------+
|
|
void CWarriorVote::TrainManagementIfDue(void)
|
|
{
|
|
if(m_mgCut <= 0.0 || m_symbol == NULL)
|
|
return;
|
|
const datetime bar = iTime(m_symbol.Name(), m_period, 0);
|
|
if(bar == m_mgLastTrain)
|
|
return;
|
|
m_mgLastTrain = bar;
|
|
const int bars = Bars(m_symbol.Name(), m_period);
|
|
if(bars < m_mgMinBars)
|
|
return;
|
|
//--- RETRAIN ON A CADENCE, LIKE THE NEURAL MODULE DOES - NOT ONCE.
|
|
//---
|
|
//--- The first version fitted at bar ~750 (early 2015, on the deepened pre-2015 history) and then
|
|
//--- used that model unchanged through 2026. Its held-out AUC - 0.77 on USDJPY - was measured on
|
|
//--- an early-2015 validation tail, and eleven years later it produced no P&L effect on any
|
|
//--- symbol (pooled t=0.38). Skill measured once and never refreshed is skill that decays out
|
|
//--- from under the run. Each refit sees only its own past, so this stays walk-forward; the AUC
|
|
//--- line printed on every refit is what says whether the skill persists across eras or was a
|
|
//--- property of one.
|
|
if(m_mgTrained && (m_mgRetrainBars <= 0 || bars < m_mgTrainedAtBars + m_mgRetrainBars))
|
|
return;
|
|
m_mgTrainedAtBars = bars;
|
|
const int want = MathMin(bars, WARRIOR_NET_HISTORY);
|
|
if(!DeepenPrices(want) || (want > m_atr.BufferSize() && !m_atr.BufferResize(want)))
|
|
return;
|
|
|
|
//--- NEVER WALK PAST THE BUFFER. `want` caps at WARRIOR_NET_HISTORY, so on H4 (18,000+ bars) the
|
|
//--- series only holds the newest 8,192 - every read beyond that returns 0.0 in SILENCE, and the
|
|
//--- loop burns thousands of iterations producing nothing. The span is the smaller of "history
|
|
//--- that exists" and "history that is readable".
|
|
const int first = 70;
|
|
const int last = MathMin(bars - MGMT_HORIZON - 2, want - 70);
|
|
if(last - first < 300)
|
|
return;
|
|
CMatrixDouble xy((last - first) * 2, MGMT_FEATURES + 1);
|
|
int rows = 0, crossed = 0;
|
|
double x[];
|
|
for(int e = last; e >= first; e--) // oldest entry bar to newest
|
|
{
|
|
const double atrE = m_atr.Main(e);
|
|
if(atrE <= 0.0 || !MathIsValidNumber(atrE))
|
|
continue;
|
|
const double entry = Close(e);
|
|
const double risk = m_stopAtr * atrE;
|
|
const double tgt = (m_targetAtr > 0.0 ? m_targetAtr : m_stopAtr) * atrE;
|
|
if(risk <= 0.0 || entry <= 0.0)
|
|
continue;
|
|
for(int d = 0; d < 2; d++)
|
|
{
|
|
const bool isLong = (d == 0);
|
|
double mae = 0.0, maeAtCross = 0.0, maeBefore = 0.0;
|
|
int crossBar = -1, crossBars = 0;
|
|
int label = -1;
|
|
for(int k = 1; k <= MGMT_HORIZON; k++)
|
|
{
|
|
const int sh = e - k; // forward in time = smaller shift
|
|
if(sh < 1)
|
|
break;
|
|
const double hi = High(sh), lo = Low(sh);
|
|
const double fav = isLong ? (hi - entry) : (entry - lo);
|
|
const double adv = isLong ? (entry - lo) : (hi - entry);
|
|
if(adv > mae)
|
|
mae = adv;
|
|
//--- STOP FIRST on a bar that touched both: bar data cannot order them, and resolving
|
|
//--- ties in our own favour reports a model no live account could reproduce.
|
|
if(adv >= risk)
|
|
{ label = (crossBar >= 0) ? 0 : -1; break; }
|
|
if(crossBar < 0 && fav >= MGMT_TRIGGER_R * risk)
|
|
{
|
|
//--- THE FEATURES DESCRIBE THE BAR BEFORE THE CROSSING, NOT THE CROSSING BAR.
|
|
//---
|
|
//--- The touch happens somewhere inside bar `sh`, but that bar's close and range are
|
|
//--- only known when it ENDS - after the touch. Whether it went on to close at its
|
|
//--- extreme (extension) or mid-range (exhaustion) is the answer, not the question,
|
|
//--- and the first version of this fed it to the model: held-out AUC 0.66-0.71 that
|
|
//--- then LOST money when acted on live, where bar 0 is still forming and only bar 1
|
|
//--- can be read. Train on what live can see or the AUC is a number about the leak.
|
|
//---
|
|
//--- Same for the adverse excursion: `maeBefore` stops at the previous bar, because
|
|
//--- the crossing bar's low may also be after the touch.
|
|
crossBar = sh + 1;
|
|
crossBars = k;
|
|
maeAtCross = maeBefore;
|
|
}
|
|
maeBefore = mae; // carried one bar behind, for the reason above
|
|
if(fav >= tgt)
|
|
{ label = (crossBar >= 0) ? 1 : -1; break; }
|
|
}
|
|
if(crossBar < 0 || label < 0)
|
|
continue; // never asked, or never answered
|
|
crossed++;
|
|
if(!MgmtFeatures(x, crossBar, crossBars, maeAtCross / risk, isLong))
|
|
continue;
|
|
bool ok = true;
|
|
for(int f = 0; f < MGMT_FEATURES; f++)
|
|
if(!MathIsValidNumber(x[f]))
|
|
{ ok = false; break; }
|
|
if(!ok)
|
|
continue;
|
|
for(int f = 0; f < MGMT_FEATURES; f++)
|
|
xy.Set(rows, f, x[f]);
|
|
xy.Set(rows, MGMT_FEATURES, (double)label);
|
|
rows++;
|
|
}
|
|
}
|
|
if(rows < 300)
|
|
{
|
|
PrintFormat("CWarriorVote: management model - only %d usable crossing(s) of %d - not trained.",
|
|
rows, crossed);
|
|
m_mgTrained = true; // do not re-walk history every bar
|
|
return;
|
|
}
|
|
string names[];
|
|
ArrayResize(names, MGMT_FEATURES);
|
|
for(int f = 0; f < MGMT_FEATURES; f++)
|
|
names[f] = CManagementNet::FeatureName(f);
|
|
if(m_mgmt.Train(xy, rows, names))
|
|
PrintFormat("CWarriorVote: management model trained on %d crossing(s).", rows);
|
|
else
|
|
PrintFormat("CWarriorVote: management model refused - %s", m_mgmt.Why());
|
|
m_mgTrained = true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| ASK ONCE, AT THE CROSSING. The latch is per ticket and the answer |
|
|
//| is remembered, so the model is not re-polled every tick with a |
|
|
//| drifting input - one trade, one decision. |
|
|
//+------------------------------------------------------------------+
|
|
bool CWarriorVote::MgmtDecide(const bool isLong)
|
|
{
|
|
if(m_mgCut <= 0.0 || !m_mgmt.Ready() || m_symbol == NULL)
|
|
return false;
|
|
if(!PositionSelect(m_symbol.Name()))
|
|
{ m_mgTicket = 0; return false; }
|
|
const ulong ticket = (ulong)PositionGetInteger(POSITION_TICKET);
|
|
if(ticket != m_mgTicket)
|
|
{
|
|
m_mgTicket = ticket;
|
|
m_mgEntry = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
const double sl = PositionGetDouble(POSITION_SL);
|
|
m_mgRisk = (sl > 0.0) ? MathAbs(m_mgEntry - sl) : 0.0;
|
|
m_mgMae = 0.0;
|
|
m_mgBars = 0;
|
|
m_mgCrossed = false;
|
|
m_mgExit = false;
|
|
}
|
|
if(m_mgExit)
|
|
return true; // already decided; keep saying so until closed
|
|
if(m_mgCrossed || m_mgRisk <= 0.0)
|
|
return false;
|
|
|
|
const double price = isLong ? m_symbol.Bid() : m_symbol.Ask();
|
|
const double fav = isLong ? (price - m_mgEntry) : (m_mgEntry - price);
|
|
const double adv = isLong ? (m_mgEntry - price) : (price - m_mgEntry);
|
|
if(adv > m_mgMae)
|
|
m_mgMae = adv;
|
|
m_mgBars = (int)MathMax(1, iBarShift(m_symbol.Name(), m_period,
|
|
(datetime)PositionGetInteger(POSITION_TIME)));
|
|
if(fav < MGMT_TRIGGER_R * m_mgRisk)
|
|
return false;
|
|
|
|
m_mgCrossed = true;
|
|
double x[];
|
|
if(!MgmtFeatures(x, 1, m_mgBars, m_mgMae / m_mgRisk, isLong))
|
|
return false;
|
|
const double p = m_mgmt.Score(x);
|
|
if(p < 0.0)
|
|
return false;
|
|
m_mgAsked++;
|
|
m_mgExit = (p < m_mgCut);
|
|
if(m_mgExit)
|
|
m_mgExited++;
|
|
return m_mgExit;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CWarriorVote::CheckCloseLong(double &price)
|
|
{
|
|
if(MgmtDecide(true))
|
|
{
|
|
price = m_symbol.Bid();
|
|
return true;
|
|
}
|
|
return CWarriorSignal::CheckCloseLong(price);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
bool CWarriorVote::CheckCloseShort(double &price)
|
|
{
|
|
if(MgmtDecide(false))
|
|
{
|
|
price = m_symbol.Ask();
|
|
return true;
|
|
}
|
|
return CWarriorSignal::CheckCloseShort(price);
|
|
}
|
|
#endif // WARRIOR_SIMPLE_VOTE_MQH
|