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>
This commit is contained in:
GifariKemal
2026-02-06 09:01:35 +07:00
co-authored by Claude Opus 4.5
commit 7af9183af3
121 changed files with 43387 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
"""
Database Module for Trading Bot
===============================
PostgreSQL integration for trade logging, training history, and analytics.
Usage:
from src.db import get_db, init_db, TradeRepository
# Initialize database
if init_db():
db = get_db()
# Use repository
repo = TradeRepository(db)
repo.insert_trade(trade_data)
"""
from .connection import DatabaseConnection, get_db, init_db
from .repository import (
TradeRepository,
TrainingRepository,
SignalRepository,
MarketSnapshotRepository,
BotStatusRepository,
DailySummaryRepository,
)
__all__ = [
# Connection
"DatabaseConnection",
"get_db",
"init_db",
# Repositories
"TradeRepository",
"TrainingRepository",
"SignalRepository",
"MarketSnapshotRepository",
"BotStatusRepository",
"DailySummaryRepository",
]
+316
View File
@@ -0,0 +1,316 @@
"""
Database Connection Module
==========================
PostgreSQL connection management with connection pooling.
Features:
- Connection pooling for performance
- Auto-reconnect on failure
- Context manager support
- Thread-safe operations
"""
import os
from typing import Optional, Dict, Any, List
from contextlib import contextmanager
import threading
import psycopg2
from psycopg2 import pool, extras
from psycopg2.extensions import connection as PgConnection
from loguru import logger
from dotenv import load_dotenv
load_dotenv()
class DatabaseConnection:
"""
PostgreSQL database connection manager with connection pooling.
Thread-safe singleton pattern for efficient connection reuse.
"""
_instance: Optional["DatabaseConnection"] = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
"""Singleton pattern - ensure only one instance exists."""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(
self,
host: Optional[str] = None,
port: Optional[int] = None,
database: Optional[str] = None,
user: Optional[str] = None,
password: Optional[str] = None,
min_connections: int = 1,
max_connections: int = 10,
):
"""
Initialize database connection.
Args:
host: Database host (default: from env)
port: Database port (default: 5432)
database: Database name (default: from env)
user: Database user (default: from env)
password: Database password (default: from env)
min_connections: Minimum pool size
max_connections: Maximum pool size
"""
# Only initialize once (singleton)
if self._initialized:
return
self.host = host or os.getenv("DB_HOST", "localhost")
self.port = port or int(os.getenv("DB_PORT", "5432"))
self.database = database or os.getenv("DB_NAME", "trading_db")
self.user = user or os.getenv("DB_USER", "trading_bot")
self.password = password or os.getenv("DB_PASSWORD", "trading_bot_2026")
self.min_connections = min_connections
self.max_connections = max_connections
self._pool: Optional[pool.ThreadedConnectionPool] = None
self._connected = False
self._initialized = True
def connect(self) -> bool:
"""
Initialize connection pool.
Returns:
True if connection successful
"""
if self._connected and self._pool:
return True
try:
self._pool = pool.ThreadedConnectionPool(
minconn=self.min_connections,
maxconn=self.max_connections,
host=self.host,
port=self.port,
database=self.database,
user=self.user,
password=self.password,
connect_timeout=10,
)
# Test connection
conn = self._pool.getconn()
with conn.cursor() as cur:
cur.execute("SELECT 1")
self._pool.putconn(conn)
self._connected = True
logger.info(f"Database connected: {self.database}@{self.host}:{self.port}")
return True
except psycopg2.Error as e:
logger.error(f"Database connection failed: {e}")
self._connected = False
return False
def disconnect(self):
"""Close all connections in the pool."""
if self._pool:
self._pool.closeall()
self._pool = None
self._connected = False
logger.info("Database disconnected")
@property
def is_connected(self) -> bool:
"""Check if database is connected."""
return self._connected and self._pool is not None
@contextmanager
def get_connection(self):
"""
Get a connection from the pool (context manager).
Usage:
with db.get_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT * FROM trades")
"""
if not self.is_connected:
self.connect()
conn = None
try:
conn = self._pool.getconn()
yield conn
conn.commit()
except psycopg2.Error as e:
if conn:
conn.rollback()
logger.error(f"Database error: {e}")
raise
finally:
if conn:
self._pool.putconn(conn)
@contextmanager
def get_cursor(self, cursor_factory=None):
"""
Get a cursor directly (context manager).
Args:
cursor_factory: Custom cursor factory (e.g., RealDictCursor)
Usage:
with db.get_cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("SELECT * FROM trades")
rows = cur.fetchall()
"""
with self.get_connection() as conn:
cursor_factory = cursor_factory or extras.RealDictCursor
with conn.cursor(cursor_factory=cursor_factory) as cur:
yield cur
def execute(
self,
query: str,
params: Optional[tuple] = None,
fetch: bool = False,
) -> Optional[List[Dict]]:
"""
Execute a query.
Args:
query: SQL query
params: Query parameters
fetch: Whether to fetch results
Returns:
List of dicts if fetch=True, None otherwise
"""
with self.get_cursor() as cur:
cur.execute(query, params)
if fetch:
return cur.fetchall()
return None
def execute_many(
self,
query: str,
params_list: List[tuple],
) -> int:
"""
Execute a query multiple times.
Args:
query: SQL query
params_list: List of parameter tuples
Returns:
Number of rows affected
"""
with self.get_cursor() as cur:
cur.executemany(query, params_list)
return cur.rowcount
def insert_returning(
self,
query: str,
params: Optional[tuple] = None,
) -> Optional[Dict]:
"""
Execute INSERT ... RETURNING and return the inserted row.
Args:
query: INSERT query with RETURNING clause
params: Query parameters
Returns:
Inserted row as dict
"""
with self.get_cursor() as cur:
cur.execute(query, params)
return cur.fetchone()
def get_status(self) -> Dict[str, Any]:
"""Get database connection status."""
status = {
"connected": self.is_connected,
"host": self.host,
"port": self.port,
"database": self.database,
"user": self.user,
"pool_min": self.min_connections,
"pool_max": self.max_connections,
}
if self.is_connected and self._pool:
# Get pool stats (approximate)
try:
with self.get_cursor() as cur:
cur.execute("SELECT count(*) FROM trades")
result = cur.fetchone()
status["total_trades"] = result["count"] if result else 0
except:
status["total_trades"] = "N/A"
return status
# Global instance
_db_instance: Optional[DatabaseConnection] = None
def get_db() -> DatabaseConnection:
"""
Get or create global database instance.
Returns:
DatabaseConnection instance
"""
global _db_instance
if _db_instance is None:
_db_instance = DatabaseConnection()
return _db_instance
def init_db() -> bool:
"""
Initialize database connection.
Returns:
True if successful
"""
db = get_db()
return db.connect()
if __name__ == "__main__":
# Test connection
print("Testing database connection...")
db = get_db()
if db.connect():
print(f"Connected to {db.database}")
# Test query
with db.get_cursor() as cur:
cur.execute("SELECT version()")
version = cur.fetchone()
print(f"PostgreSQL version: {version['version']}")
# Test status
status = db.get_status()
print(f"Status: {status}")
db.disconnect()
print("Disconnected")
else:
print("Connection failed!")
+753
View File
@@ -0,0 +1,753 @@
"""
Database Repository Module
==========================
Data access layer for PostgreSQL operations.
Repositories:
- TradeRepository: Trade CRUD operations
- TrainingRepository: ML training history
- SignalRepository: Signal logging
- MarketSnapshotRepository: Market state snapshots
"""
from typing import Optional, Dict, Any, List
from datetime import datetime, date
from decimal import Decimal
import json
from loguru import logger
from .connection import DatabaseConnection
class TradeRepository:
"""Repository for trade operations."""
def __init__(self, db: DatabaseConnection):
self.db = db
def insert_trade(self, trade_data: Dict[str, Any]) -> Optional[Dict]:
"""
Insert a new trade record.
Args:
trade_data: Trade information dict
Returns:
Inserted trade record with ID
"""
query = """
INSERT INTO trades (
ticket, symbol, direction,
entry_price, stop_loss, take_profit, lot_size,
opened_at,
entry_regime, entry_volatility, entry_session, entry_spread, entry_atr,
smc_signal, smc_confidence, smc_reason,
smc_fvg_detected, smc_ob_detected, smc_bos_detected, smc_choch_detected,
ml_signal, ml_confidence,
market_quality, market_score, dynamic_threshold,
balance_before, equity_at_entry,
features_entry, bot_version, trade_mode
) VALUES (
%(ticket)s, %(symbol)s, %(direction)s,
%(entry_price)s, %(stop_loss)s, %(take_profit)s, %(lot_size)s,
%(opened_at)s,
%(entry_regime)s, %(entry_volatility)s, %(entry_session)s, %(entry_spread)s, %(entry_atr)s,
%(smc_signal)s, %(smc_confidence)s, %(smc_reason)s,
%(smc_fvg_detected)s, %(smc_ob_detected)s, %(smc_bos_detected)s, %(smc_choch_detected)s,
%(ml_signal)s, %(ml_confidence)s,
%(market_quality)s, %(market_score)s, %(dynamic_threshold)s,
%(balance_before)s, %(equity_at_entry)s,
%(features_entry)s, %(bot_version)s, %(trade_mode)s
)
RETURNING *
"""
# Set defaults
defaults = {
'symbol': 'XAUUSD',
'stop_loss': 0,
'take_profit': 0,
'opened_at': datetime.now(),
'entry_regime': None,
'entry_volatility': None,
'entry_session': None,
'entry_spread': None,
'entry_atr': None,
'smc_signal': None,
'smc_confidence': None,
'smc_reason': None,
'smc_fvg_detected': False,
'smc_ob_detected': False,
'smc_bos_detected': False,
'smc_choch_detected': False,
'ml_signal': None,
'ml_confidence': None,
'market_quality': None,
'market_score': None,
'dynamic_threshold': None,
'balance_before': None,
'equity_at_entry': None,
'features_entry': '{}',
'bot_version': '2.1',
'trade_mode': 'SMC-ONLY',
}
# Merge defaults with provided data
params = {**defaults, **trade_data}
# Convert features to JSON string if dict
if isinstance(params.get('features_entry'), dict):
params['features_entry'] = json.dumps(params['features_entry'])
try:
result = self.db.insert_returning(query, params)
logger.info(f"Trade inserted: ticket={params['ticket']}")
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to insert trade: {e}")
raise
def update_trade_close(self, ticket: int, close_data: Dict[str, Any]) -> Optional[Dict]:
"""
Update trade with close information.
Args:
ticket: Trade ticket number
close_data: Close information dict
Returns:
Updated trade record
"""
query = """
UPDATE trades SET
exit_price = %(exit_price)s,
profit_usd = %(profit_usd)s,
profit_pips = %(profit_pips)s,
closed_at = %(closed_at)s,
duration_seconds = %(duration_seconds)s,
exit_reason = %(exit_reason)s,
exit_regime = %(exit_regime)s,
exit_ml_signal = %(exit_ml_signal)s,
exit_ml_confidence = %(exit_ml_confidence)s,
balance_after = %(balance_after)s,
features_exit = %(features_exit)s
WHERE ticket = %(ticket)s
RETURNING *
"""
defaults = {
'closed_at': datetime.now(),
'duration_seconds': None,
'exit_reason': None,
'exit_regime': None,
'exit_ml_signal': None,
'exit_ml_confidence': None,
'balance_after': None,
'features_exit': '{}',
}
params = {**defaults, **close_data, 'ticket': ticket}
# Convert features to JSON string if dict
if isinstance(params.get('features_exit'), dict):
params['features_exit'] = json.dumps(params['features_exit'])
try:
result = self.db.insert_returning(query, params)
logger.info(f"Trade closed: ticket={ticket}, profit={close_data.get('profit_usd')}")
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to update trade close: {e}")
raise
def get_trade_by_ticket(self, ticket: int) -> Optional[Dict]:
"""Get trade by ticket number."""
query = "SELECT * FROM trades WHERE ticket = %s"
result = self.db.execute(query, (ticket,), fetch=True)
return dict(result[0]) if result else None
def get_open_trades(self) -> List[Dict]:
"""Get all trades that haven't been closed."""
query = """
SELECT * FROM trades
WHERE closed_at IS NULL
ORDER BY opened_at DESC
"""
result = self.db.execute(query, fetch=True)
return [dict(r) for r in result] if result else []
def get_recent_trades(self, limit: int = 100) -> List[Dict]:
"""Get recent closed trades."""
query = """
SELECT * FROM trades
WHERE closed_at IS NOT NULL
ORDER BY closed_at DESC
LIMIT %s
"""
result = self.db.execute(query, (limit,), fetch=True)
return [dict(r) for r in result] if result else []
def get_trades_for_training(self, days: int = 30) -> List[Dict]:
"""
Get trades suitable for ML training.
Args:
days: Number of days to look back
Returns:
List of closed trades with features
"""
query = """
SELECT * FROM trades
WHERE closed_at IS NOT NULL
AND closed_at >= NOW() - INTERVAL '%s days'
AND features_entry IS NOT NULL
ORDER BY closed_at ASC
"""
result = self.db.execute(query, (days,), fetch=True)
return [dict(r) for r in result] if result else []
def get_daily_stats(self, trade_date: date) -> Dict[str, Any]:
"""Get statistics for a specific date."""
query = """
SELECT
COUNT(*) as total_trades,
SUM(CASE WHEN profit_usd > 0 THEN 1 ELSE 0 END) as wins,
SUM(CASE WHEN profit_usd < 0 THEN 1 ELSE 0 END) as losses,
SUM(profit_usd) as net_profit,
AVG(profit_usd) as avg_profit,
MAX(profit_usd) as max_profit,
MIN(profit_usd) as min_profit
FROM trades
WHERE DATE(closed_at) = %s
"""
result = self.db.execute(query, (trade_date,), fetch=True)
return dict(result[0]) if result else {}
def get_session_stats(self, session: str, days: int = 30) -> Dict[str, Any]:
"""Get statistics for a specific trading session."""
query = """
SELECT
COUNT(*) as total_trades,
SUM(CASE WHEN profit_usd > 0 THEN 1 ELSE 0 END) as wins,
SUM(profit_usd) as net_profit,
AVG(profit_usd) as avg_profit
FROM trades
WHERE entry_session = %s
AND closed_at >= NOW() - INTERVAL '%s days'
"""
result = self.db.execute(query, (session, days), fetch=True)
return dict(result[0]) if result else {}
def get_smc_pattern_stats(self, days: int = 30) -> List[Dict]:
"""Get statistics grouped by SMC pattern."""
query = """
SELECT
CASE
WHEN smc_fvg_detected THEN 'FVG'
WHEN smc_ob_detected THEN 'OB'
WHEN smc_bos_detected THEN 'BOS'
WHEN smc_choch_detected THEN 'CHoCH'
ELSE 'OTHER'
END as pattern,
COUNT(*) as total,
SUM(CASE WHEN profit_usd > 0 THEN 1 ELSE 0 END) as wins,
SUM(profit_usd) as profit
FROM trades
WHERE closed_at IS NOT NULL
AND closed_at >= NOW() - INTERVAL '%s days'
GROUP BY pattern
ORDER BY total DESC
"""
result = self.db.execute(query, (days,), fetch=True)
return [dict(r) for r in result] if result else []
class TrainingRepository:
"""Repository for ML training history."""
def __init__(self, db: DatabaseConnection):
self.db = db
def insert_training_run(self, training_data: Dict[str, Any]) -> Optional[Dict]:
"""
Record a new training run.
Args:
training_data: Training run information
Returns:
Inserted record with ID
"""
query = """
INSERT INTO training_runs (
training_type, bars_used, num_boost_rounds,
hmm_trained, hmm_n_regimes,
xgb_trained, train_auc, test_auc, train_accuracy, test_accuracy,
model_path, backup_path,
success, error_message,
started_at, completed_at, duration_seconds
) VALUES (
%(training_type)s, %(bars_used)s, %(num_boost_rounds)s,
%(hmm_trained)s, %(hmm_n_regimes)s,
%(xgb_trained)s, %(train_auc)s, %(test_auc)s, %(train_accuracy)s, %(test_accuracy)s,
%(model_path)s, %(backup_path)s,
%(success)s, %(error_message)s,
%(started_at)s, %(completed_at)s, %(duration_seconds)s
)
RETURNING *
"""
defaults = {
'training_type': 'manual',
'bars_used': None,
'num_boost_rounds': None,
'hmm_trained': False,
'hmm_n_regimes': 3,
'xgb_trained': False,
'train_auc': None,
'test_auc': None,
'train_accuracy': None,
'test_accuracy': None,
'model_path': None,
'backup_path': None,
'success': False,
'error_message': None,
'started_at': datetime.now(),
'completed_at': None,
'duration_seconds': None,
}
params = {**defaults, **training_data}
try:
result = self.db.insert_returning(query, params)
logger.info(f"Training run recorded: type={params['training_type']}")
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to insert training run: {e}")
raise
def update_training_complete(self, run_id: int, result_data: Dict[str, Any]) -> Optional[Dict]:
"""Update training run with completion data."""
query = """
UPDATE training_runs SET
completed_at = %(completed_at)s,
duration_seconds = %(duration_seconds)s,
hmm_trained = %(hmm_trained)s,
xgb_trained = %(xgb_trained)s,
train_auc = %(train_auc)s,
test_auc = %(test_auc)s,
train_accuracy = %(train_accuracy)s,
test_accuracy = %(test_accuracy)s,
model_path = %(model_path)s,
backup_path = %(backup_path)s,
success = %(success)s,
error_message = %(error_message)s
WHERE id = %(id)s
RETURNING *
"""
params = {**result_data, 'id': run_id}
try:
result = self.db.insert_returning(query, params)
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to update training run: {e}")
raise
def mark_rollback(self, run_id: int, reason: str) -> bool:
"""Mark a training run as rolled back."""
query = """
UPDATE training_runs SET
rolled_back = TRUE,
rollback_reason = %s,
rollback_at = NOW()
WHERE id = %s
"""
try:
self.db.execute(query, (reason, run_id))
return True
except Exception as e:
logger.error(f"Failed to mark rollback: {e}")
return False
def get_latest_successful(self) -> Optional[Dict]:
"""Get the most recent successful training run."""
query = """
SELECT * FROM training_runs
WHERE success = TRUE AND rolled_back = FALSE
ORDER BY completed_at DESC
LIMIT 1
"""
result = self.db.execute(query, fetch=True)
return dict(result[0]) if result else None
def get_training_history(self, limit: int = 20) -> List[Dict]:
"""Get recent training runs."""
query = """
SELECT * FROM training_runs
ORDER BY started_at DESC
LIMIT %s
"""
result = self.db.execute(query, (limit,), fetch=True)
return [dict(r) for r in result] if result else []
class SignalRepository:
"""Repository for trading signals."""
def __init__(self, db: DatabaseConnection):
self.db = db
def insert_signal(self, signal_data: Dict[str, Any]) -> Optional[Dict]:
"""
Record a trading signal.
Args:
signal_data: Signal information
Returns:
Inserted record
"""
query = """
INSERT INTO signals (
signal_time, symbol, price,
signal_type, signal_source, combined_confidence,
smc_signal, smc_confidence, smc_fvg, smc_ob, smc_bos, smc_choch, smc_reason,
ml_signal, ml_confidence,
regime, session, volatility, market_score, dynamic_threshold,
executed, execution_reason, trade_ticket
) VALUES (
%(signal_time)s, %(symbol)s, %(price)s,
%(signal_type)s, %(signal_source)s, %(combined_confidence)s,
%(smc_signal)s, %(smc_confidence)s, %(smc_fvg)s, %(smc_ob)s, %(smc_bos)s, %(smc_choch)s, %(smc_reason)s,
%(ml_signal)s, %(ml_confidence)s,
%(regime)s, %(session)s, %(volatility)s, %(market_score)s, %(dynamic_threshold)s,
%(executed)s, %(execution_reason)s, %(trade_ticket)s
)
RETURNING *
"""
defaults = {
'signal_time': datetime.now(),
'symbol': 'XAUUSD',
'signal_source': 'SMC-ONLY',
'combined_confidence': None,
'smc_signal': None,
'smc_confidence': None,
'smc_fvg': False,
'smc_ob': False,
'smc_bos': False,
'smc_choch': False,
'smc_reason': None,
'ml_signal': None,
'ml_confidence': None,
'regime': None,
'session': None,
'volatility': None,
'market_score': None,
'dynamic_threshold': None,
'executed': False,
'execution_reason': None,
'trade_ticket': None,
}
params = {**defaults, **signal_data}
try:
result = self.db.insert_returning(query, params)
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to insert signal: {e}")
raise
def mark_executed(self, signal_id: int, ticket: int) -> bool:
"""Mark a signal as executed with trade ticket."""
query = """
UPDATE signals SET
executed = TRUE,
trade_ticket = %s
WHERE id = %s
"""
try:
self.db.execute(query, (ticket, signal_id))
return True
except Exception as e:
logger.error(f"Failed to mark signal executed: {e}")
return False
def get_recent_signals(self, limit: int = 100) -> List[Dict]:
"""Get recent signals."""
query = """
SELECT * FROM signals
ORDER BY signal_time DESC
LIMIT %s
"""
result = self.db.execute(query, (limit,), fetch=True)
return [dict(r) for r in result] if result else []
def get_signal_stats(self, hours: int = 24) -> Dict[str, Any]:
"""Get signal statistics for recent period."""
query = """
SELECT
COUNT(*) as total_signals,
SUM(CASE WHEN signal_type = 'BUY' THEN 1 ELSE 0 END) as buy_signals,
SUM(CASE WHEN signal_type = 'SELL' THEN 1 ELSE 0 END) as sell_signals,
SUM(CASE WHEN executed THEN 1 ELSE 0 END) as executed_signals,
AVG(smc_confidence) as avg_smc_confidence,
AVG(ml_confidence) as avg_ml_confidence
FROM signals
WHERE signal_time >= NOW() - INTERVAL '%s hours'
"""
result = self.db.execute(query, (hours,), fetch=True)
return dict(result[0]) if result else {}
class MarketSnapshotRepository:
"""Repository for market state snapshots."""
def __init__(self, db: DatabaseConnection):
self.db = db
def insert_snapshot(self, snapshot_data: Dict[str, Any]) -> Optional[Dict]:
"""
Record a market snapshot.
Args:
snapshot_data: Market state information
Returns:
Inserted record
"""
query = """
INSERT INTO market_snapshots (
snapshot_time, symbol, price,
open_price, high_price, low_price, close_price,
regime, volatility, session, atr, spread,
ml_signal, ml_confidence, smc_signal, smc_confidence,
open_positions, floating_pnl,
features
) VALUES (
%(snapshot_time)s, %(symbol)s, %(price)s,
%(open_price)s, %(high_price)s, %(low_price)s, %(close_price)s,
%(regime)s, %(volatility)s, %(session)s, %(atr)s, %(spread)s,
%(ml_signal)s, %(ml_confidence)s, %(smc_signal)s, %(smc_confidence)s,
%(open_positions)s, %(floating_pnl)s,
%(features)s
)
ON CONFLICT (snapshot_time, symbol) DO UPDATE SET
price = EXCLUDED.price,
regime = EXCLUDED.regime,
ml_signal = EXCLUDED.ml_signal,
ml_confidence = EXCLUDED.ml_confidence
RETURNING *
"""
defaults = {
'snapshot_time': datetime.now(),
'symbol': 'XAUUSD',
'open_price': None,
'high_price': None,
'low_price': None,
'close_price': None,
'regime': None,
'volatility': None,
'session': None,
'atr': None,
'spread': None,
'ml_signal': None,
'ml_confidence': None,
'smc_signal': None,
'smc_confidence': None,
'open_positions': 0,
'floating_pnl': 0,
'features': '{}',
}
params = {**defaults, **snapshot_data}
# Convert features to JSON string if dict
if isinstance(params.get('features'), dict):
params['features'] = json.dumps(params['features'])
try:
result = self.db.insert_returning(query, params)
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to insert snapshot: {e}")
raise
def get_recent_snapshots(self, minutes: int = 60) -> List[Dict]:
"""Get snapshots from recent period."""
query = """
SELECT * FROM market_snapshots
WHERE snapshot_time >= NOW() - INTERVAL '%s minutes'
ORDER BY snapshot_time DESC
"""
result = self.db.execute(query, (minutes,), fetch=True)
return [dict(r) for r in result] if result else []
class BotStatusRepository:
"""Repository for bot health status."""
def __init__(self, db: DatabaseConnection):
self.db = db
def insert_status(self, status_data: Dict[str, Any]) -> Optional[Dict]:
"""Record bot status."""
query = """
INSERT INTO bot_status (
status_time, is_running, status,
loop_count, avg_execution_ms, uptime_seconds,
balance, equity, margin_used,
open_positions, floating_pnl,
daily_pnl, risk_mode,
current_session, is_golden_time,
last_error, last_error_at
) VALUES (
%(status_time)s, %(is_running)s, %(status)s,
%(loop_count)s, %(avg_execution_ms)s, %(uptime_seconds)s,
%(balance)s, %(equity)s, %(margin_used)s,
%(open_positions)s, %(floating_pnl)s,
%(daily_pnl)s, %(risk_mode)s,
%(current_session)s, %(is_golden_time)s,
%(last_error)s, %(last_error_at)s
)
RETURNING *
"""
defaults = {
'status_time': datetime.now(),
'is_running': True,
'status': 'active',
'loop_count': 0,
'avg_execution_ms': None,
'uptime_seconds': 0,
'balance': None,
'equity': None,
'margin_used': None,
'open_positions': 0,
'floating_pnl': 0,
'daily_pnl': None,
'risk_mode': 'normal',
'current_session': None,
'is_golden_time': False,
'last_error': None,
'last_error_at': None,
}
params = {**defaults, **status_data}
try:
result = self.db.insert_returning(query, params)
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to insert bot status: {e}")
raise
def get_latest_status(self) -> Optional[Dict]:
"""Get most recent bot status."""
query = """
SELECT * FROM bot_status
ORDER BY status_time DESC
LIMIT 1
"""
result = self.db.execute(query, fetch=True)
return dict(result[0]) if result else None
class DailySummaryRepository:
"""Repository for daily performance summaries."""
def __init__(self, db: DatabaseConnection):
self.db = db
def upsert_summary(self, summary_date: date, summary_data: Dict[str, Any]) -> Optional[Dict]:
"""Insert or update daily summary."""
query = """
INSERT INTO daily_summaries (
summary_date,
total_trades, winning_trades, losing_trades, breakeven_trades,
gross_profit, gross_loss, net_profit,
start_balance, end_balance,
win_rate, profit_factor, average_win, average_loss,
largest_win, largest_loss,
trades_sydney, trades_tokyo, trades_london, trades_ny, trades_golden,
fvg_trades, fvg_wins, ob_trades, ob_wins
) VALUES (
%(summary_date)s,
%(total_trades)s, %(winning_trades)s, %(losing_trades)s, %(breakeven_trades)s,
%(gross_profit)s, %(gross_loss)s, %(net_profit)s,
%(start_balance)s, %(end_balance)s,
%(win_rate)s, %(profit_factor)s, %(average_win)s, %(average_loss)s,
%(largest_win)s, %(largest_loss)s,
%(trades_sydney)s, %(trades_tokyo)s, %(trades_london)s, %(trades_ny)s, %(trades_golden)s,
%(fvg_trades)s, %(fvg_wins)s, %(ob_trades)s, %(ob_wins)s
)
ON CONFLICT (summary_date) DO UPDATE SET
total_trades = EXCLUDED.total_trades,
winning_trades = EXCLUDED.winning_trades,
losing_trades = EXCLUDED.losing_trades,
net_profit = EXCLUDED.net_profit,
end_balance = EXCLUDED.end_balance,
win_rate = EXCLUDED.win_rate,
updated_at = NOW()
RETURNING *
"""
defaults = {
'summary_date': summary_date,
'total_trades': 0,
'winning_trades': 0,
'losing_trades': 0,
'breakeven_trades': 0,
'gross_profit': 0,
'gross_loss': 0,
'net_profit': 0,
'start_balance': None,
'end_balance': None,
'win_rate': None,
'profit_factor': None,
'average_win': None,
'average_loss': None,
'largest_win': None,
'largest_loss': None,
'trades_sydney': 0,
'trades_tokyo': 0,
'trades_london': 0,
'trades_ny': 0,
'trades_golden': 0,
'fvg_trades': 0,
'fvg_wins': 0,
'ob_trades': 0,
'ob_wins': 0,
}
params = {**defaults, **summary_data}
try:
result = self.db.insert_returning(query, params)
return dict(result) if result else None
except Exception as e:
logger.error(f"Failed to upsert daily summary: {e}")
raise
def get_summary(self, summary_date: date) -> Optional[Dict]:
"""Get summary for specific date."""
query = "SELECT * FROM daily_summaries WHERE summary_date = %s"
result = self.db.execute(query, (summary_date,), fetch=True)
return dict(result[0]) if result else None
def get_recent_summaries(self, days: int = 30) -> List[Dict]:
"""Get recent daily summaries."""
query = """
SELECT * FROM daily_summaries
ORDER BY summary_date DESC
LIMIT %s
"""
result = self.db.execute(query, (days,), fetch=True)
return [dict(r) for r in result] if result else []