//+------------------------------------------------------------------+ //| WarriorMind.mqh | //| AnimateDread | //| | //| THE TRADER BEHIND THE SIGNAL. | //| | //| The signal modules decide THAT a setup exists. The Mind decides | //| how much to trust it, the way a discretionary trader does: | //| | //| 1. LOOK every modality reads the closed signal bar - price, | //| volatility, volume with the clock removed, Wyckoff | //| structure, the neighbouring indices, the calendar. | //| 2. RECALL the reviewer asks the journal how trades taken in | //| conditions like these have actually gone. | //| 3. SIZE the bet sizer turns that P(win) into a fraction of | //| the base risk (AFML ch. 10) - never more than 1. | //| 4. WRITE the trade is booked with the whole picture, and when | //| it closes the outcome is filed for the next review. | //| | //| This class only ORCHESTRATES. It owns no maths (Mind\EdgeStats, | //| BetSizer, JournalReviewer), no way of looking (Mind\Modality*), | //| and no file format (TradeBook); it is the one place the four are | //| wired together, so each can change without the others noticing. | //| | //| THE SAFETY CONTRACT. | //| * MIND_OBSERVE (the default) changes NOTHING about what is traded | //| or how big: it reads, books and reports. A backtest with it on | //| is trade-for-trade the same as with it off. | //| * It can only REDUCE risk (scale <= 1) or, in MIND_SIZE_GATE, skip | //| a trade whose conditions the journal rates a coin flip or worse.| //| * Any modality that fails degrades to "no reading". Nothing here | //| may be the reason a trade is missed except the gate, and the | //| gate needs MinTrades of evidence first. | //+------------------------------------------------------------------+ #ifndef WARRIOR_MIND_MQH #define WARRIOR_MIND_MQH #include "..\Enumerations\WarriorEnums.mqh" #include "..\Money\WarriorMoney.mqh" #include "..\System\BarCache.mqh" #include "VolumeProfile.mqh" #include "ModalityPrice.mqh" #include "ModalityVolume.mqh" #include "ModalityWyckoff.mqh" #include "ModalityBreadth.mqh" #include "ModalityCalendar.mqh" #include "JournalReviewer.mqh" #include "BetSizer.mqh" #include "EdgeStats.mqh" #include "TradeBook.mqh" class CWarriorMind { protected: WARRIOR_MIND m_mode; string m_symbol; ENUM_TIMEFRAMES m_tf; ulong m_magic; double m_minScale; int m_trials; CWarriorMoney *m_money; // not owned CBarCache m_cache; CVolumeProfile m_profile; CModality *m_mods[]; // owned CJournalReviewer m_rev; CTradeBook m_book; //--- the decision being made / the trade being held SMarketContext m_pending; SReview m_pendingReview; double m_pendingScale; STradeRecord m_open; bool m_have; int m_finalizeTries; //--- reported at deinit int m_assessed, m_scaled, m_vetoed; void AddModality(CModality *m); bool Holding(ulong &id, datetime &when, double &entry, double &lots, double &sl) const; void Finalize(void); string Narrate(const SMarketContext &c, const bool isLong) const; public: CWarriorMind(void) : m_mode(MIND_OFF), m_tf(PERIOD_CURRENT), m_magic(0), m_minScale(0.5), m_trials(50), m_money(NULL), m_pendingScale(1.0), m_have(false), m_finalizeTries(0), m_assessed(0), m_scaled(0), m_vetoed(0) {} ~CWarriorMind(void); //--- The money object whose risk the mind scales. Optional: without it the mind observes only. void Bind(CWarriorMoney *money) { m_money = money; } bool Init(const string symbol, const ENUM_TIMEFRAMES tf, const ulong magic, const WARRIOR_MIND mode, const bool useWyckoff, const string peers, const int minTrades, const int shrink, const double minScale, const int trials); WARRIOR_MIND Mode(void) const { return m_mode; } //--- Called with the entry about to be sent. False = stand aside (MIND_SIZE_GATE only). bool Assess(const bool isLong, const double price, const double sl, string &why); //--- Called after the entry was accepted. void Opened(const bool isLong, const double sl); //--- Every tick: notice that the position has closed and file it. void OnTick(void); void Report(void); }; //+------------------------------------------------------------------+ CWarriorMind::~CWarriorMind(void) { for(int i = 0; i < ArraySize(m_mods); i++) delete m_mods[i]; } //+------------------------------------------------------------------+ void CWarriorMind::AddModality(CModality *m) { const int k = ArraySize(m_mods); ArrayResize(m_mods, k + 1); m_mods[k] = m; } //+------------------------------------------------------------------+ bool CWarriorMind::Init(const string symbol, const ENUM_TIMEFRAMES tf, const ulong magic, const WARRIOR_MIND mode, const bool useWyckoff, const string peers, const int minTrades, const int shrink, const double minScale, const int trials) { m_mode = mode; m_symbol = symbol; m_tf = tf; m_magic = magic; m_minScale = MathMax(0.1, MathMin(1.0, minScale)); m_trials = MathMax(trials, 1); if(m_mode == MIND_OFF) return true; m_rev.Configure(minTrades, shrink); m_cache.Init(symbol, tf, 14, 30); m_profile.Init(GetPointer(m_cache)); //--- ORDER MATTERS: the volume modality reads the range the price modality wrote. AddModality(new CModalityPrice(GetPointer(m_cache))); AddModality(new CModalityVolume(GetPointer(m_cache), GetPointer(m_profile))); if(useWyckoff) AddModality(new CModalityWyckoff(symbol, tf)); if(peers != "") { CModalityBreadth *br = new CModalityBreadth(symbol, tf, peers); if(br.PeerCount() > 0) AddModality(br); else delete br; } AddModality(new CModalityCalendar); const bool tester = (bool)MQLInfoInteger(MQL_TESTER); const bool optimizing = (bool)MQLInfoInteger(MQL_OPTIMIZATION); if(!optimizing) m_book.Init(symbol, tf, tester); if(!tester) { SMarketContext ctxs[]; double rs[]; const int n = m_book.Load(ctxs, rs); for(int i = 0; i < n; i++) m_rev.Add(ctxs[i], rs[i]); if(n > 0) PrintFormat("CWarriorMind: resumed %d trade(s) from %s.", n, m_book.Path()); } string names = ""; for(int i = 0; i < ArraySize(m_mods); i++) names += (i > 0 ? ", " : "") + m_mods[i].Name(); PrintFormat("CWarriorMind: %s on %s %s - modalities: %s. Book: %s.", EnumToString(m_mode), symbol, EnumToString(tf), names, (m_book.Path() == "" ? "none" : m_book.Path())); return true; } //+------------------------------------------------------------------+ //| A trader's one-line read of the picture, in the order they would | //| say it. The same text is booked with the trade, so reading the | //| journal later is reading what they thought at the time. | //+------------------------------------------------------------------+ string CWarriorMind::Narrate(const SMarketContext &c, const bool isLong) const { string s = isLong ? "buy" : "sell"; if(c.Has(CTX_Z)) s += StringFormat(" z=%.2f", c.v[CTX_Z]); if(c.Has(CTX_VOL_PCT)) s += StringFormat(" | volatility %.0f%%ile", 100.0 * c.v[CTX_VOL_PCT]); if(c.Has(CTX_RVOL)) { const double v = c.v[CTX_RVOL]; s += StringFormat(" | volume %s for the hour", (v > 0.3 ? "heavy" : (v < -0.3 ? "light" : "normal"))); if(c.Has(CTX_ABSORB) && c.v[CTX_ABSORB] > 0.4) s += " with little progress (absorption)"; } if(c.Has(CTX_WYK_OK)) { const int ph = (int)MathAbs(c.v[CTX_WYK_PHASE]); const string character = (c.v[CTX_WYK_CHAR] > 0.0) ? "accumulation" : ((c.v[CTX_WYK_CHAR] < 0.0) ? "distribution" : "unanchored"); s += " | Wyckoff " + character + " phase " + ((ph >= 1 && ph <= 5) ? StringSubstr("-ABCDE", ph, 1) : "-"); if(c.Has(CTX_WYK_SPRING) && c.v[CTX_WYK_SPRING] != 0.0) s += StringFormat(", %s grade %.0f", (c.v[CTX_WYK_SPRING] > 0 ? "spring" : "upthrust"), MathAbs(c.v[CTX_WYK_SPRING])); if(c.Has(CTX_WYK_BIAS)) s += StringFormat(" (reads %+.2f for a long)", c.v[CTX_WYK_BIAS]); } if(c.Has(CTX_BREADTH)) s += StringFormat(" | %.0f%% of %d peer(s) dipping too", 100.0 * c.v[CTX_BREADTH], (int)c.v[CTX_BREADTH_N]); if(c.Has(CTX_DOW)) { s += StringFormat(" | %s %02d:00, %d day(s) to month end", StringSubstr("SunMonTueWedThuFriSat", 3 * (int)c.v[CTX_DOW], 3), (int)c.v[CTX_HOUR], (int)c.v[CTX_TO_MONTH_END]); } return s; } //+------------------------------------------------------------------+ bool CWarriorMind::Assess(const bool isLong, const double price, const double sl, string &why) { why = ""; m_pendingScale = 1.0; m_pendingReview.valid = false; m_pendingReview.p = CTX_NA; m_pendingReview.n = m_rev.Trades(); if(m_money != NULL) m_money.RiskScale(1.0); if(m_mode == MIND_OFF) return true; //--- LOOK. The cache holds closed bars only; its last bar is shift 1, the signal bar. if(!m_cache.Sync()) return true; SBarRef bar; bar.idx = m_cache.Last(); bar.shift = 1; bar.time = m_cache.Time(bar.idx); m_pending.Reset(bar.time); for(int i = 0; i < ArraySize(m_mods); i++) { m_mods[i].Read(bar, m_pending); } m_pending.story = Narrate(m_pending, isLong); m_assessed++; //--- RECALL and SIZE. In OBSERVE the review is computed and booked but never applied. m_pendingReview = m_rev.Review(m_pending); if(!m_pendingReview.valid) return true; const double raw = CBetSizer::RiskScale(m_pendingReview.p, m_pendingReview.p0); if(m_mode == MIND_OBSERVE) return true; if(raw <= 0.0 && m_mode == MIND_SIZE_GATE) { m_vetoed++; why = StringFormat("journal rates these conditions (%s) a coin flip: P(win) %.2f on %d trade(s)", m_pendingReview.bands, m_pendingReview.p, m_pendingReview.n); PrintFormat("CWarriorMind: STAND ASIDE - %s | %s", why, m_pending.story); return false; } m_pendingScale = MathMax(raw, m_minScale); if(m_pendingScale < 1.0) { m_scaled++; if(m_money != NULL) m_money.RiskScale(m_pendingScale); } return true; } //+------------------------------------------------------------------+ //| This chart's position - symbol AND magic. | //+------------------------------------------------------------------+ bool CWarriorMind::Holding(ulong &id, datetime &when, double &entry, double &lots, double &sl) const { for(int i = PositionsTotal() - 1; i >= 0; i--) { const ulong tk = PositionGetTicket(i); if(tk == 0 || PositionGetString(POSITION_SYMBOL) != m_symbol || (ulong)PositionGetInteger(POSITION_MAGIC) != m_magic) continue; id = (ulong)PositionGetInteger(POSITION_IDENTIFIER); when = (datetime)PositionGetInteger(POSITION_TIME); entry = PositionGetDouble(POSITION_PRICE_OPEN); lots = PositionGetDouble(POSITION_VOLUME); sl = PositionGetDouble(POSITION_SL); return true; } return false; } //+------------------------------------------------------------------+ void CWarriorMind::Opened(const bool isLong, const double sl) { if(m_mode == MIND_OFF) return; ulong id; datetime when; double entry, lots, psl; if(!Holding(id, when, entry, lots, psl)) return; m_open.position = id; m_open.closeTime = 0; m_open.exit = 0.0; m_open.net = 0.0; m_open.r = 0.0; m_open.maeR = 0.0; m_open.mfeR = 0.0; m_open.bars = 0; m_open.reason = ""; m_open.riskMoney = 0.0; m_open.openTime = when; m_open.side = isLong ? 1 : -1; m_open.entry = entry; m_open.sl = (psl > 0.0) ? psl : sl; m_open.lots = lots; double pl = 0.0; if(m_open.sl > 0.0 && OrderCalcProfit(isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, m_symbol, lots, entry, m_open.sl, pl)) m_open.riskMoney = MathAbs(pl); m_open.scale = m_pendingScale; m_open.reviewP = m_pendingReview.valid ? m_pendingReview.p : CTX_NA; m_open.reviewN = m_pendingReview.n; m_open.ctx = m_pending; m_have = true; m_finalizeTries = 0; } //+------------------------------------------------------------------+ void CWarriorMind::OnTick(void) { if(!m_have) return; //--- Asked by identifier, not by scanning and SELECTING every open position on every tick: a //--- scan changes which position the terminal has selected, and other code reads "the selected //--- position". PositionSelectByTicket keeps the question about ONE known position. if(PositionSelectByTicket(m_open.position)) return; Finalize(); } //+------------------------------------------------------------------+ //| The position is gone: read what it did from the broker's deals and | //| from the bars it lived through, then file it. | //+------------------------------------------------------------------+ void CWarriorMind::Finalize(void) { if(!HistorySelectByPosition(m_open.position)) { if(++m_finalizeTries > 50) m_have = false; return; } double net = 0.0, exitPx = 0.0; datetime exitTime = 0; long reason = -1; for(int i = 0; i < HistoryDealsTotal(); i++) { const ulong d = HistoryDealGetTicket(i); if(d == 0) continue; net += HistoryDealGetDouble(d, DEAL_PROFIT) + HistoryDealGetDouble(d, DEAL_SWAP) + HistoryDealGetDouble(d, DEAL_COMMISSION); if(HistoryDealGetInteger(d, DEAL_ENTRY) == DEAL_ENTRY_OUT) { exitPx = HistoryDealGetDouble(d, DEAL_PRICE); exitTime = (datetime)HistoryDealGetInteger(d, DEAL_TIME); reason = HistoryDealGetInteger(d, DEAL_REASON); } } if(exitTime == 0) { if(++m_finalizeTries > 50) m_have = false; // gave up: an unresolved trade is not evidence return; } m_open.closeTime = exitTime; m_open.exit = exitPx; m_open.net = net; m_open.reason = (reason == DEAL_REASON_SL ? "stop" : (reason == DEAL_REASON_SO ? "stopout" : "expert")); m_open.r = (m_open.riskMoney > 0.0) ? net / m_open.riskMoney : 0.0; //--- MAE / MFE from the bars, in R of the price distance to the stop. const double dist = MathAbs(m_open.entry - m_open.sl); MqlRates rates[]; const int got = (dist > 0.0) ? CopyRates(m_symbol, m_tf, m_open.openTime, exitTime, rates) : 0; if(got > 0) { double lo = rates[0].low, hi = rates[0].high; for(int i = 1; i < got; i++) { lo = MathMin(lo, rates[i].low); hi = MathMax(hi, rates[i].high); } m_open.bars = got; m_open.maeR = (m_open.side > 0 ? lo - m_open.entry : m_open.entry - hi) / dist; m_open.mfeR = (m_open.side > 0 ? hi - m_open.entry : m_open.entry - lo) / dist; m_open.maeR = MathMin(m_open.maeR, 0.0); m_open.mfeR = MathMax(m_open.mfeR, 0.0); } //--- FILE IT: the book for the record, the reviewer for the next decision. if(m_open.riskMoney > 0.0) m_rev.Add(m_open.ctx, m_open.r); m_book.Append(m_open); PrintFormat("CWarriorMind: closed %s %+.2fR (%s, %d bar(s), MAE %.2fR MFE %.2fR, risk x%.1f) | %s", m_symbol, m_open.r, m_open.reason, m_open.bars, m_open.maeR, m_open.mfeR, m_open.scale, m_open.ctx.story); m_have = false; } //+------------------------------------------------------------------+ void CWarriorMind::Report(void) { if(m_mode == MIND_OFF) return; PrintFormat("CWarriorMind: %s - %d setup(s) assessed, %d sized down, %d stood aside.", EnumToString(m_mode), m_assessed, m_scaled, m_vetoed); m_rev.Print(); double r[]; m_rev.Returns(r); if(ArraySize(r) >= 10) { double mean, sd, skew, kurt; if(CEdgeStats::Moments(r, mean, sd, skew, kurt)) PrintFormat("CWarriorMind: per-trade Sharpe %.3f, skew %.2f, kurtosis %.1f | PSR(0) %.3f |" " DSR at %d trial(s) %.3f (approximation: trial variance 1/(T-1); count the" " configurations you really tried)", mean / sd, skew, kurt, CEdgeStats::PSR(r, 0.0), m_trials, CEdgeStats::DSR(r, m_trials)); } } #endif // WARRIOR_MIND_MQH