fix sim and wip fix live

This commit is contained in:
Dobromir Popov
2025-07-08 02:47:10 +03:00
parent 64678bd8d3
commit 6c91bf0b93
5 changed files with 220 additions and 106 deletions

View File

@ -5,6 +5,7 @@ import requests
import hmac
import hashlib
from urllib.parse import urlencode, quote_plus
import json # Added for json.dumps
from .exchange_interface import ExchangeInterface
@ -85,37 +86,40 @@ class MEXCInterface(ExchangeInterface):
return symbol.replace('/', '_').upper()
def _generate_signature(self, timestamp: str, method: str, endpoint: str, params: Dict[str, Any]) -> str:
"""Generate signature for private API calls using MEXC's expected parameter order"""
# MEXC requires specific parameter ordering, not alphabetical
# Based on successful test: symbol, side, type, quantity, timestamp, then other params
mexc_param_order = ['symbol', 'side', 'type', 'quantity', 'timestamp', 'recvWindow']
# Build ordered parameter list
ordered_params = []
# Add parameters in MEXC's expected order
for param_name in mexc_param_order:
if param_name in params and param_name != 'signature':
ordered_params.append(f"{param_name}={params[param_name]}")
# Add any remaining parameters not in the standard order (alphabetically)
remaining_params = {k: v for k, v in params.items() if k not in mexc_param_order and k != 'signature'}
for key in sorted(remaining_params.keys()):
ordered_params.append(f"{key}={remaining_params[key]}")
# Create query string (MEXC doesn't use the api_key + timestamp prefix)
query_string = '&'.join(ordered_params)
logger.debug(f"MEXC signature query string: {query_string}")
"""Generate signature for private API calls using MEXC's official method"""
# MEXC signature format varies by method:
# For GET/DELETE: URL-encoded query string of alphabetically sorted parameters.
# For POST: JSON string of parameters (no sorting needed).
# The API-Secret is used as the HMAC SHA256 key.
# Remove signature from params to avoid circular inclusion
clean_params = {k: v for k, v in params.items() if k != 'signature'}
parameter_string: str
if method.upper() == "POST":
# For POST requests, the signature parameter is a JSON string
# Ensure sorting keys for consistent JSON string generation across runs
# even though MEXC says sorting is not required for POST params, it's good practice.
parameter_string = json.dumps(clean_params, sort_keys=True, separators=(',', ':'))
else:
# For GET/DELETE requests, parameters are spliced in dictionary order with & interval
sorted_params = sorted(clean_params.items())
parameter_string = '&'.join(f"{key}={str(value)}" for key, value in sorted_params)
# The string to be signed is: accessKey + timestamp + obtained parameter string.
string_to_sign = f"{self.api_key}{timestamp}{parameter_string}"
logger.debug(f"MEXC string to sign (method {method}): {string_to_sign}")
# Generate HMAC SHA256 signature
signature = hmac.new(
self.api_secret.encode('utf-8'),
query_string.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
logger.debug(f"MEXC signature: {signature}")
logger.debug(f"MEXC generated signature: {signature}")
return signature
def _send_public_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@ -145,7 +149,7 @@ class MEXCInterface(ExchangeInterface):
logger.error(f"Error in public request to {endpoint}: {e}")
return {}
def _send_private_request(self, method: str, endpoint: str, params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]:
def _send_private_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""Send a private request to the exchange with proper signature"""
if params is None:
params = {}
@ -170,8 +174,11 @@ class MEXCInterface(ExchangeInterface):
if method.upper() == "GET":
response = self.session.get(url, headers=headers, params=params, timeout=10)
elif method.upper() == "POST":
# MEXC expects POST parameters as query string, not in body
response = self.session.post(url, headers=headers, params=params, timeout=10)
# MEXC expects POST parameters as JSON in the request body, not as query string
# The signature is generated from the JSON string of parameters.
# We need to exclude 'signature' from the JSON body sent, as it's for the header.
params_for_body = {k: v for k, v in params.items() if k != 'signature'}
response = self.session.post(url, headers=headers, json=params_for_body, timeout=10)
else:
logger.error(f"Unsupported method: {method}")
return None
@ -217,48 +224,46 @@ class MEXCInterface(ExchangeInterface):
response = self._send_public_request('GET', endpoint, params)
if response:
# MEXC ticker returns a dictionary if single symbol, list if all symbols
if isinstance(response, dict):
ticker_data = response
elif isinstance(response, list) and len(response) > 0:
# If the response is a list, try to find the specific symbol
found_ticker = next((item for item in response if item.get('symbol') == formatted_symbol), None)
if found_ticker:
ticker_data = found_ticker
else:
logger.error(f"Ticker data for {formatted_symbol} not found in response list.")
return None
if isinstance(response, dict):
ticker_data: Dict[str, Any] = response
elif isinstance(response, list) and len(response) > 0:
found_ticker = next((item for item in response if item.get('symbol') == formatted_symbol), None)
if found_ticker:
ticker_data = found_ticker
else:
logger.error(f"Unexpected ticker response format: {response}")
logger.error(f"Ticker data for {formatted_symbol} not found in response list.")
return None
else:
logger.error(f"Unexpected ticker response format: {response}")
return None
# Extract relevant info and format for universal use
last_price = float(ticker_data.get('lastPrice', 0))
bid_price = float(ticker_data.get('bidPrice', 0))
ask_price = float(ticker_data.get('askPrice', 0))
volume = float(ticker_data.get('volume', 0)) # Base asset volume
# At this point, ticker_data is guaranteed to be a Dict[str, Any] due to the above logic
# If it was None, we would have returned early.
# Determine price change and percent change
price_change = float(ticker_data.get('priceChange', 0))
price_change_percent = float(ticker_data.get('priceChangePercent', 0))
# Extract relevant info and format for universal use
last_price = float(ticker_data.get('lastPrice', 0))
bid_price = float(ticker_data.get('bidPrice', 0))
ask_price = float(ticker_data.get('askPrice', 0))
volume = float(ticker_data.get('volume', 0)) # Base asset volume
logger.info(f"MEXC: Got ticker from {endpoint} for {symbol}: ${last_price:.2f}")
return {
'symbol': formatted_symbol,
'last': last_price,
'bid': bid_price,
'ask': ask_price,
'volume': volume,
'high': float(ticker_data.get('highPrice', 0)),
'low': float(ticker_data.get('lowPrice', 0)),
'change': price_change_percent, # This is usually priceChangePercent
'exchange': 'MEXC',
'raw_data': ticker_data
}
logger.error(f"Failed to get ticker for {symbol}")
return None
# Determine price change and percent change
price_change = float(ticker_data.get('priceChange', 0))
price_change_percent = float(ticker_data.get('priceChangePercent', 0))
logger.info(f"MEXC: Got ticker from {endpoint} for {symbol}: ${last_price:.2f}")
return {
'symbol': formatted_symbol,
'last': last_price,
'bid': bid_price,
'ask': ask_price,
'volume': volume,
'high': float(ticker_data.get('highPrice', 0)),
'low': float(ticker_data.get('lowPrice', 0)),
'change': price_change_percent, # This is usually priceChangePercent
'exchange': 'MEXC',
'raw_data': ticker_data
}
def get_api_symbols(self) -> List[str]:
"""Get list of symbols supported for API trading"""
@ -293,39 +298,89 @@ class MEXCInterface(ExchangeInterface):
logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10
return {}
# Format quantity according to symbol precision requirements
formatted_quantity = self._format_quantity_for_symbol(formatted_symbol, quantity)
if formatted_quantity is None:
logger.error(f"MEXC: Failed to format quantity {quantity} for {formatted_symbol}")
return {}
# Handle order type restrictions for specific symbols
final_order_type = self._adjust_order_type_for_symbol(formatted_symbol, order_type.upper())
# Get price for limit orders
final_price = price
if final_order_type == 'LIMIT' and price is None:
# Get current market price
ticker = self.get_ticker(symbol)
if ticker and 'last' in ticker:
final_price = ticker['last']
logger.info(f"MEXC: Using market price ${final_price:.2f} for LIMIT order")
else:
logger.error(f"MEXC: Could not get market price for LIMIT order on {formatted_symbol}")
return {}
endpoint = "order"
params: Dict[str, Any] = {
'symbol': formatted_symbol,
'side': side.upper(),
'type': order_type.upper(),
'quantity': str(quantity) # Quantity must be a string
'type': final_order_type,
'quantity': str(formatted_quantity) # Quantity must be a string
}
if price is not None:
params['price'] = str(price) # Price must be a string for limit orders
if final_price is not None:
params['price'] = str(final_price) # Price must be a string for limit orders
logger.info(f"MEXC: Placing {side.upper()} {order_type.upper()} order for {quantity} {formatted_symbol} at price {price}")
# For market orders, some parameters might be optional or handled differently.
# Check MEXC API docs for market order specifics (e.g., quoteOrderQty for buy market orders)
if order_type.upper() == 'MARKET' and side.upper() == 'BUY':
# If it's a market buy order, MEXC often expects quoteOrderQty instead of quantity
# Assuming quantity here refers to the base asset, if quoteOrderQty is needed, adjust.
# For now, we will stick to quantity and let MEXC handle the conversion if possible
pass # No specific change needed based on the current params structure
logger.info(f"MEXC: Placing {side.upper()} {final_order_type} order for {formatted_quantity} {formatted_symbol} at price {final_price}")
try:
# MEXC API endpoint for placing orders is /api/v3/order (POST)
order_result = self._send_private_request('POST', endpoint, params)
if order_result:
if order_result is not None:
logger.info(f"MEXC: Order placed successfully: {order_result}")
return order_result
else:
logger.error(f"MEXC: Error placing order: {order_result}")
logger.error(f"MEXC: Error placing order: request returned None")
return {}
except Exception as e:
logger.error(f"MEXC: Exception placing order: {e}")
return {}
def _format_quantity_for_symbol(self, formatted_symbol: str, quantity: float) -> Optional[float]:
"""Format quantity according to symbol precision requirements"""
try:
# Symbol-specific precision rules
if formatted_symbol == 'ETHUSDC':
# ETHUSDC requires max 5 decimal places, step size 0.000001
formatted_qty = round(quantity, 5)
# Ensure it meets minimum step size
step_size = 0.000001
formatted_qty = round(formatted_qty / step_size) * step_size
# Round again to remove floating point errors
formatted_qty = round(formatted_qty, 6)
logger.info(f"MEXC: Formatted ETHUSDC quantity {quantity} -> {formatted_qty}")
return formatted_qty
elif formatted_symbol == 'BTCUSDC':
# Assume similar precision for BTC
formatted_qty = round(quantity, 6)
step_size = 0.000001
formatted_qty = round(formatted_qty / step_size) * step_size
formatted_qty = round(formatted_qty, 6)
return formatted_qty
else:
# Default formatting - 6 decimal places
return round(quantity, 6)
except Exception as e:
logger.error(f"Error formatting quantity for {formatted_symbol}: {e}")
return None
def _adjust_order_type_for_symbol(self, formatted_symbol: str, order_type: str) -> str:
"""Adjust order type based on symbol restrictions"""
if formatted_symbol == 'ETHUSDC':
# ETHUSDC only supports LIMIT and LIMIT_MAKER orders
if order_type == 'MARKET':
logger.info(f"MEXC: Converting MARKET order to LIMIT for {formatted_symbol} (MARKET not supported)")
return 'LIMIT'
return order_type
def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]:
"""Cancel an existing order on MEXC."""

View File

@ -159,14 +159,14 @@ trading:
# MEXC Trading API Configuration
mexc_trading:
enabled: true
trading_mode: live # simulation, testnet, live
trading_mode: simulation # simulation, testnet, live
# Position sizing as percentage of account balance
base_position_percent: 1 # 0.5% base position of account (MUCH SAFER)
max_position_percent: 5.0 # 2% max position of account (REDUCED)
min_position_percent: 0.5 # 0.2% min position of account (REDUCED)
leverage: 1.0 # 1x leverage (NO LEVERAGE FOR TESTING)
simulation_account_usd: 100.0 # $100 simulation account balance
simulation_account_usd: 99.9 # $100 simulation account balance
# Risk management
max_daily_loss_usd: 200.0

View File

@ -114,12 +114,17 @@ class TradingExecutor:
# Thread safety
self.lock = Lock()
# Connect to exchange
# Connect to exchange - skip connection check in simulation mode
if self.trading_enabled:
logger.info("TRADING EXECUTOR: Attempting to connect to exchange...")
if not self._connect_exchange():
logger.error("TRADING EXECUTOR: Failed initial exchange connection. Trading will be disabled.")
self.trading_enabled = False
if self.simulation_mode:
logger.info("TRADING EXECUTOR: Simulation mode - skipping exchange connection check")
# In simulation mode, we don't need a real exchange connection
# Trading should remain enabled for simulation trades
else:
logger.info("TRADING EXECUTOR: Attempting to connect to exchange...")
if not self._connect_exchange():
logger.error("TRADING EXECUTOR: Failed initial exchange connection. Trading will be disabled.")
self.trading_enabled = False
else:
logger.info("TRADING EXECUTOR: Trading is explicitly disabled in config.")

View File

@ -1884,7 +1884,10 @@ class EnhancedRealtimeTrainingSystem:
if (self.orchestrator and hasattr(self.orchestrator, 'rl_agent')
and self.orchestrator.rl_agent):
# Get Q-values from model
# Use RL agent to make prediction
current_state = self._get_dqn_state(symbol)
if current_state is None:
return
action = self.orchestrator.rl_agent.act(current_state, explore=False)
# Get Q-values separately if available
if hasattr(self.orchestrator.rl_agent, 'policy_net'):
@ -1893,13 +1896,11 @@ class EnhancedRealtimeTrainingSystem:
q_values_tensor = self.orchestrator.rl_agent.policy_net(state_tensor)
if isinstance(q_values_tensor, tuple):
q_values = q_values_tensor[0].cpu().numpy()[0].tolist()
else:
q_values = q_values_tensor.cpu().numpy()[0].tolist()
else:
q_values = [0.33, 0.33, 0.34] # Default uniform distribution
confidence = max(q_values) / sum(q_values) if sum(q_values) > 0 else 0.33
else:
# Fallback to technical analysis-based prediction
action, q_values, confidence = self._technical_analysis_prediction(symbol)

View File

@ -205,6 +205,9 @@ class CleanTradingDashboard:
# Start signal generation loop to ensure continuous trading signals
self._start_signal_generation_loop()
# Start live balance sync for trading
self._start_live_balance_sync()
# Start training sessions if models are showing FRESH status
threading.Thread(target=self._delayed_training_check, daemon=True).start()
@ -329,14 +332,14 @@ class CleanTradingDashboard:
hasattr(self.trading_executor, 'simulation_mode') and
not self.trading_executor.simulation_mode)
if is_live and hasattr(self.trading_executor, 'exchange_interface'):
if is_live and hasattr(self.trading_executor, 'exchange'):
# Get real balance from exchange (throttled to avoid API spam)
import time
current_time = time.time()
# Cache balance for 10 seconds to avoid excessive API calls
if not hasattr(self, '_last_balance_check') or current_time - self._last_balance_check > 10:
exchange = self.trading_executor.exchange_interface
# Cache balance for 5 seconds for more frequent updates in live trading
if not hasattr(self, '_last_balance_check') or current_time - self._last_balance_check > 5:
exchange = self.trading_executor.exchange
if hasattr(exchange, 'get_balance'):
live_balance = exchange.get_balance('USDC')
if live_balance is not None and live_balance > 0:
@ -354,13 +357,20 @@ class CleanTradingDashboard:
logger.info(f"LIVE BALANCE: Using USDT balance ${usdt_balance:.2f}")
return usdt_balance
else:
logger.warning("LIVE BALANCE: Exchange interface does not have get_balance method")
logger.warning("LIVE BALANCE: Exchange does not have get_balance method")
else:
# Return cached balance if within 10 second window
if hasattr(self, '_cached_live_balance'):
return self._cached_live_balance
elif hasattr(self.trading_executor, 'simulation_mode') and self.trading_executor.simulation_mode:
# In simulation mode, show dynamic balance based on P&L
initial_balance = self._get_initial_balance()
realized_pnl = sum(trade.get('pnl', 0) for trade in self.closed_trades)
simulation_balance = initial_balance + realized_pnl
logger.debug(f"SIMULATION BALANCE: ${simulation_balance:.2f} (Initial: ${initial_balance:.2f} + P&L: ${realized_pnl:.2f})")
return simulation_balance
else:
logger.debug("LIVE BALANCE: Not in live trading mode, using simulation balance")
logger.debug("LIVE BALANCE: Not in live trading mode, using initial balance")
# Fallback to initial balance for simulation mode
return self._get_initial_balance()
@ -484,10 +494,13 @@ class CleanTradingDashboard:
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:
if hasattr(self.trading_executor, 'simulation_mode') and self.trading_executor.simulation_mode:
# Show simulation mode status with simulated balance
mexc_status = f"SIM - ${current_balance:.2f}"
elif hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode:
# Show live balance in MEXC status - detect currency
try:
exchange = self.trading_executor.exchange_interface
exchange = self.trading_executor.exchange
usdc_balance = exchange.get_balance('USDC') if hasattr(exchange, 'get_balance') else 0
usdt_balance = exchange.get_balance('USDT') if hasattr(exchange, 'get_balance') else 0
@ -2957,6 +2970,39 @@ class CleanTradingDashboard:
except Exception as e:
logger.error(f"Error starting signal generation loop: {e}")
def _start_live_balance_sync(self):
"""Start continuous live balance synchronization for trading"""
def balance_sync_worker():
while True:
try:
if self.trading_executor:
is_live = (hasattr(self.trading_executor, 'trading_enabled') and
self.trading_executor.trading_enabled and
hasattr(self.trading_executor, 'simulation_mode') and
not self.trading_executor.simulation_mode)
if is_live and hasattr(self.trading_executor, 'exchange'):
# Force balance refresh every 15 seconds in live mode
if hasattr(self, '_last_balance_check'):
del self._last_balance_check # Force refresh
balance = self._get_live_balance()
if balance > 0:
logger.debug(f"BALANCE SYNC: Live balance: ${balance:.2f}")
else:
logger.warning("BALANCE SYNC: Could not retrieve live balance")
# Sync balance every 15 seconds for live trading
time.sleep(15)
except Exception as e:
logger.debug(f"Error in balance sync loop: {e}")
time.sleep(30) # Wait longer on error
# Start balance sync thread only if we have trading enabled
if self.trading_executor:
threading.Thread(target=balance_sync_worker, daemon=True).start()
logger.info("BALANCE SYNC: Background balance synchronization started")
def _generate_dqn_signal(self, symbol: str, current_price: float) -> Optional[Dict]:
"""Generate trading signal using DQN agent - NOT AVAILABLE IN BASIC ORCHESTRATOR"""
@ -4600,28 +4646,35 @@ class CleanTradingDashboard:
imbalance = cob_snapshot['stats']['imbalance']
abs_imbalance = abs(imbalance)
# Dynamic threshold based on imbalance strength
# Dynamic threshold based on imbalance strength with realistic confidence
if abs_imbalance > 0.8: # Very strong imbalance (>80%)
threshold = 0.05 # 5% threshold for very strong signals
confidence_multiplier = 3.0
base_confidence = 0.85 # High but not perfect confidence
confidence_boost = (abs_imbalance - 0.8) * 0.75 # Scale remaining 15%
elif abs_imbalance > 0.5: # Strong imbalance (>50%)
threshold = 0.1 # 10% threshold for strong signals
confidence_multiplier = 2.5
base_confidence = 0.70 # Good confidence
confidence_boost = (abs_imbalance - 0.5) * 0.50 # Scale up to 85%
elif abs_imbalance > 0.3: # Moderate imbalance (>30%)
threshold = 0.15 # 15% threshold for moderate signals
confidence_multiplier = 2.0
base_confidence = 0.55 # Moderate confidence
confidence_boost = (abs_imbalance - 0.3) * 0.75 # Scale up to 70%
else: # Weak imbalance
threshold = 0.2 # 20% threshold for weak signals
confidence_multiplier = 1.5
base_confidence = 0.35 # Low confidence
confidence_boost = abs_imbalance * 0.67 # Scale up to 55%
# Generate signal if imbalance exceeds threshold
if abs_imbalance > threshold:
# Calculate more realistic confidence (never exactly 1.0)
final_confidence = min(0.95, base_confidence + confidence_boost)
signal = {
'timestamp': datetime.now(),
'type': 'cob_liquidity_imbalance',
'action': 'BUY' if imbalance > 0 else 'SELL',
'symbol': symbol,
'confidence': min(1.0, abs_imbalance * confidence_multiplier),
'confidence': final_confidence,
'strength': abs_imbalance,
'threshold_used': threshold,
'signal_strength': 'very_strong' if abs_imbalance > 0.8 else 'strong' if abs_imbalance > 0.5 else 'moderate' if abs_imbalance > 0.3 else 'weak',