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:
+93
-27
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user