deploy: 2026-03-20 07:49
This commit is contained in:
173
indicators/confluence.py
Normal file
173
indicators/confluence.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Confluence checker for ICT signals.
|
||||
|
||||
Evaluates whether enough ICT conditions align (minimum 3 out of 6)
|
||||
to produce a valid trade signal.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from config import settings
|
||||
from indicators.ict_engine import ICTSignals
|
||||
from indicators.multi_timeframe import (
|
||||
MarketBias,
|
||||
MTFAnalysis,
|
||||
TradeDirection,
|
||||
TradeZone,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConditionResult:
|
||||
"""Result for a single confluence condition."""
|
||||
|
||||
name: str
|
||||
met: bool
|
||||
detail: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfluenceResult:
|
||||
"""Aggregated confluence evaluation."""
|
||||
|
||||
score: int
|
||||
conditions: List[ConditionResult] = field(default_factory=list)
|
||||
is_valid: bool = False
|
||||
direction: TradeDirection = TradeDirection.NONE
|
||||
|
||||
def summary(self) -> str:
|
||||
met = [c.name for c in self.conditions if c.met]
|
||||
return f"Score {self.score}/6 ({', '.join(met)})"
|
||||
|
||||
|
||||
class ConfluenceChecker:
|
||||
"""Check if enough ICT conditions align for a trade entry.
|
||||
|
||||
Six conditions are evaluated:
|
||||
1. Market Structure (HTF bias alignment)
|
||||
2. Liquidity Sweep (sweep then reversal)
|
||||
3. Order Block (price in OB zone)
|
||||
4. Fair Value Gap (price in FVG)
|
||||
5. BOS (break of structure confirmation)
|
||||
6. CHOCH (change of character confirmation)
|
||||
"""
|
||||
|
||||
MIN_CONFLUENCE = settings.MIN_CONFLUENCE_SCORE
|
||||
|
||||
def check(
|
||||
self,
|
||||
mtf: MTFAnalysis,
|
||||
current_price: float,
|
||||
htf_signals: ICTSignals | None = None,
|
||||
mtf_signals: ICTSignals | None = None,
|
||||
ltf_signals: ICTSignals | None = None,
|
||||
) -> ConfluenceResult:
|
||||
"""Evaluate all six conditions and return the result."""
|
||||
conditions: List[ConditionResult] = []
|
||||
bias = mtf.htf_bias
|
||||
target = 1 if bias == MarketBias.BULLISH else (-1 if bias == MarketBias.BEARISH else 0)
|
||||
|
||||
# 1. Market Structure
|
||||
ms_met = bias != MarketBias.NEUTRAL
|
||||
conditions.append(
|
||||
ConditionResult("Market Structure", ms_met, f"HTF bias: {bias.value}")
|
||||
)
|
||||
|
||||
# 2. Liquidity Sweep
|
||||
liq_met = False
|
||||
try:
|
||||
if mtf_signals and not mtf_signals.liquidity.empty:
|
||||
liq_col = mtf_signals.liquidity.get("Liquidity", pd.Series(dtype=float))
|
||||
swept = liq_col.dropna()
|
||||
if len(swept) > 0:
|
||||
val = swept.iloc[-1]
|
||||
if pd.notna(val) and int(val) == target:
|
||||
liq_met = True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
conditions.append(
|
||||
ConditionResult("Liquidity Sweep", liq_met)
|
||||
)
|
||||
|
||||
# 3. Order Block (price in or near aligned OB)
|
||||
ob_met = False
|
||||
for zone in mtf.mtf_zones:
|
||||
if zone.zone_type == "OB" and zone.direction == target:
|
||||
if zone.contains_price(current_price):
|
||||
ob_met = True
|
||||
break
|
||||
# Also check proximity (within 0.5% of zone)
|
||||
zone_size = zone.top - zone.bottom
|
||||
margin = max(zone_size * 0.5, current_price * 0.005)
|
||||
if (zone.bottom - margin) <= current_price <= (zone.top + margin):
|
||||
ob_met = True
|
||||
break
|
||||
conditions.append(
|
||||
ConditionResult(
|
||||
"Order Block",
|
||||
ob_met,
|
||||
"Price in/near aligned OB" if ob_met else "",
|
||||
)
|
||||
)
|
||||
|
||||
# 4. Fair Value Gap (price in or near aligned FVG)
|
||||
fvg_met = False
|
||||
for zone in mtf.mtf_zones:
|
||||
if zone.zone_type == "FVG" and zone.direction == target:
|
||||
if zone.contains_price(current_price):
|
||||
fvg_met = True
|
||||
break
|
||||
zone_size = zone.top - zone.bottom
|
||||
margin = max(zone_size * 0.5, current_price * 0.005)
|
||||
if (zone.bottom - margin) <= current_price <= (zone.top + margin):
|
||||
fvg_met = True
|
||||
break
|
||||
conditions.append(
|
||||
ConditionResult(
|
||||
"Fair Value Gap",
|
||||
fvg_met,
|
||||
"Price in/near aligned FVG" if fvg_met else "",
|
||||
)
|
||||
)
|
||||
|
||||
# 5. BOS confirmation
|
||||
bos_met = False
|
||||
if ltf_signals:
|
||||
bos_met = ltf_signals.latest_bos == target
|
||||
conditions.append(
|
||||
ConditionResult("BOS", bos_met)
|
||||
)
|
||||
|
||||
# 6. CHOCH confirmation
|
||||
choch_met = False
|
||||
if ltf_signals:
|
||||
choch_met = ltf_signals.latest_choch == target
|
||||
elif mtf_signals:
|
||||
choch_met = mtf_signals.latest_choch == target
|
||||
conditions.append(
|
||||
ConditionResult("CHOCH", choch_met)
|
||||
)
|
||||
|
||||
score = sum(1 for c in conditions if c.met)
|
||||
direction = (
|
||||
TradeDirection.LONG
|
||||
if target == 1
|
||||
else TradeDirection.SHORT
|
||||
if target == -1
|
||||
else TradeDirection.NONE
|
||||
)
|
||||
is_valid = score >= self.MIN_CONFLUENCE and direction != TradeDirection.NONE
|
||||
|
||||
result = ConfluenceResult(
|
||||
score=score,
|
||||
conditions=conditions,
|
||||
is_valid=is_valid,
|
||||
direction=direction,
|
||||
)
|
||||
logger.info("Confluence check: {}", result.summary())
|
||||
return result
|
||||
Reference in New Issue
Block a user