2026-09-11 06:16:08 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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"
|
2026-09-13 14:32:40 -04:00 | | | #include "..\Enumerations\WarriorEnums.mqh"
|
2026-09-13 14:32:28 -04:00 | | | #include "..\Database\WarriorJournal.mqh"
|
2026-09-13 14:32:40 -04:00 | | | #include "..\System\TradeChecks.mqh"
|
| | | #include "..\System\ManagementNet.mqh"
|
2026-09-11 06:16:08 -04:00 | | |
|
| | | 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
|
2026-09-13 14:32:40 -04:00 | | | 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
|
2026-09-11 06:16:08 -04:00 | | | 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),
|
2026-09-13 14:32:40 -04:00 | | | 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),
|
2026-09-11 06:16:08 -04:00 | | | 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); }
|
2026-09-13 14:32:40 -04:00 | | | //--- 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);
|
| | | }
|
2026-09-11 06:16:08 -04:00 | | | //--- 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;
|
2026-09-13 14:32:40 -04:00 | | | //--- 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;
|
2026-09-11 06:16:08 -04:00 | | |
|
| | | protected:
|
| | | bool Params(const bool isLong, double &price, double &sl, double &tp,
|
| | | datetime &expiration);
|
2026-09-13 14:32:40 -04:00 | | | //--- 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);
|
2026-09-11 06:16:08 -04:00 | | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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
|
2026-09-13 14:32:40 -04:00 | | |
|
| | | //--- 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;
|
| | | }
|
2026-09-11 06:16:08 -04:00 | | | 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;
|
2026-09-13 14:32:40 -04:00 | | | 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`
|
2026-09-11 06:16:08 -04:00 | | | 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.
|
2026-09-13 14:32:40 -04:00 | | | //--- 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_;
|
2026-09-11 06:16:08 -04:00 | | | number++;
|
| | | }
|
| | | if(number != 0)
|
| | | result /= number;
|
| | |
|
2026-09-13 14:32:40 -04:00 | | | //--- 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));
|
| | |
|
2026-09-11 06:16:08 -04:00 | | | //--- 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;
|
2026-09-13 14:32:40 -04:00 | | | //--- 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();
|
2026-09-11 06:16:08 -04:00 | | | int b = (int)MathAbs(result);
|
| | | if(b < 0) b = 0;
|
| | | if(b > 100) b = 100;
|
| | | m_voteHist[b]++;
|
| | | m_voteBars++;
|
| | | }
|
2026-09-13 14:32:40 -04:00 | | | return (fadedResult == EMPTY_VALUE) ? result : fadedResult;
|
2026-09-11 06:16:08 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
2026-09-13 14:32:40 -04:00 | | | //--- 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);
|
2026-09-11 06:16:08 -04:00 | | | }
|
| | | #endif // WARRIOR_SIMPLE_VOTE_MQH
|