diff --git a/scripts/fix_trade_records.py b/scripts/fix_trade_records.py new file mode 100644 index 0000000..a5f018e --- /dev/null +++ b/scripts/fix_trade_records.py @@ -0,0 +1,251 @@ + +import asyncio +import logging +import os +import sys +from datetime import datetime, timedelta + +# Add project root to path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from trading_system.binance_client import BinanceClient +from binance import AsyncClient +from backend.database.connection import db +from backend.database.models import Trade, Account + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +async def fix_time_inversion_trades(client, account_id): + """Fix trades where exit_time < entry_time""" + logger.info("Scanning for time-inverted trades...") + + # Query invalid trades + sql = """ + SELECT * FROM trades + WHERE status = 'closed' + AND exit_time < entry_time + AND account_id = %s + """ + trades = db.execute_query(sql, (account_id,)) + + if not trades: + logger.info("No time-inverted trades found.") + + for trade in trades: + symbol = trade['symbol'] + trade_id = trade['id'] + entry_time = trade['entry_time'] + exit_time = trade['exit_time'] + quantity = trade['quantity'] + side = trade['side'] # OPEN side (BUY/SELL) + + logger.info(f"Found invalid trade #{trade_id} {symbol}: Entry={entry_time}, Exit={exit_time} (Diff: {exit_time - entry_time}s)") + + # 1. Check actual position on Binance + # Convert entry_time to ms for Binance API + start_time_ms = int(entry_time * 1000) + + try: + # Fetch user trades (fills) after entry time + fills = await client.client.futures_account_trades( + symbol=symbol, + startTime=start_time_ms, + limit=100 + ) + + # Filter for closing trades + # If entry side was BUY, we look for SELL. If SELL, look for BUY. + close_side = 'SELL' if side == 'BUY' else 'BUY' + + closing_fills = [f for f in fills if f['side'] == close_side and f['realizedPnl'] != '0'] + + if not closing_fills: + # No closing trades found. Position might still be open or we missed it? + # Check current position risk + positions = await client.get_open_positions() + current_pos = next((p for p in positions if p['symbol'] == symbol), None) + + if current_pos and float(current_pos['positionAmt']) != 0: + logger.info(f" -> Position for {symbol} is actually OPEN on Binance. Reverting status.") + # Revert to open + db.execute_update( + "UPDATE trades SET status='open', exit_time=NULL, exit_price=NULL, pnl=NULL, pnl_percent=NULL, exit_reason=NULL WHERE id=%s", + (trade_id,) + ) + else: + logger.warning(f" -> Position closed on Binance but no closing fills found after entry time. Maybe closed exactly at entry or data issue?") + else: + # Found closing fills. Calculate metrics. + # We assume the latest fills correspond to this trade. + # This is a simplification but better than "ghost" order. + + # Sort by time desc + closing_fills.sort(key=lambda x: x['time'], reverse=True) + + last_fill = closing_fills[0] + actual_exit_time_ms = last_fill['time'] + actual_exit_time_s = int(actual_exit_time_ms / 1000) + + total_pnl = sum(float(f['realizedPnl']) for f in closing_fills) + total_commission = sum(float(f['commission']) for f in closing_fills) + commission_asset = closing_fills[0]['commissionAsset'] + + # Calculate avg exit price + total_qty = sum(float(f['qty']) for f in closing_fills) + total_value = sum(float(f['qty']) * float(f['price']) for f in closing_fills) + avg_exit_price = total_value / total_qty if total_qty > 0 else 0 + + entry_price = float(trade['entry_price']) + pnl_percent = 0 + if entry_price > 0: + if side == 'BUY': + pnl_percent = ((avg_exit_price - entry_price) / entry_price) * 100 + else: + pnl_percent = ((entry_price - avg_exit_price) / entry_price) * 100 + + logger.info(f" -> Found closing fills. Updating Trade #{trade_id}:") + logger.info(f" New Exit Time: {actual_exit_time_s} (was {exit_time})") + logger.info(f" Realized PnL: {total_pnl}") + logger.info(f" Commission: {total_commission} {commission_asset}") + + # Update DB + Trade.update_exit( + trade_id=trade_id, + exit_price=avg_exit_price, + exit_reason='sync_fix', + pnl=total_pnl, # Use realized PnL as PnL + pnl_percent=pnl_percent, + exit_time_ts=actual_exit_time_s, + realized_pnl=total_pnl, + commission=total_commission, + commission_asset=commission_asset + ) + + except Exception as e: + logger.error(f"Error processing trade #{trade_id}: {type(e).__name__}: {e}") + import traceback + logger.error(traceback.format_exc()) + +async def backfill_commissions(client, account_id): + """Backfill missing commission data for recent trades""" + logger.info("Backfilling commissions for recent trades...") + + # Get recent closed trades (last 24h) + start_time = int((datetime.now() - timedelta(hours=24)).timestamp()) + + sql = """ + SELECT * FROM trades + WHERE status = 'closed' + AND account_id = %s + AND exit_time > %s + AND (commission IS NULL OR commission = 0) + """ + trades = db.execute_query(sql, (account_id, start_time)) + + if not trades: + logger.info("No recent trades with missing commissions found.") + return + + logger.info(f"Found {len(trades)} trades to update commissions.") + + for trade in trades: + symbol = trade['symbol'] + trade_id = trade['id'] + entry_time = trade['entry_time'] + + try: + # Fetch user trades (fills) after entry time + start_time_ms = int(entry_time * 1000) + fills = await client.client.futures_account_trades( + symbol=symbol, + startTime=start_time_ms, + limit=100 + ) + + if not fills: + continue + + # Filter for closing trades (approximate logic: matches trade direction) + # Actually we want ALL commissions for this round trip (Open + Close) + # But simpler to just get closing commissions for now as per `update_exit` + + # Simple approach: Sum all commissions for this symbol after entry time + # This might over-count if there are multiple trades, but better than 0. + + # Better approach: match exit order ID if available + exit_order_id = trade.get('exit_order_id') + + related_fills = [] + if exit_order_id: + related_fills = [f for f in fills if str(f['orderId']) == str(exit_order_id)] + else: + # Fallback: fills around exit time + exit_time = trade['exit_time'] + if exit_time: + exit_time_ms = exit_time * 1000 + related_fills = [f for f in fills if abs(f['time'] - exit_time_ms) < 5000] # 5s window + + if related_fills: + total_commission = sum(float(f['commission']) for f in related_fills) + commission_asset = related_fills[0]['commissionAsset'] + realized_pnl = sum(float(f['realizedPnl']) for f in related_fills) + + logger.info(f"Updating Trade #{trade_id} {symbol}: Comm={total_commission} {commission_asset}, PnL={realized_pnl}") + + db.execute_update( + "UPDATE trades SET commission=%s, commission_asset=%s, realized_pnl=%s WHERE id=%s", + (total_commission, commission_asset, realized_pnl, trade_id) + ) + + except Exception as e: + logger.error(f"Error backfilling trade #{trade_id}: {e}") + +async def main(): + try: + # Get all active accounts + accounts = Account.list_all() + active_accounts = [a for a in accounts if a['status'] == 'active' and a['api_key_enc']] + + if not active_accounts: + logger.error("No active accounts with API keys found.") + return + + for account in active_accounts: + account_id = account['id'] + account_name = account['name'] + logger.info(f"=== Processing Account: {account_name} (ID: {account_id}) ===") + + # Get credentials + api_key, api_secret, testnet, _ = Account.get_credentials(account_id) + + if not api_key or not api_secret: + logger.warning(f"Skipping account {account_id} due to missing keys.") + continue + + # Init client + client = BinanceClient(api_key, api_secret, testnet=testnet) + # Initialize connection + if not client.client: + client.client = AsyncClient(api_key, api_secret, testnet=testnet) + + try: + await fix_time_inversion_trades(client, account_id) + await backfill_commissions(client, account_id) + finally: + # Close connection properly + if client.client: + await client.client.close_connection() + + logger.info("All accounts processed.") + + except Exception as e: + logger.error(f"Script failed: {e}") + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main())