417 行
17 KiB
MQL5
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
|