diff --git a/Quant_Hiber_Risk_Stargy/reports/backtest/EURUSDm_strategy_output.parquet b/Quant_Hiber_Risk_Stargy/reports/backtest/EURUSDm_strategy_output.parquet new file mode 100644 index 0000000..9d3ada0 Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/reports/backtest/EURUSDm_strategy_output.parquet differ diff --git a/Quant_Hiber_Risk_Stargy/reports/live/EURUSDm_latest_signal.parquet b/Quant_Hiber_Risk_Stargy/reports/live/EURUSDm_latest_signal.parquet new file mode 100644 index 0000000..10773d2 Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/reports/live/EURUSDm_latest_signal.parquet differ diff --git a/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/adx_atr_filter.cpython-313.pyc b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/adx_atr_filter.cpython-313.pyc new file mode 100644 index 0000000..6e2d8f6 Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/adx_atr_filter.cpython-313.pyc differ diff --git a/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/liquidity_engine.cpython-313.pyc b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/liquidity_engine.cpython-313.pyc new file mode 100644 index 0000000..c14bf2a Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/liquidity_engine.cpython-313.pyc differ diff --git a/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/price_action_engine.cpython-313.pyc b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/price_action_engine.cpython-313.pyc new file mode 100644 index 0000000..8ab9fe9 Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/price_action_engine.cpython-313.pyc differ diff --git a/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/rsi_mtf.cpython-313.pyc b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/rsi_mtf.cpython-313.pyc index e1af778..e6ec384 100644 Binary files a/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/rsi_mtf.cpython-313.pyc and b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/rsi_mtf.cpython-313.pyc differ diff --git a/Quant_Hiber_Risk_Stargy/src/signal/adx_atr_filter.py b/Quant_Hiber_Risk_Stargy/src/signal/adx_atr_filter.py new file mode 100644 index 0000000..97d98c6 --- /dev/null +++ b/Quant_Hiber_Risk_Stargy/src/signal/adx_atr_filter.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import pandas as pd +import pandas_ta as ta + + +class ADXATRFilter: + """ + ADX + ATR filter engine (MVP v1) + + Mục tiêu: + - tính ADX để phân loại regime + - tính ATR để đo volatility + - lọc thị trường quá yên hoặc quá nóng + - tạo khoảng cách stop / spacing cơ bản cho risk engine + + Output chính: + - adx + - atr + - atr_pct + - regime + - allow_mean_reversion + - allow_trend_follow + - volatility_ok + - stop_distance + - spacing_distance + """ + + def __init__( + self, + adx_period: int = 14, + atr_period: int = 14, + range_threshold: float = 20.0, + trend_threshold: float = 25.0, + atr_min_pct: float = 0.0003, + atr_max_pct: float = 0.01, + stop_atr_multiplier: float = 1.5, + spacing_atr_multiplier: float = 1.0, + ): + """ + Args: + adx_period: chu kỳ ADX + atr_period: chu kỳ ATR + range_threshold: + ADX dưới ngưỡng này xem là range/sideway + trend_threshold: + ADX trên ngưỡng này xem là trend + atr_min_pct: + ATR / close quá thấp thì bỏ qua + atr_max_pct: + ATR / close quá cao thì bỏ qua + stop_atr_multiplier: + hệ số ATR cho stop width + spacing_atr_multiplier: + hệ số ATR cho DCA spacing + """ + self.adx_period = adx_period + self.atr_period = atr_period + self.range_threshold = range_threshold + self.trend_threshold = trend_threshold + self.atr_min_pct = atr_min_pct + self.atr_max_pct = atr_max_pct + self.stop_atr_multiplier = stop_atr_multiplier + self.spacing_atr_multiplier = spacing_atr_multiplier + + def _validate_input(self, df: pd.DataFrame) -> None: + required = {"high", "low", "close"} + missing = required - set(df.columns) + if missing: + raise ValueError(f"Missing required columns: {sorted(missing)}") + + if len(df) < max(self.adx_period, self.atr_period) + 5: + raise ValueError( + f"Not enough rows for ADX/ATR calculation. Got {len(df)} rows." + ) + + def compute_indicators(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + adx_df = ta.adx( + high=df["high"], + low=df["low"], + close=df["close"], + length=self.adx_period, + ) + + if adx_df is None or adx_df.empty: + raise ValueError("Failed to compute ADX with pandas_ta.") + + # pandas_ta thường trả tên kiểu ADX_14, DMP_14, DMN_14 + adx_col = f"ADX_{self.adx_period}" + dmp_col = f"DMP_{self.adx_period}" + dmn_col = f"DMN_{self.adx_period}" + + if adx_col not in adx_df.columns: + # fallback an toàn nếu pandas_ta đổi format + candidates = [c for c in adx_df.columns if c.startswith("ADX_")] + if not candidates: + raise ValueError("ADX column not found in pandas_ta output.") + adx_col = candidates[0] + + if dmp_col not in adx_df.columns: + candidates = [c for c in adx_df.columns if c.startswith("DMP_")] + dmp_col = candidates[0] if candidates else None + + if dmn_col not in adx_df.columns: + candidates = [c for c in adx_df.columns if c.startswith("DMN_")] + dmn_col = candidates[0] if candidates else None + + df["adx"] = adx_df[adx_col] + df["plus_di"] = adx_df[dmp_col] if dmp_col else pd.NA + df["minus_di"] = adx_df[dmn_col] if dmn_col else pd.NA + + df["atr"] = ta.atr( + high=df["high"], + low=df["low"], + close=df["close"], + length=self.atr_period, + ) + + df["atr_pct"] = df["atr"] / df["close"] + + return df + + def classify_regime(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["regime"] = "neutral" + + df.loc[df["adx"] < self.range_threshold, "regime"] = "range" + df.loc[df["adx"] > self.trend_threshold, "regime"] = "trend" + + # vùng giữa 20-25 là transition + transition_mask = ( + (df["adx"] >= self.range_threshold) + & (df["adx"] <= self.trend_threshold) + ) + df.loc[transition_mask, "regime"] = "transition" + + df["allow_mean_reversion"] = df["regime"] == "range" + df["allow_trend_follow"] = df["regime"] == "trend" + + return df + + def classify_directional_strength(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["directional_bias"] = 0 + + if "plus_di" in df.columns and "minus_di" in df.columns: + df.loc[df["plus_di"] > df["minus_di"], "directional_bias"] = 1 + df.loc[df["plus_di"] < df["minus_di"], "directional_bias"] = -1 + + return df + + def apply_volatility_filter(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["volatility_too_low"] = df["atr_pct"] < self.atr_min_pct + df["volatility_too_high"] = df["atr_pct"] > self.atr_max_pct + df["volatility_ok"] = ~(df["volatility_too_low"] | df["volatility_too_high"]) + + return df + + def build_risk_distances(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["stop_distance"] = df["atr"] * self.stop_atr_multiplier + df["spacing_distance"] = df["atr"] * self.spacing_atr_multiplier + + return df + + def build_trade_permissions(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["allow_trade"] = df["volatility_ok"] & df["regime"].isin(["range", "trend"]) + + # tín hiệu hỗ trợ chiến lược sau này + df["allow_trend_long"] = ( + df["allow_trade"] + & df["allow_trend_follow"] + & (df["directional_bias"] == 1) + ) + + df["allow_trend_short"] = ( + df["allow_trade"] + & df["allow_trend_follow"] + & (df["directional_bias"] == -1) + ) + + df["allow_range_long"] = df["allow_trade"] & df["allow_mean_reversion"] + df["allow_range_short"] = df["allow_trade"] & df["allow_mean_reversion"] + + return df + + def run(self, df: pd.DataFrame) -> pd.DataFrame: + self._validate_input(df) + + df = df.copy().sort_index() + df = self.compute_indicators(df) + df = self.classify_regime(df) + df = self.classify_directional_strength(df) + df = self.apply_volatility_filter(df) + df = self.build_risk_distances(df) + df = self.build_trade_permissions(df) + + return df + + +if __name__ == "__main__": + symbol = "EURUSDm" + timeframe = "M15" + path = f"data/processed/{symbol}/{timeframe}/{symbol}_{timeframe}_clean.parquet" + + try: + df = pd.read_parquet(path) + + engine = ADXATRFilter( + adx_period=14, + atr_period=14, + range_threshold=20, + trend_threshold=25, + atr_min_pct=0.0002, + atr_max_pct=0.005, + stop_atr_multiplier=1.5, + spacing_atr_multiplier=1.0, + ) + + result = engine.run(df) + + cols = [ + "close", + "adx", + "plus_di", + "minus_di", + "atr", + "atr_pct", + "regime", + "directional_bias", + "volatility_ok", + "allow_trade", + "allow_trend_long", + "allow_trend_short", + "allow_range_long", + "allow_range_short", + "stop_distance", + "spacing_distance", + ] + + print(f"--- ADX/ATR filter test for {symbol} {timeframe} ---") + print(result[cols].tail(10)) + + print("\nRegime counts:") + print(result["regime"].value_counts(dropna=False)) + + except Exception as e: + print(f"ADX/ATR filter test error: {e}") \ No newline at end of file diff --git a/Quant_Hiber_Risk_Stargy/src/strategies/__pycache__/smart_money_adx_atr_rsi_strategy.cpython-313.pyc b/Quant_Hiber_Risk_Stargy/src/strategies/__pycache__/smart_money_adx_atr_rsi_strategy.cpython-313.pyc new file mode 100644 index 0000000..0fcb716 Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/src/strategies/__pycache__/smart_money_adx_atr_rsi_strategy.cpython-313.pyc differ diff --git a/Quant_Hiber_Risk_Stargy/src/strategies/run_backtest.py b/Quant_Hiber_Risk_Stargy/src/strategies/run_backtest.py new file mode 100644 index 0000000..b701457 --- /dev/null +++ b/Quant_Hiber_Risk_Stargy/src/strategies/run_backtest.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import sys +from pathlib import Path +import pandas as pd + +try: + from src.strategies.smart_money_adx_atr_rsi_strategy import ( + SmartMoneyADXATRRSIStrategy, + ) +except ModuleNotFoundError: + # Hỗ trợ chạy trực tiếp file: python src/strategies/run_backtest.py + project_root = Path(__file__).resolve().parents[2] + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + + from src.strategies.smart_money_adx_atr_rsi_strategy import ( + SmartMoneyADXATRRSIStrategy, + ) + + +def load_parquet(symbol: str, timeframe: str, suffix: str = "clean") -> pd.DataFrame: + path = Path( + f"data/processed/{symbol}/{timeframe}/{symbol}_{timeframe}_{suffix}.parquet" + ) + if not path.exists(): + raise FileNotFoundError(f"Missing file: {path}") + + df = pd.read_parquet(path) + + if "timestamp" in df.columns: + df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True) + df = df.set_index("timestamp") + + df = df.sort_index() + return df + + +def summarize_signals(result: pd.DataFrame) -> None: + signals = result[result["strategy_signal"] != 0].copy() + + print("\n===== BACKTEST SUMMARY =====") + print(f"Total rows: {len(result):,}") + print(f"Total signals: {len(signals):,}") + + if "entry_mode" in result.columns: + print("\nEntry mode counts:") + print(result["entry_mode"].value_counts(dropna=False)) + + if signals.empty: + print("\nNo signals found.") + return + + cols = [ + "close", + "regime", + "liquidity_context", + "strategy_signal", + "entry_mode", + "confidence", + "stop_distance", + "spacing_distance", + ] + cols = [c for c in cols if c in signals.columns] + + print("\nLast 10 signals:") + print(signals[cols].tail(10)) + + if "strategy_signal" in signals.columns: + print("\nSignal distribution:") + print(signals["strategy_signal"].value_counts(dropna=False)) + + +def main() -> None: + symbol = "EURUSDm" + + print(f"Loading data for {symbol} ...") + + df_m15 = load_parquet(symbol, "M15") + df_h1 = load_parquet(symbol, "H1") + df_h4 = load_parquet(symbol, "H4") + + print("Running strategy ...") + + strategy = SmartMoneyADXATRRSIStrategy( + require_rsi_for_range=False, + require_rsi_for_trend=True, + ) + + result = strategy.run(df_m15, df_h1, df_h4) + + print("\n===== RESULT TAIL =====") + preview_cols = [ + "open", + "high", + "low", + "close", + "regime", + "liquidity_context", + "rsi_m15", + "rsi_h1", + "rsi_h4", + "strategy_signal", + "entry_mode", + "confidence", + "stop_distance", + "spacing_distance", + ] + preview_cols = [c for c in preview_cols if c in result.columns] + print(result[preview_cols].tail(10)) + + summarize_signals(result) + + output_path = Path(f"reports/backtest/{symbol}_strategy_output.parquet") + output_path.parent.mkdir(parents=True, exist_ok=True) + result.to_parquet(output_path) + print(f"\nSaved result to: {output_path}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Quant_Hiber_Risk_Stargy/src/strategies/run_live.py b/Quant_Hiber_Risk_Stargy/src/strategies/run_live.py new file mode 100644 index 0000000..ac25d38 --- /dev/null +++ b/Quant_Hiber_Risk_Stargy/src/strategies/run_live.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import sys +from pathlib import Path +import pandas as pd + +try: + from src.strategies.smart_money_adx_atr_rsi_strategy import ( + SmartMoneyADXATRRSIStrategy, + ) +except ModuleNotFoundError: + # Hỗ trợ chạy trực tiếp file: python src/strategies/run_live.py + project_root = Path(__file__).resolve().parents[2] + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + + from src.strategies.smart_money_adx_atr_rsi_strategy import ( + SmartMoneyADXATRRSIStrategy, + ) + + +def load_latest_data(symbol: str, timeframe: str, suffix: str = "clean") -> pd.DataFrame: + path = Path( + f"data/processed/{symbol}/{timeframe}/{symbol}_{timeframe}_{suffix}.parquet" + ) + if not path.exists(): + raise FileNotFoundError(f"Missing file: {path}") + + df = pd.read_parquet(path) + + if "timestamp" in df.columns: + df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True) + df = df.set_index("timestamp") + + return df.sort_index() + + +def main() -> None: + symbol = "EURUSDm" + + print(f"Loading latest market data for {symbol} ...") + + df_m15 = load_latest_data(symbol, "M15") + df_h1 = load_latest_data(symbol, "H1") + df_h4 = load_latest_data(symbol, "H4") + + strategy = SmartMoneyADXATRRSIStrategy( + require_rsi_for_range=False, + require_rsi_for_trend=True, + ) + + result = strategy.run(df_m15, df_h1, df_h4) + + latest = result.tail(1).copy() + + cols = [ + "close", + "regime", + "liquidity_context", + "rsi_m15", + "rsi_h1", + "rsi_h4", + "trigger_buy", + "trigger_sell", + "strategy_signal", + "entry_mode", + "confidence", + "stop_distance", + "spacing_distance", + ] + cols = [c for c in cols if c in latest.columns] + + print("\n===== LIVE SIGNAL CHECK =====") + print(latest[cols]) + + signal = int(latest["strategy_signal"].iloc[0]) + + if signal == 1: + print("\n[BUY] Live setup detected.") + elif signal == -1: + print("\n[SELL] Live setup detected.") + else: + print("\n[HOLD] No live setup.") + + output_path = Path(f"reports/live/{symbol}_latest_signal.parquet") + output_path.parent.mkdir(parents=True, exist_ok=True) + latest.to_parquet(output_path) + print(f"Saved latest live snapshot to: {output_path}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Quant_Hiber_Risk_Stargy/src/strategies/smart_money_adx_atr_rsi_strategy.py b/Quant_Hiber_Risk_Stargy/src/strategies/smart_money_adx_atr_rsi_strategy.py new file mode 100644 index 0000000..d3f0bb0 --- /dev/null +++ b/Quant_Hiber_Risk_Stargy/src/strategies/smart_money_adx_atr_rsi_strategy.py @@ -0,0 +1,309 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pandas as pd + +try: + from src.signal.adx_atr_filter import ADXATRFilter + from src.signal.liquidity_engine import LiquidityEngine + from src.signal.price_action_engine import PriceActionEngine + from src.signal.rsi_mtf import RSIMultiTimeframe +except ModuleNotFoundError: + # Hỗ trợ chạy trực tiếp file: python src/strategies/smart_money_adx_atr_rsi_strategy.py + project_root = Path(__file__).resolve().parents[2] + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + + from src.signal.adx_atr_filter import ADXATRFilter + from src.signal.liquidity_engine import LiquidityEngine + from src.signal.price_action_engine import PriceActionEngine + from src.signal.rsi_mtf import RSIMultiTimeframe + + +class SmartMoneyADXATRRSIStrategy: + """ + Smart Money strategy v1 + + Logic ưu tiên: + 1. Liquidity + 2. Price Action + 3. ADX / ATR regime filter + 4. RSI MTF trigger phụ + + Hai mode chính: + - Range / mean reversion + - Trend / continuation sau pullback hoặc liquidity grab + + Output chính: + - strategy_signal: 1 buy, -1 sell, 0 no trade + - entry_mode: range_reversal / trend_pullback / none + """ + + def __init__( + self, + liquidity_engine: LiquidityEngine | None = None, + price_action_engine: PriceActionEngine | None = None, + adx_atr_filter: ADXATRFilter | None = None, + rsi_mtf_engine: RSIMultiTimeframe | None = None, + require_rsi_for_range: bool = False, + require_rsi_for_trend: bool = True, + ): + self.liquidity_engine = liquidity_engine or LiquidityEngine() + self.price_action_engine = price_action_engine or PriceActionEngine() + self.adx_atr_filter = adx_atr_filter or ADXATRFilter() + self.rsi_mtf_engine = rsi_mtf_engine or RSIMultiTimeframe() + + self.require_rsi_for_range = require_rsi_for_range + self.require_rsi_for_trend = require_rsi_for_trend + + def _validate_inputs( + self, + df_m15: pd.DataFrame, + df_h1: pd.DataFrame, + df_h4: pd.DataFrame, + ) -> None: + if df_m15.empty: + raise ValueError("df_m15 is empty") + if df_h1.empty: + raise ValueError("df_h1 is empty") + if df_h4.empty: + raise ValueError("df_h4 is empty") + + required_cols = {"open", "high", "low", "close"} + for name, df in {"m15": df_m15, "h1": df_h1, "h4": df_h4}.items(): + missing = required_cols - set(df.columns) + if missing: + raise ValueError(f"{name} dataframe missing columns: {sorted(missing)}") + + def _merge_rsi_context( + self, + base_df: pd.DataFrame, + df_m15: pd.DataFrame, + df_h1: pd.DataFrame, + df_h4: pd.DataFrame, + ) -> pd.DataFrame: + rsi_df = self.rsi_mtf_engine.run(df_m15, df_h1, df_h4) + + keep_cols = [ + "rsi_m15", + "rsi_h1", + "rsi_h4", + "trend", + "bias", + "trigger_buy", + "trigger_sell", + "signal", + ] + keep_cols = [c for c in keep_cols if c in rsi_df.columns] + + df = base_df.join(rsi_df[keep_cols], how="left") + return df + + def _build_range_setups(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + range_long_base = ( + df["allow_range_long"] + & df["reclaim_after_sell_sweep"] + & df["pa_bullish_confirm"] + ) + + range_short_base = ( + df["allow_range_short"] + & df["reclaim_after_buy_sweep"] + & df["pa_bearish_confirm"] + ) + + if self.require_rsi_for_range: + df["range_long_setup"] = range_long_base & df["trigger_buy"].fillna(False) + df["range_short_setup"] = range_short_base & df["trigger_sell"].fillna(False) + else: + df["range_long_setup"] = range_long_base + df["range_short_setup"] = range_short_base + + return df + + def _build_trend_setups(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + trend_long_base = ( + df["allow_trend_long"] + & ( + df["reclaim_after_sell_sweep"] + | df["sweep_sell_side"] + | df["close_back_above_swing_low"] + ) + & df["pa_bullish_confirm"] + & (df["trend"] == 1) + & (df["bias"] == 1) + ) + + trend_short_base = ( + df["allow_trend_short"] + & ( + df["reclaim_after_buy_sweep"] + | df["sweep_buy_side"] + | df["close_back_below_swing_high"] + ) + & df["pa_bearish_confirm"] + & (df["trend"] == -1) + & (df["bias"] == -1) + ) + + if self.require_rsi_for_trend: + df["trend_long_setup"] = trend_long_base & df["trigger_buy"].fillna(False) + df["trend_short_setup"] = trend_short_base & df["trigger_sell"].fillna(False) + else: + df["trend_long_setup"] = trend_long_base + df["trend_short_setup"] = trend_short_base + + return df + + def _build_final_signal(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["strategy_signal"] = 0 + df["entry_mode"] = "none" + + range_long = df["range_long_setup"] + range_short = df["range_short_setup"] + trend_long = df["trend_long_setup"] + trend_short = df["trend_short_setup"] + + df.loc[range_long, "strategy_signal"] = 1 + df.loc[range_short, "strategy_signal"] = -1 + df.loc[trend_long, "strategy_signal"] = 1 + df.loc[trend_short, "strategy_signal"] = -1 + + df.loc[range_long, "entry_mode"] = "range_reversal" + df.loc[range_short, "entry_mode"] = "range_reversal" + df.loc[trend_long, "entry_mode"] = "trend_pullback" + df.loc[trend_short, "entry_mode"] = "trend_pullback" + + # confidence score đơn giản cho v1 + df["confidence"] = 0.0 + + df.loc[df["reclaim_after_sell_sweep"], "confidence"] += 0.25 + df.loc[df["reclaim_after_buy_sweep"], "confidence"] += 0.25 + + df.loc[df["pa_bullish_confirm"], "confidence"] += 0.25 + df.loc[df["pa_bearish_confirm"], "confidence"] += 0.25 + + df.loc[df["allow_trend_follow"], "confidence"] += 0.15 + df.loc[df["allow_mean_reversion"], "confidence"] += 0.10 + + df.loc[df["trigger_buy"].fillna(False), "confidence"] += 0.10 + df.loc[df["trigger_sell"].fillna(False), "confidence"] += 0.10 + + df["confidence"] = df["confidence"].clip(upper=1.0) + + return df + + def run( + self, + df_m15: pd.DataFrame, + df_h1: pd.DataFrame, + df_h4: pd.DataFrame, + ) -> pd.DataFrame: + self._validate_inputs(df_m15, df_h1, df_h4) + + # 1. Liquidity trên M15 + df = self.liquidity_engine.run(df_m15) + + # 2. Price Action trên M15 + df = self.price_action_engine.run(df) + + # 3. ADX / ATR filter trên M15 + df = self.adx_atr_filter.run(df) + + # 4. RSI MTF context + df = self._merge_rsi_context(df, df_m15, df_h1, df_h4) + + # 5. Build setups + df = self._build_range_setups(df) + df = self._build_trend_setups(df) + + # 6. Final signal + df = self._build_final_signal(df) + + return df + + +if __name__ == "__main__": + symbol = "EURUSDm" + + path_m15 = f"data/processed/{symbol}/M15/{symbol}_M15_clean.parquet" + path_h1 = f"data/processed/{symbol}/H1/{symbol}_H1_clean.parquet" + path_h4 = f"data/processed/{symbol}/H4/{symbol}_H4_clean.parquet" + + try: + df_m15 = pd.read_parquet(path_m15) + df_h1 = pd.read_parquet(path_h1) + df_h4 = pd.read_parquet(path_h4) + + strategy = SmartMoneyADXATRRSIStrategy( + require_rsi_for_range=False, + require_rsi_for_trend=True, + ) + + result = strategy.run(df_m15, df_h1, df_h4) + + cols = [ + "open", + "high", + "low", + "close", + "regime", + "liquidity_context", + "pa_bullish_confirm", + "pa_bearish_confirm", + "rsi_m15", + "rsi_h1", + "rsi_h4", + "trigger_buy", + "trigger_sell", + "range_long_setup", + "range_short_setup", + "trend_long_setup", + "trend_short_setup", + "strategy_signal", + "entry_mode", + "confidence", + "stop_distance", + "spacing_distance", + ] + + available_cols = [c for c in cols if c in result.columns] + + print(f"--- Smart Money Strategy test for {symbol} ---") + print(result[available_cols].tail(10)) + + signals = result[result["strategy_signal"] != 0] + print(f"\nTotal signals found: {len(signals)}") + + if not signals.empty: + print("\nLast 10 signals:") + print( + signals[ + [ + c for c in [ + "close", + "regime", + "liquidity_context", + "strategy_signal", + "entry_mode", + "confidence", + "stop_distance", + "spacing_distance", + ] if c in signals.columns + ] + ].tail(10) + ) + + print("\nEntry mode counts:") + print(result["entry_mode"].value_counts(dropna=False)) + + except Exception as e: + print(f"Smart money strategy test error: {e}") \ No newline at end of file