Warrior_EA/Mind/JournalReviewer.mqh
AnimateDread e9c562b39f Add Feature Scaling and Regime Math Classes; Implement Mind Trading Logic
- Introduced `FeatureScale.mqh` with `FeatSquash` function for stateless feature scaling.
- Added `RegimeMath.mqh` class for regime arithmetic, including efficiency and variance calculations.
- Documented the Mind trading logic in `MIND.md`, detailing the trading process and modes.
- Created `VOLNORM_PLAN.md` and `VOLNORM_RESULTS.md` for tick-volume normalization testing.
- Implemented `read_book.py` for analyzing trade book data and correlations.
- Developed `volnorm.py` for testing tick-volume normalization with new and old methods.
2026-09-30 18:36:33 -04:00

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