//+------------------------------------------------------------------+ //| 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