update quant

This commit is contained in:
tinh
2026-03-21 14:36:58 +07:00
parent 2db5635602
commit 0948bda053
5 changed files with 534 additions and 49 deletions
+24 -48
View File
@@ -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()
# 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
@@ -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}")
@@ -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}")
+35 -1
View File
@@ -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
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 đó.")