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