307 lines
13 KiB
Python
307 lines
13 KiB
Python
"""
|
|
Enhanced Position Synchronization System
|
|
Addresses the gap between dashboard position display and actual exchange account state
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional, Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class EnhancedPositionSync:
|
|
"""Enhanced position synchronization to ensure dashboard matches actual exchange state"""
|
|
|
|
def __init__(self, trading_executor, dashboard):
|
|
self.trading_executor = trading_executor
|
|
self.dashboard = dashboard
|
|
self.last_sync_time = 0
|
|
self.sync_interval = 10 # Sync every 10 seconds
|
|
self.position_history = [] # Track position changes
|
|
|
|
def sync_all_positions(self) -> Dict[str, Any]:
|
|
"""Comprehensive position sync for all symbols"""
|
|
try:
|
|
sync_results = {}
|
|
|
|
# 1. Get actual exchange positions
|
|
exchange_positions = self._get_actual_exchange_positions()
|
|
|
|
# 2. Get dashboard positions
|
|
dashboard_positions = self._get_dashboard_positions()
|
|
|
|
# 3. Compare and sync
|
|
for symbol in ['ETH/USDT', 'BTC/USDT']:
|
|
sync_result = self._sync_symbol_position(
|
|
symbol,
|
|
exchange_positions.get(symbol),
|
|
dashboard_positions.get(symbol)
|
|
)
|
|
sync_results[symbol] = sync_result
|
|
|
|
# 4. Update closed trades list from exchange
|
|
self._sync_closed_trades()
|
|
|
|
return {
|
|
'sync_time': datetime.now().isoformat(),
|
|
'results': sync_results,
|
|
'total_synced': len(sync_results),
|
|
'issues_found': sum(1 for r in sync_results.values() if not r['in_sync'])
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in comprehensive position sync: {e}")
|
|
return {'error': str(e)}
|
|
|
|
def _get_actual_exchange_positions(self) -> Dict[str, Dict]:
|
|
"""Get actual positions from exchange account"""
|
|
try:
|
|
positions = {}
|
|
|
|
if not self.trading_executor:
|
|
return positions
|
|
|
|
# Get account balances
|
|
if hasattr(self.trading_executor, 'get_account_balance'):
|
|
balances = self.trading_executor.get_account_balance()
|
|
|
|
for symbol in ['ETH/USDT', 'BTC/USDT']:
|
|
# Parse symbol to get base asset
|
|
base_asset = symbol.split('/')[0]
|
|
|
|
# Get balance for base asset
|
|
base_balance = balances.get(base_asset, {}).get('total', 0.0)
|
|
|
|
if base_balance > 0.001: # Minimum threshold
|
|
positions[symbol] = {
|
|
'side': 'LONG',
|
|
'size': base_balance,
|
|
'value': base_balance * self._get_current_price(symbol),
|
|
'source': 'exchange_balance'
|
|
}
|
|
|
|
# Also check trading executor's position tracking
|
|
if hasattr(self.trading_executor, 'get_positions'):
|
|
executor_positions = self.trading_executor.get_positions()
|
|
for symbol, position in executor_positions.items():
|
|
if position and hasattr(position, 'quantity') and position.quantity > 0:
|
|
positions[symbol] = {
|
|
'side': position.side,
|
|
'size': position.quantity,
|
|
'entry_price': position.entry_price,
|
|
'value': position.quantity * self._get_current_price(symbol),
|
|
'source': 'executor_tracking'
|
|
}
|
|
|
|
return positions
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting actual exchange positions: {e}")
|
|
return {}
|
|
|
|
def _get_dashboard_positions(self) -> Dict[str, Dict]:
|
|
"""Get positions as shown on dashboard"""
|
|
try:
|
|
positions = {}
|
|
|
|
# Get from dashboard's current_position
|
|
if self.dashboard.current_position:
|
|
symbol = self.dashboard.current_position.get('symbol', 'ETH/USDT')
|
|
positions[symbol] = {
|
|
'side': self.dashboard.current_position.get('side'),
|
|
'size': self.dashboard.current_position.get('size'),
|
|
'entry_price': self.dashboard.current_position.get('price'),
|
|
'value': self.dashboard.current_position.get('size', 0) * self._get_current_price(symbol),
|
|
'source': 'dashboard_display'
|
|
}
|
|
|
|
return positions
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting dashboard positions: {e}")
|
|
return {}
|
|
|
|
def _sync_symbol_position(self, symbol: str, exchange_pos: Optional[Dict], dashboard_pos: Optional[Dict]) -> Dict[str, Any]:
|
|
"""Sync position for a specific symbol"""
|
|
try:
|
|
sync_result = {
|
|
'symbol': symbol,
|
|
'exchange_position': exchange_pos,
|
|
'dashboard_position': dashboard_pos,
|
|
'in_sync': True,
|
|
'action_taken': 'none'
|
|
}
|
|
|
|
# Case 1: Exchange has position, dashboard doesn't
|
|
if exchange_pos and not dashboard_pos:
|
|
logger.warning(f"SYNC ISSUE: Exchange has {symbol} position but dashboard shows none")
|
|
|
|
# Update dashboard to reflect exchange position
|
|
self.dashboard.current_position = {
|
|
'symbol': symbol,
|
|
'side': exchange_pos['side'],
|
|
'size': exchange_pos['size'],
|
|
'price': exchange_pos.get('entry_price', self._get_current_price(symbol)),
|
|
'entry_time': datetime.now(),
|
|
'leverage': self.dashboard.current_leverage,
|
|
'source': 'sync_correction'
|
|
}
|
|
|
|
sync_result['in_sync'] = False
|
|
sync_result['action_taken'] = 'updated_dashboard_from_exchange'
|
|
|
|
# Case 2: Dashboard has position, exchange doesn't
|
|
elif dashboard_pos and not exchange_pos:
|
|
logger.warning(f"SYNC ISSUE: Dashboard shows {symbol} position but exchange has none")
|
|
|
|
# Clear dashboard position
|
|
self.dashboard.current_position = None
|
|
|
|
sync_result['in_sync'] = False
|
|
sync_result['action_taken'] = 'cleared_dashboard_position'
|
|
|
|
# Case 3: Both have positions but they differ
|
|
elif exchange_pos and dashboard_pos:
|
|
if (exchange_pos['side'] != dashboard_pos['side'] or
|
|
abs(exchange_pos['size'] - dashboard_pos['size']) > 0.001):
|
|
|
|
logger.warning(f"SYNC ISSUE: {symbol} position mismatch - Exchange: {exchange_pos['side']} {exchange_pos['size']:.3f}, Dashboard: {dashboard_pos['side']} {dashboard_pos['size']:.3f}")
|
|
|
|
# Update dashboard to match exchange
|
|
self.dashboard.current_position.update({
|
|
'side': exchange_pos['side'],
|
|
'size': exchange_pos['size'],
|
|
'price': exchange_pos.get('entry_price', dashboard_pos['entry_price'])
|
|
})
|
|
|
|
sync_result['in_sync'] = False
|
|
sync_result['action_taken'] = 'updated_dashboard_to_match_exchange'
|
|
|
|
return sync_result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error syncing position for {symbol}: {e}")
|
|
return {'symbol': symbol, 'error': str(e), 'in_sync': False}
|
|
|
|
def _sync_closed_trades(self):
|
|
"""Sync closed trades list with actual exchange trade history"""
|
|
try:
|
|
if not self.trading_executor:
|
|
return
|
|
|
|
# Get trade history from executor
|
|
if hasattr(self.trading_executor, 'get_trade_history'):
|
|
executor_trades = self.trading_executor.get_trade_history()
|
|
|
|
# Clear and rebuild closed_trades list
|
|
self.dashboard.closed_trades = []
|
|
|
|
for trade in executor_trades:
|
|
# Convert to dashboard format
|
|
trade_record = {
|
|
'symbol': getattr(trade, 'symbol', 'ETH/USDT'),
|
|
'side': getattr(trade, 'side', 'UNKNOWN'),
|
|
'quantity': getattr(trade, 'quantity', 0),
|
|
'entry_price': getattr(trade, 'entry_price', 0),
|
|
'exit_price': getattr(trade, 'exit_price', 0),
|
|
'entry_time': getattr(trade, 'entry_time', datetime.now()),
|
|
'exit_time': getattr(trade, 'exit_time', datetime.now()),
|
|
'pnl': getattr(trade, 'pnl', 0),
|
|
'fees': getattr(trade, 'fees', 0),
|
|
'confidence': getattr(trade, 'confidence', 1.0),
|
|
'trade_type': 'synced_from_executor'
|
|
}
|
|
|
|
# Only add completed trades (with exit_time)
|
|
if trade_record['exit_time']:
|
|
self.dashboard.closed_trades.append(trade_record)
|
|
|
|
# Update session PnL
|
|
self.dashboard.session_pnl = sum(trade['pnl'] for trade in self.dashboard.closed_trades)
|
|
|
|
logger.info(f"Synced {len(self.dashboard.closed_trades)} closed trades from executor")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error syncing closed trades: {e}")
|
|
|
|
def _get_current_price(self, symbol: str) -> float:
|
|
"""Get current price for a symbol"""
|
|
try:
|
|
return self.dashboard._get_current_price(symbol) or 3500.0
|
|
except:
|
|
return 3500.0 # Fallback price
|
|
|
|
def should_sync(self) -> bool:
|
|
"""Check if sync is needed based on time interval"""
|
|
current_time = time.time()
|
|
if current_time - self.last_sync_time >= self.sync_interval:
|
|
self.last_sync_time = current_time
|
|
return True
|
|
return False
|
|
|
|
def create_sync_status_display(self) -> Dict[str, Any]:
|
|
"""Create detailed sync status for dashboard display"""
|
|
try:
|
|
# Get current sync status
|
|
sync_results = self.sync_all_positions()
|
|
|
|
# Create display-friendly format
|
|
status_display = {
|
|
'last_sync': datetime.now().strftime('%H:%M:%S'),
|
|
'sync_healthy': sync_results.get('issues_found', 0) == 0,
|
|
'positions': {},
|
|
'closed_trades_count': len(self.dashboard.closed_trades),
|
|
'session_pnl': self.dashboard.session_pnl
|
|
}
|
|
|
|
# Add position details
|
|
for symbol, result in sync_results.get('results', {}).items():
|
|
status_display['positions'][symbol] = {
|
|
'in_sync': result['in_sync'],
|
|
'action_taken': result.get('action_taken', 'none'),
|
|
'has_exchange_position': result['exchange_position'] is not None,
|
|
'has_dashboard_position': result['dashboard_position'] is not None
|
|
}
|
|
|
|
return status_display
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating sync status display: {e}")
|
|
return {'error': str(e)}
|
|
|
|
|
|
# Integration with existing dashboard
|
|
def integrate_enhanced_sync(dashboard):
|
|
"""Integrate enhanced sync with existing dashboard"""
|
|
|
|
# Create enhanced sync instance
|
|
enhanced_sync = EnhancedPositionSync(dashboard.trading_executor, dashboard)
|
|
|
|
# Add to dashboard
|
|
dashboard.enhanced_sync = enhanced_sync
|
|
|
|
# Modify existing metrics update to include sync
|
|
original_update_metrics = dashboard.update_metrics
|
|
|
|
def enhanced_update_metrics(n):
|
|
"""Enhanced metrics update with position sync"""
|
|
try:
|
|
# Perform periodic sync
|
|
if enhanced_sync.should_sync():
|
|
sync_results = enhanced_sync.sync_all_positions()
|
|
if sync_results.get('issues_found', 0) > 0:
|
|
logger.info(f"Position sync performed: {sync_results['issues_found']} issues corrected")
|
|
|
|
# Call original metrics update
|
|
return original_update_metrics(n)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in enhanced metrics update: {e}")
|
|
return original_update_metrics(n)
|
|
|
|
# Replace the update method
|
|
dashboard.update_metrics = enhanced_update_metrics
|
|
|
|
return enhanced_sync
|