in the bussiness -but wip
This commit is contained in:
@ -149,7 +149,7 @@ class MEXCInterface(ExchangeInterface):
|
||||
return {}
|
||||
|
||||
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"""
|
||||
"""Send a private request to the exchange with proper signature and MEXC error handling"""
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
@ -191,8 +191,51 @@ class MEXCInterface(ExchangeInterface):
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"API error: Status Code: {response.status_code}, Response: {response.text}")
|
||||
return None
|
||||
# Parse error response for specific error codes
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_code = error_data.get('code')
|
||||
error_msg = error_data.get('msg', 'Unknown error')
|
||||
|
||||
# Handle specific MEXC error codes
|
||||
if error_code == 30005: # Oversold
|
||||
logger.warning(f"MEXC Oversold detected (Code 30005) for {endpoint}. This indicates risk control measures are active.")
|
||||
logger.warning(f"Possible causes: Market manipulation detection, abnormal trading patterns, or position limits.")
|
||||
logger.warning(f"Action: Waiting before retry and reducing position size if needed.")
|
||||
|
||||
# For oversold errors, we should not retry immediately
|
||||
# Return a special error structure that the trading executor can handle
|
||||
return {
|
||||
'error': 'oversold',
|
||||
'code': 30005,
|
||||
'message': error_msg,
|
||||
'retry_after': 60 # Suggest waiting 60 seconds
|
||||
}
|
||||
elif error_code == 30001: # Transaction direction not allowed
|
||||
logger.error(f"MEXC: Transaction direction not allowed for {endpoint}")
|
||||
return {
|
||||
'error': 'direction_not_allowed',
|
||||
'code': 30001,
|
||||
'message': error_msg
|
||||
}
|
||||
elif error_code == 30004: # Insufficient position
|
||||
logger.error(f"MEXC: Insufficient position for {endpoint}")
|
||||
return {
|
||||
'error': 'insufficient_position',
|
||||
'code': 30004,
|
||||
'message': error_msg
|
||||
}
|
||||
else:
|
||||
logger.error(f"MEXC API error: Code: {error_code}, Message: {error_msg}")
|
||||
return {
|
||||
'error': 'api_error',
|
||||
'code': error_code,
|
||||
'message': error_msg
|
||||
}
|
||||
except:
|
||||
# Fallback if response is not JSON
|
||||
logger.error(f"API error: Status Code: {response.status_code}, Response: {response.text}")
|
||||
return None
|
||||
|
||||
except requests.exceptions.HTTPError as http_err:
|
||||
logger.error(f"HTTP error for {endpoint}: Status Code: {response.status_code}, Response: {response.text}")
|
||||
@ -297,75 +340,92 @@ class MEXCInterface(ExchangeInterface):
|
||||
|
||||
def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]:
|
||||
"""Place a new order on MEXC."""
|
||||
formatted_symbol = self._format_spot_symbol(symbol)
|
||||
|
||||
# Check if symbol is supported for API trading
|
||||
if not self.is_symbol_supported(symbol):
|
||||
supported_symbols = self.get_api_symbols()
|
||||
logger.error(f"Symbol {formatted_symbol} is not supported for API trading")
|
||||
logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10
|
||||
return {}
|
||||
|
||||
# Round quantity to MEXC precision requirements and ensure minimum order value
|
||||
# MEXC ETHUSDC requires precision based on baseAssetPrecision (5 decimals for ETH)
|
||||
if 'ETH' in formatted_symbol:
|
||||
quantity = round(quantity, 5) # MEXC ETHUSDC precision: 5 decimals
|
||||
# Ensure minimum order value (typically $10+ for MEXC)
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 5) # Adjust to minimum $10 order
|
||||
elif 'BTC' in formatted_symbol:
|
||||
quantity = round(quantity, 6) # MEXC BTCUSDC precision: 6 decimals
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 6) # Adjust to minimum $10 order
|
||||
else:
|
||||
quantity = round(quantity, 5) # Default precision for MEXC
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 5) # Adjust to minimum $10 order
|
||||
|
||||
# MEXC doesn't support MARKET orders for many pairs - use LIMIT orders instead
|
||||
if order_type.upper() == 'MARKET':
|
||||
# Convert market order to limit order with aggressive pricing for immediate execution
|
||||
if price is None:
|
||||
ticker = self.get_ticker(symbol)
|
||||
if ticker and 'last' in ticker:
|
||||
current_price = float(ticker['last'])
|
||||
# For buy orders, use slightly above market to ensure immediate execution
|
||||
# For sell orders, use slightly below market to ensure immediate execution
|
||||
if side.upper() == 'BUY':
|
||||
price = current_price * 1.002 # 0.2% premium for immediate buy execution
|
||||
else:
|
||||
price = current_price * 0.998 # 0.2% discount for immediate sell execution
|
||||
else:
|
||||
logger.error("Cannot get current price for market order conversion")
|
||||
return {}
|
||||
try:
|
||||
logger.info(f"MEXC: place_order called with symbol={symbol}, side={side}, order_type={order_type}, quantity={quantity}, price={price}")
|
||||
|
||||
# Convert to limit order with immediate execution pricing
|
||||
order_type = 'LIMIT'
|
||||
logger.info(f"MEXC: Converting MARKET to aggressive LIMIT order at ${price:.2f} for immediate execution")
|
||||
formatted_symbol = self._format_spot_symbol(symbol)
|
||||
logger.info(f"MEXC: Formatted symbol: {symbol} -> {formatted_symbol}")
|
||||
|
||||
# Check if symbol is supported for API trading
|
||||
if not self.is_symbol_supported(symbol):
|
||||
supported_symbols = self.get_api_symbols()
|
||||
logger.error(f"Symbol {formatted_symbol} is not supported for API trading")
|
||||
logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10
|
||||
return {}
|
||||
|
||||
# Round quantity to MEXC precision requirements and ensure minimum order value
|
||||
# MEXC ETHUSDC requires precision based on baseAssetPrecision (5 decimals for ETH)
|
||||
original_quantity = quantity
|
||||
if 'ETH' in formatted_symbol:
|
||||
quantity = round(quantity, 5) # MEXC ETHUSDC precision: 5 decimals
|
||||
# Ensure minimum order value (typically $10+ for MEXC)
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 5) # Adjust to minimum $10 order
|
||||
elif 'BTC' in formatted_symbol:
|
||||
quantity = round(quantity, 6) # MEXC BTCUSDC precision: 6 decimals
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 6) # Adjust to minimum $10 order
|
||||
else:
|
||||
quantity = round(quantity, 5) # Default precision for MEXC
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 5) # Adjust to minimum $10 order
|
||||
|
||||
if quantity != original_quantity:
|
||||
logger.info(f"MEXC: Adjusted quantity: {original_quantity} -> {quantity}")
|
||||
|
||||
# MEXC doesn't support MARKET orders for many pairs - use LIMIT orders instead
|
||||
if order_type.upper() == 'MARKET':
|
||||
# Convert market order to limit order with aggressive pricing for immediate execution
|
||||
if price is None:
|
||||
ticker = self.get_ticker(symbol)
|
||||
if ticker and 'last' in ticker:
|
||||
current_price = float(ticker['last'])
|
||||
# For buy orders, use slightly above market to ensure immediate execution
|
||||
# For sell orders, use slightly below market to ensure immediate execution
|
||||
if side.upper() == 'BUY':
|
||||
price = current_price * 1.002 # 0.2% premium for immediate buy execution
|
||||
else:
|
||||
price = current_price * 0.998 # 0.2% discount for immediate sell execution
|
||||
else:
|
||||
logger.error("Cannot get current price for market order conversion")
|
||||
return {}
|
||||
|
||||
# Convert to limit order with immediate execution pricing
|
||||
order_type = 'LIMIT'
|
||||
logger.info(f"MEXC: Converting MARKET to aggressive LIMIT order at ${price:.2f} for immediate execution")
|
||||
|
||||
# Prepare order parameters
|
||||
params = {
|
||||
'symbol': formatted_symbol,
|
||||
'side': side.upper(),
|
||||
'type': order_type.upper(),
|
||||
'quantity': str(quantity) # Quantity must be a string
|
||||
}
|
||||
|
||||
if price is not None:
|
||||
# Format price to remove unnecessary decimal places (e.g., 2900.0 -> 2900)
|
||||
params['price'] = str(int(price)) if price == int(price) else str(price)
|
||||
# Prepare order parameters
|
||||
params = {
|
||||
'symbol': formatted_symbol,
|
||||
'side': side.upper(),
|
||||
'type': order_type.upper(),
|
||||
'quantity': str(quantity) # Quantity must be a string
|
||||
}
|
||||
|
||||
if price is not None:
|
||||
# Format price to remove unnecessary decimal places (e.g., 2900.0 -> 2900)
|
||||
params['price'] = str(int(price)) if price == int(price) else str(price)
|
||||
|
||||
logger.info(f"MEXC: Placing {side.upper()} {order_type.upper()} order for {quantity} {formatted_symbol} at price {price}")
|
||||
|
||||
# Use the standard private request method which handles timestamp and signature
|
||||
endpoint = "order"
|
||||
result = self._send_private_request("POST", endpoint, params)
|
||||
|
||||
if result:
|
||||
logger.info(f"MEXC: Order placed successfully: {result}")
|
||||
return result
|
||||
else:
|
||||
logger.error(f"MEXC: Failed to place order")
|
||||
logger.info(f"MEXC: Placing {side.upper()} {order_type.upper()} order for {quantity} {formatted_symbol} at price {price}")
|
||||
logger.info(f"MEXC: Order parameters: {params}")
|
||||
|
||||
# Use the standard private request method which handles timestamp and signature
|
||||
endpoint = "order"
|
||||
result = self._send_private_request("POST", endpoint, params)
|
||||
|
||||
if result:
|
||||
logger.info(f"MEXC: Order placed successfully: {result}")
|
||||
return result
|
||||
else:
|
||||
logger.error(f"MEXC: Failed to place order - _send_private_request returned None/empty result")
|
||||
logger.error(f"MEXC: Failed order details - symbol: {formatted_symbol}, side: {side}, type: {order_type}, quantity: {quantity}, price: {price}")
|
||||
return {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MEXC: Exception in place_order: {e}")
|
||||
logger.error(f"MEXC: Exception details - symbol: {symbol}, side: {side}, type: {order_type}, quantity: {quantity}, price: {price}")
|
||||
import traceback
|
||||
logger.error(f"MEXC: Full traceback: {traceback.format_exc()}")
|
||||
return {}
|
||||
|
||||
def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]:
|
||||
|
@ -757,20 +757,98 @@ class DQNAgent:
|
||||
# Sanitize and stack states and next_states
|
||||
sanitized_states = []
|
||||
sanitized_next_states = []
|
||||
sanitized_experiences = []
|
||||
|
||||
for i, e in enumerate(experiences):
|
||||
try:
|
||||
state = np.asarray(e[0], dtype=np.float32)
|
||||
next_state = np.asarray(e[3], dtype=np.float32)
|
||||
# Extract experience components
|
||||
state, action, reward, next_state, done = e
|
||||
|
||||
# Sanitize state - convert any dict/object to float arrays
|
||||
state = self._sanitize_state_data(state)
|
||||
next_state = self._sanitize_state_data(next_state)
|
||||
|
||||
# Sanitize action - ensure it's an integer
|
||||
if isinstance(action, dict):
|
||||
# If action is a dict, try to extract action value
|
||||
action = action.get('action', action.get('value', 0))
|
||||
action = int(action) if not isinstance(action, (int, np.integer)) else action
|
||||
|
||||
# Sanitize reward - ensure it's a float
|
||||
if isinstance(reward, dict):
|
||||
# If reward is a dict, try to extract reward value
|
||||
reward = reward.get('reward', reward.get('value', 0.0))
|
||||
reward = float(reward) if not isinstance(reward, (float, np.floating)) else reward
|
||||
|
||||
# Sanitize done - ensure it's a boolean/float
|
||||
if isinstance(done, dict):
|
||||
done = done.get('done', done.get('value', False))
|
||||
done = bool(done) if not isinstance(done, (bool, np.bool_)) else done
|
||||
|
||||
# Convert state to proper numpy array
|
||||
state = np.asarray(state, dtype=np.float32)
|
||||
next_state = np.asarray(next_state, dtype=np.float32)
|
||||
|
||||
# Add to sanitized lists
|
||||
sanitized_states.append(state)
|
||||
sanitized_next_states.append(next_state)
|
||||
sanitized_experiences.append((state, action, reward, next_state, done))
|
||||
|
||||
except Exception as ex:
|
||||
print(f"[DQNAgent] Bad experience at index {i}: {ex}")
|
||||
continue
|
||||
|
||||
if not sanitized_states or not sanitized_next_states:
|
||||
print("[DQNAgent] No valid states in replay batch.")
|
||||
return 0.0 # Return float instead of None for consistency
|
||||
states = torch.FloatTensor(np.stack(sanitized_states)).to(self.device)
|
||||
next_states = torch.FloatTensor(np.stack(sanitized_next_states)).to(self.device)
|
||||
|
||||
# Validate all states have the same dimensions before stacking
|
||||
expected_dim = getattr(self, 'state_size', getattr(self, 'state_dim', 403))
|
||||
if isinstance(expected_dim, tuple):
|
||||
expected_dim = np.prod(expected_dim)
|
||||
|
||||
# Filter out states with wrong dimensions and fix them
|
||||
valid_states = []
|
||||
valid_next_states = []
|
||||
valid_experiences = []
|
||||
|
||||
for i, (state, next_state, exp) in enumerate(zip(sanitized_states, sanitized_next_states, sanitized_experiences)):
|
||||
# Ensure states have correct dimensions
|
||||
if len(state) != expected_dim:
|
||||
logger.debug(f"Fixing state dimension: {len(state)} -> {expected_dim}")
|
||||
if len(state) < expected_dim:
|
||||
# Pad with zeros
|
||||
padded_state = np.zeros(expected_dim, dtype=np.float32)
|
||||
padded_state[:len(state)] = state
|
||||
state = padded_state
|
||||
else:
|
||||
# Truncate
|
||||
state = state[:expected_dim]
|
||||
|
||||
if len(next_state) != expected_dim:
|
||||
logger.debug(f"Fixing next_state dimension: {len(next_state)} -> {expected_dim}")
|
||||
if len(next_state) < expected_dim:
|
||||
# Pad with zeros
|
||||
padded_next_state = np.zeros(expected_dim, dtype=np.float32)
|
||||
padded_next_state[:len(next_state)] = next_state
|
||||
next_state = padded_next_state
|
||||
else:
|
||||
# Truncate
|
||||
next_state = next_state[:expected_dim]
|
||||
|
||||
valid_states.append(state)
|
||||
valid_next_states.append(next_state)
|
||||
valid_experiences.append(exp)
|
||||
|
||||
if not valid_states:
|
||||
print("[DQNAgent] No valid states after dimension fixing.")
|
||||
return 0.0
|
||||
|
||||
# Use validated experiences for training
|
||||
experiences = valid_experiences
|
||||
|
||||
states = torch.FloatTensor(np.stack(valid_states)).to(self.device)
|
||||
next_states = torch.FloatTensor(np.stack(valid_next_states)).to(self.device)
|
||||
|
||||
# Choose appropriate replay method
|
||||
if self.use_mixed_precision:
|
||||
@ -797,28 +875,42 @@ class DQNAgent:
|
||||
extrema_indices = np.random.choice(len(self.extrema_memory), size=min(self.batch_size, len(self.extrema_memory)), replace=False)
|
||||
extrema_batch = [self.extrema_memory[i] for i in extrema_indices]
|
||||
|
||||
# Extract tensors from extrema batch
|
||||
extrema_states = torch.FloatTensor(np.array([e[0] for e in extrema_batch])).to(self.device)
|
||||
extrema_actions = torch.LongTensor(np.array([e[1] for e in extrema_batch])).to(self.device)
|
||||
extrema_rewards = torch.FloatTensor(np.array([e[2] for e in extrema_batch])).to(self.device)
|
||||
extrema_next_states = torch.FloatTensor(np.array([e[3] for e in extrema_batch])).to(self.device)
|
||||
extrema_dones = torch.FloatTensor(np.array([e[4] for e in extrema_batch])).to(self.device)
|
||||
# Sanitize extrema batch
|
||||
sanitized_extrema = []
|
||||
for e in extrema_batch:
|
||||
try:
|
||||
state, action, reward, next_state, done = e
|
||||
state = self._sanitize_state_data(state)
|
||||
next_state = self._sanitize_state_data(next_state)
|
||||
state = np.asarray(state, dtype=np.float32)
|
||||
next_state = np.asarray(next_state, dtype=np.float32)
|
||||
sanitized_extrema.append((state, action, reward, next_state, done))
|
||||
except:
|
||||
continue
|
||||
|
||||
# Use a slightly reduced learning rate for extrema training
|
||||
old_lr = self.optimizer.param_groups[0]['lr']
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr * 0.8
|
||||
|
||||
# Train on extrema memory
|
||||
if self.use_mixed_precision:
|
||||
extrema_loss = self._replay_mixed_precision(extrema_states, extrema_actions, extrema_rewards, extrema_next_states, extrema_dones)
|
||||
else:
|
||||
extrema_loss = self._replay_standard(extrema_batch)
|
||||
|
||||
# Reset learning rate
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr
|
||||
|
||||
# Log extrema loss
|
||||
logger.info(f"Extra training on extrema points, loss: {extrema_loss:.4f}")
|
||||
if sanitized_extrema:
|
||||
# Extract tensors from extrema batch
|
||||
extrema_states = torch.FloatTensor(np.array([e[0] for e in sanitized_extrema])).to(self.device)
|
||||
extrema_actions = torch.LongTensor(np.array([e[1] for e in sanitized_extrema])).to(self.device)
|
||||
extrema_rewards = torch.FloatTensor(np.array([e[2] for e in sanitized_extrema])).to(self.device)
|
||||
extrema_next_states = torch.FloatTensor(np.array([e[3] for e in sanitized_extrema])).to(self.device)
|
||||
extrema_dones = torch.FloatTensor(np.array([e[4] for e in sanitized_extrema])).to(self.device)
|
||||
|
||||
# Use a slightly reduced learning rate for extrema training
|
||||
old_lr = self.optimizer.param_groups[0]['lr']
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr * 0.8
|
||||
|
||||
# Train on extrema memory
|
||||
if self.use_mixed_precision:
|
||||
extrema_loss = self._replay_mixed_precision(extrema_states, extrema_actions, extrema_rewards, extrema_next_states, extrema_dones)
|
||||
else:
|
||||
extrema_loss = self._replay_standard(sanitized_extrema)
|
||||
|
||||
# Reset learning rate
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr
|
||||
|
||||
# Log extrema loss
|
||||
logger.info(f"Extra training on extrema points, loss: {extrema_loss:.4f}")
|
||||
|
||||
# Randomly train on price movement examples (similar to extrema)
|
||||
if random.random() < 0.3 and len(self.price_movement_memory) >= self.batch_size:
|
||||
@ -826,28 +918,42 @@ class DQNAgent:
|
||||
price_indices = np.random.choice(len(self.price_movement_memory), size=min(self.batch_size, len(self.price_movement_memory)), replace=False)
|
||||
price_batch = [self.price_movement_memory[i] for i in price_indices]
|
||||
|
||||
# Extract tensors from price movement batch
|
||||
price_states = torch.FloatTensor(np.array([e[0] for e in price_batch])).to(self.device)
|
||||
price_actions = torch.LongTensor(np.array([e[1] for e in price_batch])).to(self.device)
|
||||
price_rewards = torch.FloatTensor(np.array([e[2] for e in price_batch])).to(self.device)
|
||||
price_next_states = torch.FloatTensor(np.array([e[3] for e in price_batch])).to(self.device)
|
||||
price_dones = torch.FloatTensor(np.array([e[4] for e in price_batch])).to(self.device)
|
||||
# Sanitize price movement batch
|
||||
sanitized_price = []
|
||||
for e in price_batch:
|
||||
try:
|
||||
state, action, reward, next_state, done = e
|
||||
state = self._sanitize_state_data(state)
|
||||
next_state = self._sanitize_state_data(next_state)
|
||||
state = np.asarray(state, dtype=np.float32)
|
||||
next_state = np.asarray(next_state, dtype=np.float32)
|
||||
sanitized_price.append((state, action, reward, next_state, done))
|
||||
except:
|
||||
continue
|
||||
|
||||
# Use a slightly reduced learning rate for price movement training
|
||||
old_lr = self.optimizer.param_groups[0]['lr']
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr * 0.75
|
||||
|
||||
# Train on price movement memory
|
||||
if self.use_mixed_precision:
|
||||
price_loss = self._replay_mixed_precision(price_states, price_actions, price_rewards, price_next_states, price_dones)
|
||||
else:
|
||||
price_loss = self._replay_standard(price_batch)
|
||||
|
||||
# Reset learning rate
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr
|
||||
|
||||
# Log price movement loss
|
||||
logger.info(f"Extra training on price movement examples, loss: {price_loss:.4f}")
|
||||
if sanitized_price:
|
||||
# Extract tensors from price movement batch
|
||||
price_states = torch.FloatTensor(np.array([e[0] for e in sanitized_price])).to(self.device)
|
||||
price_actions = torch.LongTensor(np.array([e[1] for e in sanitized_price])).to(self.device)
|
||||
price_rewards = torch.FloatTensor(np.array([e[2] for e in sanitized_price])).to(self.device)
|
||||
price_next_states = torch.FloatTensor(np.array([e[3] for e in sanitized_price])).to(self.device)
|
||||
price_dones = torch.FloatTensor(np.array([e[4] for e in sanitized_price])).to(self.device)
|
||||
|
||||
# Use a slightly reduced learning rate for price movement training
|
||||
old_lr = self.optimizer.param_groups[0]['lr']
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr * 0.75
|
||||
|
||||
# Train on price movement memory
|
||||
if self.use_mixed_precision:
|
||||
price_loss = self._replay_mixed_precision(price_states, price_actions, price_rewards, price_next_states, price_dones)
|
||||
else:
|
||||
price_loss = self._replay_standard(sanitized_price)
|
||||
|
||||
# Reset learning rate
|
||||
self.optimizer.param_groups[0]['lr'] = old_lr
|
||||
|
||||
# Log price movement loss
|
||||
logger.info(f"Extra training on price movement examples, loss: {price_loss:.4f}")
|
||||
|
||||
return loss
|
||||
|
||||
@ -1452,4 +1558,106 @@ class DQNAgent:
|
||||
total_params = 0
|
||||
for param in self.policy_net.parameters():
|
||||
total_params += param.numel()
|
||||
return total_params
|
||||
return total_params
|
||||
|
||||
def _sanitize_state_data(self, state):
|
||||
"""Sanitize state data to ensure it's a proper numeric array"""
|
||||
try:
|
||||
# If state is already a numpy array, return it
|
||||
if isinstance(state, np.ndarray):
|
||||
# Check for non-numeric data and handle it
|
||||
if state.dtype == object:
|
||||
# Convert object array to float array
|
||||
sanitized = np.zeros_like(state, dtype=np.float32)
|
||||
for i in range(state.shape[0]):
|
||||
if len(state.shape) > 1:
|
||||
for j in range(state.shape[1]):
|
||||
sanitized[i, j] = self._extract_numeric_value(state[i, j])
|
||||
else:
|
||||
sanitized[i] = self._extract_numeric_value(state[i])
|
||||
return sanitized
|
||||
else:
|
||||
return state.astype(np.float32)
|
||||
|
||||
# If state is a list or tuple, convert to array
|
||||
elif isinstance(state, (list, tuple)):
|
||||
# Recursively sanitize each element
|
||||
sanitized = []
|
||||
for item in state:
|
||||
if isinstance(item, (list, tuple)):
|
||||
sanitized_row = []
|
||||
for sub_item in item:
|
||||
sanitized_row.append(self._extract_numeric_value(sub_item))
|
||||
sanitized.append(sanitized_row)
|
||||
else:
|
||||
sanitized.append(self._extract_numeric_value(item))
|
||||
return np.array(sanitized, dtype=np.float32)
|
||||
|
||||
# If state is a dict, try to extract values
|
||||
elif isinstance(state, dict):
|
||||
# Try to extract meaningful values from dict
|
||||
values = []
|
||||
for key in sorted(state.keys()): # Sort for consistency
|
||||
values.append(self._extract_numeric_value(state[key]))
|
||||
return np.array(values, dtype=np.float32)
|
||||
|
||||
# If state is a single value, make it an array
|
||||
else:
|
||||
return np.array([self._extract_numeric_value(state)], dtype=np.float32)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error sanitizing state data: {e}. Using zero array with expected dimensions.")
|
||||
# Return a zero array as fallback with the expected state dimension
|
||||
# Use the state_dim from initialization, fallback to 403 if not available
|
||||
expected_size = getattr(self, 'state_size', getattr(self, 'state_dim', 403))
|
||||
if isinstance(expected_size, tuple):
|
||||
expected_size = np.prod(expected_size)
|
||||
return np.zeros(int(expected_size), dtype=np.float32)
|
||||
|
||||
def _extract_numeric_value(self, value):
|
||||
"""Extract a numeric value from various data types"""
|
||||
try:
|
||||
# Handle None values
|
||||
if value is None:
|
||||
return 0.0
|
||||
|
||||
# Handle numeric types
|
||||
if isinstance(value, (int, float, np.number)):
|
||||
return float(value)
|
||||
|
||||
# Handle dict values
|
||||
elif isinstance(value, dict):
|
||||
# Try common keys for numeric data
|
||||
for key in ['value', 'price', 'close', 'last', 'amount', 'quantity']:
|
||||
if key in value:
|
||||
return self._extract_numeric_value(value[key])
|
||||
# If no common keys, try to get first numeric value
|
||||
for v in value.values():
|
||||
if isinstance(v, (int, float, np.number)):
|
||||
return float(v)
|
||||
return 0.0
|
||||
|
||||
# Handle string values that might be numeric
|
||||
elif isinstance(value, str):
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
# Handle datetime objects
|
||||
elif hasattr(value, 'timestamp'):
|
||||
return float(value.timestamp())
|
||||
|
||||
# Handle boolean values
|
||||
elif isinstance(value, bool):
|
||||
return float(value)
|
||||
|
||||
# Handle list/tuple - take first numeric value
|
||||
elif isinstance(value, (list, tuple)) and len(value) > 0:
|
||||
return self._extract_numeric_value(value[0])
|
||||
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
except:
|
||||
return 0.0
|
@ -365,66 +365,27 @@ class TradingExecutor:
|
||||
self._cancel_open_orders(symbol)
|
||||
|
||||
# Calculate position size
|
||||
position_value = self._calculate_position_size(confidence, current_price)
|
||||
quantity = position_value / current_price
|
||||
position_size = self._calculate_position_size(confidence, current_price)
|
||||
quantity = position_size / current_price
|
||||
|
||||
logger.info(f"Executing BUY: {quantity:.6f} {symbol} at ${current_price:.2f} "
|
||||
f"(value: ${position_value:.2f}, confidence: {confidence:.2f}) "
|
||||
f"[{'SIMULATION' if self.simulation_mode else 'LIVE'}]")
|
||||
logger.info(f"Executing BUY: {quantity:.6f} {symbol} at ${current_price:.2f} (value: ${position_size:.2f}, confidence: {confidence:.2f}) [{'SIM' if self.simulation_mode else 'LIVE'}]")
|
||||
|
||||
if self.simulation_mode:
|
||||
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Trade logged but not executed")
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = quantity * current_price * taker_fee_rate
|
||||
|
||||
# Create mock position for tracking
|
||||
# Create simulated position
|
||||
self.positions[symbol] = Position(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
entry_time=datetime.now(),
|
||||
order_id=f"sim_{int(time.time())}"
|
||||
order_id=f"sim_{int(datetime.now().timestamp())}"
|
||||
)
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
logger.info(f"Simulated BUY order: {quantity:.6f} {symbol} at ${current_price:.2f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Get order type from config
|
||||
order_type = self.mexc_config.get('order_type', 'market').lower()
|
||||
|
||||
# For limit orders, set price slightly above market for immediate execution
|
||||
limit_price = None
|
||||
if order_type == 'limit':
|
||||
# Set buy price slightly above market to ensure immediate execution
|
||||
limit_price = current_price * 1.001 # 0.1% above market
|
||||
|
||||
# Place buy order
|
||||
if order_type == 'market':
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='buy',
|
||||
order_type=order_type,
|
||||
quantity=quantity
|
||||
)
|
||||
else:
|
||||
# For limit orders, price is required
|
||||
assert limit_price is not None, "limit_price required for limit orders"
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='buy',
|
||||
order_type=order_type,
|
||||
quantity=quantity,
|
||||
price=limit_price
|
||||
)
|
||||
|
||||
if order:
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = quantity * current_price * taker_fee_rate
|
||||
|
||||
else:
|
||||
# Place real order with enhanced error handling
|
||||
result = self._place_order_with_retry(symbol, 'BUY', 'MARKET', quantity, current_price)
|
||||
if result and 'orderId' in result:
|
||||
# Create position record
|
||||
self.positions[symbol] = Position(
|
||||
symbol=symbol,
|
||||
@ -432,233 +393,58 @@ class TradingExecutor:
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
entry_time=datetime.now(),
|
||||
order_id=order.get('orderId', 'unknown')
|
||||
order_id=result['orderId']
|
||||
)
|
||||
|
||||
logger.info(f"BUY order executed: {result}")
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"BUY order executed: {order}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to place BUY order")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing BUY order: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _execute_sell(self, symbol: str, confidence: float, current_price: float) -> bool:
|
||||
"""Execute a sell order"""
|
||||
# Check if we have a position to sell
|
||||
if symbol not in self.positions:
|
||||
"""Execute a sell order (close long position or open short position)"""
|
||||
if symbol in self.positions:
|
||||
position = self.positions[symbol]
|
||||
if position.side == 'LONG':
|
||||
logger.info(f"Closing LONG position in {symbol}")
|
||||
return self._close_long_position(symbol, confidence, current_price)
|
||||
else:
|
||||
logger.info(f"Already have SHORT position in {symbol}")
|
||||
return False
|
||||
else:
|
||||
# No position to sell, open short position
|
||||
logger.info(f"No position to sell in {symbol}. Opening short position")
|
||||
return self._execute_short(symbol, confidence, current_price)
|
||||
|
||||
position = self.positions[symbol]
|
||||
|
||||
|
||||
def _execute_short(self, symbol: str, confidence: float, current_price: float) -> bool:
|
||||
"""Execute a short order (sell without holding the asset)"""
|
||||
# Cancel any existing open orders before placing new order
|
||||
if not self.simulation_mode:
|
||||
self._cancel_open_orders(symbol)
|
||||
|
||||
logger.info(f"Executing SELL: {position.quantity:.6f} {symbol} at ${current_price:.2f} "
|
||||
f"(confidence: {confidence:.2f}) [{'SIMULATION' if self.simulation_mode else 'LIVE'}]")
|
||||
|
||||
if self.simulation_mode:
|
||||
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Trade logged but not executed")
|
||||
# Calculate P&L and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = position.quantity * current_price * taker_fee_rate
|
||||
|
||||
# Create trade record
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
quantity=position.quantity,
|
||||
entry_price=position.entry_price,
|
||||
exit_price=current_price,
|
||||
entry_time=position.entry_time,
|
||||
exit_time=exit_time,
|
||||
pnl=pnl,
|
||||
fees=simulated_fees,
|
||||
confidence=confidence,
|
||||
hold_time_seconds=hold_time_seconds
|
||||
)
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.daily_loss += max(0, -pnl) # Add to daily loss if negative
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
|
||||
# Remove position
|
||||
del self.positions[symbol]
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"Position closed - P&L: ${pnl:.2f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Get order type from config
|
||||
order_type = self.mexc_config.get('order_type', 'market').lower()
|
||||
|
||||
# For limit orders, set price slightly below market for immediate execution
|
||||
limit_price = None
|
||||
if order_type == 'limit':
|
||||
# Set sell price slightly below market to ensure immediate execution
|
||||
limit_price = current_price * 0.999 # 0.1% below market
|
||||
|
||||
# Place sell order
|
||||
if order_type == 'market':
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='sell',
|
||||
order_type=order_type,
|
||||
quantity=position.quantity
|
||||
)
|
||||
else:
|
||||
# For limit orders, price is required
|
||||
assert limit_price is not None, "limit_price required for limit orders"
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='sell',
|
||||
order_type=order_type,
|
||||
quantity=position.quantity,
|
||||
price=limit_price
|
||||
)
|
||||
|
||||
if order:
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = position.quantity * current_price * taker_fee_rate
|
||||
|
||||
# Calculate P&L, fees, and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
fees = simulated_fees
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
quantity=position.quantity,
|
||||
entry_price=position.entry_price,
|
||||
exit_price=current_price,
|
||||
entry_time=position.entry_time,
|
||||
exit_time=exit_time,
|
||||
pnl=pnl - fees,
|
||||
fees=fees,
|
||||
confidence=confidence,
|
||||
hold_time_seconds=hold_time_seconds
|
||||
)
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.daily_loss += max(0, -(pnl - fees)) # Add to daily loss if negative
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
|
||||
# Remove position
|
||||
del self.positions[symbol]
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"SELL order executed: {order}")
|
||||
logger.info(f"Position closed - P&L: ${pnl - fees:.2f}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to place SELL order")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing SELL order: {e}")
|
||||
return False
|
||||
|
||||
def _execute_short(self, symbol: str, confidence: float, current_price: float) -> bool:
|
||||
"""Execute a short position opening"""
|
||||
# Check if we already have a position
|
||||
if symbol in self.positions:
|
||||
logger.info(f"Already have position in {symbol}")
|
||||
return False
|
||||
|
||||
# Calculate position size
|
||||
position_value = self._calculate_position_size(confidence, current_price)
|
||||
quantity = position_value / current_price
|
||||
position_size = self._calculate_position_size(confidence, current_price)
|
||||
quantity = position_size / current_price
|
||||
|
||||
logger.info(f"Executing SHORT: {quantity:.6f} {symbol} at ${current_price:.2f} "
|
||||
f"(value: ${position_value:.2f}, confidence: {confidence:.2f}) "
|
||||
f"[{'SIMULATION' if self.simulation_mode else 'LIVE'}]")
|
||||
logger.info(f"Executing SHORT: {quantity:.6f} {symbol} at ${current_price:.2f} (value: ${position_size:.2f}, confidence: {confidence:.2f}) [{'SIM' if self.simulation_mode else 'LIVE'}]")
|
||||
|
||||
if self.simulation_mode:
|
||||
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Short position logged but not executed")
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = quantity * current_price * taker_fee_rate
|
||||
|
||||
# Create mock short position for tracking
|
||||
# Create simulated short position
|
||||
self.positions[symbol] = Position(
|
||||
symbol=symbol,
|
||||
side='SHORT',
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
entry_time=datetime.now(),
|
||||
order_id=f"sim_short_{int(time.time())}"
|
||||
order_id=f"sim_{int(datetime.now().timestamp())}"
|
||||
)
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
logger.info(f"Simulated SHORT order: {quantity:.6f} {symbol} at ${current_price:.2f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Get order type from config
|
||||
order_type = self.mexc_config.get('order_type', 'market').lower()
|
||||
|
||||
# For limit orders, set price slightly below market for immediate execution
|
||||
limit_price = None
|
||||
if order_type == 'limit':
|
||||
# Set short price slightly below market to ensure immediate execution
|
||||
limit_price = current_price * 0.999 # 0.1% below market
|
||||
|
||||
# Place short sell order
|
||||
if order_type == 'market':
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='sell', # Short selling starts with a sell order
|
||||
order_type=order_type,
|
||||
quantity=quantity
|
||||
)
|
||||
else:
|
||||
# For limit orders, price is required
|
||||
assert limit_price is not None, "limit_price required for limit orders"
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='sell', # Short selling starts with a sell order
|
||||
order_type=order_type,
|
||||
quantity=quantity,
|
||||
price=limit_price
|
||||
)
|
||||
|
||||
if order:
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = quantity * current_price * taker_fee_rate
|
||||
|
||||
else:
|
||||
# Place real short order with enhanced error handling
|
||||
result = self._place_order_with_retry(symbol, 'SELL', 'MARKET', quantity, current_price)
|
||||
if result and 'orderId' in result:
|
||||
# Create short position record
|
||||
self.positions[symbol] = Position(
|
||||
symbol=symbol,
|
||||
@ -666,24 +452,88 @@ class TradingExecutor:
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
entry_time=datetime.now(),
|
||||
order_id=order.get('orderId', 'unknown')
|
||||
order_id=result['orderId']
|
||||
)
|
||||
|
||||
logger.info(f"SHORT order executed: {result}")
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"SHORT order executed: {order}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to place SHORT order")
|
||||
return False
|
||||
|
||||
def _place_order_with_retry(self, symbol: str, side: str, order_type: str, quantity: float, current_price: float, max_retries: int = 3) -> Dict[str, Any]:
|
||||
"""Place order with retry logic for MEXC error handling"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
result = self.exchange.place_order(symbol, side, order_type, quantity, current_price)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing SHORT order: {e}")
|
||||
return False
|
||||
# Check if result contains error information
|
||||
if isinstance(result, dict) and 'error' in result:
|
||||
error_type = result.get('error')
|
||||
error_code = result.get('code')
|
||||
error_msg = result.get('message', 'Unknown error')
|
||||
|
||||
if error_type == 'oversold' and error_code == 30005:
|
||||
logger.warning(f"MEXC Oversold error on attempt {attempt + 1}/{max_retries}")
|
||||
logger.warning(f"Error: {error_msg}")
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
# Wait with exponential backoff
|
||||
wait_time = result.get('retry_after', 60) * (2 ** attempt)
|
||||
logger.info(f"Waiting {wait_time} seconds before retry due to oversold condition...")
|
||||
time.sleep(wait_time)
|
||||
|
||||
# Reduce quantity for next attempt to avoid oversold
|
||||
quantity = quantity * 0.8 # Reduce by 20%
|
||||
logger.info(f"Reducing quantity to {quantity:.6f} for retry")
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Max retries reached for oversold condition")
|
||||
return {}
|
||||
|
||||
elif error_type == 'direction_not_allowed':
|
||||
logger.error(f"Trading direction not allowed for {symbol} {side}")
|
||||
return {}
|
||||
|
||||
elif error_type == 'insufficient_position':
|
||||
logger.error(f"Insufficient position for {symbol} {side}")
|
||||
return {}
|
||||
|
||||
else:
|
||||
logger.error(f"MEXC API error: {error_code} - {error_msg}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(5 * (attempt + 1)) # Wait 5, 10, 15 seconds
|
||||
continue
|
||||
else:
|
||||
return {}
|
||||
|
||||
# Success case
|
||||
elif isinstance(result, dict) and ('orderId' in result or 'symbol' in result):
|
||||
logger.info(f"Order placed successfully on attempt {attempt + 1}")
|
||||
return result
|
||||
|
||||
# Empty result - treat as failure
|
||||
else:
|
||||
logger.warning(f"Empty result on attempt {attempt + 1}/{max_retries}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(2 * (attempt + 1)) # Wait 2, 4, 6 seconds
|
||||
continue
|
||||
else:
|
||||
return {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception on order attempt {attempt + 1}/{max_retries}: {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(3 * (attempt + 1)) # Wait 3, 6, 9 seconds
|
||||
continue
|
||||
else:
|
||||
return {}
|
||||
|
||||
logger.error(f"Failed to place order after {max_retries} attempts")
|
||||
return {}
|
||||
|
||||
def _close_short_position(self, symbol: str, confidence: float, current_price: float) -> bool:
|
||||
"""Close a short position by buying back"""
|
||||
"""Close a short position by buying"""
|
||||
if symbol not in self.positions:
|
||||
logger.warning(f"No position to close in {symbol}")
|
||||
return False
|
||||
@ -724,13 +574,21 @@ class TradingExecutor:
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.daily_loss += max(0, -pnl) # Add to daily loss if negative
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
|
||||
# Remove position
|
||||
del self.positions[symbol]
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"SHORT position closed - P&L: ${pnl:.2f}")
|
||||
logger.info(f"Position closed - P&L: ${pnl:.2f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
@ -814,6 +672,147 @@ class TradingExecutor:
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing SHORT position: {e}")
|
||||
return False
|
||||
|
||||
def _close_long_position(self, symbol: str, confidence: float, current_price: float) -> bool:
|
||||
"""Close a long position by selling"""
|
||||
if symbol not in self.positions:
|
||||
logger.warning(f"No position to close in {symbol}")
|
||||
return False
|
||||
|
||||
position = self.positions[symbol]
|
||||
if position.side != 'LONG':
|
||||
logger.warning(f"Position in {symbol} is not LONG, cannot close with SELL")
|
||||
return False
|
||||
|
||||
logger.info(f"Closing LONG position: {position.quantity:.6f} {symbol} at ${current_price:.2f} "
|
||||
f"(confidence: {confidence:.2f})")
|
||||
|
||||
if self.simulation_mode:
|
||||
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Long close logged but not executed")
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = position.quantity * current_price * taker_fee_rate
|
||||
|
||||
# Calculate P&L for long position and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
quantity=position.quantity,
|
||||
entry_price=position.entry_price,
|
||||
exit_price=current_price,
|
||||
entry_time=position.entry_time,
|
||||
exit_time=exit_time,
|
||||
pnl=pnl,
|
||||
fees=simulated_fees,
|
||||
confidence=confidence,
|
||||
hold_time_seconds=hold_time_seconds
|
||||
)
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.daily_loss += max(0, -pnl) # Add to daily loss if negative
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
|
||||
# Remove position
|
||||
del self.positions[symbol]
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"Position closed - P&L: ${pnl:.2f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Get order type from config
|
||||
order_type = self.mexc_config.get('order_type', 'market').lower()
|
||||
|
||||
# For limit orders, set price slightly below market for immediate execution
|
||||
limit_price = None
|
||||
if order_type == 'limit':
|
||||
# Set sell price slightly below market to ensure immediate execution
|
||||
limit_price = current_price * 0.999 # 0.1% below market
|
||||
|
||||
# Place sell order to close long
|
||||
if order_type == 'market':
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='sell', # Sell to close long position
|
||||
order_type=order_type,
|
||||
quantity=position.quantity
|
||||
)
|
||||
else:
|
||||
# For limit orders, price is required
|
||||
assert limit_price is not None, "limit_price required for limit orders"
|
||||
order = self.exchange.place_order(
|
||||
symbol=symbol,
|
||||
side='sell', # Sell to close long position
|
||||
order_type=order_type,
|
||||
quantity=position.quantity,
|
||||
price=limit_price
|
||||
)
|
||||
|
||||
if order:
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
simulated_fees = position.quantity * current_price * taker_fee_rate
|
||||
|
||||
# Calculate P&L, fees, and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
fees = simulated_fees
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
quantity=position.quantity,
|
||||
entry_price=position.entry_price,
|
||||
exit_price=current_price,
|
||||
entry_time=position.entry_time,
|
||||
exit_time=exit_time,
|
||||
pnl=pnl - fees,
|
||||
fees=fees,
|
||||
confidence=confidence,
|
||||
hold_time_seconds=hold_time_seconds
|
||||
)
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.daily_loss += max(0, -(pnl - fees)) # Add to daily loss if negative
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
|
||||
# Remove position
|
||||
del self.positions[symbol]
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"LONG close order executed: {order}")
|
||||
logger.info(f"LONG position closed - P&L: ${pnl - fees:.2f}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to place LONG close order")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing LONG position: {e}")
|
||||
return False
|
||||
|
||||
def _calculate_position_size(self, confidence: float, current_price: float) -> float:
|
||||
"""Calculate position size - use 100% of account balance for short-term scalping"""
|
||||
|
@ -134,6 +134,9 @@ class CleanTradingDashboard:
|
||||
self.total_fees = 0.0
|
||||
self.current_position: Optional[dict] = None
|
||||
|
||||
# Live balance caching for real-time portfolio updates
|
||||
self._cached_live_balance: float = 0.0
|
||||
|
||||
# ENHANCED: Model control toggles - separate inference and training
|
||||
self.dqn_inference_enabled = True # Default: enabled
|
||||
self.dqn_training_enabled = True # Default: enabled
|
||||
@ -319,6 +322,42 @@ class CleanTradingDashboard:
|
||||
logger.warning(f"Error getting balance: {e}")
|
||||
return 100.0 # Default balance
|
||||
|
||||
def _get_live_account_balance(self) -> float:
|
||||
"""Get live account balance from MEXC API in real-time"""
|
||||
try:
|
||||
if not self.trading_executor:
|
||||
return self._get_initial_balance()
|
||||
|
||||
# If in simulation mode, use simulation balance
|
||||
if hasattr(self.trading_executor, 'simulation_mode') and self.trading_executor.simulation_mode:
|
||||
return self._get_initial_balance()
|
||||
|
||||
# For live trading, get actual MEXC balance
|
||||
if hasattr(self.trading_executor, 'get_account_balance'):
|
||||
balances = self.trading_executor.get_account_balance()
|
||||
if balances:
|
||||
# Get USDC balance (MEXC primary) and USDT as fallback
|
||||
usdc_balance = balances.get('USDC', {}).get('total', 0.0)
|
||||
usdt_balance = balances.get('USDT', {}).get('total', 0.0)
|
||||
|
||||
# Use the higher balance (primary currency)
|
||||
live_balance = max(usdc_balance, usdt_balance)
|
||||
|
||||
if live_balance > 0:
|
||||
logger.debug(f"Live MEXC balance: USDC=${usdc_balance:.2f}, USDT=${usdt_balance:.2f}, Using=${live_balance:.2f}")
|
||||
return live_balance
|
||||
else:
|
||||
logger.warning("Live account balance is $0.00 - check MEXC account funding")
|
||||
return 0.0
|
||||
|
||||
# Fallback to initial balance if API calls fail
|
||||
logger.warning("Failed to get live balance, using initial balance")
|
||||
return self._get_initial_balance()
|
||||
|
||||
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()
|
||||
@ -411,9 +450,15 @@ class CleanTradingDashboard:
|
||||
trade_count = len(self.closed_trades)
|
||||
trade_str = f"{trade_count} Trades"
|
||||
|
||||
# Portfolio value
|
||||
initial_balance = self._get_initial_balance()
|
||||
portfolio_value = initial_balance + total_session_pnl # Use total P&L including unrealized
|
||||
# Portfolio value - use live balance every 10 seconds to avoid API spam
|
||||
if n % 10 == 0 or not hasattr(self, '_cached_live_balance'):
|
||||
self._cached_live_balance = self._get_live_account_balance()
|
||||
logger.debug(f"Updated live balance cache: ${self._cached_live_balance:.2f}")
|
||||
|
||||
# For live trading, show actual account balance + session P&L
|
||||
# For simulation, show starting balance + session P&L
|
||||
current_balance = self._cached_live_balance if hasattr(self, '_cached_live_balance') else self._get_initial_balance()
|
||||
portfolio_value = current_balance + total_session_pnl # Live balance + unrealized P&L
|
||||
portfolio_str = f"${portfolio_value:.2f}"
|
||||
|
||||
# MEXC status
|
||||
|
@ -33,7 +33,7 @@ class DashboardLayoutManager:
|
||||
"Clean Trading Dashboard"
|
||||
], className="text-light mb-0"),
|
||||
html.P(
|
||||
f"Ultra-Fast Updates • Portfolio: ${self.starting_balance:,.0f} • {trading_mode}",
|
||||
f"Ultra-Fast Updates • Live Account Balance Sync • {trading_mode}",
|
||||
className="text-light mb-0 opacity-75 small"
|
||||
)
|
||||
], className="bg-dark p-2 mb-2")
|
||||
|
Reference in New Issue
Block a user