feat: implement dynamic entry filter control system

**Backend Integration:**
- Added FilterConfigManager to bot __init__ (loads data/filter_config.json)
- Reload filter config every loop (lightweight JSON read)
- Added `_is_filter_enabled(filter_key)` helper method
- Wrapped 8 entry filters with enable/disable checks:
  1. flash_crash_guard — Flash crash detection
  2. regime_filter — HMM regime SLEEP check
  3. risk_check — Daily/total loss limits
  4. session_filter — Sydney/London/NY session validation
  5. signal_combination — SMC + ML signal agreement
  6. h1_bias — H1 EMA20 alignment (#31B filter)
  7. time_filter — Skip hours 9,21 WIB (#34A)
  8. cooldown — 150s minimum between trades

**Filter Behavior:**
- All filters enabled by default (11/11)
- When disabled: filter check passes automatically
- Dashboard shows "[DISABLED]" tag in filter detail
- Live reload — edit filter_config.json or use API, no bot restart needed

**Next:** Frontend dashboard card with toggle switches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-09 08:13:39 +07:00
parent 85161b4965
commit d5bfc52447
+93 -27
View File
@@ -66,6 +66,7 @@ from src.smart_risk_manager import SmartRiskManager, create_smart_risk_manager
from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence
# from src.news_agent import NewsAgent, create_news_agent, MarketCondition # DISABLED
from src.trade_logger import TradeLogger, get_trade_logger
from src.filter_config import FilterConfigManager
class TradingBot:
@@ -125,7 +126,10 @@ class TradingBot:
# Initialize risk engine
self.risk_engine = RiskEngine(self.config)
# Initialize filter config manager
self.filter_config = FilterConfigManager("data/filter_config.json")
# Initialize ML V2 Model D (76 features, AUC 0.7339)
self.ml_model = TradingModelV2(
confidence_threshold=self.config.ml.confidence_threshold,
@@ -816,6 +820,18 @@ class TradingBot:
logger.debug(f"H1 bias error: {e}")
return "NEUTRAL"
def _is_filter_enabled(self, filter_key: str) -> bool:
"""
Check if a filter is enabled via filter_config.json.
Args:
filter_key: Filter key (e.g., "h1_bias", "ml_confidence")
Returns:
True if enabled, False if disabled
"""
return self.filter_config.is_enabled(filter_key)
def _register_telegram_commands(self):
"""Register Telegram command handlers from separate module."""
from src.telegram_commands import register_commands
@@ -986,6 +1002,9 @@ class TradingBot:
# Reset filter tracking for dashboard
self._last_filter_results = []
# Reload filter config (lightweight JSON read, allows live updates from dashboard)
self.filter_config.load()
# 1. Fetch fresh data
df = self.mt5.get_market_data(
symbol=self.config.symbol,
@@ -1025,8 +1044,14 @@ class TradingBot:
# 5. Check flash crash
is_flash, move_pct = self.flash_crash.detect(df.tail(5))
self._last_filter_results.append({"name": "Flash Crash Guard", "passed": not is_flash, "detail": f"{move_pct:.2f}% move" if is_flash else "OK"})
if is_flash:
flash_enabled = self._is_filter_enabled("flash_crash_guard")
flash_blocked = is_flash and flash_enabled
self._last_filter_results.append({
"name": "Flash Crash Guard",
"passed": not flash_blocked,
"detail": f"{move_pct:.2f}% move" if is_flash else "OK" + (" [DISABLED]" if not flash_enabled else "")
})
if flash_blocked:
logger.warning(f"Flash crash detected: {move_pct:.2f}% move")
try:
await self._emergency_close_all()
@@ -1098,20 +1123,38 @@ class TradingBot:
# 7. Check regime allows trading
regime_sleep = regime_state and regime_state.recommendation == "SLEEP"
self._last_filter_results.append({"name": "Regime Filter", "passed": not regime_sleep, "detail": regime_state.regime.value if regime_state else "N/A"})
if regime_sleep:
regime_enabled = self._is_filter_enabled("regime_filter")
regime_blocked = regime_sleep and regime_enabled
self._last_filter_results.append({
"name": "Regime Filter",
"passed": not regime_blocked,
"detail": (regime_state.regime.value if regime_state else "N/A") + (" [DISABLED]" if not regime_enabled else "")
})
if regime_blocked:
logger.debug(f"Regime SLEEP: {regime_state.regime.value}")
return
self._last_filter_results.append({"name": "Risk Check", "passed": risk_metrics.can_trade, "detail": risk_metrics.reason if not risk_metrics.can_trade else "OK"})
if not risk_metrics.can_trade:
risk_enabled = self._is_filter_enabled("risk_check")
risk_blocked = not risk_metrics.can_trade and risk_enabled
self._last_filter_results.append({
"name": "Risk Check",
"passed": not risk_blocked,
"detail": (risk_metrics.reason if not risk_metrics.can_trade else "OK") + (" [DISABLED]" if not risk_enabled else "")
})
if risk_blocked:
logger.debug(f"Risk blocked: {risk_metrics.reason}")
return
# 7.5 Check trading session (WIB timezone)
session_ok, session_reason, session_multiplier = self.session_filter.can_trade()
self._last_filter_results.append({"name": "Session Filter", "passed": session_ok, "detail": session_reason})
if not session_ok:
session_enabled = self._is_filter_enabled("session_filter")
session_blocked = not session_ok and session_enabled
self._last_filter_results.append({
"name": "Session Filter",
"passed": not session_blocked,
"detail": session_reason + (" [DISABLED]" if not session_enabled else "")
})
if session_blocked:
if self._loop_count % 300 == 0: # Log every 5 minutes
logger.info(f"Session filter: {session_reason}")
next_window = self.session_filter.get_next_trading_window()
@@ -1162,39 +1205,56 @@ class TradingBot:
# 10. Combine signals
final_signal = self._combine_signals(smc_signal, ml_prediction, regime_state)
self._last_filter_results.append({"name": "Signal Combination", "passed": final_signal is not None, "detail": f"{final_signal.signal_type} ({final_signal.confidence:.0%})" if final_signal else "Filtered out"})
signal_enabled = self._is_filter_enabled("signal_combination")
signal_blocked = final_signal is None and signal_enabled
self._last_filter_results.append({
"name": "Signal Combination",
"passed": not signal_blocked,
"detail": (f"{final_signal.signal_type} ({final_signal.confidence:.0%})" if final_signal else "Filtered out") + (" [DISABLED]" if not signal_enabled else "")
})
if final_signal is None:
if signal_blocked:
return
# 10.1 H1 Multi-Timeframe Filter (#31B: Price vs EMA20 — backtest +$343)
# BUY only when H1 is BULLISH, SELL only when H1 is BEARISH
h1_enabled = self._is_filter_enabled("h1_bias")
h1_passed = True
h1_detail = f"H1={h1_bias}"
if h1_bias != "NEUTRAL":
if (final_signal.signal_type == "BUY" and h1_bias != "BULLISH") or \
(final_signal.signal_type == "SELL" and h1_bias != "BEARISH"):
if h1_enabled:
if h1_bias != "NEUTRAL":
if (final_signal.signal_type == "BUY" and h1_bias != "BULLISH") or \
(final_signal.signal_type == "SELL" and h1_bias != "BEARISH"):
h1_passed = False
h1_detail = f"{final_signal.signal_type} vs H1={h1_bias}"
self._last_filter_results.append({"name": "H1 Bias (#31B)", "passed": False, "detail": h1_detail})
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1={h1_bias})")
return
logger.info(f"H1 Filter: {final_signal.signal_type} aligned with H1={h1_bias}")
else:
h1_passed = False
h1_detail = f"{final_signal.signal_type} vs H1={h1_bias}"
h1_detail = f"{final_signal.signal_type} blocked (NEUTRAL)"
self._last_filter_results.append({"name": "H1 Bias (#31B)", "passed": False, "detail": h1_detail})
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1={h1_bias})")
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1=NEUTRAL)")
return
logger.info(f"H1 Filter: {final_signal.signal_type} aligned with H1={h1_bias}")
self._last_filter_results.append({"name": "H1 Bias (#31B)", "passed": True, "detail": f"Aligned {h1_bias}"})
else:
h1_passed = False
h1_detail = f"{final_signal.signal_type} blocked (NEUTRAL)"
self._last_filter_results.append({"name": "H1 Bias (#31B)", "passed": False, "detail": h1_detail})
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1=NEUTRAL)")
return
self._last_filter_results.append({"name": "H1 Bias (#31B)", "passed": True, "detail": f"Aligned {h1_bias}"})
self._last_filter_results.append({"name": "H1 Bias (#31B)", "passed": True, "detail": f"H1={h1_bias} [DISABLED]"})
# 10.2 Time-of-Hour Filter (#34A: skip WIB hours 9 and 21 — backtest +$356)
# Hour 9 WIB (02:00 UTC) = end of NY session, low liquidity
# Hour 21 WIB (14:00 UTC) = London-NY transition, whipsaw prone
wib_hour = datetime.now(ZoneInfo("Asia/Jakarta")).hour
time_blocked = wib_hour in (9, 21)
self._last_filter_results.append({"name": "Time Filter (#34A)", "passed": not time_blocked, "detail": f"WIB {wib_hour}" + (" BLOCKED" if time_blocked else "")})
if time_blocked:
time_enabled = self._is_filter_enabled("time_filter")
time_filter_blocked = time_blocked and time_enabled
self._last_filter_results.append({
"name": "Time Filter (#34A)",
"passed": not time_filter_blocked,
"detail": f"WIB {wib_hour}" + (" BLOCKED" if time_blocked else "") + (" [DISABLED]" if not time_enabled else "")
})
if time_filter_blocked:
logger.info(f"Time Filter: {final_signal.signal_type} blocked (WIB hour {wib_hour} is skip hour)")
return
@@ -1206,8 +1266,14 @@ class TradingBot:
cooldown_remaining = self._trade_cooldown_seconds - time_since_last
if cooldown_remaining > 0:
cooldown_blocked = True
self._last_filter_results.append({"name": "Trade Cooldown", "passed": not cooldown_blocked, "detail": f"{cooldown_remaining:.0f}s left" if cooldown_blocked else "OK"})
if cooldown_blocked:
cooldown_enabled = self._is_filter_enabled("cooldown")
cooldown_filter_blocked = cooldown_blocked and cooldown_enabled
self._last_filter_results.append({
"name": "Trade Cooldown",
"passed": not cooldown_filter_blocked,
"detail": (f"{cooldown_remaining:.0f}s left" if cooldown_blocked else "OK") + (" [DISABLED]" if not cooldown_enabled else "")
})
if cooldown_filter_blocked:
logger.info(f"Trade cooldown: {cooldown_remaining:.0f}s remaining")
return