//+------------------------------------------------------------------+ //| CStructureAnalyzer.mqh | //| The brain that analyzes Market Structure (Swings & Trend) | //| v12.0 - Final Stable "Marking" Arch | //+------------------------------------------------------------------+ #ifndef CSTRUCTUREANALYZER_MQH #define CSTRUCTUREANALYZER_MQH #include "CZoneManager.mqh" #include "CEnumSharing.mqh" class CStructureAnalyzer { private: CZoneManager* m_zone_manager_ptr; double m_atr_noise_filter_multiplier; int m_max_bars_for_bos; bool m_use_trend_filter; ENUM_BOS_TYPE m_bos_confirmation_type; int m_pd_array_lookback; datetime m_last_daily_swing_promo_day; bool m_use_rsi_filter; int m_rsi_overbought; int m_rsi_oversold; double m_retest_min_atr_moveaway; ENUM_MARKET_STRUCTURE_STATE m_market_structure_state; bool m_use_volume_filter; bool m_filter_swings_by_fvg; bool m_filter_swings_by_sweep; ENUM_ZONE_QUALITY_LEVEL m_min_zone_quality; // --- Private Helper Functions --- void FilterSwingsByRSI(CArrayObj* swings_all, CDataContext &context) { if(!m_use_rsi_filter || swings_all == NULL) return; for(int i = 0; i < swings_all.Total(); i++) { SwingPoint* swing = swings_all.At(i); // กรองเฉพาะสวิงที่ยังไม่ถูกตัดสิน และไม่ใช่ Noise if(swing == NULL || swing.status != STATUS_UNCONFIRMED) continue; double rsi_value = context.rsi_buffer[swing.bar_index]; bool rsi_condition_met = false; if(swing.type == SWING_HIGH && rsi_value >= m_rsi_overbought) { rsi_condition_met = true; } else if(swing.type == SWING_LOW && rsi_value <= m_rsi_oversold) { rsi_condition_met = true; } // ถ้าไม่เข้าเงื่อนไข RSI ให้ตีตราเป็น NOISE ทันที if(!rsi_condition_met) { swing.status = STATUS_NOISE; } } } // เปลี่ยนหน้าที่เป็น "ตีตรา" Swing ที่เป็น Noise (ไม่ลบ) void MarkNoiseSwings(CArrayObj* swings_all, CDataContext &context) { if(swings_all == NULL || swings_all.Total() < 2) return; // วนลูปจากหลังมาหน้าเพื่อความปลอดภัยในการเปรียบเทียบ for(int i = swings_all.Total() - 1; i >= 1; i--) { SwingPoint *current_swing = swings_all.At(i); SwingPoint *prev_swing = swings_all.At(i - 1); if(current_swing == NULL || prev_swing == NULL) continue; // ถ้าอันใดอันหนึ่งถูกประมวลผลไปแล้ว ให้ข้ามไป if(current_swing.status != STATUS_UNCONFIRMED || prev_swing.status != STATUS_UNCONFIRMED) continue; double swing_length_price = MathAbs(current_swing.price - prev_swing.price); double atr_val = context.atr_buffer[current_swing.bar_index]; if(atr_val > 0 && swing_length_price < (atr_val * m_atr_noise_filter_multiplier)) { current_swing.status = STATUS_NOISE; prev_swing.status = STATUS_NOISE; } } } void ConfirmAndLinkSwings(CArrayObj* swings_all, CDataContext &context) { if(swings_all == NULL || swings_all.Total() < 2) return; for(int i = 0; i < swings_all.Total(); i++) { SwingPoint* current_swing = swings_all.At(i); if(current_swing == NULL || current_swing.status != STATUS_UNCONFIRMED) { continue; } ENUM_SWING_TYPE opposite_type = (current_swing.type == SWING_HIGH) ? SWING_LOW : SWING_HIGH; // *** FIXED: Changed variable name from all_swings to swings_all *** SwingPoint* prev_opposite_swing = GetPreviousSwing(i, opposite_type, swings_all); if(prev_opposite_swing == NULL) { current_swing.status = STATUS_MINOR; continue; } bool is_bos = false; if(current_swing.type == SWING_HIGH && current_swing.price < prev_opposite_swing.price) { // Bearish BOS is_bos = true; } else if (current_swing.type == SWING_LOW && current_swing.price > prev_opposite_swing.price) { // Bullish BOS is_bos = true; } if(is_bos) { current_swing.status = STATUS_MINOR; prev_opposite_swing.status = STATUS_INDUCEMENT; // *** FIXED: Changed variable name from all_swings to swings_all *** SwingPoint* origin_swing = GetPreviousSwing(i, current_swing.type, swings_all); if(origin_swing != NULL) { origin_swing.status = STATUS_MAJOR; // ใช้เวลาของ prev_opposite_swing เป็นจุดยืนยัน BOS ที่ชัดเจนกว่า origin_swing.bos_confirmation_time = prev_opposite_swing.time; } } else { current_swing.status = STATUS_MINOR; } } } bool CheckBOSAfterSwing(SwingPoint* candidate, int candidate_idx, CArrayObj* swings_all, CDataContext &context) { if(candidate == NULL) return false; if(candidate.type == SWING_LOW) { SwingPoint* prev_high = GetPreviousSwing(candidate_idx, SWING_HIGH, swings_all); if(prev_high == NULL) return false; for(int i = candidate.bar_index - 1; i >= 0 && i > candidate.bar_index - m_max_bars_for_bos; i--) { if(context.rates[i].close > prev_high.price) { candidate.bos_confirmation_time = context.rates[i].time; return true; } } } else if(candidate.type == SWING_HIGH) { SwingPoint* prev_low = GetPreviousSwing(candidate_idx, SWING_LOW, swings_all); if(prev_low == NULL) return false; for(int i = candidate.bar_index - 1; i >= 0 && i > candidate.bar_index - m_max_bars_for_bos; i--) { if(context.rates[i].close < prev_low.price) { candidate.bos_confirmation_time = context.rates[i].time; return true; } } } return false; } SwingPoint* GetPreviousSwing(int start_index, ENUM_SWING_TYPE type_to_find, CArrayObj* swings_all) { if(start_index <= 0) return NULL; for(int i = start_index - 1; i >= 0; i--) { SwingPoint *swing = swings_all.At(i); if(swing != NULL && swing.status != STATUS_NOISE && swing.type == type_to_find) return swing; } return NULL; } void AssociateZonesWithSwings(CArrayObj* swings_all) { if(CheckPointer(m_zone_manager_ptr) == POINTER_INVALID || CheckPointer(swings_all) == POINTER_INVALID) return; CArrayObj* all_zones = m_zone_manager_ptr.GetZoneList(); if(CheckPointer(all_zones) == POINTER_INVALID) return; for(int i = 0; i < all_zones.Total(); i++) { CSignalZone* zone = all_zones.At(i); if(zone == NULL || zone.origin_swing_ptr != NULL || zone.zone_type != ZONE_OB) continue; SwingPoint* closest_confirmed_swing = NULL; long min_time_diff = LONG_MAX; for(int j = 0; j < swings_all.Total(); j++) { SwingPoint* swing = swings_all.At(j); if(swing == NULL || (swing.status != STATUS_MAJOR && swing.status != STATUS_MINOR)) continue; if(swing.time < zone.time) { long time_diff = zone.time - swing.time; if(time_diff < min_time_diff) { min_time_diff = time_diff; closest_confirmed_swing = swing; } } } if(closest_confirmed_swing != NULL) { zone.origin_swing_ptr = closest_confirmed_swing; } } } bool IsZoneInList(CSignalZone* zone, CArrayObj* list) { if(zone == NULL || list == NULL) return false; for(int i=0; i < list.Total(); i++) { CSignalZone* z_in_list = list.At(i); if(z_in_list != NULL && z_in_list.zone_id == zone.zone_id) { return true; // เจอแล้ว } } return false; // ไม่เจอ } bool IsBosConfirmedFvg(CSignalZone* fvg_zone, CArrayObj* all_swings, CDataContext &context) { if(fvg_zone == NULL || fvg_zone.zone_type != ZONE_FVG || all_swings == NULL) return false; // 1. หา Swing ก่อนหน้าที่ใกล้ที่สุดกับ FVG เพื่อเป็นจุดอ้างอิงของโครงสร้าง SwingPoint* preceding_swing = NULL; int fvg_bar_index_start = iBarShift(_Symbol, PERIOD_CURRENT, fvg_zone.time); if(fvg_zone.direction == DIRECTION_BULLISH) { // Bullish FVG เกิดจากราคาพุ่งขึ้น, ต้องหา Swing High ก่อนหน้าเพื่อเบรค preceding_swing = GetPreviousSwing(fvg_bar_index_start, SWING_HIGH, all_swings); } else { // Bearish FVG เกิดจากราคาทิ้งตัวลง, ต้องหา Swing Low ก่อนหน้าเพื่อเบรค preceding_swing = GetPreviousSwing(fvg_bar_index_start, SWING_LOW, all_swings); } if(preceding_swing == NULL) return false; // ถ้าไม่มีสวิงก่อนหน้าให้อ้างอิง ก็ไม่นับ // 2. ตรวจสอบว่า "แท่งเทียนที่สร้าง FVG" (Displacement Candle) สามารถปิดทะลุ Swing ก่อนหน้านั้นได้หรือไม่ // โดยเราจะเช็คย้อนหลังไป 3 แท่งจากตัว FVG เผื่อกรณีที่การเคลื่อนที่รุนแรงมาก for(int k=0; k<3; k++) { int displacement_candle_index = fvg_bar_index_start + 1 + k; if(displacement_candle_index < 0 || displacement_candle_index >= context.bars_to_load) continue; MqlRates displacement_candle = context.rates[displacement_candle_index]; if(fvg_zone.direction == DIRECTION_BULLISH) { // การเคลื่อนที่ขึ้นที่ทรงพลัง ต้องปิดเหนือ Swing High ก่อนหน้าได้ if(displacement_candle.close > preceding_swing.price) return true; } else { // BEARISH // การเคลื่อนที่ลงที่ทรงพลัง ต้องปิดใต้ Swing Low ก่อนหน้าได้ if(displacement_candle.close < preceding_swing.price) return true; } } return false; } void InvalidateBrokenZones(CArrayObj* all_zones, CArrayObj* major_swings) { if(all_zones == NULL || major_swings == NULL || major_swings.Total() == 0) return; // หา Major Swing ล่าสุด SwingPoint* last_major_swing = major_swings.At(major_swings.Total() - 1); if(last_major_swing == NULL) return; for(int i = 0; i < all_zones.Total(); i++) { CSignalZone* zone = all_zones.At(i); if(zone == NULL || zone.state == STATE_INVALIDATED) continue; // โซนจะถูก Invalidate ถ้าเกิด Major Swing ใหม่ฝั่งตรงข้าม "หลัง" จากที่โซนเกิด if(last_major_swing.time > zone.time) { // ถ้าโซน Demand (Bullish) เจอกับ Major High ใหม่ . Invalidate if(zone.direction == DIRECTION_BULLISH && last_major_swing.type == SWING_HIGH) { zone.state = STATE_INVALIDATED; } // ถ้าโซน Supply (Bearish) เจอกับ Major Low ใหม่ . Invalidate else if(zone.direction == DIRECTION_BEARISH && last_major_swing.type == SWING_LOW) { zone.state = STATE_INVALIDATED; } } } } // นี่คือฟังก์ชันที่เราจะใช้เป็นตัวควบคุมคุณภาพโซนหลัก void GradeAllZones(CDataContext &context) { if(CheckPointer(m_zone_manager_ptr) == POINTER_INVALID) return; CArrayObj* all_zones = m_zone_manager_ptr.GetZoneList(); CArrayObj* all_swings = m_zone_manager_ptr.GetAllSwings(); if(all_zones == NULL || all_swings == NULL) return; // คำนวณ Premium/Discount Array double equilibrium = 0; int swings_to_check = MathMin(all_swings.Total(), m_pd_array_lookback); if(swings_to_check > 1) { double highest_high = 0; double lowest_low = DBL_MAX; int count = 0; for(int j = all_swings.Total() - 1; j >= 0 && count < swings_to_check; j--) { SwingPoint *s = all_swings.At(j); if(s == NULL || s.status == STATUS_NOISE) continue; if(s.price > highest_high) highest_high = s.price; if(s.price < lowest_low) lowest_low = s.price; count++; } if(highest_high > 0 && lowest_low < DBL_MAX) { equilibrium = lowest_low + (highest_high - lowest_low) / 2.0; } } // วนลูปโซน "ทุกตัว" เพื่อ "ให้เกรด" หรือ "ตีตรา" สถานะความสำคัญ for(int i = 0; i < all_zones.Total(); i++) { CSignalZone* zone = all_zones.At(i); if(zone == NULL) continue; // ถ้าโซนถูก Invalidate ไปแล้ว ให้ถือว่าไม่สำคัญทันที if(zone.state == STATE_INVALIDATED) { zone.is_significant = false; continue; } // --- เริ่มกระบวนการฟิลเตอร์ --- // Filter 1: Trend Alignment bool is_trend_aligned = false; if(zone.zone_type == ZONE_FLIP) { is_trend_aligned = true; // Flip zone เป็นข้อยกเว้น } else { switch(m_market_structure_state) { case MS_BULLISH: case MS_BULLISH_CHoCH: if(zone.direction == DIRECTION_BULLISH) is_trend_aligned = true; break; case MS_BEARISH: case MS_BEARISH_CHoCH: if(zone.direction == DIRECTION_BEARISH) is_trend_aligned = true; break; default: is_trend_aligned = true; break; } } // Filter 2: Premium/Discount Array bool is_in_pd_array = (equilibrium <= 0) || (zone.direction == DIRECTION_BULLISH && zone.top < equilibrium) || (zone.direction == DIRECTION_BEARISH && zone.bottom > equilibrium); // Filter 3: Structural Relevance bool passed_structure_filter = false; if(zone.origin_swing_ptr != NULL) { if(zone.zone_type == ZONE_FVG) { if(IsBosConfirmedFvg(zone, all_swings, context)) passed_structure_filter = true; } else { // OB และ FLIP passed_structure_filter = true; } } // ---- ตัดสินใจ "ตีตรา" is_significant ---- if(is_trend_aligned && is_in_pd_array && passed_structure_filter) { zone.is_significant = true; } else { zone.is_significant = false; } } } // +++ NEW FUNCTION to find and create Flip Zones +++ void CheckForFlipZones(CDataContext &context) { CArrayObj* all_zones = m_zone_manager_ptr.GetZoneList(); if(all_zones == NULL) return; // วนลูปดูโซนทั้งหมด for(int i = 0; i < all_zones.Total(); i++) { CSignalZone* zone = all_zones.At(i); // เราสนใจเฉพาะโซนที่เพิ่งถูก Invalidate ไป if(zone == NULL || zone.state != STATE_INVALIDATED || zone.zone_type == ZONE_FLIP) continue; // ตรวจสอบว่าแท่งเทียนที่ทำลายโซนนั้นเป็น Impulse Candle หรือไม่ // เราจะมองหาแท่งที่เพิ่งปิดล่าสุด (last_closed_bar = 1) int last_closed_bar = 1; double impulse_body = MathAbs(context.rates[last_closed_bar].close - context.rates[last_closed_bar].open); double atr_val = context.atr_buffer[last_closed_bar]; bool is_strong_break = (atr_val > 0 && impulse_body >= atr_val * 1.5); // ใช้ 1.5 เท่าของ ATR เป็นเกณฑ์ if(is_strong_break) { // ถ้าการทะลุแข็งแรงพอ ให้ "พลิก" โซนนี้ if(zone.direction == DIRECTION_BEARISH && context.rates[last_closed_bar].close > zone.top) // Supply ถูกทะลุขึ้น . กลายเป็น Demand { //PrintFormat("FLIP ZONE CREATED: Bearish zone at %s flipped to Bullish.", TimeToString(zone.time)); zone.direction = DIRECTION_BULLISH; zone.state = STATE_FRESH; // รีเซ็ตสถานะให้พร้อมใช้งาน zone.zone_type = ZONE_FLIP; zone.mitigation_count = 0; zone.trades_taken_count = 0; } else if(zone.direction == DIRECTION_BULLISH && context.rates[last_closed_bar].close < zone.bottom) // Demand ถูกทะลุลง . กลายเป็น Supply { //PrintFormat("FLIP ZONE CREATED: Bullish zone at %s flipped to Bearish.", TimeToString(zone.time)); zone.direction = DIRECTION_BEARISH; zone.state = STATE_FRESH; zone.zone_type = ZONE_FLIP; zone.mitigation_count = 0; zone.trades_taken_count = 0; } } } } // This function replaces GradeAllZones. It uses a stateless, re-evaluation model. void FilterSignificantZones(CDataContext &context) { // --- 1. เตรียมข้อมูล และสร้างลิสต์ชั่วคราว --- CArrayObj* all_zones = m_zone_manager_ptr.GetZoneList(); CArrayObj* all_swings = m_zone_manager_ptr.GetAllSwings(); CArrayObj* significant_zones_list = m_zone_manager_ptr.GetSignificantZones(); if(all_zones == NULL || all_swings == NULL || significant_zones_list == NULL) return; // สร้างลิสต์ชั่วคราว (Workbench) เพื่อพักผลลัพธ์ CArrayObj* temp_list = new CArrayObj(); temp_list.FreeMode(false); // --- 2. คำนวณ Premium/Discount Array --- double equilibrium = 0; int swings_to_check = MathMin(all_swings.Total(), m_pd_array_lookback); if(swings_to_check > 1) { double highest_high = 0; double lowest_low = DBL_MAX; int count = 0; for(int j = all_swings.Total() - 1; j >= 0 && count < swings_to_check; j--) { SwingPoint *s = all_swings.At(j); if(s == NULL || s.status == STATUS_NOISE) continue; if(s.price > highest_high) highest_high = s.price; if(s.price < lowest_low) lowest_low = s.price; count++; } if(highest_high > 0 && lowest_low < DBL_MAX) { equilibrium = lowest_low + (highest_high - lowest_low) / 2.0; } } // --- 3. วนลูปโซนทั้งหมดเพื่อคัดกรองลงในลิสต์ชั่วคราว --- for(int i = 0; i < all_zones.Total(); i++) { CSignalZone* zone = all_zones.At(i); // ด่านที่ A: ฟิลเตอร์พื้นฐาน if(zone == NULL || zone.state == STATE_INVALIDATED) continue; // ด่านที่ B: ฟิลเตอร์ Trend Alignment bool is_trend_aligned = false; if(zone.zone_type == ZONE_FLIP) { is_trend_aligned = true; } else { switch(m_market_structure_state) { case MS_BULLISH: case MS_BULLISH_CHoCH: if(zone.direction == DIRECTION_BULLISH) is_trend_aligned = true; break; case MS_BEARISH: case MS_BEARISH_CHoCH: if(zone.direction == DIRECTION_BEARISH) is_trend_aligned = true; break; default: is_trend_aligned = true; break; } } if(!is_trend_aligned) continue; // ด่านที่ C: ฟิลเตอร์ Premium/Discount Array bool is_in_pd_array = (equilibrium <= 0) || (zone.direction == DIRECTION_BULLISH && zone.top < equilibrium) || (zone.direction == DIRECTION_BEARISH && zone.bottom > equilibrium); if(!is_in_pd_array) continue; // ด่านที่ D: ฟิลเตอร์คุณภาพโซน (Zone Quality Dial) bool quality_passed = false; switch(m_min_zone_quality) { case ZQ_STANDARD: // คุณภาพมาตรฐาน: ขอแค่มีสวิงต้นกำเนิดก็พอ if(zone.origin_swing_ptr != NULL) quality_passed = true; break; case ZQ_BOS_CONFIRMED: // คุณภาพสูง: ถ้าเป็น FVG ต้องยืนยันด้วย BOS if(zone.origin_swing_ptr != NULL) { if(zone.zone_type == ZONE_OB || zone.zone_type == ZONE_FLIP || zone.zone_type == ZONE_BREAKOUT_RETEST) { quality_passed = true; } else if (zone.zone_type == ZONE_FVG) { if(IsBosConfirmedFvg(zone, all_swings, context)) { quality_passed = true; } } } break; } if(!quality_passed) continue; // ---- ถ้าโซนผ่านทุกด่าน ให้เพิ่มลงในลิสต์ชั่วคราว ---- temp_list.Add(zone); } // --- 4. Atomic Update: ล้างลิสต์เก่า แล้วนำข้อมูลจากลิสต์ชั่วคราวมาใส่แทน --- significant_zones_list.Clear(); for(int i = 0; i < temp_list.Total(); i++) { significant_zones_list.Add(temp_list.At(i)); } //PrintFormat("FilterSignificantZones Complete. Found %d significant zones.", significant_zones_list.Total()); // 5. ทำความสะอาด delete temp_list; } void PromoteDailySwings(CArrayObj* swings_all) { if(swings_all == NULL || swings_all.Total() == 0) return; datetime today = iTime(_Symbol, PERIOD_D1, 0); if(today == m_last_daily_swing_promo_day) return; // วันนี้ทำไปแล้ว ไม่ต้องทำซ้ำ datetime prev_day_start = iTime(_Symbol, PERIOD_D1, 1); datetime prev_day_end = today - 1; int prev_day_start_idx = iBarShift(_Symbol, PERIOD_CURRENT, prev_day_start); int prev_day_end_idx = iBarShift(_Symbol, PERIOD_CURRENT, prev_day_end); if(prev_day_start_idx < 0 || prev_day_end_idx < 0) return; // หา High/Low ของวันก่อนหน้า int high_idx = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, prev_day_start_idx - prev_day_end_idx + 1, prev_day_end_idx); int low_idx = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, prev_day_start_idx - prev_day_end_idx + 1, prev_day_end_idx); datetime pdh_time = iTime(_Symbol, PERIOD_CURRENT, high_idx); datetime pdl_time = iTime(_Symbol, PERIOD_CURRENT, low_idx); // ค้นหาสวิงที่ตรงกับ PDH/PDL แล้วโปรโมทเป็น Major for(int i=0; i prev_swing_of_same_type.price) { sweep_condition_met = true; } // Swing Low ใหม่ ต้องต่ำกว่า Swing Low เก่า else if(current_swing.type == SWING_LOW && current_swing.price < prev_swing_of_same_type.price) { sweep_condition_met = true; } // ถ้าไม่ผ่านเงื่อนไข Sweep ให้ตีตราเป็น NOISE if(!sweep_condition_met) { current_swing.status = STATUS_NOISE; } } else { // ถ้าเป็นสวิงแรกสุดที่หาตัวเปรียบเทียบไม่ได้ ก็อาจจะปล่อยผ่านไปก่อน // หรือจะคัดทิ้งเลยก็ได้เพื่อความเข้มงวด // current_swing.status = STATUS_NOISE; } } } // --- NEW HELPER: Grades swings based on whether they leave an FVG --- void GradeSwingsByDisplacement(CArrayObj* swings_all, CDataContext &context) { if(!m_filter_swings_by_fvg || swings_all == NULL) return; for(int i = 0; i < swings_all.Total(); i++) { SwingPoint* swing = swings_all.At(i); // กรองเฉพาะสวิงที่ยังไม่ถูกตัดสิน และไม่ใช่ Noise if(swing == NULL || swing.status != STATUS_UNCONFIRMED) continue; bool fvg_found = false; // มองหา FVG ใน "ขา" ที่พุ่งออกจากสวิง (ประมาณ 5 แท่ง) for(int j = 1; j <= 5; j++) { int fvg_middle_idx = swing.bar_index - j; if(fvg_middle_idx - 1 < 0 || fvg_middle_idx + 1 >= context.bars_to_load) continue; if(swing.type == SWING_LOW) { // มองหา Bullish FVG if(context.rates[fvg_middle_idx-1].low > context.rates[fvg_middle_idx+1].high) { fvg_found = true; break; } } else if(swing.type == SWING_HIGH) { // มองหา Bearish FVG if(context.rates[fvg_middle_idx-1].high < context.rates[fvg_middle_idx+1].low) { fvg_found = true; break; } } } // ถ้าหา FVG ใน leg ที่ออกจากสวิงไม่เจอ ให้ตีตราเป็น NOISE if(!fvg_found) { swing.status = STATUS_NOISE; } } } // --- UPDATED to prioritize MAJOR swings for structure analysis --- void UpdateMarketStructureState(CArrayObj* all_swings) { // --- 1. ค้นหา "นักแสดงหลัก" 4 ตัว (เฉพาะ Major Swings) --- if(all_swings == NULL) { m_market_structure_state = MS_UNDEFINED; return; } SwingPoint* H1 = NULL; // Major High ล่าสุด SwingPoint* H2 = NULL; // Major High ก่อนหน้า SwingPoint* L1 = NULL; // Major Low ล่าสุด SwingPoint* L2 = NULL; // Major Low ก่อนหน้า int highs_found = 0; int lows_found = 0; // วนลูปจากสวิงใหม่สุดไปหาเก่าสุด for(int i = all_swings.Total() - 1; i >= 0; i--) { SwingPoint* s = all_swings.At(i); // *** การเปลี่ยนแปลงที่สำคัญที่สุด: สนใจเฉพาะสวิงที่เป็น MAJOR เท่านั้น *** if(s == NULL || s.status != STATUS_MAJOR) continue; if(s == NULL) continue; if(s.type == SWING_HIGH) { if(highs_found == 0) H1 = s; else if(highs_found == 1) H2 = s; highs_found++; } else if(s.type == SWING_LOW) { if(lows_found == 0) L1 = s; else if(lows_found == 1) L2 = s; lows_found++; } if(highs_found >= 2 && lows_found >= 2) break; } // --- 2. ตรวจสอบความสมบูรณ์ของข้อมูล --- if(H1 == NULL || H2 == NULL || L1 == NULL || L2 == NULL) { m_market_structure_state = MS_UNDEFINED; // ไม่ต้อง Print ก็ได้ เพื่อไม่ให้ Log รกเกินไปตอนที่ยังไม่มี Major Swing ครบ return; } // --- 3. การวิเคราะห์โครงสร้าง (Logic เดิม แต่ใช้ข้อมูลที่ดีขึ้น) --- bool is_making_higher_highs = H1.price > H2.price; bool is_making_higher_lows = L1.price > L2.price; bool is_making_lower_lows = L1.price < L2.price; bool is_making_lower_highs = H1.price < H2.price; if(is_making_higher_highs && is_making_higher_lows) { m_market_structure_state = MS_BULLISH; } else if(is_making_lower_highs && is_making_lower_lows) { m_market_structure_state = MS_BEARISH; } else if(is_making_higher_highs && is_making_lower_lows) { m_market_structure_state = MS_BEARISH_CHoCH; } else if(is_making_lower_highs && is_making_higher_lows) { m_market_structure_state = MS_BULLISH_CHoCH; } else { m_market_structure_state = MS_UNDEFINED; } //PrintFormat("Market Structure State Updated: %s (Based on Major Swings)", EnumToString(m_market_structure_state)); } // +++ NEW: Function to update advanced confirmation flags for zones +++ void UpdateZoneConfirmationFlags(CArrayObj* all_zones, CArrayObj* all_swings, CDataContext &context, double min_atr_moveaway) { if(all_zones == NULL || all_swings == NULL) return; for(int i = 0; i < all_zones.Total(); i++) { CSignalZone* zone = all_zones.At(i); if(zone == NULL || zone.state == STATE_INVALIDATED) continue; // --- 1. ตรวจสอบ Displacement Confirmation --- if(!zone.has_displacement) { // หาแท่งเทียนที่อยู่หลังโซน int zone_idx = iBarShift(_Symbol, PERIOD_CURRENT, zone.time); for(int k = zone_idx - 1; k >= 0; k--) { double atr_val = context.atr_buffer[k]; double required_dist = atr_val * min_atr_moveaway; if(zone.direction == DIRECTION_BULLISH && context.rates[k].high - zone.top >= required_dist) { zone.has_displacement = true; break; } if(zone.direction == DIRECTION_BEARISH && zone.bottom - context.rates[k].low >= required_dist) { zone.has_displacement = true; break; } } } // --- 2. ตรวจสอบ Structure Confirmation --- if(!zone.has_structure_confirmation) { for(int k = 0; k < all_swings.Total(); k++) { SwingPoint* s = all_swings.At(k); // หา Minor/Major Swing ที่เกิดหลังโซน และเป็นฝั่งตรงข้าม if(s.time > zone.time && s.status >= STATUS_MINOR) { if((zone.direction == DIRECTION_BULLISH && s.type == SWING_HIGH) || (zone.direction == DIRECTION_BEARISH && s.type == SWING_LOW)) { zone.has_structure_confirmation = true; break; } } } } } } public: CStructureAnalyzer(CZoneManager* zone_manager, double atr_filter, int max_bos_bars, ENUM_BOS_TYPE bos_type, int pd_lookback, bool use_rsi, int rsi_period, int rsi_ob, int rsi_os, bool filter_fvg, bool filter_sweep, double retest_atr_mult, ENUM_ZONE_QUALITY_LEVEL min_quality) { m_zone_manager_ptr = zone_manager; m_atr_noise_filter_multiplier = atr_filter; m_max_bars_for_bos = max_bos_bars; m_bos_confirmation_type = bos_type; m_pd_array_lookback = pd_lookback; m_use_rsi_filter = use_rsi; m_rsi_overbought = rsi_ob; m_rsi_oversold = rsi_os; m_filter_swings_by_fvg = filter_fvg; m_filter_swings_by_sweep = filter_sweep; m_retest_min_atr_moveaway = retest_atr_mult; m_min_zone_quality = min_quality; m_market_structure_state = MS_UNDEFINED; } // This is the master function that orchestrates the entire analysis pipeline. void Analyze(CDataContext &context) { // --- 0. เตรียมข้อมูลเบื้องต้น --- if(m_zone_manager_ptr == NULL) return; CArrayObj* all_swings = m_zone_manager_ptr.GetAllSwings(); // This should be GetAllSwings() CArrayObj* all_zones = m_zone_manager_ptr.GetZoneList(); if(all_swings == NULL || all_zones == NULL) return; // ============================================================= // --- PIPELINE การวิเคราะห์โครงสร้างและโซน --- // ============================================================= // --- ส่วนที่ 1: การวิเคราะห์สวิง (Swing Analysis) --- // 1.1 โปรโมทสวิงสำคัญจากโครงสร้างรายวันก่อนเสมอ PromoteDailySwings(all_swings); // 1.2 กรองสวิงคุณภาพต่ำ (Noise) ด้วยขนาดเทียบกับ ATR MarkNoiseSwings(all_swings, context); // 1.3 กรองสวิงคุณภาพต่ำด้วย Indicator เพิ่มเติม (ถ้าเปิดใช้งาน) FilterSwingsByRSI(all_swings, context); // 1.4 กรองสวิงที่ไม่มีแรงส่ง (ไม่มี FVG ตามมา) GradeSwingsByDisplacement(all_swings, context); // 1.5 กรองสวิงที่ไม่ได้กวาดสภาพคล่อง GradeSwingsByLiquiditySweep(all_swings); // 1.6 วิเคราะห์ความสัมพันธ์และยืนยันสถานะสวิง (Major/Minor/Inducement) จากการเกิด BOS ConfirmAndLinkSwings(all_swings, context); // 1.7 จัดเรียงสวิงที่ผ่านการกรองแล้วลงในลิสต์ย่อย m_zone_manager_ptr.RepopulateFilteredSwingLists(); // 1.8 สรุปโครงสร้างตลาดโดยรวม (HH/HL/LH/LL) จาก Major Swings UpdateMarketStructureState(all_swings); // --- ส่วนที่ 2: การวิเคราะห์โซน (Zone Analysis) --- // 2.1 อัปเดตสถานะพื้นฐานของโซน (Fresh . Active . Mitigated) m_zone_manager_ptr.UpdateAllZoneStates(context); // 2.2 ตรวจหาและสร้าง Flip Zone จากโซนที่เพิ่งถูกทำลาย CheckForFlipZones(context); // 2.3 ให้เกรดโซนทั้งหมด (ตีตรา is_significant) โดยอิงกับข้อมูลล่าสุดทั้งหมด UpdateZoneConfirmationFlags(all_zones, all_swings, context, m_retest_min_atr_moveaway); FilterSignificantZones(context); GradeAllZones(context); // 2.4 ทำลายโซนอย่างถาวรตามโครงสร้างหลัก (ขั้นตอนสุดท้ายเพื่อความปลอดภัย) InvalidateBrokenZones(all_zones, m_zone_manager_ptr.GetMajorSwings()); //Print("--- Analysis pipeline for this bar completed. ---"); } ENUM_MARKET_STRUCTURE_STATE GetMarketStructureState() { return m_market_structure_state; } }; #endif