live position sync for LIMIT orders

This commit is contained in:
Dobromir Popov
2025-07-14 14:50:30 +03:00
parent f861559319
commit d53a2ba75d
5 changed files with 822 additions and 51 deletions

View File

@ -357,7 +357,7 @@ class CleanTradingDashboard:
except Exception as e:
logger.warning(f"Error getting live account balance: {e}, using initial balance")
return self._get_initial_balance()
def _setup_layout(self):
"""Setup the dashboard layout using layout manager"""
self.app.layout = self.layout_manager.create_main_layout()
@ -377,8 +377,12 @@ class CleanTradingDashboard:
[Input('interval-component', 'n_intervals')]
)
def update_metrics(n):
"""Update key metrics - FIXED callback mismatch"""
"""Update key metrics - ENHANCED with position sync monitoring"""
try:
# PERIODIC POSITION SYNC: Every 30 seconds, verify position sync
if n % 30 == 0 and n > 0: # Skip initial load (n=0)
self._periodic_position_sync_check()
# Sync position from trading executor first
symbol = 'ETH/USDT'
self._sync_position_from_executor(symbol)
@ -461,12 +465,12 @@ class CleanTradingDashboard:
portfolio_value = current_balance + total_session_pnl # Live balance + unrealized P&L
portfolio_str = f"${portfolio_value:.2f}"
# MEXC status
# MEXC status - enhanced with sync status
mexc_status = "SIM"
if self.trading_executor:
if hasattr(self.trading_executor, 'trading_enabled') and self.trading_executor.trading_enabled:
if hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode:
mexc_status = "LIVE"
mexc_status = "LIVE+SYNC" # Indicate live trading with position sync
return price_str, session_pnl_str, position_str, trade_str, portfolio_str, mexc_status
@ -541,6 +545,18 @@ class CleanTradingDashboard:
logger.error(f"Error updating trades table: {e}")
return html.P(f"Error: {str(e)}", className="text-danger")
@self.app.callback(
Output('pending-orders-content', 'children'),
[Input('interval-component', 'n_intervals')]
)
def update_pending_orders(n):
"""Update pending orders and position sync status"""
try:
return self._create_pending_orders_panel()
except Exception as e:
logger.error(f"Error updating pending orders: {e}")
return html.Div("Error loading pending orders", className="text-danger")
@self.app.callback(
[Output('eth-cob-content', 'children'),
Output('btc-cob-content', 'children')],
@ -3480,7 +3496,7 @@ class CleanTradingDashboard:
if hasattr(self.orchestrator.cnn_model, 'train_on_batch'):
for _ in range(int(training_weight)):
loss = self.orchestrator.cnn_model.train_on_batch(feature_tensor, target_tensor)
logger.info(f"CNN enhanced training on executed signal - loss: {loss:.4f}, pnl: {pnl:.2f}")
logger.info(f"CNN enhanced training on executed signal - loss: {loss:.4f}, pnl: {pnl:.2f}")
except Exception as e:
logger.debug(f"Error training CNN on executed signal: {e}")
@ -3646,7 +3662,7 @@ class CleanTradingDashboard:
return 0.5
def _execute_manual_trade(self, action: str):
"""Execute manual trading action - ENHANCED with PERSISTENT SIGNAL STORAGE"""
"""Execute manual trading action - ENHANCED with POSITION SYNCHRONIZATION"""
try:
if not self.trading_executor:
logger.warning("No trading executor available")
@ -3659,7 +3675,16 @@ class CleanTradingDashboard:
logger.warning("No current price available for manual trade")
return
# Sync current position from trading executor first
# STEP 1: Synchronize position with MEXC account before executing trade
desired_state = self._determine_desired_position_state(action)
logger.info(f"MANUAL TRADE: Syncing position to {desired_state} before executing {action}")
sync_success = self._sync_position_with_mexc(symbol, desired_state)
if not sync_success:
logger.error(f"MANUAL TRADE: Position sync failed - aborting {action}")
return
# STEP 2: Sync current position from trading executor
self._sync_position_from_executor(symbol)
# DEBUG: Log current position state before trade
@ -3916,6 +3941,297 @@ class CleanTradingDashboard:
# Model input capture moved to core.trade_data_manager.TradeDataManager
def _determine_desired_position_state(self, action: str) -> str:
"""Determine the desired position state based on the manual action"""
if action == 'BUY':
return 'LONG'
elif action == 'SELL':
# If we have a position, selling should result in NO_POSITION
# If we don't have a position, selling should result in SHORT
if self.current_position:
return 'NO_POSITION'
else:
return 'SHORT'
else: # HOLD or unknown
# Maintain current state
if self.current_position:
side = self.current_position.get('side', 'UNKNOWN')
if side.upper() in ['LONG', 'BUY']:
return 'LONG'
elif side.upper() in ['SHORT', 'SELL']:
return 'SHORT'
return 'NO_POSITION'
def _sync_position_with_mexc(self, symbol: str, desired_state: str) -> bool:
"""Synchronize position with MEXC account using trading executor"""
try:
if not self.trading_executor:
logger.warning("No trading executor available for position sync")
return False
if hasattr(self.trading_executor, 'sync_position_with_mexc'):
return self.trading_executor.sync_position_with_mexc(symbol, desired_state)
else:
logger.warning("Trading executor does not support position synchronization")
return False
except Exception as e:
logger.error(f"Error syncing position with MEXC: {e}")
return False
def _verify_position_sync_after_trade(self, symbol: str, action: str):
"""Verify that position sync is correct after trade execution"""
try:
# Wait a moment for position updates
time.sleep(1)
# Sync position from executor
self._sync_position_from_executor(symbol)
# Log the final position state
if self.current_position:
logger.info(f"POSITION VERIFICATION: After {action} - "
f"{self.current_position['side']} {self.current_position['size']:.3f} @ ${self.current_position['price']:.2f}")
else:
logger.info(f"POSITION VERIFICATION: After {action} - No position")
except Exception as e:
logger.error(f"Error verifying position sync after trade: {e}")
def _periodic_position_sync_check(self):
"""Periodically check and sync position with MEXC account"""
try:
symbol = 'ETH/USDT'
# Only perform sync check for live trading
if not self.trading_executor or getattr(self.trading_executor, 'simulation_mode', True):
return
# Determine current desired state based on dashboard position
if self.current_position:
side = self.current_position.get('side', 'UNKNOWN')
if side.upper() in ['LONG', 'BUY']:
desired_state = 'LONG'
elif side.upper() in ['SHORT', 'SELL']:
desired_state = 'SHORT'
else:
desired_state = 'NO_POSITION'
else:
desired_state = 'NO_POSITION'
# Perform periodic sync check
logger.debug(f"PERIODIC SYNC: Checking position sync for {symbol} (desired: {desired_state})")
sync_success = self._sync_position_with_mexc(symbol, desired_state)
if sync_success:
logger.debug(f"PERIODIC SYNC: Position sync verified for {symbol}")
else:
logger.warning(f"PERIODIC SYNC: Position sync issue detected for {symbol}")
except Exception as e:
logger.debug(f"Error in periodic position sync check: {e}")
def _create_pending_orders_panel(self):
"""Create pending orders and position sync status panel"""
try:
symbol = 'ETH/USDT'
# Get pending orders from MEXC
pending_orders = self._get_pending_orders(symbol)
# Get current account balances and position state
position_sync_status = self._get_position_sync_status(symbol)
# Create the panel content
content = []
# Position Sync Status Section
content.append(html.Div([
html.H6([
html.I(className="fas fa-sync me-1"),
"Position Sync Status"
], className="mb-2 text-primary"),
html.Div([
html.Small("Dashboard Position:", className="text-muted"),
html.Span(f" {position_sync_status['dashboard_state']}", className="badge bg-info ms-1")
], className="mb-1"),
html.Div([
html.Small("MEXC Account State:", className="text-muted"),
html.Span(f" {position_sync_status['mexc_state']}", className="badge bg-secondary ms-1")
], className="mb-1"),
html.Div([
html.Small("Sync Status:", className="text-muted"),
html.Span(f" {position_sync_status['sync_status']}",
className=f"badge {'bg-success' if position_sync_status['in_sync'] else 'bg-warning'} ms-1")
], className="mb-2"),
html.Div([
html.Small("ETH Balance:", className="text-muted"),
html.Span(f" {position_sync_status['eth_balance']:.6f}", className="text-info ms-1"),
], className="mb-1"),
html.Div([
html.Small("USDC Balance:", className="text-muted"),
html.Span(f" ${position_sync_status['usdc_balance']:.2f}", className="text-info ms-1"),
], className="mb-2"),
], className="border-bottom pb-2 mb-2"))
# Pending Orders Section
content.append(html.Div([
html.H6([
html.I(className="fas fa-clock me-1"),
f"Pending Orders ({len(pending_orders)})"
], className="mb-2 text-warning"),
]))
if pending_orders:
# Create table of pending orders
order_rows = []
for order in pending_orders:
side_class = "text-success" if order.get('side', '').upper() == 'BUY' else "text-danger"
status_class = "bg-warning" if order.get('status') == 'NEW' else "bg-secondary"
order_rows.append(html.Tr([
html.Td(order.get('side', 'N/A'), className=side_class),
html.Td(f"{float(order.get('origQty', 0)):.6f}"),
html.Td(f"${float(order.get('price', 0)):.2f}"),
html.Td(html.Span(order.get('status', 'UNKNOWN'), className=f"badge {status_class}")),
html.Td(order.get('orderId', 'N/A')[-8:] if order.get('orderId') else 'N/A'), # Last 8 chars
]))
orders_table = html.Div([
html.Table([
html.Thead([
html.Tr([
html.Th("Side", style={"fontSize": "10px"}),
html.Th("Qty", style={"fontSize": "10px"}),
html.Th("Price", style={"fontSize": "10px"}),
html.Th("Status", style={"fontSize": "10px"}),
html.Th("Order ID", style={"fontSize": "10px"}),
])
]),
html.Tbody(order_rows)
], className="table table-sm", style={"fontSize": "11px"})
])
content.append(orders_table)
else:
content.append(html.Div([
html.P("No pending orders", className="text-muted small text-center mt-2")
]))
# Last sync check time
content.append(html.Div([
html.Hr(),
html.Small([
html.I(className="fas fa-clock me-1"),
f"Last updated: {datetime.now().strftime('%H:%M:%S')}"
], className="text-muted")
]))
return content
except Exception as e:
logger.error(f"Error creating pending orders panel: {e}")
return html.Div([
html.P("Error loading pending orders", className="text-danger"),
html.Small(str(e), className="text-muted")
])
def _get_pending_orders(self, symbol: str) -> List[Dict]:
"""Get pending orders from MEXC for the symbol"""
try:
if not self.trading_executor or getattr(self.trading_executor, 'simulation_mode', True):
return [] # No pending orders in simulation mode
if hasattr(self.trading_executor, 'exchange') and self.trading_executor.exchange:
orders = self.trading_executor.exchange.get_open_orders(symbol)
return orders if orders else []
return []
except Exception as e:
logger.error(f"Error getting pending orders: {e}")
return []
def _get_position_sync_status(self, symbol: str) -> Dict[str, Any]:
"""Get comprehensive position synchronization status"""
try:
# Determine dashboard position state
if self.current_position:
side = self.current_position.get('side', 'UNKNOWN')
if side.upper() in ['LONG', 'BUY']:
dashboard_state = 'LONG'
elif side.upper() in ['SHORT', 'SELL']:
dashboard_state = 'SHORT'
else:
dashboard_state = 'UNKNOWN'
else:
dashboard_state = 'NO_POSITION'
# Get MEXC account balances and determine state
mexc_state = 'UNKNOWN'
eth_balance = 0.0
usdc_balance = 0.0
if self.trading_executor and not getattr(self.trading_executor, 'simulation_mode', True):
try:
if hasattr(self.trading_executor, '_get_mexc_account_balances'):
balances = self.trading_executor._get_mexc_account_balances()
eth_balance = balances.get('ETH', {}).get('total', 0.0)
usdc_balance = max(
balances.get('USDC', {}).get('total', 0.0),
balances.get('USDT', {}).get('total', 0.0)
)
# Determine MEXC state using same logic as trading executor
if hasattr(self.trading_executor, '_determine_position_state'):
holdings = {
'base': eth_balance,
'quote': usdc_balance,
'base_asset': 'ETH',
'quote_asset': 'USDC'
}
mexc_state = self.trading_executor._determine_position_state(symbol, holdings)
except Exception as e:
logger.debug(f"Error getting MEXC account state: {e}")
else:
mexc_state = 'SIMULATION'
# In simulation, use some placeholder values
if self.current_position:
eth_balance = self.current_position.get('size', 0.0) if dashboard_state == 'LONG' else 0.0
usdc_balance = 100.0 if dashboard_state != 'LONG' else 10.0
# Determine sync status
in_sync = (dashboard_state == mexc_state) or mexc_state == 'SIMULATION'
if in_sync:
sync_status = 'IN_SYNC'
else:
sync_status = f'{dashboard_state}{mexc_state}'
return {
'dashboard_state': dashboard_state,
'mexc_state': mexc_state,
'sync_status': sync_status,
'in_sync': in_sync,
'eth_balance': eth_balance,
'usdc_balance': usdc_balance
}
except Exception as e:
logger.error(f"Error getting position sync status: {e}")
return {
'dashboard_state': 'ERROR',
'mexc_state': 'ERROR',
'sync_status': 'ERROR',
'in_sync': False,
'eth_balance': 0.0,
'usdc_balance': 0.0
}
def _get_comprehensive_market_state(self, symbol: str, current_price: float) -> Dict[str, float]:
"""Get comprehensive market state features"""
try:

View File

@ -197,6 +197,10 @@ class DashboardLayoutManager:
html.I(className="fas fa-save me-1"),
"Store All Models"
], id="store-models-btn", className="btn btn-info btn-sm w-100 mt-2"),
html.Button([
html.I(className="fas fa-arrows-rotate me-1"),
"Sync Positions/Orders"
], id="manual-sync-btn", className="btn btn-primary btn-sm w-100 mt-2"),
html.Hr(className="my-2"),
html.Small("System Status", className="text-muted d-block mb-1"),
html.Div([
@ -248,7 +252,7 @@ class DashboardLayoutManager:
])
def _create_cob_and_trades_row(self):
"""Creates the row for COB ladders, closed trades, and model status - REORGANIZED LAYOUT"""
"""Creates the row for COB ladders, closed trades, pending orders, and model status"""
return html.Div([
# Top row: COB Ladders (left) and Models/Training (right)
html.Div([
@ -273,7 +277,7 @@ class DashboardLayoutManager:
], className="d-flex")
], style={"width": "60%"}),
# Right side: Models & Training Progress (40% width) - MOVED UP
# Right side: Models & Training Progress (40% width)
html.Div([
html.Div([
html.Div([
@ -283,28 +287,47 @@ class DashboardLayoutManager:
], className="card-title mb-2"),
html.Div(
id="training-metrics",
style={"height": "300px", "overflowY": "auto"}, # Increased height
style={"height": "300px", "overflowY": "auto"},
),
], className="card-body p-2")
], className="card")
], style={"width": "38%", "marginLeft": "2%"}),
], className="d-flex mb-3"),
# Bottom row: Closed Trades (full width) - MOVED BELOW COB
# Second row: Pending Orders (left) and Closed Trades (right)
html.Div([
# Left side: Pending Orders (40% width)
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-history me-2"),
"Recent Closed Trades",
], className="card-title mb-2"),
html.Div(
id="closed-trades-table",
style={"height": "200px", "overflowY": "auto"}, # Reduced height
),
], className="card-body p-2")
], className="card")
])
html.Div([
html.H6([
html.I(className="fas fa-clock me-2"),
"Pending Orders & Position Sync",
], className="card-title mb-2"),
html.Div(
id="pending-orders-content",
style={"height": "200px", "overflowY": "auto"},
),
], className="card-body p-2")
], className="card")
], style={"width": "40%"}),
# Right side: Closed Trades (58% width)
html.Div([
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-history me-2"),
"Recent Closed Trades",
], className="card-title mb-2"),
html.Div(
id="closed-trades-table",
style={"height": "200px", "overflowY": "auto"},
),
], className="card-body p-2")
], className="card")
], style={"width": "58%", "marginLeft": "2%"}),
], className="d-flex")
])
def _create_analytics_and_performance_row(self):