//+------------------------------------------------------------------+ //| JournalReviewer.mqh | //| AnimateDread | //| | //| THE WEEKLY REVIEW A DISCIPLINED TRADER DOES WITH THEIR JOURNAL. | //| | //| Every closed trade arrives with the context it was taken in and | //| what it paid (in R). The reviewer files it under a small number of | //| coarse conditions - how volatile, how much volume for the hour, | //| whether the Wyckoff structure agreed, whether the other indices | //| were selling off too - and, when a new setup appears, answers | //| "in the conditions this looks like, how have my trades done?" | //| | //| WHY IT IS BUILT TO BE TIMID. | //| * ADDITIVE, NOT A LOOK-UP OF THE FULL COMBINATION. Four bands of | //| three is 81 cells; the book makes ~650 trades in five years. | //| Each dimension is estimated on its own and the deviations are | //| AVERAGED - this ignores interactions (the memory of this repo | //| is that the edge lives in combinations) and pays for it in | //| honesty: it can be wrong about a joint effect, it cannot be | //| precise about one it lacks the data for. | //| * SHRUNK. A band's deviation from the book average counts for | //| n / (n + k) of its size: 30 trades at k = 20 is 60% believed. | //| * SILENT until MinTrades have closed, and CAUSAL by construction: | //| a trade is added only when it closes, so an estimate can never | //| include a trade that is still open or lies in the future. | //| * ONE RUN, NO MEMORY ACROSS RUNS in the tester (the book decides | //| that, not this class): a persisted verdict would leak one | //| backtest into the next. | //| | //| The output is a shrunk P(win) and its support (how many trades | //| stand behind it). CBetSizer turns that into a size. | //+------------------------------------------------------------------+ #ifndef WARRIOR_JOURNALREVIEWER_MQH #define WARRIOR_JOURNALREVIEWER_MQH #include "MarketContext.mqh" #define REVIEW_DIMS 4 #define REVIEW_BANDS 3 //--- What the reviewer concluded for one prospective trade. struct SReview { bool valid; // false = too few trades to say anything double p; // shrunk P(win) for these conditions double p0; // the book's average P(win) int n; // trades in the book int support; // trades in the LEAST-populated band that contributed string bands; // "vol=hi volume=mid wyckoff=lo breadth=hi", for the log }; class CJournalReviewer { protected: int m_k; // shrinkage strength int m_minTrades; int m_n, m_wins; double m_sumR; int m_cnt[REVIEW_DIMS][REVIEW_BANDS]; int m_win[REVIEW_DIMS][REVIEW_BANDS]; double m_r[]; // every R, for the edge statistics //--- The dimension table: which context field, and the two edges that cut it into low/mid/high. //--- Edges are fixed, not fitted - fitting them to the book would be one more free parameter //--- chosen on the very trades it then judges. static int FieldOf(const int d) { switch(d) { case 0: return CTX_VOL_PCT; case 1: return CTX_RVOL; case 2: return CTX_WYK_BIAS; default: return CTX_BREADTH; } } static void EdgesOf(const int d, double &lo, double &hi) { switch(d) { case 0: lo = 0.70; hi = 0.85; break; // the gate already demands >= 0.50 case 1: lo = -0.15; hi = 0.15; break; // ln volume vs the hour's norm, detrended case 2: lo = -0.25; hi = 0.25; break; // the reader's verdict default: lo = 0.34; hi = 0.67; break; // share of peers dipping too } } static string DimName(const int d) { switch(d) { case 0: return "vol"; case 1: return "volume"; case 2: return "wyckoff"; default: return "breadth"; } } //--- 0 low, 1 mid, 2 high. A reading that is absent files under MID: unknown must not push //--- an estimate either way. int BandOf(const int d, const SMarketContext &c) const { const double x = c.v[FieldOf(d)]; if(x == CTX_NA) return 1; double lo, hi; EdgesOf(d, lo, hi); return (x < lo) ? 0 : ((x > hi) ? 2 : 1); } public: CJournalReviewer(void) : m_k(20), m_minTrades(40), m_n(0), m_wins(0), m_sumR(0.0) { ArrayInitialize(m_cnt, 0); ArrayInitialize(m_win, 0); } void Configure(const int minTrades, const int shrink) { m_minTrades = MathMax(minTrades, 10); m_k = MathMax(shrink, 1); } int Trades(void) const { return m_n; } double MeanR(void) const { return (m_n > 0) ? m_sumR / m_n : 0.0; } double WinRate(void) const { return (m_n > 0) ? (double)m_wins / m_n : 0.0; } void Returns(double &out[]) const { ArrayCopy(out, m_r); } //--- File one closed trade. void Add(const SMarketContext &c, const double r) { const bool win = (r > 0.0); m_n++; m_sumR += r; if(win) m_wins++; for(int d = 0; d < REVIEW_DIMS; d++) { const int b = BandOf(d, c); m_cnt[d][b]++; if(win) m_win[d][b]++; } const int k = ArraySize(m_r); ArrayResize(m_r, k + 1, 256); m_r[k] = r; } SReview Review(const SMarketContext &c) const { SReview v; v.valid = false; v.n = m_n; v.p0 = WinRate(); v.p = v.p0; v.support = 0; v.bands = ""; if(m_n < m_minTrades) return v; double dev = 0.0; int used = 0, minSupport = m_n; for(int d = 0; d < REVIEW_DIMS; d++) { const int b = BandOf(d, c); const int nb = m_cnt[d][b]; const string tag = (b == 0 ? "lo" : (b == 1 ? "mid" : "hi")); v.bands += (d > 0 ? " " : "") + DimName(d) + "=" + tag; if(nb == 0) continue; const double w = (double)nb / (nb + m_k); dev += w * ((double)m_win[d][b] / nb - v.p0); used++; minSupport = MathMin(minSupport, nb); } if(used == 0) return v; v.valid = true; v.p = MathMax(0.05, MathMin(0.95, v.p0 + dev / used)); v.support = minSupport; return v; } //--- The review, as a table a person can read at the end of a run. void Print(void) const { if(m_n == 0) { ::Print("CJournalReviewer: no closed trades."); return; } PrintFormat("CJournalReviewer: %d trade(s), win rate %.1f%%, mean R %+.3f.", m_n, 100.0 * WinRate(), MeanR()); for(int d = 0; d < REVIEW_DIMS; d++) { double lo, hi; EdgesOf(d, lo, hi); string line = " " + DimName(d) + " (" + CtxName(FieldOf(d)) + "; cuts " + DoubleToString(lo, 2) + " / " + DoubleToString(hi, 2) + "):"; for(int b = 0; b < REVIEW_BANDS; b++) { const int nb = m_cnt[d][b]; line += StringFormat(" %s n=%d win=%s", (b == 0 ? "lo" : (b == 1 ? "mid" : "hi")), nb, (nb > 0 ? DoubleToString(100.0 * m_win[d][b] / nb, 0) + "%" : "-")); } ::Print(line); } } }; #endif // WARRIOR_JOURNALREVIEWER_MQH