fix: remaining medium-priority issues from code review + log analysis
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>
This commit is contained in:
+58
-23
@@ -12,10 +12,13 @@ FEATURES:
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -32,8 +35,12 @@ class TradingDatabase:
|
||||
self.db_path = db_path
|
||||
self.conn = None
|
||||
self.cursor = None
|
||||
self._connect()
|
||||
self._create_tables()
|
||||
try:
|
||||
self._connect()
|
||||
self._create_tables()
|
||||
except Exception:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def _connect(self):
|
||||
"""Establish database connection"""
|
||||
@@ -179,6 +186,15 @@ class TradingDatabase:
|
||||
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()
|
||||
|
||||
|
||||
@@ -292,8 +308,6 @@ class TradingDatabase:
|
||||
commission: Commission paid
|
||||
swap: Swap paid
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
# Calculate duration if we have entry_time
|
||||
duration_hours = None
|
||||
try:
|
||||
@@ -307,8 +321,8 @@ class TradingDatabase:
|
||||
else:
|
||||
exit_dt = exit_time
|
||||
duration_hours = (exit_dt - entry_time).total_seconds() / 3600
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not calculate duration for ticket {ticket}: {e}")
|
||||
|
||||
net_profit = profit - commission - swap
|
||||
|
||||
@@ -557,14 +571,7 @@ class TradingDatabase:
|
||||
Returns:
|
||||
Dictionary with comprehensive statistics
|
||||
"""
|
||||
date_filter = ""
|
||||
params = []
|
||||
|
||||
if days:
|
||||
date_filter = "AND entry_time >= datetime('now', '-' || ? || ' days')"
|
||||
params.append(days)
|
||||
|
||||
query = f"""
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_trades,
|
||||
SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins,
|
||||
@@ -580,8 +587,11 @@ class TradingDatabase:
|
||||
ROUND(AVG(duration_hours), 2) as avg_duration
|
||||
FROM trades
|
||||
WHERE status = 'closed'
|
||||
{date_filter}
|
||||
"""
|
||||
params = []
|
||||
if days:
|
||||
query += " AND entry_time >= datetime('now', '-' || ? || ' days')"
|
||||
params.append(days)
|
||||
|
||||
self.cursor.execute(query, params)
|
||||
row = self.cursor.fetchone()
|
||||
@@ -639,24 +649,49 @@ class TradingDatabase:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Insert trade
|
||||
# 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 trade is closed, update exit data
|
||||
if trade.get('exit_time'):
|
||||
self.update_trade_exit(
|
||||
trade['ticket'],
|
||||
trade
|
||||
)
|
||||
if exit_str:
|
||||
self.update_trade_exit(trade['ticket'], trade)
|
||||
|
||||
migrated += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error migrating trade {trade.get('ticket')}: {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:
|
||||
|
||||
Reference in New Issue
Block a user