feat: apply #28B smart breakeven + #31B H1 EMA20 filter, add backtests #26-#32

Live trading optimizations (cumulative: $2,807 net, 81.8% WR, Sharpe 3.97):
- #28B: Smart breakeven locks profit at entry + 0.5x ATR instead of fixed $2
- #31B: H1 Price vs EMA20 filter — BUY only when H1 bullish, SELL only when bearish

Backtests #26-#32 (7 scripts testing sell improvement, regime-aware entry,
confluence scoring, dynamic RR, multi-TF H1, and ML exit optimizer).
Winners: #28B (+$229), #31B (+$343). Failed: #26, #27, #29, #30, #32.

Also includes: web dashboard redesign, Docker setup, startup scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-08 10:33:24 +07:00
co-authored by Claude Opus 4.6
parent 53d8cd26a2
commit 214b64945d
64 changed files with 11073 additions and 929 deletions
+59 -258
View File
@@ -1,42 +1,21 @@
"""
FastAPI Backend for Web Dashboard
=================================
FastAPI Backend for Web Dashboard (Docker-compatible)
=====================================================
Serves trading bot status data to the web frontend.
Reads from data/bot_status.json which is written by main_live.py.
This allows the API to run in Docker without needing MT5 (Windows-only).
"""
import sys
import json
from pathlib import Path
from datetime import datetime
from zoneinfo import ZoneInfo
from collections import deque
import asyncio
from typing import Optional
import json
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from dotenv import load_dotenv
load_dotenv()
# Import bot components
try:
from src.mt5_connector import MT5Connector
from src.smc_polars import SMCAnalyzer
from src.ml_model import TradingModel
from src.regime_detector import MarketRegimeDetector
from src.session_filter import SessionFilter
from src.feature_eng import FeatureEngineer
from src.config import TradingConfig
except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running from the correct directory")
app = FastAPI(title="Trading Bot API", version="1.0.0")
app = FastAPI(title="Trading Bot API", version="2.0.0")
# CORS for frontend
app.add_middleware(
@@ -47,246 +26,68 @@ app.add_middleware(
allow_headers=["*"],
)
# Global state
class BotState:
def __init__(self):
self.mt5: Optional[MT5Connector] = None
self.smc: Optional[SMCAnalyzer] = None
self.ml: Optional[TradingModel] = None
self.hmm: Optional[MarketRegimeDetector] = None
self.session: Optional[SessionFilter] = None
self.feature_eng: Optional[FeatureEngineer] = None
self.config: Optional[TradingConfig] = None
self.connected = False
# Status file path (mounted as volume in Docker)
STATUS_FILE = Path("/app/data/bot_status.json")
# History buffers
self.price_history = deque(maxlen=120)
self.equity_history = deque(maxlen=120)
self.balance_history = deque(maxlen=120)
self.logs = deque(maxlen=50)
# Last known values
self.last_price = 0.0
self.last_update = None
state = BotState()
def add_log(level: str, message: str):
"""Add log entry to buffer"""
now = datetime.now(ZoneInfo("Asia/Jakarta"))
state.logs.append({
"time": now.strftime("%H:%M:%S"),
"level": level,
"message": message
})
@app.on_event("startup")
async def startup():
"""Initialize bot components on startup"""
add_log("info", "Starting API server...")
try:
state.config = TradingConfig()
state.mt5 = MT5Connector(
login=state.config.mt5_login,
password=state.config.mt5_password,
server=state.config.mt5_server,
path=state.config.mt5_path,
)
if state.mt5.connect():
state.connected = True
add_log("info", "MT5 connected successfully")
# Initialize components
state.smc = SMCAnalyzer()
state.ml = TradingModel(model_path="models/xgboost_model")
state.ml.load()
state.hmm = MarketRegimeDetector(model_path="models/hmm_regime")
state.hmm.load()
state.session = SessionFilter()
state.feature_eng = FeatureEngineer()
add_log("info", f"ML Model loaded ({len(state.ml.feature_names)} features)")
else:
add_log("error", "Failed to connect to MT5")
except Exception as e:
add_log("error", f"Startup error: {e}")
@app.on_event("shutdown")
async def shutdown():
"""Cleanup on shutdown"""
if state.mt5:
state.mt5.disconnect()
add_log("info", "API server stopped")
# Default empty response
DEFAULT_STATUS = {
"timestamp": "00:00:00",
"connected": False,
"price": 0.0,
"spread": 0.0,
"priceChange": 0.0,
"priceHistory": [],
"balance": 0.0,
"equity": 0.0,
"profit": 0.0,
"equityHistory": [],
"balanceHistory": [],
"session": "Unknown",
"isGoldenTime": False,
"canTrade": False,
"dailyLoss": 0.0,
"dailyProfit": 0.0,
"consecutiveLosses": 0,
"riskPercent": 0.0,
"smc": {"signal": "", "confidence": 0.0, "reason": ""},
"ml": {"signal": "", "confidence": 0.0, "buyProb": 0.0, "sellProb": 0.0},
"regime": {"name": "", "volatility": 0.0, "confidence": 0.0},
"positions": [],
"logs": [],
}
@app.get("/api/status")
async def get_status():
"""Get current trading status"""
wib = ZoneInfo("Asia/Jakarta")
now = datetime.now(wib)
result = {
"timestamp": now.strftime("%H:%M:%S"),
"connected": state.connected,
"price": 0.0,
"spread": 0.0,
"priceChange": 0.0,
"priceHistory": list(state.price_history),
"balance": 0.0,
"equity": 0.0,
"profit": 0.0,
"equityHistory": list(state.equity_history),
"balanceHistory": list(state.balance_history),
"session": "Unknown",
"isGoldenTime": 19 <= now.hour < 23,
"canTrade": False,
"dailyLoss": 0.0,
"dailyProfit": 0.0,
"consecutiveLosses": 0,
"riskPercent": 0.0,
"smc": {"signal": "", "confidence": 0.0, "reason": ""},
"ml": {"signal": "", "confidence": 0.0, "buyProb": 0.0, "sellProb": 0.0},
"regime": {"name": "", "volatility": 0.0, "confidence": 0.0},
"positions": [],
"logs": list(state.logs),
}
if not state.connected or not state.mt5:
return result
try:
# Price
tick = state.mt5.get_tick(state.config.symbol)
if tick:
price = (tick.bid + tick.ask) / 2
spread = (tick.ask - tick.bid) * 100
# Calculate change
price_change = price - state.last_price if state.last_price > 0 else 0
state.last_price = price
# Update history
state.price_history.append(price)
result["price"] = price
result["spread"] = spread
result["priceChange"] = price_change
result["priceHistory"] = list(state.price_history)
# Account
balance = state.mt5.account_balance or 0
equity = state.mt5.account_equity or 0
profit = equity - balance
state.equity_history.append(equity)
state.balance_history.append(balance)
result["balance"] = balance
result["equity"] = equity
result["profit"] = profit
result["equityHistory"] = list(state.equity_history)
result["balanceHistory"] = list(state.balance_history)
# Session
if state.session:
session_info = state.session.get_status_report()
if session_info:
result["session"] = session_info.get('current_session', 'Unknown')
can_trade, _, _ = state.session.can_trade()
result["canTrade"] = can_trade
# Risk state from file
risk_file = Path("data/risk_state.txt")
if risk_file.exists():
content = risk_file.read_text()
for line in content.strip().split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
if key == 'daily_loss':
result["dailyLoss"] = float(value)
elif key == 'daily_profit':
result["dailyProfit"] = float(value)
elif key == 'consecutive_losses':
result["consecutiveLosses"] = int(value)
# Calculate risk percent
max_loss = state.config.capital * (state.config.risk.max_daily_loss / 100)
if max_loss > 0:
result["riskPercent"] = (result["dailyLoss"] / max_loss) * 100
# Signals
df = state.mt5.get_market_data(state.config.symbol, state.config.execution_timeframe, 200)
if df is not None and len(df) > 50:
# Feature engineering
df = state.feature_eng.calculate_all(df, include_ml_features=True)
df = state.smc.calculate_all(df)
# Regime
if state.hmm:
df = state.hmm.predict(df)
regime = state.hmm.get_current_state(df)
if regime:
result["regime"] = {
"name": regime.regime.value.replace('_', ' ').title(),
"volatility": regime.volatility,
"confidence": regime.confidence,
}
# SMC Signal
smc_signal = state.smc.generate_signal(df)
if smc_signal:
result["smc"] = {
"signal": smc_signal.signal_type,
"confidence": smc_signal.confidence,
"reason": smc_signal.reason or "",
}
# ML Prediction
if state.ml and state.ml.fitted:
available_features = [f for f in state.ml.feature_names if f in df.columns]
ml_pred = state.ml.predict(df, available_features)
if ml_pred:
result["ml"] = {
"signal": ml_pred.signal,
"confidence": ml_pred.confidence,
"buyProb": ml_pred.probability,
"sellProb": 1.0 - ml_pred.probability,
}
# Positions
positions = state.mt5.get_open_positions(state.config.symbol)
if positions is not None and not positions.is_empty():
pos_list = []
for row in positions.iter_rows(named=True):
pos_list.append({
"ticket": row.get('ticket', 0),
"type": "BUY" if row.get('type', 0) == 0 else "SELL",
"volume": row.get('volume', 0),
"priceOpen": row.get('price_open', 0),
"profit": row.get('profit', 0),
})
result["positions"] = pos_list
state.last_update = now
except Exception as e:
add_log("error", f"Status error: {str(e)[:50]}")
"""Get current trading status from bot's status file."""
# Try local path first (non-Docker), then Docker path
for path in [STATUS_FILE, Path("data/bot_status.json")]:
if path.exists():
try:
data = json.loads(path.read_text())
return data
except (json.JSONDecodeError, OSError):
continue
# No status file — bot not running
now = datetime.now(ZoneInfo("Asia/Jakarta"))
result = DEFAULT_STATUS.copy()
result["timestamp"] = now.strftime("%H:%M:%S")
result["logs"] = [
{
"time": now.strftime("%H:%M:%S"),
"level": "warning",
"message": "Bot is not running — waiting for bot_status.json",
}
]
return result
@app.get("/api/health")
async def health():
"""Health check endpoint"""
return {"status": "ok", "connected": state.connected}
"""Health check endpoint."""
bot_running = STATUS_FILE.exists() or Path("data/bot_status.json").exists()
return {"status": "ok", "bot_running": bot_running}
if __name__ == "__main__":
+1 -3
View File
@@ -1,4 +1,2 @@
fastapi>=0.109.0
uvicorn>=0.27.0
python-dotenv>=1.0.0
pydantic>=2.5.0
uvicorn[standard]>=0.27.0