forked from animatedread/Warrior_EA
- 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.
417 lines
17 KiB
MQL5
417 lines
17 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|