255 lines
11 KiB
MQL5
255 lines
11 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| ContextPackager.mqh |
|
||
|
|
//| Centaur Quant Architecture — Data Module |
|
||
|
|
//| Historical Context & Swing Mapper (SDP-compatible) |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| PURPOSE |
|
||
|
|
//| Scans recent bars (any timeframe) for significant swing highs |
|
||
|
|
//| and swing lows. Fractal pivots are filtered by a dynamic ATR |
|
||
|
|
//| threshold supplied by CSymbolNormalizer, so noise rejection |
|
||
|
|
//| adapts to each instrument's volatility. Outputs parallel arrays |
|
||
|
|
//| (swing_high, swing_low, swing_time) directly consumable by |
|
||
|
|
//| CSDPEncoder::BuildHistoricalContext(). |
|
||
|
|
//| NOTE: the ATR threshold is sampled on the chart's current period |
|
||
|
|
//| (CSymbolNormalizer); when scanning a different timeframe, tune |
|
||
|
|
//| atr_multiplier to keep the threshold dimensionally consistent. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
#property strict
|
||
|
|
|
||
|
|
#ifndef CONTEXTPACKAGER_MQH
|
||
|
|
#define CONTEXTPACKAGER_MQH
|
||
|
|
|
||
|
|
#include "../Core/CSymbolNormalizer.mqh"
|
||
|
|
#include "../Network/CSDPEncoder.mqh"
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
class CContextPackager
|
||
|
|
{
|
||
|
|
private:
|
||
|
|
string m_symbol; // instrument whose structure is scanned
|
||
|
|
ENUM_TIMEFRAMES m_scan_period; // timeframe the swing scan runs on
|
||
|
|
string m_tf_name; // SDP wire name of m_scan_period
|
||
|
|
CSymbolNormalizer m_normalizer; // dynamic ATR threshold source
|
||
|
|
CSDPEncoder m_encoder; // SDP serializer
|
||
|
|
|
||
|
|
//--- detected swing bars (parallel arrays, oldest -> newest) ---
|
||
|
|
double m_swing_high[];
|
||
|
|
double m_swing_low[];
|
||
|
|
datetime m_swing_time[];
|
||
|
|
|
||
|
|
string PeriodName(const ENUM_TIMEFRAMES period);
|
||
|
|
void ResetSwings();
|
||
|
|
|
||
|
|
public:
|
||
|
|
CContextPackager(const string symbol, const ENUM_TIMEFRAMES scan_period = PERIOD_CURRENT);
|
||
|
|
~CContextPackager();
|
||
|
|
|
||
|
|
//--- scan recent bars and rebuild the swing arrays ---
|
||
|
|
bool Scan(const int bars_to_scan,
|
||
|
|
const int fractal_radius = 2,
|
||
|
|
const double atr_multiplier = 1.0,
|
||
|
|
const int atr_period = 14);
|
||
|
|
|
||
|
|
//--- serialize detected swings to the SDP historical_context array ---
|
||
|
|
string ContextJSON();
|
||
|
|
|
||
|
|
//--- read access ---
|
||
|
|
int SwingCount() const { return ArraySize(m_swing_time); }
|
||
|
|
bool GetSwing(const int index,
|
||
|
|
double &swing_high, double &swing_low,
|
||
|
|
datetime &swing_time) const;
|
||
|
|
string Symbol() const { return m_symbol; }
|
||
|
|
string Timeframe() const { return m_tf_name; }
|
||
|
|
};
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Constructor — bind symbol and resolve the scan period. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
CContextPackager::CContextPackager(const string symbol, const ENUM_TIMEFRAMES scan_period)
|
||
|
|
: m_symbol(symbol),
|
||
|
|
m_scan_period(scan_period),
|
||
|
|
m_tf_name(""),
|
||
|
|
m_normalizer(symbol),
|
||
|
|
m_encoder()
|
||
|
|
{
|
||
|
|
ENUM_TIMEFRAMES p = m_scan_period;
|
||
|
|
if(p == PERIOD_CURRENT)
|
||
|
|
p = (ENUM_TIMEFRAMES)_Period;
|
||
|
|
m_scan_period = p;
|
||
|
|
m_tf_name = PeriodName(p);
|
||
|
|
if(StringLen(m_symbol) == 0 || !m_normalizer.IsReady())
|
||
|
|
PrintFormat("[CContextPackager] ERROR: invalid symbol '%s' or normalizer not ready.", m_symbol);
|
||
|
|
else
|
||
|
|
PrintFormat("[CContextPackager] INFO: ready on %s %s.", m_symbol, m_tf_name);
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Destructor — nothing to release; present for symmetry. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
CContextPackager::~CContextPackager()
|
||
|
|
{
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Scan — detect significant swings over the recent window. |
|
||
|
|
//| 1) Dynamic noise threshold = ATR(period) * multiplier (normalizer).|
|
||
|
|
//| 2) Fractal pivot scan: a bar is a pivot when its extreme strictly |
|
||
|
|
//| dominates the +/-fractal_radius window. |
|
||
|
|
//| 3) Significance filter: pivots must ALTERNATE (high/low) and move |
|
||
|
|
//| at least one threshold from the previous accepted swing. |
|
||
|
|
//| Output arrays are chronological (oldest bar first). |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool CContextPackager::Scan(const int bars_to_scan,
|
||
|
|
const int fractal_radius,
|
||
|
|
const double atr_multiplier,
|
||
|
|
const int atr_period)
|
||
|
|
{
|
||
|
|
ResetSwings();
|
||
|
|
//--- parameter validation ---
|
||
|
|
if(StringLen(m_symbol) == 0)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] ERROR: no symbol configured.");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if(fractal_radius < 1)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] ERROR: fractal_radius must be >= 1 (got %d).", fractal_radius);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if(atr_multiplier <= 0.0)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] ERROR: atr_multiplier must be > 0 (got %G).", atr_multiplier);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
int bars = bars_to_scan;
|
||
|
|
if(bars > 10000)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] WARNING: bars_to_scan %d clamped to 10000.", bars);
|
||
|
|
bars = 10000;
|
||
|
|
}
|
||
|
|
const int min_bars = 2 * fractal_radius + 2;
|
||
|
|
if(bars < min_bars)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] ERROR: bars_to_scan %d < minimum %d.", bars, min_bars);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
//--- dynamic noise threshold from the normalizer ---
|
||
|
|
const double threshold = m_normalizer.GetATRBuffer(atr_period, atr_multiplier);
|
||
|
|
if(threshold <= 0.0)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] ERROR: ATR threshold unavailable for %s (warm-up?). Scan aborted.",
|
||
|
|
m_symbol);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
//--- copy the scan window; index 0 is the most recent bar ---
|
||
|
|
double high[], low[];
|
||
|
|
datetime time[];
|
||
|
|
const int got_high = CopyHigh(m_symbol, m_scan_period, 0, bars, high);
|
||
|
|
const int got_low = CopyLow(m_symbol, m_scan_period, 0, bars, low);
|
||
|
|
const int got_time = CopyTime(m_symbol, m_scan_period, 0, bars, time);
|
||
|
|
if(got_high < min_bars || got_low < min_bars || got_time < min_bars)
|
||
|
|
{
|
||
|
|
PrintFormat("[CContextPackager] ERROR: insufficient history for %s %s (need %d bars, got %d/%d/%d).",
|
||
|
|
m_symbol, m_tf_name, min_bars, got_high, got_low, got_time);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const int n = got_time; // all three arrays hold the same window in practice
|
||
|
|
//--- fractal pivot scan, oldest -> newest (chronological output) ---
|
||
|
|
bool have_last = false;
|
||
|
|
bool last_is_high = false;
|
||
|
|
double last_price = 0.0;
|
||
|
|
for(int i = n - 1 - fractal_radius; i >= fractal_radius; i--)
|
||
|
|
{
|
||
|
|
// pivot high: strict max over the surrounding window
|
||
|
|
bool is_high = true;
|
||
|
|
for(int k = i - fractal_radius; k <= i + fractal_radius && is_high; k++)
|
||
|
|
{
|
||
|
|
if(k != i && high[k] >= high[i])
|
||
|
|
is_high = false;
|
||
|
|
}
|
||
|
|
// pivot low: strict min over the surrounding window
|
||
|
|
bool is_low = true;
|
||
|
|
for(int k = i - fractal_radius; k <= i + fractal_radius && is_low; k++)
|
||
|
|
{
|
||
|
|
if(k != i && low[k] <= low[i])
|
||
|
|
is_low = false;
|
||
|
|
}
|
||
|
|
// significance: alternation + at least one threshold of travel
|
||
|
|
bool accept_high = is_high && (!have_last || !last_is_high);
|
||
|
|
if(accept_high && have_last && (high[i] - last_price) < threshold)
|
||
|
|
accept_high = false;
|
||
|
|
bool accept_low = is_low && (!have_last || last_is_high);
|
||
|
|
if(accept_low && have_last && (last_price - low[i]) < threshold)
|
||
|
|
accept_low = false;
|
||
|
|
if(accept_high || accept_low)
|
||
|
|
{
|
||
|
|
const int s = ArraySize(m_swing_time);
|
||
|
|
ArrayResize(m_swing_high, s + 1);
|
||
|
|
ArrayResize(m_swing_low, s + 1);
|
||
|
|
ArrayResize(m_swing_time, s + 1);
|
||
|
|
m_swing_high[s] = high[i];
|
||
|
|
m_swing_low[s] = low[i];
|
||
|
|
m_swing_time[s] = time[i];
|
||
|
|
if(accept_high)
|
||
|
|
{
|
||
|
|
last_is_high = true;
|
||
|
|
last_price = high[i];
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
last_is_high = false;
|
||
|
|
last_price = low[i];
|
||
|
|
}
|
||
|
|
have_last = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
PrintFormat("[CContextPackager] INFO: %s %s scan complete — %d significant swing(s), threshold %.5f.",
|
||
|
|
m_symbol, m_tf_name, ArraySize(m_swing_time), threshold);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| ContextJSON — SDP historical_context JSON array of the detected |
|
||
|
|
//| swings; "" when nothing was detected. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
string CContextPackager::ContextJSON()
|
||
|
|
{
|
||
|
|
if(ArraySize(m_swing_time) == 0)
|
||
|
|
return "";
|
||
|
|
return m_encoder.BuildHistoricalContext(m_symbol, m_swing_high, m_swing_low, m_swing_time);
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| GetSwing — copy one detected swing bar; false on bad index. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool CContextPackager::GetSwing(const int index,
|
||
|
|
double &swing_high, double &swing_low,
|
||
|
|
datetime &swing_time) const
|
||
|
|
{
|
||
|
|
if(index < 0 || index >= ArraySize(m_swing_time))
|
||
|
|
return false;
|
||
|
|
swing_high = m_swing_high[index];
|
||
|
|
swing_low = m_swing_low[index];
|
||
|
|
swing_time = m_swing_time[index];
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| ResetSwings — clear all detected swing arrays. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void CContextPackager::ResetSwings()
|
||
|
|
{
|
||
|
|
ArrayResize(m_swing_high, 0);
|
||
|
|
ArrayResize(m_swing_low, 0);
|
||
|
|
ArrayResize(m_swing_time, 0);
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| PeriodName — wire name of an ENUM_TIMEFRAMES value. |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
string CContextPackager::PeriodName(const ENUM_TIMEFRAMES period)
|
||
|
|
{
|
||
|
|
ENUM_TIMEFRAMES p = period;
|
||
|
|
if(p == PERIOD_CURRENT)
|
||
|
|
p = (ENUM_TIMEFRAMES)_Period;
|
||
|
|
string name = EnumToString(p); // e.g. "PERIOD_H1"
|
||
|
|
StringReplace(name, "PERIOD_", ""); // "H1"
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // CONTEXTPACKAGER_MQH
|