Warrior_EA/Mind/WarriorMind.mqh

417 行
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