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:
GifariKemal
2026-02-08 13:45:31 +07:00
parent cb41bfe5ba
commit 61877480b3
11 changed files with 1056 additions and 47 deletions
+346
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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},
}
+20 -8
View File
@@ -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>
);
+81
View File
@@ -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 {