Files
XauBot/src/utils.py
T
GifariKemal 7af9183af3 feat: Smart AI Trading Bot for XAUUSD with ML and SMC
- XGBoost ML model with 37 features for market direction prediction
- Smart Money Concepts (SMC): Order Blocks, FVG, BOS, CHoCH
- HMM market regime detection (trending/ranging/volatile)
- ATR-based stop loss with 1.5 ATR minimum distance
- Broker-level SL protection with fallback
- Time-based exit (max 6 hours per trade)
- Session-aware trading optimized for London/NY overlap
- Auto-retraining based on market conditions
- Telegram notifications and web dashboard
- Backtest results: 63.9% win rate, 2.64 profit factor, 4.83 Sharpe

Backtest period: Jan 2025 - Feb 2026, 654 trades, $4,189 net P/L

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 09:01:35 +07:00

351 lines
9.4 KiB
Python

"""
Utility Functions
=================
Helper functions for the trading system.
"""
import polars as pl
import numpy as np
from typing import Dict, List, Optional, Tuple
from datetime import datetime, timedelta
from loguru import logger
def validate_ohlcv_data(df: pl.DataFrame) -> Tuple[bool, List[str]]:
"""
Validate OHLCV DataFrame structure.
Args:
df: DataFrame to validate
Returns:
Tuple of (is_valid, list_of_issues)
"""
issues = []
# Required columns
required = ["time", "open", "high", "low", "close"]
for col in required:
if col not in df.columns:
issues.append(f"Missing required column: {col}")
if issues:
return False, issues
# Check data types
if df["time"].dtype not in [pl.Datetime, pl.Date]:
issues.append(f"'time' should be datetime, got {df['time'].dtype}")
for col in ["open", "high", "low", "close"]:
if df[col].dtype not in [pl.Float64, pl.Float32, pl.Int64, pl.Int32]:
issues.append(f"'{col}' should be numeric, got {df[col].dtype}")
# Check for null values
null_counts = df.select([
pl.col(c).is_null().sum().alias(c) for c in required
]).row(0)
for i, col in enumerate(required):
if null_counts[i] > 0:
issues.append(f"Column '{col}' has {null_counts[i]} null values")
# Check OHLC relationship
invalid_candles = df.filter(
(pl.col("high") < pl.col("low")) |
(pl.col("high") < pl.col("open")) |
(pl.col("high") < pl.col("close")) |
(pl.col("low") > pl.col("open")) |
(pl.col("low") > pl.col("close"))
)
if len(invalid_candles) > 0:
issues.append(f"Found {len(invalid_candles)} invalid OHLC relationships")
# Check time ordering
if df["time"].is_sorted():
pass # OK
else:
issues.append("Time column is not sorted")
return len(issues) == 0, issues
def resample_ohlcv(
df: pl.DataFrame,
target_timeframe: str,
) -> pl.DataFrame:
"""
Resample OHLCV data to a higher timeframe.
Args:
df: Source DataFrame with OHLCV data
target_timeframe: Target timeframe ("5m", "15m", "1h", "4h", "1d")
Returns:
Resampled DataFrame
"""
# Map timeframe strings to durations
tf_map = {
"1m": "1m",
"5m": "5m",
"15m": "15m",
"30m": "30m",
"1h": "1h",
"4h": "4h",
"1d": "1d",
"M1": "1m",
"M5": "5m",
"M15": "15m",
"M30": "30m",
"H1": "1h",
"H4": "4h",
"D1": "1d",
}
every = tf_map.get(target_timeframe, target_timeframe)
df = df.sort("time")
resampled = df.group_by_dynamic("time", every=every).agg([
pl.col("open").first(),
pl.col("high").max(),
pl.col("low").min(),
pl.col("close").last(),
pl.col("volume").sum() if "volume" in df.columns else pl.lit(0).alias("volume"),
])
return resampled
def calculate_pip_value(
symbol: str,
lot_size: float,
account_currency: str = "USD",
) -> float:
"""
Calculate pip value for a symbol.
Args:
symbol: Trading symbol
lot_size: Lot size
account_currency: Account currency
Returns:
Pip value in account currency
"""
# Standard forex pairs (per standard lot)
pip_values = {
"EURUSD": 10.0,
"GBPUSD": 10.0,
"AUDUSD": 10.0,
"NZDUSD": 10.0,
"USDJPY": 9.1, # Approximate, varies
"USDCHF": 10.0,
"USDCAD": 7.5, # Approximate
"XAUUSD": 1.0, # Per 0.1 move
"XAGUSD": 0.5, # Per 0.01 move
}
base_pip = pip_values.get(symbol, 10.0)
return base_pip * lot_size
def calculate_trade_statistics(trades: List[Dict]) -> Dict:
"""
Calculate trading statistics from trade history.
Args:
trades: List of trade dictionaries with 'pnl', 'is_win' keys
Returns:
Dictionary of statistics
"""
if not trades:
return {
"total_trades": 0,
"win_rate": 0,
"profit_factor": 0,
"avg_win": 0,
"avg_loss": 0,
"max_win": 0,
"max_loss": 0,
"total_pnl": 0,
"sharpe_ratio": 0,
}
wins = [t for t in trades if t.get("is_win", False)]
losses = [t for t in trades if not t.get("is_win", True)]
total_trades = len(trades)
win_count = len(wins)
win_rate = win_count / total_trades if total_trades > 0 else 0
win_pnls = [t.get("pnl", 0) for t in wins]
loss_pnls = [abs(t.get("pnl", 0)) for t in losses]
all_pnls = [t.get("pnl", 0) for t in trades]
total_wins = sum(win_pnls)
total_losses = sum(loss_pnls)
profit_factor = total_wins / total_losses if total_losses > 0 else float("inf")
avg_win = np.mean(win_pnls) if win_pnls else 0
avg_loss = np.mean(loss_pnls) if loss_pnls else 0
max_win = max(win_pnls) if win_pnls else 0
max_loss = max(loss_pnls) if loss_pnls else 0
total_pnl = sum(all_pnls)
# Sharpe ratio (simplified)
if len(all_pnls) > 1:
returns = np.array(all_pnls)
sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) if np.std(returns) > 0 else 0
else:
sharpe = 0
return {
"total_trades": total_trades,
"win_rate": win_rate,
"profit_factor": profit_factor,
"avg_win": avg_win,
"avg_loss": avg_loss,
"max_win": max_win,
"max_loss": max_loss,
"total_pnl": total_pnl,
"sharpe_ratio": sharpe,
}
def format_price(price: float, digits: int = 5) -> str:
"""Format price with correct decimal places."""
return f"{price:.{digits}f}"
def format_lot(lot: float) -> str:
"""Format lot size."""
return f"{lot:.2f}"
def format_percentage(value: float) -> str:
"""Format as percentage."""
return f"{value * 100:.2f}%"
def format_currency(value: float, currency: str = "USD") -> str:
"""Format as currency."""
symbols = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
symbol = symbols.get(currency, currency)
return f"{symbol}{value:,.2f}"
class PerformanceTimer:
"""Context manager for timing code execution."""
def __init__(self, name: str = "Operation", log: bool = True):
self.name = name
self.log = log
self.elapsed = 0.0
def __enter__(self):
import time
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self._start
if self.log:
logger.debug(f"{self.name}: {self.elapsed*1000:.2f}ms")
return False
def create_synthetic_data(
n_bars: int = 1000,
base_price: float = 2000.0,
volatility: float = 0.002,
seed: Optional[int] = 42,
) -> pl.DataFrame:
"""
Create synthetic OHLCV data for testing.
Args:
n_bars: Number of bars to generate
base_price: Starting price
volatility: Daily volatility
seed: Random seed
Returns:
Polars DataFrame with OHLCV data
"""
if seed is not None:
np.random.seed(seed)
# Generate random walk prices
returns = np.random.randn(n_bars) * volatility
prices = base_price * np.exp(np.cumsum(returns))
# Generate OHLC
opens = prices
closes = prices * (1 + np.random.randn(n_bars) * volatility * 0.5)
highs = np.maximum(opens, closes) * (1 + np.abs(np.random.randn(n_bars)) * volatility * 0.3)
lows = np.minimum(opens, closes) * (1 - np.abs(np.random.randn(n_bars)) * volatility * 0.3)
volumes = np.random.randint(1000, 10000, n_bars)
# Generate timestamps
end_time = datetime.now()
times = [end_time - timedelta(minutes=15 * (n_bars - i - 1)) for i in range(n_bars)]
return pl.DataFrame({
"time": times,
"open": opens,
"high": highs,
"low": lows,
"close": closes,
"volume": volumes,
})
if __name__ == "__main__":
# Test utilities
print("=== Utility Tests ===\n")
# Test synthetic data
df = create_synthetic_data(100)
print(f"Synthetic data shape: {df.shape}")
# Validate
is_valid, issues = validate_ohlcv_data(df)
print(f"Data valid: {is_valid}")
if issues:
print(f"Issues: {issues}")
# Test resampling
df_resampled = resample_ohlcv(df, "1h")
print(f"Resampled shape: {df_resampled.shape}")
# Test statistics
trades = [
{"pnl": 100, "is_win": True},
{"pnl": -50, "is_win": False},
{"pnl": 75, "is_win": True},
{"pnl": -30, "is_win": False},
{"pnl": 120, "is_win": True},
]
stats = calculate_trade_statistics(trades)
print(f"\nTrade Statistics:")
for key, value in stats.items():
print(f" {key}: {value:.2f}" if isinstance(value, float) else f" {key}: {value}")
# Test formatting
print(f"\nFormatting:")
print(f" Price: {format_price(2000.12345)}")
print(f" Lot: {format_lot(0.05)}")
print(f" Percentage: {format_percentage(0.55)}")
print(f" Currency: {format_currency(1234.56)}")
# Test timer
with PerformanceTimer("Test operation"):
import time
time.sleep(0.1)