From 61877480b3b63d43d5b7a682bc25b92fd0474c72 Mon Sep 17 00:00:00 2001 From: GifariKemal Date: Sun, 8 Feb 2026 13:45:31 +0700 Subject: [PATCH] feat: add full dashboard monitoring + FEATURES.md documentation - Create docs/FEATURES.md with complete feature reference (14 entry filters, 12 exit conditions, backtest history, risk modes, session rules, auto-trainer, active components table, architecture diagram) - Extend main_live.py _write_dashboard_status() with 10 new data sections: entryFilters, riskMode, cooldown, timeFilter, sessionMultiplier, positionDetails, autoTrainer, performance, marketClose, h1BiasDetails. Add filter tracking at each checkpoint in _trading_iteration() and 7 helper methods. - Add 9 TypeScript interfaces and extend TradingStatus in trading.ts - Create BotStatusCard (risk mode, cooldown bar, AUC, uptime, market close) and EntryFilterCard (14 filters with pass/block/skip icons) - Enhance SessionCard (lot multiplier badge + time filter status), RiskCard (risk mode badge + total loss progress bar), PositionsCard (expandable per-position details with momentum, TP probability) - Update page.tsx layout: BotStatusCard replaces SettingsCard in Row 2, EntryFilterCard added to Row 3 sidebar - Add API defaults for all new fields Dashboard now monitors 100% of bot features. Verified: Next.js build 0 errors, bot + API + dashboard all run clean, Docker rebuilt OK. Co-Authored-By: Claude Opus 4.6 --- docs/FEATURES.md | 346 ++++++++++++++++++ main_live.py | 226 +++++++++++- web-dashboard/api/main.py | 10 + web-dashboard/src/app/page.tsx | 28 +- .../components/dashboard/bot-status-card.tsx | 114 ++++++ .../dashboard/entry-filter-card.tsx | 86 +++++ .../src/components/dashboard/index.ts | 2 + .../components/dashboard/positions-card.tsx | 121 ++++-- .../src/components/dashboard/risk-card.tsx | 56 ++- .../src/components/dashboard/session-card.tsx | 33 +- web-dashboard/src/types/trading.ts | 81 ++++ 11 files changed, 1056 insertions(+), 47 deletions(-) create mode 100644 docs/FEATURES.md create mode 100644 web-dashboard/src/components/dashboard/bot-status-card.tsx create mode 100644 web-dashboard/src/components/dashboard/entry-filter-card.tsx diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..839354b --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,346 @@ +# XAUBot AI — Feature Reference + +## Overview + +XAUBot AI is an automated XAUUSD (Gold) trading bot that combines **XGBoost Machine Learning**, **Smart Money Concepts (SMC)**, and **Hidden Markov Model (HMM)** regime detection. It operates on MetaTrader 5 via an asynchronous Python loop, executing trades on the M15 (15-minute) timeframe. + +The bot follows a strict pipeline: data is fetched, features are engineered, market structure is analyzed, regime is classified, ML predictions are generated, and a series of 14 sequential filters determine whether a trade is executed. Once in a position, 12 exit conditions are monitored every 5-10 seconds. + +--- + +## Entry Filter Pipeline + +There are **14 filters** that run in order during `_trading_iteration()`. A signal must pass **ALL** of them to execute a trade. + +### 1. Data Fetch +- Pulls **200 M15 bars** from MetaTrader 5. +- Data is converted to a **Polars DataFrame** (not Pandas). + +### 2. Feature Engineering +- Calculates **37 technical features** from the OHLCV data. +- Includes: RSI, ATR, MACD, Bollinger Bands, EMA (multiple periods), Stochastic, volume-based indicators, and more. +- All computations use Polars for performance. + +### 3. SMC Analysis +- Detects institutional **Smart Money Concepts** structures: + - **Order Blocks (OB)** — supply/demand zones from institutional activity. + - **Fair Value Gaps (FVG)** — imbalances in price action. + - **Break of Structure (BOS)** — continuation signals. + - **Change of Character (CHoCH)** — reversal signals. + +### 4. Regime Detection +- **HMM (Hidden Markov Model)** classifies the current market state: + - `TRENDING` — directional movement, favorable for entries. + - `RANGING` — sideways consolidation, reduced sizing. + - `HIGH_VOLATILITY` — erratic movement, caution required. + - `CRISIS` — extreme conditions, trading blocked. + +### 5. Flash Crash Guard +- Emergency protection: if price move exceeds a threshold percentage, **all positions are immediately closed**. +- Prevents catastrophic loss during sudden market dislocations. + +### 6. Regime Filter +- Blocks trading entirely if the regime recommendation is `SLEEP`. +- Prevents entries during unfavorable market conditions identified by the HMM. + +### 7. Risk Check +- Blocks trading if: + - **Daily loss limit** has been reached (5% of capital). + - **Equity** is too low relative to required margin. + - **Total loss limit** has been breached (10% of capital). + +### 8. Session Filter +- Filters based on **WIB (Western Indonesian Time)** trading sessions. +- Each session applies a **lot size multiplier** to control exposure: + - **Sydney** (06:00-13:00 WIB) — 0.5x multiplier (low volatility). + - **Tokyo** (07:00-16:00 WIB) — 0.7x multiplier (medium volatility). + - **London** (15:00-24:00 WIB) — 1.0x multiplier (high volatility). + - **New York** (20:00-24:00 WIB) — 1.0x multiplier (extreme volatility). + - **Off-Hours** (00:00-06:00 WIB) — **blocked entirely**. + +### 9. H1 Bias Filter (#31B) +- Multi-timeframe confirmation using **EMA20 on the H1 chart**. +- Price position relative to H1 EMA20 determines directional bias: + - **BULLISH** (price above EMA20) — only BUY signals allowed. + - **BEARISH** (price below EMA20) — only SELL signals allowed. + - **NEUTRAL** (price near EMA20) — **all signals blocked**. +- Backtest result: **+$343 improvement, 81.8% win rate, Sharpe 3.97**. + +### 10. SMC Signal Generation +- Generates a **BUY or SELL signal** based on SMC structure analysis. +- Each signal includes a **confidence score** derived from the quality of the detected structures (OB proximity, FVG alignment, BOS/CHoCH context). + +### 11. Signal Combination +- Combines **SMC signal + ML (XGBoost) prediction**. +- Applies a **dynamic confidence threshold** that adapts based on: + - Current trading session. + - Market regime. + - Recent volatility. +- Both signals must agree on direction; combined confidence must exceed the threshold. + +### 12. Time Filter (#34A) +- Skips specific WIB hours known for poor conditions: + - **Hour 9 WIB** — end of New York session, low liquidity. + - **Hour 21 WIB** — London-New York transition, prone to whipsaw. +- Backtest result: **+$356 improvement**. + +### 13. Trade Cooldown +- Enforces a minimum **150 seconds (2.5 minutes)** between consecutive trades. +- Prevents overtrading and rapid-fire entries from noisy signals. + +### 14. Smart Risk Gate +- Final gate before execution. Checks: + - **Trading mode**: `NORMAL`, `RECOVERY`, `PROTECTED`, or `STOPPED`. + - **Lot size calculation**: Based on ATR, capital mode, and session multiplier. + - **Position limit**: Maximum **2 concurrent positions** allowed. +- If mode is `STOPPED`, no trade is executed regardless of signal quality. + +--- + +## Exit Conditions + +**12 exit conditions** are checked every **5-10 seconds** while a position is open. + +### 1. Take Profit (Broker-Level TP) +- TP is set at the broker level at entry time. +- Calculated using ATR-based risk-reward ratios. + +### 2. Trailing Stop (#24B) +- **ATR-adaptive trailing stop**: + - Activation distance: **ATR x 4.0**. + - Step size: **ATR x 3.0**. +- Locks in profits as price moves favorably. + +### 3. Breakeven Move (#24B) +- Moves stop loss to **entry price** (breakeven) when unrealized profit exceeds **ATR x 2.0**. +- Eliminates risk on the trade after a favorable move. + +### 4. ML Reversal Exit +- Closes the position if the ML model's confidence **flips direction** with confidence exceeding **75%**. +- Responds to changing market conditions detected by XGBoost. + +### 5. Max Loss Per Trade +- **Software-level stop loss** at **1% of capital**. +- Acts as a safety net in addition to broker SL. + +### 6. Daily Loss Limit +- If cumulative daily loss reaches **5% of capital**, **all positions are closed** and trading halts for the day. + +### 7. Total Loss Limit +- If cumulative total loss reaches **10% of capital**, **trading is stopped entirely** until manual intervention. + +### 8. Market Close Handler +- Before daily close or weekend close: + - Takes profit on positions with unrealized profit **> $5**. + - Prevents gap risk from overnight/weekend holds. + +### 9. Flash Crash Emergency +- Triggered by sudden extreme price movement. +- **Immediately closes all open positions** without delay. + +### 10. Drawdown Protection +- Monitors drawdown from equity peak. +- Closes all positions if drawdown exceeds **50%** from the peak. + +### 11. Impulse Trail (#33B) +- Enhanced trailing stop using **impulse candle detection**. +- Identifies strong momentum candles and trails the stop behind them. +- More responsive than standard ATR trailing in trending conditions. + +### 12. Smart Breakeven (#28B) +- Enhanced breakeven logic with **ATR multiplier triggers**: + - Trigger: profit exceeds **ATR x 2.0**. + - Moves SL to entry + small buffer. +- More adaptive than fixed-pip breakeven. + +--- + +## Backtest Optimization History + +Summary of key optimizations applied to the live bot, tested and validated through backtesting. + +| # | Name | Key Change | Result | +|---|------|------------|--------| +| #24B | ATR-Adaptive Exit | ATR-based trailing (4.0x) and breakeven (2.0x) multipliers | Base optimization for exit logic | +| #28B | Smart Breakeven | Enhanced breakeven with ATR x 2.0 trigger | Improved exit timing on winning trades | +| #31B | H1 EMA20 Filter | H1 price vs EMA20 multi-timeframe filter | +$343, WR 81.8%, Sharpe 3.97 | +| #33B | Impulse Trail | Trail using impulse candle detection | Better trailing in trending markets | +| #34A | Skip Hours | Skip WIB hours 9 and 21 | +$356, reduced whipsaw losses | + +--- + +## Risk Management + +### Capital Modes + +Capital modes are auto-configured based on account balance. Each mode sets risk parameters appropriate for the account size. + +| Mode | Capital Range | Risk/Trade | Max Lot | +|------|--------------|------------|---------| +| MICRO | < $500 | 2% | 0.02 | +| SMALL | $500 - $10,000 | 1.5% | 0.05 | +| MEDIUM | $10,000 - $100,000 | 0.5% | 0.10 | +| LARGE | > $100,000 | 0.25% | 0.50 | + +### Trading Modes + +The Smart Risk Manager dynamically adjusts the trading mode based on recent performance. + +| Mode | Trigger | Lot Adjustment | +|------|---------|---------------| +| NORMAL | Default state | Base lot (0.01-0.03) | +| RECOVERY | After a losing trade | Recovery lot (0.01) | +| PROTECTED | Approaching daily loss limit | Minimum lot (0.01) | +| STOPPED | Daily or total loss limit hit | No trading allowed | + +### Risk Limits + +| Limit | Value | Action | +|-------|-------|--------| +| Max daily loss | 5% of capital | Close all positions, halt trading for the day | +| Max total loss | 10% of capital | Stop all trading until manual reset | +| Max loss per trade | 1% of capital | Software stop loss | +| Emergency broker SL | 2% of capital | Broker-level hard stop | +| Max concurrent positions | 2 | Reject new entries if at limit | + +--- + +## Session Filter (WIB) + +All session times are in **WIB (Western Indonesian Time, UTC+7)**. + +| Session | Hours (WIB) | Volatility | Lot Multiplier | +|---------|-------------|------------|----------------| +| Sydney | 06:00 - 13:00 | Low | 0.5x | +| Tokyo | 07:00 - 16:00 | Medium | 0.7x | +| London | 15:00 - 24:00 | High | 1.0x | +| New York | 20:00 - 24:00 | Extreme | 1.0x | +| Off-Hours | 00:00 - 06:00 | N/A | **Blocked** | + +### Golden Hour +- **19:00 - 23:00 WIB** (London-New York Overlap). +- Highest liquidity and volatility period for XAUUSD. +- Best trading conditions; full lot multiplier applied. + +### Skip Hours (#34A) +- **Hour 9 WIB** — End of New York session; low liquidity leads to erratic fills. +- **Hour 21 WIB** — London-New York transition; prone to whipsaw and false breakouts. + +--- + +## Auto-Trainer + +The bot includes an automatic model retraining pipeline to keep the ML model current with market conditions. + +| Parameter | Value | +|-----------|-------| +| Check interval | Every 20 candles (~5 hours on M15) | +| Daily retrain | 05:00 WIB (during market close) | +| Weekend training | Deep training with expanded data window | +| Min AUC threshold | 0.65 | +| Rollback policy | If new model performs worse, revert to backup | + +### Retraining Flow +1. Every 20 candles, the auto-trainer checks model performance metrics. +2. If AUC drops below **0.65**, a retrain is triggered. +3. At **05:00 WIB daily** (market close), a scheduled retrain runs. +4. On **weekends**, deep training uses a larger historical dataset. +5. After training, the new model is validated against the previous one. +6. If the new model underperforms, the system **rolls back** to the backup model. + +--- + +## ML Model + +### Algorithm +- **XGBoost** gradient-boosted decision trees. + +### Features +- **37 technical indicators** computed by `src/feature_eng.py`: + - Trend: EMA (multiple periods), MACD, ADX. + - Momentum: RSI, Stochastic K/D. + - Volatility: ATR, Bollinger Bands (width, %B). + - Volume: Volume-weighted indicators. + - Custom: SMC-derived features, regime features. + +### Output +- **Signal**: BUY, SELL, or HOLD. +- **Confidence score**: 0.0 to 1.0, used in combination with SMC confidence. + +### Dynamic Threshold +- The confidence threshold for trade execution is not fixed. +- It adjusts based on: + - **Session**: Higher threshold during low-volatility sessions. + - **Regime**: Higher threshold during ranging/volatile regimes. + - **Recent performance**: Tightens after losses, relaxes after wins. + +--- + +## Active Components + +| Component | File | Status | Description | +|-----------|------|--------|-------------| +| SMC Analyzer | `src/smc_polars.py` | Active | Order Block, FVG, BOS, CHoCH detection | +| XGBoost ML | `src/ml_model.py` | Active | Signal prediction with confidence | +| HMM Regime | `src/regime_detector.py` | Active | Market regime classification | +| Feature Engine | `src/feature_eng.py` | Active | 37 technical feature computation | +| Risk Engine | `src/risk_engine.py` | Active | ATR-based SL/TP, position sizing | +| Smart Risk Manager | `src/smart_risk_manager.py` | Active | Dynamic mode management | +| Position Manager | `src/position_manager.py` | Active | Exit condition monitoring | +| Session Filter | `src/session_filter.py` | Active | WIB session-based filtering | +| Dynamic Confidence | `src/dynamic_confidence.py` | Active | Adaptive threshold adjustment | +| Auto Trainer | `src/auto_trainer.py` | Active | Scheduled model retraining | +| Telegram Notifier | `src/telegram_notifier.py` | Active | Trade alerts via Telegram | +| Trade Logger | `src/trade_logger.py` | Active | PostgreSQL trade logging | +| News Agent | `src/news_agent.py` | **DISABLED** | Economic news filter (costs $178 profit in backtest) | +| Flash Crash Detector | `src/regime_detector.py` | Active | Emergency position closure | + +--- + +## Architecture Diagram + +``` +MT5 Broker + | + v +[Data Fetch] --> [Feature Eng (37)] --> [SMC Analysis] --> [Regime Detection (HMM)] + | + v + [Flash Crash Guard] + | + v + [Regime Filter] + | + v + [Risk Check] + | + v + [Session Filter] + | + v + [H1 Bias Filter (#31B)] + | + v + [SMC Signal Gen] + | + v + [Signal Combination (ML+SMC)] + | + v + [Time Filter (#34A)] + | + v + [Trade Cooldown] + | + v + [Smart Risk Gate] + | + v + [TRADE EXECUTION] + | + v + [Position Manager (12 exits)] + | + v + [Telegram + PostgreSQL Logging] +``` diff --git a/main_live.py b/main_live.py index b3c6171..4165c16 100644 --- a/main_live.py +++ b/main_live.py @@ -170,6 +170,8 @@ class TradingBot: # State tracking self._running = False self._loop_count = 0 + self._h1_bias_cache = "NEUTRAL" + self._h1_bias_loop = 0 self._last_signal: Optional[SMCSignal] = None self._last_retrain_check: Optional[datetime] = None self._last_trade_time: Optional[datetime] = None @@ -190,6 +192,13 @@ class TradingBot: self._last_candle_time: Optional[datetime] = None # Track last processed candle self._position_check_interval: int = 10 # Check positions every N seconds between candles + # Entry filter tracking for dashboard + self._last_filter_results: list = [] + + # H1 EMA cache for dashboard + self._h1_ema20_value: float = 0.0 + self._h1_current_price: float = 0.0 + # Dashboard status bridge (written to JSON for Docker API) self._dash_price_history: deque = deque(maxlen=120) self._dash_equity_history: deque = deque(maxlen=120) @@ -392,6 +401,40 @@ class TradingBot: "dynamicThreshold": getattr(self, "_last_dynamic_threshold", self.config.ml.confidence_threshold), "marketQuality": getattr(self, "_last_market_quality", "unknown"), "marketScore": getattr(self, "_last_market_score", 0), + + # === NEW: Entry Filter Pipeline === + "entryFilters": getattr(self, "_last_filter_results", []), + + # === NEW: Risk Mode === + "riskMode": self._get_risk_mode_status(), + + # === NEW: Cooldown === + "cooldown": self._get_cooldown_status(), + + # === NEW: Time Filter === + "timeFilter": self._get_time_filter_status(), + + # === NEW: Session extras === + "sessionMultiplier": getattr(self, "_current_session_multiplier", 1.0), + + # === NEW: Position Details === + "positionDetails": self._get_position_details(), + + # === NEW: Auto Trainer === + "autoTrainer": self._get_auto_trainer_status(), + + # === NEW: Performance === + "performance": self._get_performance_status(), + + # === NEW: Market Close === + "marketClose": self._get_market_close_status(), + + # === NEW: H1 Bias Details === + "h1BiasDetails": { + "bias": getattr(self, "_h1_bias_cache", "NEUTRAL"), + "ema20": getattr(self, "_h1_ema20_value", 0.0), + "price": getattr(self, "_h1_current_price", 0.0), + }, } # Atomic write (write to temp then rename) @@ -402,6 +445,144 @@ class TradingBot: except Exception as e: logger.debug(f"Dashboard status write error: {e}") + def _get_risk_mode_status(self) -> dict: + """Get risk mode info for dashboard.""" + try: + rec = self.smart_risk.get_trading_recommendation() + return { + "mode": rec.get("mode", "normal"), + "reason": rec.get("reason", ""), + "recommendedLot": rec.get("recommended_lot", 0.01), + "maxAllowedLot": rec.get("max_lot", 0.03), + "totalLoss": rec.get("total_loss", 0.0), + "maxTotalLoss": self.smart_risk.max_total_loss_usd, + "remainingDailyRisk": rec.get("remaining_daily_risk", 0.0), + } + except Exception: + return {"mode": "unknown", "reason": "", "recommendedLot": 0.01, "maxAllowedLot": 0.03, "totalLoss": 0.0, "maxTotalLoss": 0.0, "remainingDailyRisk": 0.0} + + def _get_cooldown_status(self) -> dict: + """Get trade cooldown info for dashboard.""" + try: + if self._last_trade_time: + elapsed = (datetime.now() - self._last_trade_time).total_seconds() + remaining = max(0, self._trade_cooldown_seconds - elapsed) + return { + "active": remaining > 0, + "secondsRemaining": round(remaining), + "totalSeconds": self._trade_cooldown_seconds, + } + return {"active": False, "secondsRemaining": 0, "totalSeconds": self._trade_cooldown_seconds} + except Exception: + return {"active": False, "secondsRemaining": 0, "totalSeconds": 150} + + def _get_time_filter_status(self) -> dict: + """Get time filter (#34A) status for dashboard.""" + try: + wib_hour = datetime.now(ZoneInfo("Asia/Jakarta")).hour + blocked_hours = [9, 21] + return { + "wibHour": wib_hour, + "isBlocked": wib_hour in blocked_hours, + "blockedHours": blocked_hours, + } + except Exception: + return {"wibHour": 0, "isBlocked": False, "blockedHours": [9, 21]} + + def _get_position_details(self) -> list: + """Get detailed position info from SmartRiskManager guards.""" + details = [] + try: + for ticket, guard in self.smart_risk._position_guards.items(): + trade_hours = (datetime.now(ZoneInfo("Asia/Jakarta")) - guard.entry_time).total_seconds() / 3600 + drawdown_pct = 0.0 + if guard.peak_profit > 0: + drawdown_pct = ((guard.peak_profit - guard.current_profit) / guard.peak_profit) * 100 + + details.append({ + "ticket": ticket, + "peakProfit": guard.peak_profit, + "drawdownFromPeak": round(drawdown_pct, 1), + "momentum": round(guard.momentum_score, 1), + "tpProbability": round(guard.get_tp_probability(), 1), + "reversalWarnings": guard.reversal_warnings, + "stalls": guard.stall_count, + "tradeHours": round(trade_hours, 1), + }) + except Exception: + pass + return details + + def _get_auto_trainer_status(self) -> dict: + """Get auto trainer status for dashboard.""" + try: + hours_since = 0.0 + if self.auto_trainer._last_retrain_time: + hours_since = (datetime.now(ZoneInfo("Asia/Jakarta")) - self.auto_trainer._last_retrain_time).total_seconds() / 3600 + + return { + "lastRetrain": self.auto_trainer._last_retrain_time.strftime("%Y-%m-%d %H:%M") if self.auto_trainer._last_retrain_time else None, + "currentAuc": self.auto_trainer._current_auc, + "minAucThreshold": self.auto_trainer.min_auc_threshold, + "hoursSinceRetrain": round(hours_since, 1), + "nextRetrainHour": self.auto_trainer.daily_retrain_hour, + "modelsFitted": self.ml_model.fitted and self.regime_detector.fitted, + } + except Exception: + return {"lastRetrain": None, "currentAuc": None, "minAucThreshold": 0.65, "hoursSinceRetrain": 0, "nextRetrainHour": 5, "modelsFitted": False} + + def _get_performance_status(self) -> dict: + """Get bot performance stats for dashboard.""" + try: + uptime_hours = (datetime.now() - self._start_time).total_seconds() / 3600 + avg_ms = 0.0 + if self._execution_times: + recent = self._execution_times[-20:] + avg_ms = (sum(recent) / len(recent)) * 1000 + + return { + "loopCount": self._loop_count, + "avgExecutionMs": round(avg_ms, 1), + "uptimeHours": round(uptime_hours, 1), + "totalSessionTrades": self._total_session_trades, + "totalSessionProfit": round(self._total_session_profit, 2), + } + except Exception: + return {"loopCount": 0, "avgExecutionMs": 0, "uptimeHours": 0, "totalSessionTrades": 0, "totalSessionProfit": 0} + + def _get_market_close_status(self) -> dict: + """Get market close timing info for dashboard.""" + try: + now = datetime.now(ZoneInfo("Asia/Jakarta")) + # Daily close: ~05:00 WIB (rollover) + daily_close_hour = 5 + if now.hour >= daily_close_hour: + hours_to_daily = (24 - now.hour + daily_close_hour) + (0 - now.minute) / 60 + else: + hours_to_daily = (daily_close_hour - now.hour) + (0 - now.minute) / 60 + + # Weekend close: Friday ~04:00 WIB (Saturday) + weekday = now.weekday() # 0=Mon + if weekday < 4: # Mon-Thu + days_to_fri = 4 - weekday + hours_to_weekend = days_to_fri * 24 + (daily_close_hour - now.hour) + elif weekday == 4: # Friday + hours_to_weekend = max(0, (24 + daily_close_hour - now.hour)) + else: # Sat-Sun + hours_to_weekend = 0 + + # Market open: Mon-Fri 06:00-05:00 WIB (next day) + market_open = weekday < 5 and (now.hour >= 6 or now.hour < 4) + + return { + "hoursToDailyClose": round(max(0, hours_to_daily), 1), + "hoursToWeekendClose": round(max(0, hours_to_weekend), 1), + "nearWeekend": weekday == 4 and now.hour >= 20, + "marketOpen": market_open, + } + except Exception: + return {"hoursToDailyClose": 0, "hoursToWeekendClose": 0, "nearWeekend": False, "marketOpen": False} + async def start(self): """Start the trading bot.""" logger.info("=" * 60) @@ -571,6 +752,8 @@ class TradingBot: # Cache result self._h1_bias_cache = bias self._h1_bias_loop = self._loop_count + self._h1_ema20_value = float(ema) + self._h1_current_price = float(current_price) if self._loop_count % 4 == 0: logger.info(f"H1 Bias: {bias} (price={current_price:.2f}, EMA20={ema:.2f})") @@ -741,6 +924,9 @@ class TradingBot: async def _trading_iteration(self): """Single trading iteration.""" + # Reset filter tracking for dashboard + self._last_filter_results = [] + # 1. Fetch fresh data df = self.mt5.get_market_data( symbol=self.config.symbol, @@ -777,6 +963,7 @@ 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: logger.warning(f"Flash crash detected: {move_pct:.2f}% move") try: @@ -857,16 +1044,20 @@ class TradingBot: ) # 7. Check regime allows trading - if regime_state and regime_state.recommendation == "SLEEP": + 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: 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: 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: if self._loop_count % 300 == 0: # Log every 5 minutes logger.info(f"Session filter: {session_reason}") @@ -913,40 +1104,59 @@ class TradingBot: if self._loop_count > 0 and self._loop_count % 30 == 0: await self._send_market_update(df, regime_state, ml_prediction) + # Track SMC signal for filter pipeline + self._last_filter_results.append({"name": "SMC Signal", "passed": smc_signal is not None, "detail": f"{smc_signal.signal_type} ({smc_signal.confidence:.0%})" if smc_signal else "No signal"}) + # 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"}) if final_signal is None: 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_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"): + 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 NEUTRAL = block both directions (strict mode from backtest) + 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}"}) # 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 - from zoneinfo import ZoneInfo wib_hour = datetime.now(ZoneInfo("Asia/Jakarta")).hour - if wib_hour in (9, 21): + 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: logger.info(f"Time Filter: {final_signal.signal_type} blocked (WIB hour {wib_hour} is skip hour)") return # 10.5 Check trade cooldown + cooldown_blocked = False + cooldown_remaining = 0 if self._last_trade_time: time_since_last = (datetime.now() - self._last_trade_time).total_seconds() - if time_since_last < self._trade_cooldown_seconds: - logger.info(f"Trade cooldown: {self._trade_cooldown_seconds - time_since_last:.0f}s remaining") - return + 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: + logger.info(f"Trade cooldown: {cooldown_remaining:.0f}s remaining") + return # 10.6 PULLBACK FILTER - DISABLED (SMC-only mode) # SMC structure already validates entry zones @@ -954,6 +1164,7 @@ class TradingBot: # 11. SMART RISK CHECK - Ultra safe mode self.smart_risk.check_new_day() risk_rec = self.smart_risk.get_trading_recommendation() + self._last_filter_results.append({"name": "Smart Risk Gate", "passed": risk_rec["can_trade"], "detail": risk_rec.get("reason", risk_rec["mode"])}) if not risk_rec["can_trade"]: logger.warning(f"Smart Risk: Trading blocked - {risk_rec['reason']}") @@ -1005,6 +1216,7 @@ class TradingBot: # 13. Check position limit (max 2 concurrent positions) can_open, limit_reason = self.smart_risk.can_open_position() + self._last_filter_results.append({"name": "Position Limit", "passed": can_open, "detail": limit_reason if not can_open else "OK"}) if not can_open: logger.warning(f"Position limit: {limit_reason} - skipping trade") return diff --git a/web-dashboard/api/main.py b/web-dashboard/api/main.py index c731422..140fb0c 100644 --- a/web-dashboard/api/main.py +++ b/web-dashboard/api/main.py @@ -54,6 +54,16 @@ DEFAULT_STATUS = { "regime": {"name": "", "volatility": 0.0, "confidence": 0.0}, "positions": [], "logs": [], + "entryFilters": [], + "riskMode": {"mode": "unknown", "reason": "", "recommendedLot": 0, "maxAllowedLot": 0, "totalLoss": 0, "maxTotalLoss": 0, "remainingDailyRisk": 0}, + "cooldown": {"active": False, "secondsRemaining": 0, "totalSeconds": 150}, + "timeFilter": {"wibHour": 0, "isBlocked": False, "blockedHours": [9, 21]}, + "sessionMultiplier": 1.0, + "positionDetails": [], + "autoTrainer": {"lastRetrain": None, "currentAuc": None, "minAucThreshold": 0.65, "hoursSinceRetrain": 0, "nextRetrainHour": 5, "modelsFitted": False}, + "performance": {"loopCount": 0, "avgExecutionMs": 0, "uptimeHours": 0, "totalSessionTrades": 0, "totalSessionProfit": 0}, + "marketClose": {"hoursToDailyClose": 0, "hoursToWeekendClose": 0, "nearWeekend": False, "marketOpen": False}, + "h1BiasDetails": {"bias": "NEUTRAL", "ema20": 0, "price": 0}, } diff --git a/web-dashboard/src/app/page.tsx b/web-dashboard/src/app/page.tsx index 5a7336e..7a68671 100644 --- a/web-dashboard/src/app/page.tsx +++ b/web-dashboard/src/app/page.tsx @@ -12,7 +12,8 @@ import { PositionsCard, LogCard, PriceChart, - SettingsCard, + BotStatusCard, + EntryFilterCard, } from "@/components/dashboard"; import { Skeleton } from "@/components/ui/skeleton"; @@ -121,6 +122,8 @@ export default function Dashboard() { session={data.session} isGoldenTime={data.isGoldenTime} canTrade={data.canTrade} + sessionMultiplier={data.sessionMultiplier} + timeFilter={data.timeFilter} />
@@ -129,11 +132,12 @@ export default function Dashboard() { dailyProfit={data.dailyProfit} consecutiveLosses={data.consecutiveLosses} riskPercent={data.riskPercent} + riskMode={data.riskMode} />
- {/* ── Row 2: Signals ── */} + {/* ── Row 2: Signals + Bot Status ── */}
- {data.settings ? ( - - ) : ( -
- )} +
@@ -188,8 +194,14 @@ export default function Dashboard() {
+
+ +
- +
diff --git a/web-dashboard/src/components/dashboard/bot-status-card.tsx b/web-dashboard/src/components/dashboard/bot-status-card.tsx new file mode 100644 index 0000000..5307cda --- /dev/null +++ b/web-dashboard/src/components/dashboard/bot-status-card.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Activity, Timer, Brain, Gauge, Clock } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { RiskMode, CooldownStatus, AutoTrainerStatus, PerformanceStatus, MarketCloseStatus } from "@/types/trading"; + +interface BotStatusCardProps { + riskMode?: RiskMode; + cooldown?: CooldownStatus; + autoTrainer?: AutoTrainerStatus; + performance?: PerformanceStatus; + marketClose?: MarketCloseStatus; +} + +function getRiskModeVariant(mode: string) { + switch (mode) { + case "normal": return "success"; + case "recovery": return "warning"; + case "protected": return "danger"; + case "stopped": return "danger"; + default: return "secondary"; + } +} + +export function BotStatusCard({ riskMode, cooldown, autoTrainer, performance, marketClose }: BotStatusCardProps) { + const mode = riskMode?.mode || "unknown"; + const aucColor = (autoTrainer?.currentAuc ?? 0) >= 0.7 ? "text-success" : (autoTrainer?.currentAuc ?? 0) >= 0.65 ? "text-warning" : "text-danger"; + + return ( + + + + + Bot Status + + + + {/* Risk Mode */} +
+ Risk Mode + + {mode} + +
+ + {/* Cooldown */} +
+
+ + + Cooldown + + + {cooldown?.active ? `${cooldown.secondsRemaining}s` : "Ready"} + +
+ {cooldown?.active && ( +
+
0 ? ((cooldown.totalSeconds - cooldown.secondsRemaining) / cooldown.totalSeconds) * 100 : 0}%` }} + /> +
+ )} +
+ + {/* Auto Trainer */} +
+ + + Model AUC + + + {autoTrainer?.currentAuc != null ? autoTrainer.currentAuc.toFixed(3) : "N/A"} + +
+ + {/* Performance */} +
+ + + Uptime + + + {performance ? `${performance.uptimeHours}h | ${performance.loopCount} loops` : "—"} + +
+ +
+ Exec Speed + 50 ? "text-warning" : "text-success")}> + {performance ? `${performance.avgExecutionMs}ms` : "—"} + +
+ + {/* Market Close */} +
+ + + Close + + + {marketClose ? `D:${marketClose.hoursToDailyClose}h W:${marketClose.hoursToWeekendClose}h` : "—"} + +
+ + + ); +} diff --git a/web-dashboard/src/components/dashboard/entry-filter-card.tsx b/web-dashboard/src/components/dashboard/entry-filter-card.tsx new file mode 100644 index 0000000..ebc4b40 --- /dev/null +++ b/web-dashboard/src/components/dashboard/entry-filter-card.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Filter, Check, X, Minus } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { EntryFilter } from "@/types/trading"; + +interface EntryFilterCardProps { + filters: EntryFilter[]; +} + +export function EntryFilterCard({ filters }: EntryFilterCardProps) { + const passedCount = filters.filter((f) => f.passed).length; + const totalCount = filters.length; + const hasBlocker = filters.some((f) => !f.passed); + + // Find the first blocker index — filters after it were not evaluated + const firstBlockerIdx = filters.findIndex((f) => !f.passed); + + return ( + + + + + Entry Filters + {totalCount > 0 && ( + + {passedCount}/{totalCount} + + )} + + + + {totalCount === 0 ? ( +
+ +

Waiting for candle...

+
+ ) : ( +
+ {filters.map((filter, idx) => { + // Determine status: passed, blocked, or not evaluated + const isNotEvaluated = firstBlockerIdx >= 0 && idx > firstBlockerIdx; + const isBlocker = !filter.passed && idx === firstBlockerIdx; + + return ( +
+ {isNotEvaluated ? ( + + ) : filter.passed ? ( + + ) : ( + + )} + + {filter.name} + + + {filter.detail} + +
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/web-dashboard/src/components/dashboard/index.ts b/web-dashboard/src/components/dashboard/index.ts index f8c9499..f995da1 100644 --- a/web-dashboard/src/components/dashboard/index.ts +++ b/web-dashboard/src/components/dashboard/index.ts @@ -11,3 +11,5 @@ export { EquityChart } from "./equity-chart"; export { Header } from "./header"; export { Sparkline } from "./sparkline"; export { SettingsCard } from "./settings-card"; +export { BotStatusCard } from "./bot-status-card"; +export { EntryFilterCard } from "./entry-filter-card"; diff --git a/web-dashboard/src/components/dashboard/positions-card.tsx b/web-dashboard/src/components/dashboard/positions-card.tsx index 9d403cd..42441c1 100644 --- a/web-dashboard/src/components/dashboard/positions-card.tsx +++ b/web-dashboard/src/components/dashboard/positions-card.tsx @@ -1,16 +1,23 @@ "use client"; +import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Layers, Inbox } from "lucide-react"; +import { Layers, Inbox, ChevronDown, ChevronUp } from "lucide-react"; import { cn } from "@/lib/utils"; -import type { Position } from "@/types/trading"; +import type { Position, PositionDetail } from "@/types/trading"; interface PositionsCardProps { positions: Position[]; + positionDetails?: PositionDetail[]; } -export function PositionsCard({ positions }: PositionsCardProps) { +export function PositionsCard({ positions, positionDetails }: PositionsCardProps) { + const [expandedTicket, setExpandedTicket] = useState(null); + + const getDetail = (ticket: number) => + positionDetails?.find((d) => d.ticket === ticket); + return ( @@ -32,33 +39,91 @@ export function PositionsCard({ positions }: PositionsCardProps) {
) : (
- {positions.map((pos) => ( -
-
- { + const detail = getDetail(pos.ticket); + const isExpanded = expandedTicket === pos.ticket; + const hasDetail = !!detail; + + return ( +
+
hasDetail && setExpandedTicket(isExpanded ? null : pos.ticket)} > - {pos.type} - - - {pos.volume} @ {pos.priceOpen.toFixed(2)} - +
+ + {pos.type} + + + {pos.volume} @ {pos.priceOpen.toFixed(2)} + +
+
+ = 0 ? "text-success" : "text-danger" + )}> + {pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)} + + {hasDetail && ( + isExpanded + ? + : + )} +
+
+ + {/* Expandable Details */} + {isExpanded && detail && ( +
+
+ Peak Profit + ${detail.peakProfit.toFixed(2)} +
+
+ DD from Peak + 30 ? "text-danger" : "text-muted-foreground")}> + {detail.drawdownFromPeak.toFixed(1)}% + +
+
+ Momentum + 0 ? "text-success" : detail.momentum < 0 ? "text-danger" : "text-muted-foreground")}> + {detail.momentum > 0 ? "+" : ""}{detail.momentum} + +
+
+ TP Probability + = 50 ? "text-success" : "text-warning")}> + {detail.tpProbability}% + +
+
+ Duration + {detail.tradeHours}h +
+ {(detail.reversalWarnings > 0 || detail.stalls > 0) && ( +
+ {detail.reversalWarnings > 0 && ( + Rev: {detail.reversalWarnings} + )} + {detail.stalls > 0 && ( + Stalls: {detail.stalls} + )} +
+ )} +
+ )}
- = 0 ? "text-success" : "text-danger" - )}> - {pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)} - -
- ))} + ); + })}
)} diff --git a/web-dashboard/src/components/dashboard/risk-card.tsx b/web-dashboard/src/components/dashboard/risk-card.tsx index b1237b1..a094676 100644 --- a/web-dashboard/src/components/dashboard/risk-card.tsx +++ b/web-dashboard/src/components/dashboard/risk-card.tsx @@ -1,17 +1,30 @@ "use client"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { ShieldAlert, AlertTriangle } from "lucide-react"; import { cn, formatUSD } from "@/lib/utils"; +import type { RiskMode } from "@/types/trading"; interface RiskCardProps { dailyLoss: number; dailyProfit: number; consecutiveLosses: number; riskPercent: number; + riskMode?: RiskMode; } -export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercent }: RiskCardProps) { +function getRiskModeVariant(mode: string): "success" | "warning" | "danger" | "secondary" { + switch (mode) { + case "normal": return "success"; + case "recovery": return "warning"; + case "protected": return "danger"; + case "stopped": return "danger"; + default: return "secondary"; + } +} + +export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercent, riskMode }: RiskCardProps) { const isCritical = riskPercent >= 100; const isHigh = riskPercent >= 80; const isMedium = riskPercent >= 50; @@ -28,6 +41,8 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen return "bg-success"; }; + const mode = riskMode?.mode || "unknown"; + return ( Risk + {/* Risk Mode Badge */} + + {mode} + {isCritical && ( - + BREACHED @@ -81,7 +103,37 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen style={{ width: `${Math.min(riskPercent, 100)}%` }} />
+ {/* Remaining daily risk */} + {riskMode && riskMode.remainingDailyRisk > 0 && ( +
+ Remaining + + {formatUSD(riskMode.remainingDailyRisk)} + +
+ )}
+ + {/* Total Loss Progress */} + {riskMode && riskMode.maxTotalLoss > 0 && ( +
+
+ Total Loss + + {formatUSD(riskMode.totalLoss)} / {formatUSD(riskMode.maxTotalLoss)} + +
+
+
= 0.8 ? "bg-danger" : "bg-warning/60" + )} + style={{ width: `${riskMode.maxTotalLoss > 0 ? Math.min((riskMode.totalLoss / riskMode.maxTotalLoss) * 100, 100) : 0}%` }} + /> +
+
+ )} ); diff --git a/web-dashboard/src/components/dashboard/session-card.tsx b/web-dashboard/src/components/dashboard/session-card.tsx index f7d3b0f..3e394d3 100644 --- a/web-dashboard/src/components/dashboard/session-card.tsx +++ b/web-dashboard/src/components/dashboard/session-card.tsx @@ -2,16 +2,19 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Clock, Sparkles, CheckCircle2, XCircle } from "lucide-react"; +import { Clock, Sparkles, CheckCircle2, XCircle, Ban } from "lucide-react"; import { cn } from "@/lib/utils"; +import type { TimeFilter } from "@/types/trading"; interface SessionCardProps { session: string; isGoldenTime: boolean; canTrade: boolean; + sessionMultiplier?: number; + timeFilter?: TimeFilter; } -export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProps) { +export function SessionCard({ session, isGoldenTime, canTrade, sessionMultiplier, timeFilter }: SessionCardProps) { const getSessionColor = (s: string) => { const lower = s.toLowerCase(); if (lower.includes("london")) return "text-info"; @@ -20,12 +23,21 @@ export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProp return "text-warning"; }; + const mult = sessionMultiplier ?? 1.0; + const multLabel = `${mult}x`; + const multVariant = mult < 1 ? "warning" : mult > 1 ? "success" : "secondary"; + return ( Session + {sessionMultiplier != null && ( + + {multLabel} + + )} @@ -56,6 +68,23 @@ export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProp {canTrade ? "CAN TRADE" : "NO TRADE"}
+ + {/* Time Filter Status */} + {timeFilter && ( +
+ {timeFilter.isBlocked ? ( + + ) : ( + + )} + + WIB {timeFilter.wibHour}:00{timeFilter.isBlocked ? " BLOCKED" : ""} + +
+ )} ); diff --git a/web-dashboard/src/types/trading.ts b/web-dashboard/src/types/trading.ts index 6d57042..6911f7e 100644 --- a/web-dashboard/src/types/trading.ts +++ b/web-dashboard/src/types/trading.ts @@ -1,5 +1,74 @@ // Trading data types +export interface EntryFilter { + name: string; + passed: boolean; + detail: string; +} + +export interface RiskMode { + mode: string; + reason: string; + recommendedLot: number; + maxAllowedLot: number; + totalLoss: number; + maxTotalLoss: number; + remainingDailyRisk: number; +} + +export interface CooldownStatus { + active: boolean; + secondsRemaining: number; + totalSeconds: number; +} + +export interface TimeFilter { + wibHour: number; + isBlocked: boolean; + blockedHours: number[]; +} + +export interface PositionDetail { + ticket: number; + peakProfit: number; + drawdownFromPeak: number; + momentum: number; + tpProbability: number; + reversalWarnings: number; + stalls: number; + tradeHours: number; +} + +export interface AutoTrainerStatus { + lastRetrain: string | null; + currentAuc: number | null; + minAucThreshold: number; + hoursSinceRetrain: number; + nextRetrainHour: number; + modelsFitted: boolean; +} + +export interface PerformanceStatus { + loopCount: number; + avgExecutionMs: number; + uptimeHours: number; + totalSessionTrades: number; + totalSessionProfit: number; +} + +export interface MarketCloseStatus { + hoursToDailyClose: number; + hoursToWeekendClose: number; + nearWeekend: boolean; + marketOpen: boolean; +} + +export interface H1BiasDetails { + bias: string; + ema20: number; + price: number; +} + export interface TradingStatus { timestamp: string; connected: boolean; @@ -63,6 +132,18 @@ export interface TradingStatus { dynamicThreshold?: number; marketQuality?: string; marketScore?: number; + + // === NEW: Extended monitoring === + entryFilters?: EntryFilter[]; + riskMode?: RiskMode; + cooldown?: CooldownStatus; + timeFilter?: TimeFilter; + sessionMultiplier?: number; + positionDetails?: PositionDetail[]; + autoTrainer?: AutoTrainerStatus; + performance?: PerformanceStatus; + marketClose?: MarketCloseStatus; + h1BiasDetails?: H1BiasDetails; } export interface BotSettings {