| //+------------------------------------------------------------------+
|
| //| 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
|