From 0948bda053bc42f2a8d4a2fd09fe25cac536331d Mon Sep 17 00:00:00 2001 From: tinh Date: Sat, 21 Mar 2026 14:36:58 +0700 Subject: [PATCH] update quant --- Quant_Hiber_Risk_Stargy/data/preprocessor.py | 72 ++--- .../__pycache__/rsi_mtf.cpython-313.pyc | Bin 0 -> 5395 bytes .../src/signal/liquidity_engine.py | 214 ++++++++++++++ .../src/signal/price_action_engine.py | 261 ++++++++++++++++++ Quant_Hiber_Risk_Stargy/src/signal/rsi_mtf.py | 36 ++- 5 files changed, 534 insertions(+), 49 deletions(-) create mode 100644 Quant_Hiber_Risk_Stargy/src/signal/__pycache__/rsi_mtf.cpython-313.pyc create mode 100644 Quant_Hiber_Risk_Stargy/src/signal/liquidity_engine.py create mode 100644 Quant_Hiber_Risk_Stargy/src/signal/price_action_engine.py 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 0000000000000000000000000000000000000000..e1af778a080cd3ae1570c0a8af52a2077cef5c52 GIT binary patch literal 5395 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 L7W3T}3()@unM$@~ literal 0 HcmV?d00001 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