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 <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
```
|
||||
+219
-7
@@ -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
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
@@ -129,11 +132,12 @@ export default function Dashboard() {
|
||||
dailyProfit={data.dailyProfit}
|
||||
consecutiveLosses={data.consecutiveLosses}
|
||||
riskPercent={data.riskPercent}
|
||||
riskMode={data.riskMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 2: Signals ── */}
|
||||
{/* ── Row 2: Signals + Bot Status ── */}
|
||||
<div
|
||||
className="grid gap-1.5 overflow-hidden"
|
||||
style={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
|
||||
@@ -171,11 +175,13 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
{data.settings ? (
|
||||
<SettingsCard settings={data.settings} />
|
||||
) : (
|
||||
<div className="glass rounded-lg h-full" />
|
||||
)}
|
||||
<BotStatusCard
|
||||
riskMode={data.riskMode}
|
||||
cooldown={data.cooldown}
|
||||
autoTrainer={data.autoTrainer}
|
||||
performance={data.performance}
|
||||
marketClose={data.marketClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,8 +194,14 @@ export default function Dashboard() {
|
||||
<PriceChart data={data.priceHistory} />
|
||||
</div>
|
||||
<div className="min-w-0 min-h-0 overflow-hidden flex flex-col gap-1.5">
|
||||
<div className="min-h-0" style={{ flex: '0 0 auto', maxHeight: '40%' }}>
|
||||
<EntryFilterCard filters={data.entryFilters || []} />
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<PositionsCard positions={data.positions} />
|
||||
<PositionsCard
|
||||
positions={data.positions}
|
||||
positionDetails={data.positionDetails}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<LogCard logs={data.logs} />
|
||||
|
||||
@@ -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 (
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
Bot Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
{/* Risk Mode */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Risk Mode</span>
|
||||
<Badge
|
||||
variant={getRiskModeVariant(mode) as "success" | "warning" | "danger" | "secondary"}
|
||||
className={cn("text-[10px] h-4 px-1.5 uppercase", mode === "stopped" && "animate-pulse")}
|
||||
>
|
||||
{mode}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Cooldown */}
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Timer className="h-2.5 w-2.5" />
|
||||
Cooldown
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-number", cooldown?.active ? "text-warning" : "text-muted-foreground/60")}>
|
||||
{cooldown?.active ? `${cooldown.secondsRemaining}s` : "Ready"}
|
||||
</span>
|
||||
</div>
|
||||
{cooldown?.active && (
|
||||
<div className="h-1 w-full bg-surface-light rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-warning transition-all duration-1000"
|
||||
style={{ width: `${cooldown.totalSeconds > 0 ? ((cooldown.totalSeconds - cooldown.secondsRemaining) / cooldown.totalSeconds) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auto Trainer */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Brain className="h-2.5 w-2.5" />
|
||||
Model AUC
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-bold font-number", aucColor)}>
|
||||
{autoTrainer?.currentAuc != null ? autoTrainer.currentAuc.toFixed(3) : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Performance */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Gauge className="h-2.5 w-2.5" />
|
||||
Uptime
|
||||
</span>
|
||||
<span className="text-[10px] font-number text-foreground">
|
||||
{performance ? `${performance.uptimeHours}h | ${performance.loopCount} loops` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Exec Speed</span>
|
||||
<span className={cn("text-[10px] font-number", (performance?.avgExecutionMs ?? 0) > 50 ? "text-warning" : "text-success")}>
|
||||
{performance ? `${performance.avgExecutionMs}ms` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Market Close */}
|
||||
<div className="pt-0.5 border-t border-border flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
Close
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-number", marketClose?.nearWeekend ? "text-warning font-bold" : "text-muted-foreground")}>
|
||||
{marketClose ? `D:${marketClose.hoursToDailyClose}h W:${marketClose.hoursToWeekendClose}h` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Filter className="h-3.5 w-3.5" />
|
||||
Entry Filters
|
||||
{totalCount > 0 && (
|
||||
<Badge
|
||||
variant={hasBlocker ? "danger" : "success"}
|
||||
className="ml-auto text-[10px] h-4 px-1.5"
|
||||
>
|
||||
{passedCount}/{totalCount}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 min-h-0 overflow-auto">
|
||||
{totalCount === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<Minus className="h-4 w-4 text-muted-foreground/30 mb-1" />
|
||||
<p className="text-[10px] text-muted-foreground/60">Waiting for candle...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{filters.map((filter, idx) => {
|
||||
// Determine status: passed, blocked, or not evaluated
|
||||
const isNotEvaluated = firstBlockerIdx >= 0 && idx > firstBlockerIdx;
|
||||
const isBlocker = !filter.passed && idx === firstBlockerIdx;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${filter.name}-${idx}`}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-1.5 py-0.5 rounded text-[10px]",
|
||||
isBlocker && "bg-danger/10",
|
||||
isNotEvaluated && "opacity-40"
|
||||
)}
|
||||
>
|
||||
{isNotEvaluated ? (
|
||||
<Minus className="h-2.5 w-2.5 text-muted-foreground/40 flex-shrink-0" />
|
||||
) : filter.passed ? (
|
||||
<Check className="h-2.5 w-2.5 text-success flex-shrink-0" />
|
||||
) : (
|
||||
<X className="h-2.5 w-2.5 text-danger flex-shrink-0" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"truncate flex-1",
|
||||
isBlocker ? "text-danger font-semibold" : "text-muted-foreground"
|
||||
)}>
|
||||
{filter.name}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"text-[9px] truncate max-w-[80px]",
|
||||
isBlocker ? "text-danger" : "text-muted-foreground/60"
|
||||
)}>
|
||||
{filter.detail}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
|
||||
const getDetail = (ticket: number) =>
|
||||
positionDetails?.find((d) => d.ticket === ticket);
|
||||
|
||||
return (
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
@@ -32,33 +39,91 @@ export function PositionsCard({ positions }: PositionsCardProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{positions.map((pos) => (
|
||||
<div
|
||||
key={pos.ticket}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-1.5 rounded-md bg-surface-light/50",
|
||||
pos.type === "BUY" ? "border-l-2 border-l-success" : "border-l-2 border-l-danger"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant={pos.type === "BUY" ? "success" : "danger"}
|
||||
className="text-[10px] h-4 px-1"
|
||||
{positions.map((pos) => {
|
||||
const detail = getDetail(pos.ticket);
|
||||
const isExpanded = expandedTicket === pos.ticket;
|
||||
const hasDetail = !!detail;
|
||||
|
||||
return (
|
||||
<div key={pos.ticket}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between p-1.5 rounded-md bg-surface-light/50",
|
||||
pos.type === "BUY" ? "border-l-2 border-l-success" : "border-l-2 border-l-danger",
|
||||
hasDetail && "cursor-pointer hover:bg-surface-light/80"
|
||||
)}
|
||||
onClick={() => hasDetail && setExpandedTicket(isExpanded ? null : pos.ticket)}
|
||||
>
|
||||
{pos.type}
|
||||
</Badge>
|
||||
<span className="text-[11px] font-number">
|
||||
{pos.volume} @ {pos.priceOpen.toFixed(2)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant={pos.type === "BUY" ? "success" : "danger"}
|
||||
className="text-[10px] h-4 px-1"
|
||||
>
|
||||
{pos.type}
|
||||
</Badge>
|
||||
<span className="text-[11px] font-number">
|
||||
{pos.volume} @ {pos.priceOpen.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={cn(
|
||||
"text-[11px] font-bold font-number",
|
||||
pos.profit >= 0 ? "text-success" : "text-danger"
|
||||
)}>
|
||||
{pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)}
|
||||
</span>
|
||||
{hasDetail && (
|
||||
isExpanded
|
||||
? <ChevronUp className="h-3 w-3 text-muted-foreground/40" />
|
||||
: <ChevronDown className="h-3 w-3 text-muted-foreground/40" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && detail && (
|
||||
<div className="ml-2 mt-0.5 p-1.5 rounded bg-surface-light/30 space-y-0.5 text-[10px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Peak Profit</span>
|
||||
<span className="font-number text-success">${detail.peakProfit.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">DD from Peak</span>
|
||||
<span className={cn("font-number", detail.drawdownFromPeak > 30 ? "text-danger" : "text-muted-foreground")}>
|
||||
{detail.drawdownFromPeak.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Momentum</span>
|
||||
<span className={cn("font-number", detail.momentum > 0 ? "text-success" : detail.momentum < 0 ? "text-danger" : "text-muted-foreground")}>
|
||||
{detail.momentum > 0 ? "+" : ""}{detail.momentum}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">TP Probability</span>
|
||||
<span className={cn("font-number", detail.tpProbability >= 50 ? "text-success" : "text-warning")}>
|
||||
{detail.tpProbability}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Duration</span>
|
||||
<span className="font-number text-muted-foreground">{detail.tradeHours}h</span>
|
||||
</div>
|
||||
{(detail.reversalWarnings > 0 || detail.stalls > 0) && (
|
||||
<div className="flex gap-2 pt-0.5 border-t border-border/50">
|
||||
{detail.reversalWarnings > 0 && (
|
||||
<span className="text-warning">Rev: {detail.reversalWarnings}</span>
|
||||
)}
|
||||
{detail.stalls > 0 && (
|
||||
<span className="text-muted-foreground">Stalls: {detail.stalls}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-[11px] font-bold font-number",
|
||||
pos.profit >= 0 ? "text-success" : "text-danger"
|
||||
)}>
|
||||
{pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -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 (
|
||||
<Card className={cn(
|
||||
"glass",
|
||||
@@ -41,8 +56,15 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen
|
||||
)}>
|
||||
<ShieldAlert className="h-3.5 w-3.5" />
|
||||
Risk
|
||||
{/* Risk Mode Badge */}
|
||||
<Badge
|
||||
variant={getRiskModeVariant(mode)}
|
||||
className={cn("ml-auto text-[9px] h-4 px-1 uppercase", mode === "stopped" && "animate-pulse")}
|
||||
>
|
||||
{mode}
|
||||
</Badge>
|
||||
{isCritical && (
|
||||
<span className="ml-auto flex items-center gap-1 text-[10px] bg-danger text-white px-1.5 py-0.5 rounded-full animate-pulse">
|
||||
<span className="flex items-center gap-1 text-[10px] bg-danger text-white px-1.5 py-0.5 rounded-full animate-pulse">
|
||||
<AlertTriangle className="h-2.5 w-2.5" />
|
||||
BREACHED
|
||||
</span>
|
||||
@@ -81,7 +103,37 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen
|
||||
style={{ width: `${Math.min(riskPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{/* Remaining daily risk */}
|
||||
{riskMode && riskMode.remainingDailyRisk > 0 && (
|
||||
<div className="flex justify-between items-center mt-0.5">
|
||||
<span className="text-[9px] text-muted-foreground/60">Remaining</span>
|
||||
<span className="text-[9px] font-number text-muted-foreground/60">
|
||||
{formatUSD(riskMode.remainingDailyRisk)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Total Loss Progress */}
|
||||
{riskMode && riskMode.maxTotalLoss > 0 && (
|
||||
<div className="pt-0.5">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="text-[10px] text-muted-foreground">Total Loss</span>
|
||||
<span className="text-[10px] font-number text-muted-foreground">
|
||||
{formatUSD(riskMode.totalLoss)} / {formatUSD(riskMode.maxTotalLoss)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1 w-full bg-surface-light rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all duration-500",
|
||||
(riskMode.totalLoss / riskMode.maxTotalLoss) >= 0.8 ? "bg-danger" : "bg-warning/60"
|
||||
)}
|
||||
style={{ width: `${riskMode.maxTotalLoss > 0 ? Math.min((riskMode.totalLoss / riskMode.maxTotalLoss) * 100, 100) : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Session
|
||||
{sessionMultiplier != null && (
|
||||
<Badge variant={multVariant as "warning" | "success" | "secondary"} className="ml-auto text-[10px] h-4 px-1">
|
||||
{multLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
@@ -56,6 +68,23 @@ export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProp
|
||||
{canTrade ? "CAN TRADE" : "NO TRADE"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Time Filter Status */}
|
||||
{timeFilter && (
|
||||
<div className="flex items-center gap-1.5 pt-0.5 border-t border-border">
|
||||
{timeFilter.isBlocked ? (
|
||||
<Ban className="h-3 w-3 text-danger" />
|
||||
) : (
|
||||
<Clock className="h-3 w-3 text-muted-foreground/40" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"text-[10px]",
|
||||
timeFilter.isBlocked ? "text-danger font-semibold" : "text-muted-foreground"
|
||||
)}>
|
||||
WIB {timeFilter.wibHour}:00{timeFilter.isBlocked ? " BLOCKED" : ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user