forked from animatedread/Warrior_EA
203 lines
8 KiB
MQL5
203 lines
8 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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
|