forked from mnbvc188199/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.
194 lines
12 KiB
MQL5
194 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| WarriorSignal.mqh |
| |
//| AnimateDread |
| |
//| |
| |
//| THE THIN BASE. Everything a Warrior signal module needs on top of |
| |
//| the standard library, and nothing else. |
| |
//| |
| |
//| CExpertSignalCustom grew to ~4,000 lines because the vote, the |
| |
//| journal, the chart overlay, the arming machinery, the meta-label |
| |
//| and the per-setup order shaping all ended up inside the class the |
| |
//| indicator modules inherit. A module that only wants to say "RSI |
| |
//| is oversold, weight 30" paid for all of it, and could not be read |
| |
//| without reading all of it. |
| |
//| |
| |
//| Measured against the modules themselves (2026-09-11): beyond what |
| |
//| CExpertSignal and CExpertBase already give them - m_symbol, |
| |
//| m_period, m_used_series, m_patterns_usage, m_base_price, |
| |
//| StartIndex(), PriceHigh/Low/Open/Close - every classic module in |
| |
//| this repo uses exactly FOUR members of the old base. They are the |
| |
//| four below. That is the whole reason this file can exist. |
| |
//| |
| |
//| SO A MODULE IS JUST A CExpertSignal. It implements LongCondition |
| |
//| and ShortCondition returning 0..100, and the standard library |
| |
//| does the rest: Direction() weights it, the threshold decides, and |
| |
//| CExpert places the order. Exactly the shape MetaEditor's wizard |
| |
//| generates - which is the point. |
| |
//+------------------------------------------------------------------+
| |
#ifndef WARRIOR_SIMPLE_SIGNAL_MQH
| |
#define WARRIOR_SIMPLE_SIGNAL_MQH
| |
| |
#include <Expert\ExpertSignal.mqh>
| |
#include "..\System\RegimeMath.mqh"
| |
| |
class CWarriorSignal : public CExpertSignal
| |
{
| |
protected:
| |
//--- SHORT, STABLE, AND THE DATABASE KEY. Rows are grouped by this, so renaming one silently
| |
//--- splits a module's history in two. Treat it as an identifier, not a label.
| |
string m_id;
| |
//--- WHAT MATCHED ON THIS EVALUATION, for the journal. Set by the module inside LongCondition()/
| |
//--- ShortCondition(); read by the parent immediately after the call and then cleared. A module
| |
//--- that never sets it still votes - it simply records the vote without naming a pattern.
| |
string m_active_pattern;
| |
string m_active_direction;
| |
//--- How many distinct patterns this module can express. The database keeps one row group per
| |
//--- pattern, and the ranking averages within a pattern rather than across a module, because
| |
//--- "RSI" is not one behaviour - an oversold bounce and a divergence are different claims.
| |
int m_pattern_count;
| |
//--- CONFIRMATION PATTERNS - bit i set means pattern i is one. Bit, not a list, so the test is
| |
//--- free on the hot path where every firing is checked.
| |
//---
| |
//--- A confirmation pattern answers "is price on the bullish side of this indicator" rather than
| |
//--- "has something happened". It is true on roughly every bar, ships at weight 10 because it
| |
//--- must never trade on its own, and it is POISON to the ranking layer for two separate
| |
//--- reasons. It floods the journal - measured, CCI_Pattern_0_Sell alone held 1,139 rows against
| |
//--- 107 for a real trigger - and, being close to a coin flip, the database re-weights it from
| |
//--- its authored 10 up to ~50, a five-fold amplification of the one pattern that was
| |
//--- deliberately made quiet. So these are neither recorded nor re-weighted: they keep the
| |
//--- weight their author gave them, and the database never sees them.
| |
uint m_confirm_mask;
| |
| |
public:
| |
CWarriorSignal(void) : m_id("?"), m_active_pattern(""),
| |
m_active_direction(""), m_pattern_count(0),
| |
m_confirm_mask(0) {}
| |
~CWarriorSignal(void) {}
| |
| |
string FilterID(void) const { return m_id; }
| |
int PatternCount(void) const { return m_pattern_count; }
| |
bool IsConfirmation(const int p) const
| |
{ return (p >= 0 && p < 32 && ((m_confirm_mask >> p) & 1) != 0); }
| |
//--- "Pattern_7" -> 7, and -1 for anything that is not a pattern name. The journal and the
| |
//--- ranking both need the INDEX, and the only thing a module publishes is the string.
| |
static int PatternIndex(const string name)
| |
{
| |
if(StringLen(name) <= 8 || StringSubstr(name, 0, 8) != "Pattern_")
| |
return -1;
| |
return (int)StringToInteger(StringSubstr(name, 8));
| |
}
| |
string ActivePattern(void) const { return m_active_pattern; }
| |
string ActiveDirection(void) const { return m_active_direction; }
| |
void ClearActive(void) { m_active_pattern = ""; m_active_direction = ""; }
| |
| |
//--- THE ONE HOOK THE DATABASE NEEDS. The ranking layer calls this to hand a module the weight it
| |
//--- earned for one of its patterns. Default does nothing, so a module that does not want to be
| |
//--- ranked simply does not override it - it keeps its fixed prior and stays in the vote.
| |
//--- Signature matches the ~20 existing modules exactly (no `const` on the value parameters).
| |
//--- MQL5 treats a const-qualified parameter as a different signature for override purposes and
| |
//--- warns rather than errors, so a mismatch here would leave every module's override silently
| |
//--- attached to a slightly different function.
| |
virtual void ApplyPatternWeight(int pattern, int weight) { }
| |
| |
//--- A SETUP THAT OWNS ITS STOP. The vote's default stop is StopMode x iATR from the entry price,
| |
//--- one rule for every module. A setup whose edge was MEASURED with a specific stop - the dip-buy
| |
//--- was priced with 3 x WILDER ATR of the signal bar, anchored to the bid, and iATR is an SMA of
| |
//--- the true range, so the two differ on every trade - answers here with that stop, and the vote
| |
//--- places it instead. Default: no opinion. Only asked of a module whose vote is opening the trade.
| |
virtual bool SetupStop(const bool isLong, double &sl) { return false; }
| |
| |
//--- GROW THE SHARED PRICE SERIES PAST THE STANDARD LIBRARY'S 1024-BAR CEILING.
| |
//---
| |
//--- CSeries allocates DEFAULT_BUFFER_SIZE = 1024 bars (Include\Indicators\Series.mqh:11), so
| |
//--- Close(shift) reads 0.0 for any shift past 1023 - SILENTLY. There is no error and no empty
| |
//--- value to test; a module that walks history just stops finding data, and whatever it was
| |
//--- building comes out short. Measured 2026-09-11: the neural module reported "915 rows" on
| |
//--- every one of its four training events across an 11-year run, on 3,029 bars of history.
| |
//--- 915 is not a coincidence or a window policy - it is 974 (the deepest shift whose 50-bar
| |
//--- SMA lookback still lands inside 1023) minus the module's 60-bar floor, plus one. The
| |
//--- stdlib default was choosing the training set, and through it the model's width.
| |
//---
| |
//--- ONLY EVER ASK FOR BARS THAT ALREADY EXIST. CSeries::BufferResize() loads history and
| |
//--- printf()s a failure line when it cannot supply the request, so passing an optimistic
| |
//--- constant here buys a log full of noise. Callers pass min(want, Bars()) - which in the
| |
//--- tester is history-so-far, so growing the buffer cannot reach past the bar being decided.
| |
//---
| |
//--- Grows only. The stdlib's own BufferResize() would happily SHRINK a buffer (its size>
| |
//--- guard covers the history load, not the buff.Size() call underneath), which would quietly
| |
//--- truncate a series another module is mid-way through reading.
| |
bool DeepenPrices(const int size);
| |
| |
//--- WHICH GAME IS BEING PLAYED, on the shared base because TWO things need it and they must
| |
//--- agree: CSignalRegime votes on it, and CWarriorVote stamps it onto every journal row so a
| |
//--- pattern's record can be split by the conditions it was earned in. Two copies of this would
| |
//--- eventually disagree, and the disagreement would look like a regime change in the data.
| |
//---
| |
//--- EFFICIENCY RATIO - net distance over path walked. 1 = a straight line, 0 = thrash that ends
| |
//--- where it began. Needs no volatility estimate: a ratio of two distances in the same units
| |
//--- cancels the instrument, so one threshold means the same thing on gold and on EURUSD.
| |
double EfficiencyRatio(const int shift, const int period) const;
| |
//--- VARIANCE RATIO - Var(q-bar) / (q * Var(1-bar)). A random walk gives exactly 1.0 because
| |
//--- variance scales with time. Above 1 the moves compound (trend); below 1 they cancel (mean
| |
//--- reversion). Sharper than ER: ER says "is there a trend", VR says "does this CONTINUE".
| |
double VarianceRatio(const int shift, const int period, const int q) const;
| |
//--- The coarse label those two agree on. Deliberately few: split the journal too finely and
| |
//--- every cell falls under the sample floor, which is the same as having no ranking at all.
| |
int RegimeCode(const int shift) const;
| |
};
| |
//+------------------------------------------------------------------+
| |
//--- The arithmetic lives in System\RegimeMath.mqh; this only gathers the closes, newest first.
| |
double CWarriorSignal::EfficiencyRatio(const int shift, const int period) const
| |
{
| |
double x[];
| |
ArrayResize(x, period + 1);
| |
for(int k = 0; k <= period; k++)
| |
x[k] = Close(shift + k);
| |
return CRegimeMath::Efficiency(x, period);
| |
}
| |
//+------------------------------------------------------------------+
| |
double CWarriorSignal::VarianceRatio(const int shift, const int period, const int q) const
| |
{
| |
double x[];
| |
ArrayResize(x, period + 1);
| |
for(int k = 0; k <= period; k++)
| |
x[k] = Close(shift + k);
| |
return CRegimeMath::Variance(x, period, q);
| |
}
| |
//+------------------------------------------------------------------+
| |
//| 0 = consolidation, 1 = trending, 2 = mean reverting. |
| |
//| |
| |
//| THREE, NOT MORE. Every extra regime divides the journal again, and |
| |
//| a pattern needs WARRIOR_MIN_FIRINGS observations per cell before |
| |
//| it is ranked at all - so a finer split does not produce a sharper |
| |
//| estimate, it produces no estimate. Three is what ~15k firings per |
| |
//| symbol can actually support. |
| |
//+------------------------------------------------------------------+
| |
int CWarriorSignal::RegimeCode(const int shift) const
| |
{
| |
return CRegimeMath::Code(EfficiencyRatio(shift, 20), VarianceRatio(shift, 60, 5));
| |
}
| |
//+------------------------------------------------------------------+
| |
bool CWarriorSignal::DeepenPrices(const int size)
| |
{
| |
if(size <= 0)
| |
return false;
| |
bool ok = true;
| |
if(CheckPointer(m_open) != POINTER_INVALID && size > m_open.BufferSize())
| |
ok = m_open.BufferResize(size) && ok;
| |
if(CheckPointer(m_high) != POINTER_INVALID && size > m_high.BufferSize())
| |
ok = m_high.BufferResize(size) && ok;
| |
if(CheckPointer(m_low) != POINTER_INVALID && size > m_low.BufferSize())
| |
ok = m_low.BufferResize(size) && ok;
| |
if(CheckPointer(m_close) != POINTER_INVALID && size > m_close.BufferSize())
| |
ok = m_close.BufferResize(size) && ok;
| |
//--- Tick volume is a CSeries too and carries the SAME 1024-bar ceiling. Left shallow it would
| |
//--- read 0 past shift 1023, and a zero volume denominator makes BuildFeatures refuse the row -
| |
//--- so the training set would silently stop at 1024 bars again, by a different route.
| |
if(CheckPointer(m_tick_volume) != POINTER_INVALID && size > m_tick_volume.BufferSize())
| |
ok = m_tick_volume.BufferResize(size) && ok;
| |
//--- Time is a CSeries with the same ceiling; a calendar feature read past it is a 1970 date.
| |
if(CheckPointer(m_time) != POINTER_INVALID && size > m_time.BufferSize())
| |
ok = m_time.BufferResize(size) && ok;
| |
return ok;
| |
}
| |
#endif // WARRIOR_SIMPLE_SIGNAL_MQH
|