forked from ntsinh90/EA_SMC_Mql5
812 lines
70 KiB
MQL5
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
|
|
|