deploy: 2026-03-20 07:49
This commit is contained in:
0
indicators/__init__.py
Normal file
0
indicators/__init__.py
Normal file
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
|
||||
164
indicators/ict_engine.py
Normal file
164
indicators/ict_engine.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""ICT Smart Money Concepts indicator engine.
|
||||
|
||||
Wraps the smartmoneyconcepts library to provide a unified analysis
|
||||
interface producing structured ICTSignals results.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
try:
|
||||
from smartmoneyconcepts import smc
|
||||
except ImportError:
|
||||
smc = None
|
||||
logger.warning(
|
||||
"smartmoneyconcepts not installed. "
|
||||
"Run: pip install smartmoneyconcepts"
|
||||
)
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class ICTSignals:
|
||||
"""Container for all ICT analysis results on a single timeframe."""
|
||||
|
||||
swing_highs_lows: pd.DataFrame # HighLow, Level
|
||||
fvg: pd.DataFrame # FVG(1/-1), Top, Bottom, MitigatedIndex
|
||||
bos_choch: pd.DataFrame # BOS(1/-1), CHoCH(1/-1), Level, BrokenIndex
|
||||
order_blocks: pd.DataFrame # OB(1/-1), Top, Bottom, OBVolume, Percentage
|
||||
liquidity: pd.DataFrame # Liquidity(1/-1), Level, End, SweptIndex
|
||||
prev_high_low: pd.DataFrame # PreviousHigh, PreviousLow
|
||||
retracements: pd.DataFrame # Direction, CurrentRetracement, DeepestRetracement
|
||||
|
||||
@property
|
||||
def latest_bos(self) -> Optional[int]:
|
||||
"""Return the most recent BOS direction (1=bullish, -1=bearish) or None."""
|
||||
col = "BOS"
|
||||
if col not in self.bos_choch.columns:
|
||||
return None
|
||||
try:
|
||||
valid = self.bos_choch[col].dropna()
|
||||
if len(valid) == 0:
|
||||
return None
|
||||
val = valid.iloc[-1]
|
||||
return int(val) if pd.notna(val) else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def latest_choch(self) -> Optional[int]:
|
||||
"""Return the most recent CHOCH direction or None."""
|
||||
col = "CHOCH"
|
||||
if col not in self.bos_choch.columns:
|
||||
return None
|
||||
try:
|
||||
valid = self.bos_choch[col].dropna()
|
||||
if len(valid) == 0:
|
||||
return None
|
||||
val = valid.iloc[-1]
|
||||
return int(val) if pd.notna(val) else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
@property
|
||||
def active_order_blocks(self) -> pd.DataFrame:
|
||||
"""Return order blocks that have not been mitigated (OB == 1 or -1).
|
||||
|
||||
MitigatedIndex: NaN or 0 means not mitigated in the SMC library.
|
||||
"""
|
||||
if "OB" not in self.order_blocks.columns:
|
||||
return pd.DataFrame()
|
||||
ob_col = self.order_blocks["OB"]
|
||||
mask = ob_col.notna() & (ob_col.isin([1, -1]))
|
||||
mitigated = self.order_blocks.get("MitigatedIndex", pd.Series(dtype=float))
|
||||
if not mitigated.empty:
|
||||
# 0 means "not mitigated" in SMC lib, NaN also means not mitigated
|
||||
mask = mask & (mitigated.isna() | (mitigated == 0))
|
||||
return self.order_blocks[mask].tail(10)
|
||||
|
||||
@property
|
||||
def active_fvg(self) -> pd.DataFrame:
|
||||
"""Return FVGs that have not been mitigated."""
|
||||
if "FVG" not in self.fvg.columns:
|
||||
return pd.DataFrame()
|
||||
fvg_col = self.fvg["FVG"]
|
||||
mask = fvg_col.notna() & (fvg_col.isin([1, -1]))
|
||||
mitigated = self.fvg.get("MitigatedIndex", pd.Series(dtype=float))
|
||||
if not mitigated.empty:
|
||||
mask = mask & (mitigated.isna() | (mitigated == 0))
|
||||
return self.fvg[mask].tail(10)
|
||||
|
||||
|
||||
class ICTEngine:
|
||||
"""ICT Smart Money Concepts analysis engine.
|
||||
|
||||
Processes OHLC DataFrames through the smc library to detect
|
||||
Order Blocks, Fair Value Gaps, BOS/CHOCH, Liquidity, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, swing_length: int | None = None):
|
||||
self.swing_length = swing_length or settings.SWING_LENGTH
|
||||
if smc is None:
|
||||
raise ImportError("smartmoneyconcepts package is required")
|
||||
|
||||
def analyze(self, ohlc: pd.DataFrame) -> ICTSignals:
|
||||
"""Run full ICT analysis on an OHLC DataFrame.
|
||||
|
||||
Args:
|
||||
ohlc: DataFrame with columns [open, high, low, close, volume].
|
||||
|
||||
Returns:
|
||||
ICTSignals with all indicator results.
|
||||
"""
|
||||
if ohlc.empty or len(ohlc) < self.swing_length:
|
||||
raise ValueError(
|
||||
f"Need at least {self.swing_length} candles, got {len(ohlc)}"
|
||||
)
|
||||
|
||||
logger.debug("Running ICT analysis on {} candles (swing={})", len(ohlc), self.swing_length)
|
||||
|
||||
swing = smc.swing_highs_lows(ohlc, self.swing_length)
|
||||
|
||||
fvg = smc.fvg(ohlc, join_consecutive=settings.FVG_JOIN_CONSECUTIVE)
|
||||
bos_choch = smc.bos_choch(ohlc, swing, close_break=True)
|
||||
order_blocks = smc.ob(ohlc, swing, close_mitigation=settings.OB_CLOSE_MITIGATION)
|
||||
liquidity = smc.liquidity(
|
||||
ohlc, swing, range_percent=settings.LIQUIDITY_RANGE_PERCENT
|
||||
)
|
||||
|
||||
# previous_high_low and retracements may not be available in all versions
|
||||
try:
|
||||
prev_high_low = smc.previous_high_low(ohlc, time_frame="1D")
|
||||
except Exception:
|
||||
prev_high_low = pd.DataFrame(index=ohlc.index)
|
||||
|
||||
try:
|
||||
retracements = smc.retracements(ohlc, swing)
|
||||
except Exception:
|
||||
retracements = pd.DataFrame(index=ohlc.index)
|
||||
|
||||
return ICTSignals(
|
||||
swing_highs_lows=swing,
|
||||
fvg=fvg,
|
||||
bos_choch=bos_choch,
|
||||
order_blocks=order_blocks,
|
||||
liquidity=liquidity,
|
||||
prev_high_low=prev_high_low,
|
||||
retracements=retracements,
|
||||
)
|
||||
|
||||
def detect_swing_highs(self, ohlc: pd.DataFrame) -> pd.Series:
|
||||
"""Return a boolean series marking swing highs."""
|
||||
swing = smc.swing_highs_lows(ohlc, self.swing_length)
|
||||
return swing.get("HighLow", pd.Series(dtype=float)) == 1
|
||||
|
||||
def detect_swing_lows(self, ohlc: pd.DataFrame) -> pd.Series:
|
||||
"""Return a boolean series marking swing lows."""
|
||||
swing = smc.swing_highs_lows(ohlc, self.swing_length)
|
||||
return swing.get("HighLow", pd.Series(dtype=float)) == -1
|
||||
314
indicators/multi_timeframe.py
Normal file
314
indicators/multi_timeframe.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Multi-timeframe ICT analysis.
|
||||
|
||||
Combines Higher, Middle, and Lower timeframe signals to determine
|
||||
market bias, key zones, and precise entry points.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from config import settings
|
||||
from core.data_feed import DataFeed
|
||||
from indicators.ict_engine import ICTEngine, ICTSignals
|
||||
|
||||
|
||||
class MarketBias(str, Enum):
|
||||
BULLISH = "BULLISH"
|
||||
BEARISH = "BEARISH"
|
||||
NEUTRAL = "NEUTRAL"
|
||||
|
||||
|
||||
class TradeDirection(str, Enum):
|
||||
LONG = "LONG"
|
||||
SHORT = "SHORT"
|
||||
NONE = "NONE"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradeZone:
|
||||
"""A price zone of interest (OB or FVG)."""
|
||||
|
||||
zone_type: str # "OB" or "FVG"
|
||||
direction: int # 1 = bullish, -1 = bearish
|
||||
top: float
|
||||
bottom: float
|
||||
timeframe: str
|
||||
strength: float = 1.0
|
||||
|
||||
@property
|
||||
def mid(self) -> float:
|
||||
return (self.top + self.bottom) / 2
|
||||
|
||||
def contains_price(self, price: float) -> bool:
|
||||
return self.bottom <= price <= self.top
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntryPoint:
|
||||
"""Precise entry details derived from LTF analysis."""
|
||||
|
||||
direction: TradeDirection
|
||||
price: float
|
||||
stop_loss: float
|
||||
take_profit: float
|
||||
zone: TradeZone
|
||||
timeframe: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MTFAnalysis:
|
||||
"""Result of multi-timeframe analysis."""
|
||||
|
||||
htf_bias: MarketBias
|
||||
mtf_zones: List[TradeZone] = field(default_factory=list)
|
||||
ltf_entry: Optional[EntryPoint] = None
|
||||
confluence_score: int = 0
|
||||
|
||||
|
||||
class MultiTimeframeAnalyzer:
|
||||
"""Analyze market structure across Higher, Middle, and Lower timeframes."""
|
||||
|
||||
TIMEFRAMES = {
|
||||
"htf": settings.HTF_TIMEFRAME,
|
||||
"mtf": settings.MTF_TIMEFRAME,
|
||||
"ltf": settings.LTF_TIMEFRAME,
|
||||
}
|
||||
|
||||
def __init__(self, ict_engine: ICTEngine):
|
||||
self.engine = ict_engine
|
||||
|
||||
async def analyze_all(
|
||||
self, data_feed: DataFeed, symbol: str
|
||||
) -> MTFAnalysis:
|
||||
"""Run ICT analysis on all three timeframes and synthesise results."""
|
||||
# Fetch data for all timeframes
|
||||
tf_data = await data_feed.fetch_multi_timeframe(
|
||||
symbol, list(self.TIMEFRAMES.values())
|
||||
)
|
||||
|
||||
htf_df = tf_data[self.TIMEFRAMES["htf"]]
|
||||
mtf_df = tf_data[self.TIMEFRAMES["mtf"]]
|
||||
ltf_df = tf_data[self.TIMEFRAMES["ltf"]]
|
||||
|
||||
# Analyze each timeframe
|
||||
htf_signals = self.engine.analyze(htf_df)
|
||||
mtf_signals = self.engine.analyze(mtf_df)
|
||||
ltf_signals = self.engine.analyze(ltf_df)
|
||||
|
||||
# Determine bias from HTF
|
||||
bias = self.get_htf_bias(htf_signals)
|
||||
|
||||
# Find key zones from MTF
|
||||
zones = self.find_mtf_zones(mtf_signals, self.TIMEFRAMES["mtf"])
|
||||
|
||||
# Find precise entry from LTF
|
||||
current_price = float(ltf_df["close"].iloc[-1])
|
||||
entry = self.find_ltf_entry(ltf_signals, bias, zones, current_price)
|
||||
|
||||
# Calculate confluence
|
||||
score = self._calculate_confluence(htf_signals, mtf_signals, ltf_signals, bias)
|
||||
|
||||
result = MTFAnalysis(
|
||||
htf_bias=bias,
|
||||
mtf_zones=zones,
|
||||
ltf_entry=entry,
|
||||
confluence_score=score,
|
||||
)
|
||||
logger.info(
|
||||
"MTF Analysis for {}: bias={}, zones={}, score={}",
|
||||
symbol, bias.value, len(zones), score,
|
||||
)
|
||||
return result
|
||||
|
||||
def get_htf_bias(self, htf_signals: ICTSignals) -> MarketBias:
|
||||
"""Determine overall market direction from the Higher Timeframe.
|
||||
|
||||
Uses BOS/CHOCH and swing structure to decide bias.
|
||||
"""
|
||||
latest_bos = htf_signals.latest_bos
|
||||
latest_choch = htf_signals.latest_choch
|
||||
|
||||
# CHOCH takes precedence (trend reversal)
|
||||
if latest_choch == 1:
|
||||
return MarketBias.BULLISH
|
||||
if latest_choch == -1:
|
||||
return MarketBias.BEARISH
|
||||
|
||||
# BOS confirms existing trend
|
||||
if latest_bos == 1:
|
||||
return MarketBias.BULLISH
|
||||
if latest_bos == -1:
|
||||
return MarketBias.BEARISH
|
||||
|
||||
return MarketBias.NEUTRAL
|
||||
|
||||
def find_mtf_zones(
|
||||
self, mtf_signals: ICTSignals, timeframe: str
|
||||
) -> List[TradeZone]:
|
||||
"""Extract active Order Block and FVG zones from the Middle Timeframe."""
|
||||
zones: List[TradeZone] = []
|
||||
|
||||
# Order Blocks
|
||||
obs = mtf_signals.active_order_blocks
|
||||
if not obs.empty and "Top" in obs.columns and "Bottom" in obs.columns:
|
||||
for _, row in obs.iterrows():
|
||||
ob_val = row.get("OB", 0)
|
||||
top_val = row.get("Top")
|
||||
bottom_val = row.get("Bottom")
|
||||
if pd.isna(ob_val) or pd.isna(top_val) or pd.isna(bottom_val):
|
||||
continue
|
||||
zones.append(
|
||||
TradeZone(
|
||||
zone_type="OB",
|
||||
direction=int(ob_val),
|
||||
top=float(top_val),
|
||||
bottom=float(bottom_val),
|
||||
timeframe=timeframe,
|
||||
strength=float(row.get("OBVolume", 1.0)) if pd.notna(row.get("OBVolume")) else 1.0,
|
||||
)
|
||||
)
|
||||
|
||||
# Fair Value Gaps
|
||||
fvgs = mtf_signals.active_fvg
|
||||
if not fvgs.empty and "Top" in fvgs.columns and "Bottom" in fvgs.columns:
|
||||
for _, row in fvgs.iterrows():
|
||||
fvg_val = row.get("FVG", 0)
|
||||
top_val = row.get("Top")
|
||||
bottom_val = row.get("Bottom")
|
||||
if pd.isna(fvg_val) or pd.isna(top_val) or pd.isna(bottom_val):
|
||||
continue
|
||||
zones.append(
|
||||
TradeZone(
|
||||
zone_type="FVG",
|
||||
direction=int(fvg_val),
|
||||
top=float(top_val),
|
||||
bottom=float(bottom_val),
|
||||
timeframe=timeframe,
|
||||
)
|
||||
)
|
||||
|
||||
return zones
|
||||
|
||||
def find_ltf_entry(
|
||||
self,
|
||||
ltf_signals: ICTSignals,
|
||||
bias: MarketBias,
|
||||
zones: List[TradeZone],
|
||||
current_price: float,
|
||||
) -> Optional[EntryPoint]:
|
||||
"""Search for a precise entry on the Lower Timeframe.
|
||||
|
||||
Only looks for entries aligned with the HTF bias within
|
||||
identified MTF zones.
|
||||
"""
|
||||
if bias == MarketBias.NEUTRAL:
|
||||
return None
|
||||
|
||||
target_dir = 1 if bias == MarketBias.BULLISH else -1
|
||||
|
||||
# Check if price is inside any zone aligned with the bias
|
||||
for zone in zones:
|
||||
if zone.direction != target_dir:
|
||||
continue
|
||||
if not zone.contains_price(current_price):
|
||||
continue
|
||||
|
||||
# Confirm with LTF BOS in the same direction
|
||||
ltf_bos = ltf_signals.latest_bos
|
||||
if ltf_bos != target_dir:
|
||||
continue
|
||||
|
||||
direction = (
|
||||
TradeDirection.LONG
|
||||
if bias == MarketBias.BULLISH
|
||||
else TradeDirection.SHORT
|
||||
)
|
||||
|
||||
# SL beyond the zone, TP at 2:1 ratio
|
||||
if direction == TradeDirection.LONG:
|
||||
stop_loss = zone.bottom * 0.998 # small buffer below zone
|
||||
risk = current_price - stop_loss
|
||||
take_profit = current_price + risk * 2
|
||||
else:
|
||||
stop_loss = zone.top * 1.002
|
||||
risk = stop_loss - current_price
|
||||
take_profit = current_price - risk * 2
|
||||
|
||||
return EntryPoint(
|
||||
direction=direction,
|
||||
price=current_price,
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
zone=zone,
|
||||
timeframe=settings.LTF_TIMEFRAME,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _calculate_confluence(
|
||||
self,
|
||||
htf: ICTSignals,
|
||||
mtf: ICTSignals,
|
||||
ltf: ICTSignals,
|
||||
bias: MarketBias,
|
||||
) -> int:
|
||||
"""Count how many ICT conditions align (0-6)."""
|
||||
score = 0
|
||||
target = 1 if bias == MarketBias.BULLISH else (-1 if bias == MarketBias.BEARISH else 0)
|
||||
if target == 0:
|
||||
return 0
|
||||
|
||||
# 1. Market structure (HTF bias exists)
|
||||
if bias != MarketBias.NEUTRAL:
|
||||
score += 1
|
||||
|
||||
# 2. Liquidity sweep detected on MTF
|
||||
try:
|
||||
if not mtf.liquidity.empty:
|
||||
liq = mtf.liquidity.get("Liquidity", pd.Series(dtype=float))
|
||||
swept = liq.dropna()
|
||||
if len(swept) > 0:
|
||||
val = swept.iloc[-1]
|
||||
if pd.notna(val) and int(val) == target:
|
||||
score += 1
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 3. Order Block present on MTF
|
||||
try:
|
||||
if not mtf.active_order_blocks.empty:
|
||||
ob_dirs = mtf.active_order_blocks.get("OB", pd.Series(dtype=float))
|
||||
valid_obs = ob_dirs.dropna()
|
||||
if (valid_obs == target).any():
|
||||
score += 1
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 4. FVG present on MTF
|
||||
try:
|
||||
if not mtf.active_fvg.empty:
|
||||
fvg_dirs = mtf.active_fvg.get("FVG", pd.Series(dtype=float))
|
||||
valid_fvgs = fvg_dirs.dropna()
|
||||
if (valid_fvgs == target).any():
|
||||
score += 1
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 5. BOS on LTF
|
||||
if ltf.latest_bos is not None and ltf.latest_bos == target:
|
||||
score += 1
|
||||
|
||||
# 6. CHOCH on MTF or LTF
|
||||
mtf_choch = mtf.latest_choch
|
||||
ltf_choch = ltf.latest_choch
|
||||
if (mtf_choch is not None and mtf_choch == target) or \
|
||||
(ltf_choch is not None and ltf_choch == target):
|
||||
score += 1
|
||||
|
||||
return score
|
||||
Reference in New Issue
Block a user