diff --git a/Quant_Hiber_Risk_Stargy/data/preprocessor.py b/Quant_Hiber_Risk_Stargy/data/preprocessor.py index 702a9eb..be3ae89 100644 --- a/Quant_Hiber_Risk_Stargy/data/preprocessor.py +++ b/Quant_Hiber_Risk_Stargy/data/preprocessor.py @@ -1,52 +1,28 @@ import pandas as pd -import os -from pathlib import Path +import numpy as np -def preprocess_all_symbols(): - symbols = ['EURUSDm', 'GBPUSDm', 'USDJPYm', 'USDCHFm', 'USDCADm', 'NZDUSDm'] - timeframes = ['M15', 'H1', 'H4'] - raw_base = Path("data/raw") - processed_base = Path("data/processed") +def clean_and_prepare_data(df, symbol_name): + # 1. Loại bỏ nến trùng lặp + df = df[~df.index.duplicated(keep='first')] - print("--- BẮT ĐẦU TIỀN XỬ LÝ DỮ LIỆU ---") + # 2. Đảm bảo nến logic (OHLC) + bad_candles = df[(df['high'] < df['low']) | (df['high'] < df['open']) | (df['high'] < df['close'])] + if not bad_candles.empty: + print(f"Cảnh báo: Phát hiện {len(bad_candles)} nến lỗi tại {symbol_name}") + # Có thể xóa hoặc sửa tùy bạn, ở đây ta chọn xóa nến lỗi cực nặng + df = df.drop(bad_candles.index) - for symbol in symbols: - for tf in timeframes: - file_path = raw_base / symbol / tf / f"{symbol}_{tf}.parquet" - - if not file_path.exists(): - continue - - # 1. Đọc dữ liệu - df = pd.read_parquet(file_path) - initial_count = len(df) - - # 2. Xử lý logic cơ bản - # Chuyển index sang datetime nếu chưa có - df.index = pd.to_datetime(df['timestamp']) - df = df.sort_index() - - # Loại bỏ trùng lặp - df = df[~df.index.duplicated(keep='first')] - - # Loại bỏ nến lỗi (High < Low) - df = df[df['high'] >= df['low']] - - # Lọc từ năm 2022 trở lại đây như bạn yêu cầu - df = df.loc['2022-01-01':] - - # 3. Loại bỏ ngày cuối tuần (Forex nghỉ) - df = df[df.index.dayofweek < 5] - - # 4. Lưu dữ liệu sạch - output_dir = processed_base / symbol / tf - output_dir.mkdir(parents=True, exist_ok=True) - output_path = output_dir / f"{symbol}_{tf}_clean.parquet" - df.to_parquet(output_path) - - print(f"[OK] {symbol} {tf}: {initial_count} -> {len(df)} nến (Đã lưu tại {output_path})") - - print("--- HOÀN THÀNH ---") - -if __name__ == "__main__": - preprocess_all_symbols() \ No newline at end of file + # 3. Tạo khung thời gian đầy đủ (không ngắt quãng) + # Giả sử khung M15, ta tạo range từ đầu đến cuối + full_range = pd.date_range(start=df.index.min(), end=df.index.max(), freq='15min') + df = df.reindex(full_range) + + # 4. Điền nến thiếu (ffill cho giá, 0 cho volume) + df[['open', 'high', 'low', 'close']] = df[['open', 'high', 'low', 'close']].ffill() + df['tick_volume'] = df['tick_volume'].fillna(0) + + # 5. Loại bỏ ngày cuối tuần (Forex nghỉ) + # Thứ 7 (5) và Chủ Nhật (6) + df = df[df.index.dayofweek < 5] + + return df \ No newline at end of file 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 new file mode 100644 index 0000000..e1af778 Binary files /dev/null and b/Quant_Hiber_Risk_Stargy/src/signal/__pycache__/rsi_mtf.cpython-313.pyc differ diff --git a/Quant_Hiber_Risk_Stargy/src/signal/liquidity_engine.py b/Quant_Hiber_Risk_Stargy/src/signal/liquidity_engine.py new file mode 100644 index 0000000..fa380af --- /dev/null +++ b/Quant_Hiber_Risk_Stargy/src/signal/liquidity_engine.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import pandas as pd + + +class LiquidityEngine: + """ + Liquidity detection engine (MVP v1) + + Mục tiêu: + - tìm swing high / swing low + - tìm vùng quét thanh khoản trên / dưới + - đánh dấu equal highs / equal lows đơn giản + - phục vụ chiến lược Smart Money: + liquidity -> price action -> ADX/ATR -> RSI trigger + """ + + def __init__( + self, + swing_lookback: int = 20, + equal_threshold: float = 0.0005, + ): + """ + Args: + swing_lookback: số nến lookback để tìm swing liquidity + equal_threshold: ngưỡng xem 2 đỉnh/đáy là gần bằng nhau + - với EURUSD có thể dùng 0.0003 ~ 0.0010 + - với USDJPY nên chỉnh riêng lớn hơn, ví dụ 0.03 ~ 0.10 + """ + self.swing_lookback = swing_lookback + self.equal_threshold = equal_threshold + + def _validate_input(self, df: pd.DataFrame) -> None: + required_cols = {"open", "high", "low", "close"} + missing = required_cols - set(df.columns) + if missing: + raise ValueError(f"Missing required columns: {sorted(missing)}") + + if len(df) < self.swing_lookback + 5: + raise ValueError( + f"Not enough rows for liquidity detection. " + f"Need at least {self.swing_lookback + 5}, got {len(df)}" + ) + + def detect_swing_liquidity(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Tìm swing high / swing low bằng rolling window. + Dùng shift(1) để tránh nhìn vào chính nến hiện tại. + """ + df = df.copy() + + df["swing_high"] = df["high"].rolling(self.swing_lookback).max().shift(1) + df["swing_low"] = df["low"].rolling(self.swing_lookback).min().shift(1) + + return df + + def detect_equal_high_low(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Equal highs / equal lows đơn giản: + - so sánh high hiện tại với high trước đó + - so sánh low hiện tại với low trước đó + """ + df = df.copy() + + prev_high = df["high"].shift(1) + prev_low = df["low"].shift(1) + + df["equal_highs"] = (df["high"] - prev_high).abs() <= self.equal_threshold + df["equal_lows"] = (df["low"] - prev_low).abs() <= self.equal_threshold + + return df + + def detect_liquidity_sweeps(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Phát hiện sweep thanh khoản. + + Sweep up: + - high chọc lên swing_high + - nhưng close đóng lại dưới swing_high + => quét buy-side liquidity rồi bị đạp xuống + + Sweep down: + - low chọc xuống swing_low + - nhưng close đóng lại trên swing_low + => quét sell-side liquidity rồi bị kéo lên + """ + df = df.copy() + + df["took_buy_side"] = df["high"] > df["swing_high"] + df["took_sell_side"] = df["low"] < df["swing_low"] + + df["sweep_buy_side"] = ( + (df["high"] > df["swing_high"]) + & (df["close"] < df["swing_high"]) + ) + + df["sweep_sell_side"] = ( + (df["low"] < df["swing_low"]) + & (df["close"] > df["swing_low"]) + ) + + return df + + def detect_reclaim(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Reclaim sau sweep: + - nến trước sweep + - nến hiện tại tiếp tục xác nhận quay lại trong vùng + + Dùng cho logic: + sweep trước -> reclaim sau -> mới cho trigger RSI / price action + """ + df = df.copy() + + df["reclaim_after_sell_sweep"] = ( + df["sweep_sell_side"].shift(1).fillna(False) + & (df["close"] > df["swing_low"]) + ) + + df["reclaim_after_buy_sweep"] = ( + df["sweep_buy_side"].shift(1).fillna(False) + & (df["close"] < df["swing_high"]) + ) + + return df + + def classify_liquidity_context(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Phân loại context thanh khoản đơn giản. + """ + df = df.copy() + df["liquidity_context"] = "neutral" + + df.loc[df["sweep_sell_side"], "liquidity_context"] = "sell_side_swept" + df.loc[df["sweep_buy_side"], "liquidity_context"] = "buy_side_swept" + df.loc[df["reclaim_after_sell_sweep"], "liquidity_context"] = "bullish_reclaim" + df.loc[df["reclaim_after_buy_sweep"], "liquidity_context"] = "bearish_reclaim" + + return df + + def run(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Full pipeline. + """ + self._validate_input(df) + + df = df.copy().sort_index() + + df = self.detect_swing_liquidity(df) + df = self.detect_equal_high_low(df) + df = self.detect_liquidity_sweeps(df) + df = self.detect_reclaim(df) + df = self.classify_liquidity_context(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 = LiquidityEngine( + swing_lookback=20, + equal_threshold=0.0005, + ) + result = engine.run(df) + + cols = [ + "open", + "high", + "low", + "close", + "swing_high", + "swing_low", + "equal_highs", + "equal_lows", + "took_buy_side", + "took_sell_side", + "sweep_buy_side", + "sweep_sell_side", + "reclaim_after_buy_sweep", + "reclaim_after_sell_sweep", + "liquidity_context", + ] + + print(f"--- Liquidity test for {symbol} {timeframe} ---") + print(result[cols].tail(10)) + + sweeps = result[result["sweep_buy_side"] | result["sweep_sell_side"]] + print(f"\nTotal sweeps found: {len(sweeps)}") + + if not sweeps.empty: + print("\nLast 5 sweep events:") + print( + sweeps[ + [ + "high", + "low", + "close", + "swing_high", + "swing_low", + "sweep_buy_side", + "sweep_sell_side", + "liquidity_context", + ] + ].tail(5) + ) + + except Exception as e: + print(f"Liquidity engine test error: {e}") \ No newline at end of file diff --git a/Quant_Hiber_Risk_Stargy/src/signal/price_action_engine.py b/Quant_Hiber_Risk_Stargy/src/signal/price_action_engine.py new file mode 100644 index 0000000..e9d25e9 --- /dev/null +++ b/Quant_Hiber_Risk_Stargy/src/signal/price_action_engine.py @@ -0,0 +1,261 @@ +from __future__ import annotations + +import pandas as pd + + +class PriceActionEngine: + """ + Price Action confirmation engine (MVP v1) + + Mục tiêu: + - xác nhận phản ứng giá sau liquidity sweep + - tạo các cờ price action sạch để strategy dùng + - không dự đoán, chỉ đọc hành vi nến đã đóng + + Output chính: + - bullish_rejection + - bearish_rejection + - bullish_engulfing + - bearish_engulfing + - close_back_above_swing_low + - close_back_below_swing_high + - bullish_momentum_close + - bearish_momentum_close + - pa_bullish_confirm + - pa_bearish_confirm + """ + + def __init__( + self, + min_wick_body_ratio: float = 1.5, + min_body_ratio: float = 0.4, + momentum_close_ratio: float = 0.7, + ): + """ + Args: + min_wick_body_ratio: + tỷ lệ wick/body tối thiểu để xem là rejection rõ + min_body_ratio: + tỷ lệ body/range tối thiểu để xem thân nến đủ rõ + momentum_close_ratio: + vị trí đóng cửa trong range nến để xem là momentum close + 0.7 nghĩa là đóng trong 30% đầu/cuối cây nến + """ + self.min_wick_body_ratio = min_wick_body_ratio + self.min_body_ratio = min_body_ratio + self.momentum_close_ratio = momentum_close_ratio + + def _validate_input(self, df: pd.DataFrame) -> None: + required = {"open", "high", "low", "close"} + missing = required - set(df.columns) + if missing: + raise ValueError(f"Missing required columns: {sorted(missing)}") + + if len(df) < 5: + raise ValueError("Not enough rows for price action detection.") + + def _base_features(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + df["range"] = (df["high"] - df["low"]).clip(lower=1e-12) + df["body"] = (df["close"] - df["open"]).abs() + df["body_ratio"] = df["body"] / df["range"] + + df["upper_wick"] = df["high"] - df[["open", "close"]].max(axis=1) + df["lower_wick"] = df[["open", "close"]].min(axis=1) - df["low"] + + df["bullish_bar"] = df["close"] > df["open"] + df["bearish_bar"] = df["close"] < df["open"] + + return df + + def detect_rejection_wicks(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Bullish rejection: + - lower wick dài + - thân nến không quá nhỏ + - đóng nến thiên về phía trên + + Bearish rejection: + - upper wick dài + - thân nến không quá nhỏ + - đóng nến thiên về phía dưới + """ + df = df.copy() + + df["bullish_rejection"] = ( + (df["lower_wick"] >= df["body"] * self.min_wick_body_ratio) + & (df["body_ratio"] >= self.min_body_ratio) + & (df["close"] >= df["low"] + df["range"] * 0.6) + ) + + df["bearish_rejection"] = ( + (df["upper_wick"] >= df["body"] * self.min_wick_body_ratio) + & (df["body_ratio"] >= self.min_body_ratio) + & (df["close"] <= df["low"] + df["range"] * 0.4) + ) + + return df + + def detect_engulfing(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Engulfing đơn giản theo thân nến. + """ + df = df.copy() + + prev_open = df["open"].shift(1) + prev_close = df["close"].shift(1) + + prev_bullish = prev_close > prev_open + prev_bearish = prev_close < prev_open + + curr_bullish = df["close"] > df["open"] + curr_bearish = df["close"] < df["open"] + + prev_body_low = pd.concat([prev_open, prev_close], axis=1).min(axis=1) + prev_body_high = pd.concat([prev_open, prev_close], axis=1).max(axis=1) + + curr_body_low = df[["open", "close"]].min(axis=1) + curr_body_high = df[["open", "close"]].max(axis=1) + + df["bullish_engulfing"] = ( + prev_bearish + & curr_bullish + & (curr_body_low <= prev_body_low) + & (curr_body_high >= prev_body_high) + ) + + df["bearish_engulfing"] = ( + prev_bullish + & curr_bearish + & (curr_body_low <= prev_body_low) + & (curr_body_high >= prev_body_high) + ) + + return df + + def detect_close_back_inside(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Nếu dataframe đã có swing_high / swing_low từ liquidity engine + thì tạo thêm tín hiệu close back inside range. + """ + df = df.copy() + + if "swing_low" in df.columns: + df["close_back_above_swing_low"] = ( + (df["low"] < df["swing_low"]) + & (df["close"] > df["swing_low"]) + ) + else: + df["close_back_above_swing_low"] = False + + if "swing_high" in df.columns: + df["close_back_below_swing_high"] = ( + (df["high"] > df["swing_high"]) + & (df["close"] < df["swing_high"]) + ) + else: + df["close_back_below_swing_high"] = False + + return df + + def detect_momentum_close(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Momentum close: + - bullish: đóng gần đỉnh nến + - bearish: đóng gần đáy nến + """ + df = df.copy() + + upper_threshold = 1.0 - self.momentum_close_ratio + lower_threshold = self.momentum_close_ratio + + df["bullish_momentum_close"] = ( + (df["close"] >= df["low"] + df["range"] * lower_threshold) + & (df["body_ratio"] >= self.min_body_ratio) + & df["bullish_bar"] + ) + + df["bearish_momentum_close"] = ( + (df["close"] <= df["low"] + df["range"] * upper_threshold) + & (df["body_ratio"] >= self.min_body_ratio) + & df["bearish_bar"] + ) + + return df + + def build_confirmation_flags(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Gom cụm xác nhận bullish / bearish. + """ + df = df.copy() + + df["pa_bullish_confirm"] = ( + df["bullish_rejection"] + | df["bullish_engulfing"] + | df["close_back_above_swing_low"] + | df["bullish_momentum_close"] + ) + + df["pa_bearish_confirm"] = ( + df["bearish_rejection"] + | df["bearish_engulfing"] + | df["close_back_below_swing_high"] + | df["bearish_momentum_close"] + ) + + return df + + def run(self, df: pd.DataFrame) -> pd.DataFrame: + self._validate_input(df) + + df = df.copy().sort_index() + df = self._base_features(df) + df = self.detect_rejection_wicks(df) + df = self.detect_engulfing(df) + df = self.detect_close_back_inside(df) + df = self.detect_momentum_close(df) + df = self.build_confirmation_flags(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 = PriceActionEngine() + result = engine.run(df) + + cols = [ + "open", + "high", + "low", + "close", + "bullish_rejection", + "bearish_rejection", + "bullish_engulfing", + "bearish_engulfing", + "close_back_above_swing_low", + "close_back_below_swing_high", + "bullish_momentum_close", + "bearish_momentum_close", + "pa_bullish_confirm", + "pa_bearish_confirm", + ] + + print(f"--- Price Action test for {symbol} {timeframe} ---") + print(result[cols].tail(10)) + + bullish = result[result["pa_bullish_confirm"]] + bearish = result[result["pa_bearish_confirm"]] + + print(f"\nBullish confirmations: {len(bullish)}") + print(f"Bearish confirmations: {len(bearish)}") + + except Exception as e: + print(f"Price action engine test error: {e}") \ No newline at end of file diff --git a/Quant_Hiber_Risk_Stargy/src/signal/rsi_mtf.py b/Quant_Hiber_Risk_Stargy/src/signal/rsi_mtf.py index 7122afb..a14ad2c 100644 --- a/Quant_Hiber_Risk_Stargy/src/signal/rsi_mtf.py +++ b/Quant_Hiber_Risk_Stargy/src/signal/rsi_mtf.py @@ -85,4 +85,38 @@ class RSIMultiTimeframe: df = self.align_timeframes(df_m15, df_h1, df_h4) df = self.classify(df) df = self.generate_signal(df) - return df \ No newline at end of file + return df + + +if __name__ == "__main__": + import pandas as pd + import os + + # 1. Thử load dữ liệu thực tế (đã processed) để test + symbol = "EURUSDm" + base_path = f"data/processed/{symbol}" + + try: + df_m15 = pd.read_parquet(f"{base_path}/M15/{symbol}_M15_clean.parquet") + df_h1 = pd.read_parquet(f"{base_path}/H1/{symbol}_H1_clean.parquet") + df_h4 = pd.read_parquet(f"{base_path}/H4/{symbol}_H4_clean.parquet") + + # 2. Khởi tạo Engine + engine = RSIMultiTimeframe() + + # 3. Chạy thử + print(f"--- Đang test chiến thuật cho {symbol} ---") + result = engine.run(df_m15, df_h1, df_h4) + + # 4. In kết quả ra màn hình + signals = result[result['signal'] != 0] + print(f"Tổng số nến: {len(result)}") + print(f"Số lượng tín hiệu tìm thấy: {len(signals)}") + + if not signals.empty: + print("\n5 tín hiệu cuối cùng:") + print(signals[['rsi_h4', 'rsi_h1', 'rsi_m15', 'signal']].tail()) + + except Exception as e: + print(f"Lỗi khi chạy test: {e}") + print("Hãy chắc chắn bạn đã chạy preprocessor.py trước đó.") \ No newline at end of file