mirror of
https://github.com/quachtinh113/main-fx.git
synced 2026-08-09 00:27:46 +00:00
statagy
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}")
|
||||
BIN
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user