trading_database.py:
- migrate_from_json: validate exit_time > entry_time before applying exit update
Trades with exit before entry are logged as open (no invalid exit applied)
This prevents the timestamp inversion bug that corrupted the DB with 625 bad trades
position_monitor.py:
- Replace fragile datetime.strptime('%Y-%m-%d %H:%M:%S') with fromisoformat()
Handles both space-separated and ISO 8601 T-separated formats, strips microseconds
trading_bot_gui.py:
- Call infra.log_bot_status('running') on bot start -> bot_status table now populated
- Call infra.log_bot_status('stopped') on bot stop
Previously bot_status table remained empty (0 rows), making monitoring impossible
telegram_bot_commands_old.py:
- Remove superseded file (replaced by telegram_bot_commands.py)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
747 lines
24 KiB
Python
747 lines
24 KiB
Python
"""
|
|
🗄️ TRADING DATABASE MODULE - SQLite Integration
|
|
Ersetzt JSON Logging mit strukturierter Datenbank
|
|
|
|
FEATURES:
|
|
- Trade History Tracking
|
|
- Performance Analytics
|
|
- Session-based Queries
|
|
- Confidence-based Analysis
|
|
- Easy Migration von JSON
|
|
"""
|
|
|
|
import sqlite3
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional, Tuple
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TradingDatabase:
|
|
"""
|
|
SQLite Database für Trading Bot Performance Tracking
|
|
"""
|
|
|
|
def __init__(self, db_path: str = "trading_bot.db"):
|
|
"""
|
|
Initialize Database Connection
|
|
|
|
Args:
|
|
db_path: Path to SQLite database file
|
|
"""
|
|
self.db_path = db_path
|
|
self.conn = None
|
|
self.cursor = None
|
|
try:
|
|
self._connect()
|
|
self._create_tables()
|
|
except Exception:
|
|
self.close()
|
|
raise
|
|
|
|
def _connect(self):
|
|
"""Establish database connection"""
|
|
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
self.conn.row_factory = sqlite3.Row # Enable column access by name
|
|
self.cursor = self.conn.cursor()
|
|
|
|
def _create_tables(self):
|
|
"""Create all necessary tables"""
|
|
|
|
# Trades Table - Main trade history
|
|
self.cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS trades (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ticket INTEGER UNIQUE,
|
|
position_id INTEGER,
|
|
symbol TEXT NOT NULL,
|
|
strategy_name TEXT NOT NULL,
|
|
|
|
-- Trade Details
|
|
type TEXT NOT NULL, -- 'BUY' or 'SELL'
|
|
volume REAL NOT NULL,
|
|
entry_price REAL NOT NULL,
|
|
exit_price REAL,
|
|
|
|
-- Stops
|
|
sl_price REAL,
|
|
tp_price REAL,
|
|
|
|
-- Timing
|
|
entry_time DATETIME NOT NULL,
|
|
exit_time DATETIME,
|
|
duration_hours REAL,
|
|
|
|
-- Session Info
|
|
session TEXT NOT NULL, -- 'asian', 'london', 'overlap', 'ny'
|
|
regime TEXT, -- 'trending', 'ranging'
|
|
quality TEXT, -- 'excellent', 'good', 'medium', 'poor'
|
|
|
|
-- Signal Quality
|
|
confidence REAL,
|
|
timeframe_alignment INTEGER, -- How many timeframes aligned
|
|
|
|
-- Performance
|
|
profit REAL,
|
|
commission REAL DEFAULT 0,
|
|
swap REAL DEFAULT 0,
|
|
net_profit REAL,
|
|
profit_pct REAL,
|
|
|
|
-- Risk Management
|
|
risk_amount REAL,
|
|
risk_pct REAL,
|
|
rr_ratio REAL, -- Risk/Reward ratio
|
|
|
|
-- Status
|
|
status TEXT DEFAULT 'open', -- 'open', 'closed', 'cancelled'
|
|
exit_reason TEXT, -- 'tp', 'sl', 'manual', 'timeout'
|
|
|
|
-- Metadata
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
# Performance Summary Table - Daily/Weekly aggregates
|
|
self.cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS performance_summary (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
date DATE NOT NULL UNIQUE,
|
|
|
|
-- Trade Counts
|
|
total_trades INTEGER DEFAULT 0,
|
|
wins INTEGER DEFAULT 0,
|
|
losses INTEGER DEFAULT 0,
|
|
win_rate REAL DEFAULT 0,
|
|
|
|
-- Profit
|
|
gross_profit REAL DEFAULT 0,
|
|
gross_loss REAL DEFAULT 0,
|
|
net_profit REAL DEFAULT 0,
|
|
profit_factor REAL DEFAULT 0,
|
|
|
|
-- Session Breakdown
|
|
asian_trades INTEGER DEFAULT 0,
|
|
london_trades INTEGER DEFAULT 0,
|
|
overlap_trades INTEGER DEFAULT 0,
|
|
ny_trades INTEGER DEFAULT 0,
|
|
|
|
asian_profit REAL DEFAULT 0,
|
|
london_profit REAL DEFAULT 0,
|
|
overlap_profit REAL DEFAULT 0,
|
|
ny_profit REAL DEFAULT 0,
|
|
|
|
-- Risk Metrics
|
|
max_drawdown REAL DEFAULT 0,
|
|
avg_trade REAL DEFAULT 0,
|
|
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
# Bot Status Table - Health monitoring
|
|
self.cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS bot_status (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
status TEXT NOT NULL, -- 'running', 'stopped', 'error'
|
|
version TEXT,
|
|
active_sessions TEXT, -- JSON array of enabled sessions
|
|
confidence_threshold REAL,
|
|
|
|
-- Health Metrics
|
|
uptime_hours REAL,
|
|
last_trade_time DATETIME,
|
|
total_positions INTEGER DEFAULT 0,
|
|
|
|
error_message TEXT,
|
|
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
# Create indexes for common queries
|
|
self.cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_trades_entry_time
|
|
ON trades(entry_time)
|
|
""")
|
|
|
|
self.cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_trades_session
|
|
ON trades(session)
|
|
""")
|
|
|
|
self.cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_trades_status
|
|
ON trades(status)
|
|
""")
|
|
|
|
self.cursor.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_trades_confidence
|
|
ON trades(confidence)
|
|
""")
|
|
|
|
# Key-value store for persistent bot settings (e.g. drawdown pause state)
|
|
self.cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS bot_settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
self.conn.commit()
|
|
|
|
|
|
# ==========================================
|
|
# INSERT OPERATIONS
|
|
# ==========================================
|
|
|
|
def log_trade_entry(self, trade_data: Dict) -> int:
|
|
"""
|
|
Log a new trade entry
|
|
|
|
Args:
|
|
trade_data: Dictionary with trade information
|
|
|
|
Returns:
|
|
trade_id: Database ID of inserted trade
|
|
"""
|
|
query = """
|
|
INSERT INTO trades (
|
|
ticket, position_id, symbol, strategy_name,
|
|
type, volume, entry_price, sl_price, tp_price,
|
|
entry_time, session, regime, quality,
|
|
confidence, timeframe_alignment,
|
|
risk_amount, risk_pct,
|
|
status
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
"""
|
|
|
|
values = (
|
|
trade_data.get('ticket'),
|
|
trade_data.get('position_id'),
|
|
trade_data.get('symbol'),
|
|
trade_data.get('strategy_name'),
|
|
trade_data.get('type'),
|
|
trade_data.get('volume'),
|
|
trade_data.get('entry_price'),
|
|
trade_data.get('sl_price'),
|
|
trade_data.get('tp_price'),
|
|
trade_data.get('entry_time'),
|
|
trade_data.get('session'),
|
|
trade_data.get('regime'),
|
|
trade_data.get('quality'),
|
|
trade_data.get('confidence'),
|
|
trade_data.get('timeframe_alignment'),
|
|
trade_data.get('risk_amount'),
|
|
trade_data.get('risk_pct'),
|
|
'open'
|
|
)
|
|
|
|
self.cursor.execute(query, values)
|
|
self.conn.commit()
|
|
|
|
return self.cursor.lastrowid
|
|
|
|
|
|
def update_trade_exit(self, ticket: int, exit_data: Dict):
|
|
"""
|
|
Update trade with exit information
|
|
|
|
Args:
|
|
ticket: MT5 ticket number
|
|
exit_data: Dictionary with exit information
|
|
"""
|
|
query = """
|
|
UPDATE trades SET
|
|
exit_price = ?,
|
|
exit_time = ?,
|
|
duration_hours = ?,
|
|
profit = ?,
|
|
commission = ?,
|
|
swap = ?,
|
|
net_profit = ?,
|
|
profit_pct = ?,
|
|
rr_ratio = ?,
|
|
status = 'closed',
|
|
exit_reason = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE ticket = ?
|
|
"""
|
|
|
|
values = (
|
|
exit_data.get('exit_price'),
|
|
exit_data.get('exit_time'),
|
|
exit_data.get('duration_hours'),
|
|
exit_data.get('profit'),
|
|
exit_data.get('commission', 0),
|
|
exit_data.get('swap', 0),
|
|
exit_data.get('net_profit'),
|
|
exit_data.get('profit_pct'),
|
|
exit_data.get('rr_ratio'),
|
|
exit_data.get('exit_reason'),
|
|
ticket
|
|
)
|
|
|
|
self.cursor.execute(query, values)
|
|
self.conn.commit()
|
|
|
|
def close_trade(self, ticket: int, exit_price: float, exit_time: str,
|
|
profit: float, status: str = 'closed', exit_reason: str = None,
|
|
commission: float = 0, swap: float = 0):
|
|
"""
|
|
Simplified wrapper for closing a trade (used by Position Monitor)
|
|
|
|
Args:
|
|
ticket: MT5 ticket number
|
|
exit_price: Exit price
|
|
exit_time: Exit datetime
|
|
profit: Trade profit
|
|
status: Trade status (default 'closed')
|
|
exit_reason: Reason for exit ('tp', 'sl', 'manual', etc.)
|
|
commission: Commission paid
|
|
swap: Swap paid
|
|
"""
|
|
# Calculate duration if we have entry_time
|
|
duration_hours = None
|
|
try:
|
|
query = "SELECT entry_time FROM trades WHERE ticket = ?"
|
|
self.cursor.execute(query, (ticket,))
|
|
row = self.cursor.fetchone()
|
|
if row:
|
|
entry_time = datetime.fromisoformat(row[0])
|
|
if isinstance(exit_time, str):
|
|
exit_dt = datetime.fromisoformat(exit_time)
|
|
else:
|
|
exit_dt = exit_time
|
|
duration_hours = (exit_dt - entry_time).total_seconds() / 3600
|
|
except Exception as e:
|
|
logger.warning(f"Could not calculate duration for ticket {ticket}: {e}")
|
|
|
|
net_profit = profit - commission - swap
|
|
|
|
exit_data = {
|
|
'exit_price': exit_price,
|
|
'exit_time': exit_time if isinstance(exit_time, str) else exit_time.isoformat(),
|
|
'duration_hours': duration_hours,
|
|
'profit': profit,
|
|
'commission': commission,
|
|
'swap': swap,
|
|
'net_profit': net_profit,
|
|
'exit_reason': exit_reason,
|
|
'profit_pct': None, # Would need entry data to calculate
|
|
'rr_ratio': None # Would need entry data to calculate
|
|
}
|
|
|
|
self.update_trade_exit(ticket, exit_data)
|
|
|
|
|
|
def get_open_trades(self) -> List[Dict]:
|
|
"""
|
|
Get all currently open trades from database
|
|
|
|
Returns:
|
|
List of open trades as dictionaries
|
|
"""
|
|
query = """
|
|
SELECT
|
|
ticket, position_id, symbol, strategy_name,
|
|
type, volume, entry_price, sl_price, tp_price,
|
|
entry_time, session, regime, quality,
|
|
confidence, timeframe_alignment,
|
|
risk_amount, risk_pct, status
|
|
FROM trades
|
|
WHERE status = 'open'
|
|
ORDER BY entry_time DESC
|
|
"""
|
|
|
|
self.cursor.execute(query)
|
|
rows = self.cursor.fetchall()
|
|
|
|
# Convert to list of dictionaries
|
|
trades = []
|
|
for row in rows:
|
|
trades.append(dict(row))
|
|
|
|
return trades
|
|
|
|
|
|
def log_bot_status(self, status_data: Dict):
|
|
"""
|
|
Log bot status for health monitoring
|
|
|
|
Args:
|
|
status_data: Dictionary with bot status information
|
|
"""
|
|
query = """
|
|
INSERT INTO bot_status (
|
|
status, version, active_sessions, confidence_threshold,
|
|
uptime_hours, last_trade_time, total_positions, error_message
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
"""
|
|
|
|
values = (
|
|
status_data.get('status', 'running'),
|
|
status_data.get('version'),
|
|
json.dumps(status_data.get('active_sessions', [])),
|
|
status_data.get('confidence_threshold'),
|
|
status_data.get('uptime_hours'),
|
|
status_data.get('last_trade_time'),
|
|
status_data.get('total_positions', 0),
|
|
status_data.get('error_message')
|
|
)
|
|
|
|
self.cursor.execute(query, values)
|
|
self.conn.commit()
|
|
|
|
|
|
# ==========================================
|
|
# QUERY OPERATIONS
|
|
# ==========================================
|
|
|
|
def get_recent_trades(self, limit: int = 50) -> List[Dict]:
|
|
"""Get most recent trades"""
|
|
query = """
|
|
SELECT * FROM trades
|
|
ORDER BY entry_time DESC
|
|
LIMIT ?
|
|
"""
|
|
self.cursor.execute(query, (limit,))
|
|
return [dict(row) for row in self.cursor.fetchall()]
|
|
|
|
|
|
def get_open_positions(self) -> List[Dict]:
|
|
"""Get all currently open positions"""
|
|
query = """
|
|
SELECT * FROM trades
|
|
WHERE status = 'open'
|
|
ORDER BY entry_time DESC
|
|
"""
|
|
self.cursor.execute(query)
|
|
return [dict(row) for row in self.cursor.fetchall()]
|
|
|
|
|
|
def get_session_performance(self, days: int = 30) -> Dict:
|
|
"""
|
|
Get performance breakdown by session
|
|
|
|
Args:
|
|
days: Number of days to analyze
|
|
|
|
Returns:
|
|
Dictionary with session statistics
|
|
"""
|
|
query = """
|
|
SELECT
|
|
session,
|
|
COUNT(*) as trade_count,
|
|
SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins,
|
|
SUM(CASE WHEN net_profit < 0 THEN 1 ELSE 0 END) as losses,
|
|
ROUND(AVG(CASE WHEN net_profit > 0 THEN 1.0 ELSE 0.0 END) * 100, 2) as win_rate,
|
|
ROUND(SUM(net_profit), 2) as total_profit,
|
|
ROUND(AVG(net_profit), 2) as avg_profit
|
|
FROM trades
|
|
WHERE status = 'closed'
|
|
AND entry_time >= datetime('now', '-' || ? || ' days')
|
|
GROUP BY session
|
|
ORDER BY total_profit DESC
|
|
"""
|
|
|
|
self.cursor.execute(query, (days,))
|
|
results = {}
|
|
|
|
for row in self.cursor.fetchall():
|
|
results[row['session']] = {
|
|
'count': row['trade_count'],
|
|
'wins': row['wins'],
|
|
'losses': row['losses'],
|
|
'win_rate': row['win_rate'],
|
|
'total_profit': row['total_profit'],
|
|
'avg_profit': row['avg_profit']
|
|
}
|
|
|
|
return results
|
|
|
|
|
|
def get_confidence_analysis(self, days: int = 30) -> Dict:
|
|
"""
|
|
Analyze performance by confidence levels
|
|
|
|
Args:
|
|
days: Number of days to analyze
|
|
|
|
Returns:
|
|
Dictionary with confidence-based statistics
|
|
"""
|
|
query = """
|
|
SELECT
|
|
CASE
|
|
WHEN confidence >= 80 THEN '80-100'
|
|
WHEN confidence >= 70 THEN '70-79'
|
|
WHEN confidence >= 60 THEN '60-69'
|
|
ELSE '<60'
|
|
END as confidence_range,
|
|
COUNT(*) as trade_count,
|
|
SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins,
|
|
ROUND(AVG(CASE WHEN net_profit > 0 THEN 1.0 ELSE 0.0 END) * 100, 2) as win_rate,
|
|
ROUND(SUM(net_profit), 2) as total_profit,
|
|
ROUND(AVG(net_profit), 2) as avg_profit
|
|
FROM trades
|
|
WHERE status = 'closed'
|
|
AND confidence IS NOT NULL
|
|
AND entry_time >= datetime('now', '-' || ? || ' days')
|
|
GROUP BY confidence_range
|
|
ORDER BY confidence_range DESC
|
|
"""
|
|
|
|
self.cursor.execute(query, (days,))
|
|
results = {}
|
|
|
|
for row in self.cursor.fetchall():
|
|
results[row['confidence_range']] = {
|
|
'count': row['trade_count'],
|
|
'wins': row['wins'],
|
|
'win_rate': row['win_rate'],
|
|
'total_profit': row['total_profit'],
|
|
'avg_profit': row['avg_profit']
|
|
}
|
|
|
|
return results
|
|
|
|
|
|
def get_daily_summary(self, date: str = None) -> Dict:
|
|
"""
|
|
Get daily performance summary
|
|
|
|
Args:
|
|
date: Date string (YYYY-MM-DD), defaults to today
|
|
|
|
Returns:
|
|
Dictionary with daily statistics
|
|
"""
|
|
if date is None:
|
|
date = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
query = """
|
|
SELECT
|
|
COUNT(*) as total_trades,
|
|
SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins,
|
|
SUM(CASE WHEN net_profit < 0 THEN 1 ELSE 0 END) as losses,
|
|
ROUND(AVG(CASE WHEN net_profit > 0 THEN 1.0 ELSE 0.0 END) * 100, 2) as win_rate,
|
|
ROUND(SUM(CASE WHEN net_profit > 0 THEN net_profit ELSE 0 END), 2) as gross_profit,
|
|
ROUND(SUM(CASE WHEN net_profit < 0 THEN net_profit ELSE 0 END), 2) as gross_loss,
|
|
ROUND(SUM(net_profit), 2) as net_profit,
|
|
ROUND(AVG(net_profit), 2) as avg_trade
|
|
FROM trades
|
|
WHERE status = 'closed'
|
|
AND DATE(entry_time) = ?
|
|
"""
|
|
|
|
self.cursor.execute(query, (date,))
|
|
row = self.cursor.fetchone()
|
|
|
|
if row and row['total_trades'] > 0:
|
|
return dict(row)
|
|
else:
|
|
return {
|
|
'total_trades': 0,
|
|
'wins': 0,
|
|
'losses': 0,
|
|
'win_rate': 0,
|
|
'gross_profit': 0,
|
|
'gross_loss': 0,
|
|
'net_profit': 0,
|
|
'avg_trade': 0
|
|
}
|
|
|
|
|
|
def get_overall_statistics(self, days: int = None) -> Dict:
|
|
"""
|
|
Get overall performance statistics
|
|
|
|
Args:
|
|
days: Number of days to analyze (None = all time)
|
|
|
|
Returns:
|
|
Dictionary with comprehensive statistics
|
|
"""
|
|
query = """
|
|
SELECT
|
|
COUNT(*) as total_trades,
|
|
SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins,
|
|
SUM(CASE WHEN net_profit < 0 THEN 1 ELSE 0 END) as losses,
|
|
ROUND(AVG(CASE WHEN net_profit > 0 THEN 1.0 ELSE 0.0 END) * 100, 2) as win_rate,
|
|
ROUND(SUM(CASE WHEN net_profit > 0 THEN net_profit ELSE 0 END), 2) as gross_profit,
|
|
ROUND(SUM(CASE WHEN net_profit < 0 THEN net_profit ELSE 0 END), 2) as gross_loss,
|
|
ROUND(SUM(net_profit), 2) as net_profit,
|
|
ROUND(AVG(net_profit), 2) as avg_trade,
|
|
ROUND(SUM(commission), 2) as total_commission,
|
|
ROUND(SUM(swap), 2) as total_swap,
|
|
ROUND(AVG(confidence), 2) as avg_confidence,
|
|
ROUND(AVG(duration_hours), 2) as avg_duration
|
|
FROM trades
|
|
WHERE status = 'closed'
|
|
"""
|
|
params = []
|
|
if days:
|
|
query += " AND entry_time >= datetime('now', '-' || ? || ' days')"
|
|
params.append(days)
|
|
|
|
self.cursor.execute(query, params)
|
|
row = self.cursor.fetchone()
|
|
|
|
stats = dict(row) if row else {}
|
|
|
|
# Calculate profit factor
|
|
gross_profit = stats.get('gross_profit') or 0
|
|
gross_loss = stats.get('gross_loss') or 0
|
|
|
|
if gross_loss != 0:
|
|
stats['profit_factor'] = round(
|
|
abs(gross_profit / gross_loss),
|
|
2
|
|
)
|
|
else:
|
|
stats['profit_factor'] = 0
|
|
|
|
return stats
|
|
|
|
|
|
# ==========================================
|
|
# UTILITY FUNCTIONS
|
|
# ==========================================
|
|
|
|
def migrate_from_json(self, json_file: str):
|
|
"""
|
|
Migrate existing JSON trade data to SQLite
|
|
|
|
Args:
|
|
json_file: Path to JSON performance file
|
|
"""
|
|
if not os.path.exists(json_file):
|
|
print(f"❌ JSON file not found: {json_file}")
|
|
return
|
|
|
|
with open(json_file, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
# Extract trades from JSON structure
|
|
trades = data.get('trades', [])
|
|
|
|
migrated = 0
|
|
skipped = 0
|
|
|
|
for trade in trades:
|
|
try:
|
|
# Check if trade already exists
|
|
self.cursor.execute(
|
|
"SELECT id FROM trades WHERE ticket = ?",
|
|
(trade.get('ticket'),)
|
|
)
|
|
|
|
if self.cursor.fetchone():
|
|
skipped += 1
|
|
continue
|
|
|
|
# Validate timestamp order before inserting
|
|
entry_str = trade.get('entry_time')
|
|
exit_str = trade.get('exit_time')
|
|
if entry_str and exit_str:
|
|
try:
|
|
entry_dt = datetime.fromisoformat(str(entry_str))
|
|
exit_dt = datetime.fromisoformat(str(exit_str))
|
|
if exit_dt < entry_dt:
|
|
logger.warning(
|
|
f"Skipping exit update for ticket {trade.get('ticket')}: "
|
|
f"exit_time ({exit_str}) is before entry_time ({entry_str})"
|
|
)
|
|
exit_str = None # insert as open, don't apply invalid exit
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
self.log_trade_entry(trade)
|
|
|
|
if exit_str:
|
|
self.update_trade_exit(trade['ticket'], trade)
|
|
|
|
migrated += 1
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error migrating trade {trade.get('ticket')}: {e}")
|
|
|
|
print(f"✅ Migration complete: {migrated} trades migrated, {skipped} skipped")
|
|
|
|
|
|
def save_setting(self, key: str, value: str):
|
|
"""Persist a key-value setting across restarts"""
|
|
self.cursor.execute("""
|
|
INSERT OR REPLACE INTO bot_settings (key, value, updated_at)
|
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
""", (key, value))
|
|
self.conn.commit()
|
|
|
|
def load_setting(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
"""Load a persisted setting, returns default if not found"""
|
|
self.cursor.execute("SELECT value FROM bot_settings WHERE key = ?", (key,))
|
|
row = self.cursor.fetchone()
|
|
return row['value'] if row else default
|
|
|
|
def close(self):
|
|
"""Close database connection"""
|
|
if self.conn:
|
|
self.conn.close()
|
|
|
|
|
|
def __enter__(self):
|
|
"""Context manager entry"""
|
|
return self
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Context manager exit"""
|
|
self.close()
|
|
|
|
|
|
# ==========================================
|
|
# USAGE EXAMPLE
|
|
# ==========================================
|
|
|
|
if __name__ == "__main__":
|
|
# Initialize database
|
|
db = TradingDatabase("trading_bot.db")
|
|
|
|
print("="*70)
|
|
print("🗄️ TRADING DATABASE - System Check")
|
|
print("="*70)
|
|
|
|
# Check tables
|
|
db.cursor.execute("""
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table'
|
|
ORDER BY name
|
|
""")
|
|
|
|
tables = db.cursor.fetchall()
|
|
print(f"\n📊 Tables created: {len(tables)}")
|
|
for table in tables:
|
|
print(f" ✅ {table['name']}")
|
|
|
|
# Get overall stats
|
|
stats = db.get_overall_statistics()
|
|
print(f"\n📈 Overall Statistics:")
|
|
print(f" Total Trades: {stats.get('total_trades', 0)}")
|
|
print(f" Win Rate: {stats.get('win_rate', 0)}%")
|
|
print(f" Net Profit: ${stats.get('net_profit', 0)}")
|
|
|
|
db.close()
|
|
|
|
print("\n" + "="*70)
|
|
print("✅ Database ready!")
|
|
print("="*70)
|