EA_SMC_Mql5/Include/CStructureAnalyzer.mqh
2025-07-03 23:37:10 +07:00

812 lines
70 KiB
MQL5

//+------------------------------------------------------------------+
//| 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<swings_all.Total(); i++)
{
SwingPoint *s = swings_all.At(i);
if(s == NULL) continue;
if(s.time == pdh_time && s.type == SWING_HIGH){
s.status = STATUS_MAJOR;
//PrintFormat("PROMOTED: Previous Day High at %s to MAJOR SWING.", TimeToString(s.time));
}
if(s.time == pdl_time && s.type == SWING_LOW){
s.status = STATUS_MAJOR;
//PrintFormat("PROMOTED: Previous Day Low at %s to MAJOR SWING.", TimeToString(s.time));
}
}
m_last_daily_swing_promo_day = today; // บันทึกว่าวันนี้ทำแล้ว
}
// --- NEW HELPER: Grades swings based on whether they swept liquidity ---
void GradeSwingsByLiquiditySweep(CArrayObj* swings_all)
{
if(!m_filter_swings_by_sweep || swings_all == NULL) 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;
// มองหาย้อนหลังเพื่อหา prev_swing ประเภทเดียวกัน
SwingPoint* prev_swing_of_same_type = GetPreviousSwing(i, current_swing.type, swings_all);
if(prev_swing_of_same_type != NULL)
{
bool sweep_condition_met = false;
// Swing High ใหม่ ต้องสูงกว่า Swing High เก่า
if(current_swing.type == SWING_HIGH && current_swing.price > 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