fix: Fix trading precision issues and improve error handling
- Fix quantity precision calculation for Binance, OKX, Bybit, Bitget, Deepcoin exchanges - Improve OpenRouter API error handling with detailed error messages - Add SECRET_KEY validation in Docker deployment entrypoint - Fix K-line chart measurement tool click issue - Adapt billing page text colors for dark theme - Update frontend build files
This commit is contained in:
@@ -16,6 +16,7 @@ import re
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Dict, List
|
||||
import builtins
|
||||
|
||||
from flask import Blueprint, Response, jsonify, request, g
|
||||
import pandas as pd
|
||||
@@ -25,6 +26,7 @@ from app.utils.db import get_db_connection
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.auth import login_required
|
||||
from app.services.indicator_params import IndicatorCaller
|
||||
from app.utils.safe_exec import validate_code_safety, safe_exec_code
|
||||
import requests
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -385,37 +387,79 @@ def verify_code():
|
||||
# 1. Generate mock data
|
||||
df = _generate_mock_df()
|
||||
|
||||
# 2. Prepare execution environment
|
||||
# 2. Prepare execution environment (sandboxed)
|
||||
exec_env = {
|
||||
'df': df.copy(),
|
||||
'pd': pd,
|
||||
'np': np,
|
||||
'output': None
|
||||
}
|
||||
|
||||
# 3. Execute code
|
||||
|
||||
# 2.1 Create restricted __import__ that only allows safe modules
|
||||
def safe_import(name, *args, **kwargs):
|
||||
"""Only allow importing a small set of safe modules inside indicator scripts."""
|
||||
allowed_modules = ['numpy', 'pandas', 'math', 'json', 'datetime', 'time']
|
||||
if name in allowed_modules or name.split('.')[0] in allowed_modules:
|
||||
return builtins.__import__(name, *args, **kwargs)
|
||||
raise ImportError(f"Import not allowed: {name}")
|
||||
|
||||
safe_builtins = {
|
||||
k: getattr(builtins, k)
|
||||
for k in dir(builtins)
|
||||
if not k.startswith('_') and k not in [
|
||||
'eval', 'exec', 'compile', 'open', 'input',
|
||||
'help', 'exit', 'quit',
|
||||
'copyright', 'credits', 'license'
|
||||
]
|
||||
}
|
||||
safe_builtins['__import__'] = safe_import
|
||||
|
||||
exec_env_sandbox = exec_env.copy()
|
||||
exec_env_sandbox['__builtins__'] = safe_builtins
|
||||
|
||||
# 2.2 Pre-import commonly used modules into the sandbox
|
||||
pre_import_code = """
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
"""
|
||||
try:
|
||||
exec(code, exec_env)
|
||||
except SyntaxError as e:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": f"Syntax Error at line {e.lineno}: {e.msg}",
|
||||
"data": {"type": "SyntaxError", "line": e.lineno, "details": str(e)}
|
||||
})
|
||||
exec(pre_import_code, exec_env_sandbox)
|
||||
except Exception as e:
|
||||
# Capture traceback for better debugging
|
||||
tb = traceback.format_exc()
|
||||
# Extract the line number from the exec() call in the traceback if possible
|
||||
# This is tricky because the traceback includes the backend frames.
|
||||
# We'll just return the exception message.
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": f"Runtime Error: {str(e)}",
|
||||
"code": 0,
|
||||
"msg": f"Runtime Error during pre-import: {str(e)}",
|
||||
"data": {"type": type(e).__name__, "details": tb}
|
||||
})
|
||||
|
||||
# 3. Static safety check
|
||||
is_safe, error_msg = validate_code_safety(code)
|
||||
if not is_safe:
|
||||
logger.error(f"Indicator verifyCode security check failed: {error_msg}")
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": f"Code contains unsafe operations: {error_msg}",
|
||||
"data": {"type": "SecurityError", "details": error_msg}
|
||||
})
|
||||
|
||||
# 4. Execute code with timeout in sandbox
|
||||
exec_result = safe_exec_code(
|
||||
code=code,
|
||||
exec_globals=exec_env_sandbox,
|
||||
exec_locals=exec_env_sandbox,
|
||||
timeout=20 # indicator verification should be quick
|
||||
)
|
||||
|
||||
if not exec_result.get('success'):
|
||||
error_detail = exec_result.get('error') or 'Unknown error'
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": f"Runtime Error: {error_detail}",
|
||||
"data": {"type": "RuntimeError", "details": error_detail}
|
||||
})
|
||||
|
||||
# 4. Check output
|
||||
output = exec_env.get('output')
|
||||
# 5. Check output
|
||||
output = exec_env_sandbox.get('output')
|
||||
|
||||
if output is None:
|
||||
return jsonify({
|
||||
|
||||
@@ -1223,9 +1223,29 @@ class BacktestService:
|
||||
f"Using available end date instead. This may affect backtest results.")
|
||||
|
||||
# Filter date range (use available data range if requested range is outside)
|
||||
effective_start = max(start_date, data_start)
|
||||
effective_end = min(end_date, data_end)
|
||||
df_filtered = df[(df.index >= effective_start) & (df.index <= effective_end)].copy()
|
||||
# If data ends before requested end_date, use the most recent data up to the requested limit
|
||||
if data_end < end_date:
|
||||
# Data ends before requested end date - use the most recent data
|
||||
# Calculate how many candles we need based on requested time range
|
||||
requested_seconds = (end_date - start_date).total_seconds()
|
||||
requested_candles = math.ceil(requested_seconds / tf_seconds)
|
||||
# Take the most recent N candles from available data
|
||||
if len(df) > requested_candles:
|
||||
df_filtered = df.tail(requested_candles).copy()
|
||||
effective_start = df_filtered.index.min()
|
||||
effective_end = df_filtered.index.max()
|
||||
else:
|
||||
# Use all available data
|
||||
df_filtered = df.copy()
|
||||
effective_start = data_start
|
||||
effective_end = data_end
|
||||
logger.warning(f"Available data ({len(df)} candles) is less than requested ({requested_candles} candles). "
|
||||
f"Using all available data from {effective_start} to {effective_end}")
|
||||
else:
|
||||
# Normal case: filter by requested date range
|
||||
effective_start = max(start_date, data_start)
|
||||
effective_end = min(end_date, data_end)
|
||||
df_filtered = df[(df.index >= effective_start) & (df.index <= effective_end)].copy()
|
||||
|
||||
if df_filtered.empty:
|
||||
logger.error(f"After filtering date range ({effective_start} to {effective_end}), no data remains. "
|
||||
@@ -3848,7 +3868,20 @@ import pandas as pd
|
||||
|
||||
# Calculate annualized return: simple, not compound
|
||||
# For high-return strategies, compound annualization produces unrealistic numbers
|
||||
actual_days = (end_date - start_date).total_seconds() / 86400
|
||||
# Use actual data time range from equity_curve instead of requested start_date/end_date
|
||||
# This fixes the issue where data may only be available until a certain date (e.g., TSLA only to January)
|
||||
try:
|
||||
# Parse actual start and end times from equity_curve
|
||||
actual_start_str = equity_curve[0]['time']
|
||||
actual_end_str = equity_curve[-1]['time']
|
||||
actual_start = datetime.strptime(actual_start_str, '%Y-%m-%d %H:%M')
|
||||
actual_end = datetime.strptime(actual_end_str, '%Y-%m-%d %H:%M')
|
||||
actual_days = (actual_end - actual_start).total_seconds() / 86400
|
||||
except (KeyError, ValueError, IndexError) as e:
|
||||
# Fallback to requested date range if parsing fails
|
||||
logger.warning(f"Failed to parse actual time range from equity_curve: {e}, using requested range")
|
||||
actual_days = (end_date - start_date).total_seconds() / 86400
|
||||
|
||||
years = actual_days / 365.0
|
||||
|
||||
# Simple annualization: annualized return = total return / years
|
||||
|
||||
@@ -351,9 +351,13 @@ def _place_mt5_order(
|
||||
else:
|
||||
raise LiveTradingError(f"Unsupported signal_type for MT5: {signal_type}")
|
||||
|
||||
# Normalize symbol before placing order (MT5 requires specific format)
|
||||
from app.services.mt5_trading.symbols import normalize_symbol
|
||||
normalized_symbol = normalize_symbol(symbol)
|
||||
|
||||
# Place market order
|
||||
result = client.place_market_order(
|
||||
symbol=symbol,
|
||||
symbol=normalized_symbol,
|
||||
side=action,
|
||||
volume=amount,
|
||||
comment="QuantDinger",
|
||||
|
||||
@@ -73,7 +73,14 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap")
|
||||
return BinanceFuturesClient(api_key=api_key, secret_key=secret_key, base_url=base_url, enable_demo_trading=is_demo)
|
||||
if exchange_id == "okx":
|
||||
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://www.okx.com"
|
||||
return OkxClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url)
|
||||
broker_code = "56fa80b0ce8cBCDE"
|
||||
return OkxClient(
|
||||
api_key=api_key,
|
||||
secret_key=secret_key,
|
||||
passphrase=passphrase,
|
||||
base_url=base_url,
|
||||
broker_code=broker_code
|
||||
)
|
||||
if exchange_id == "bitget":
|
||||
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bitget.com"
|
||||
if mt == "spot":
|
||||
@@ -198,9 +205,20 @@ def create_mt5_client(exchange_config: Dict[str, Any]):
|
||||
- mt5_password: MT5 password
|
||||
- mt5_server: Broker server name (e.g., "ICMarkets-Demo")
|
||||
- mt5_terminal_path: Optional path to terminal64.exe
|
||||
- market_category: Must be "Forex" (validated)
|
||||
|
||||
Note: MT5 is ONLY for Forex trading, not for Crypto or Stocks.
|
||||
"""
|
||||
global MT5Client, MT5Config
|
||||
|
||||
# Validate market category - MT5 is ONLY for Forex
|
||||
market_category = str(exchange_config.get("market_category") or "").strip()
|
||||
if market_category and market_category != "Forex":
|
||||
raise LiveTradingError(
|
||||
f"MT5 can only be used for Forex trading, but market_category is '{market_category}'. "
|
||||
f"MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
|
||||
)
|
||||
|
||||
# Lazy import to avoid ImportError if MetaTrader5 not installed
|
||||
if MT5Client is None or MT5Config is None:
|
||||
try:
|
||||
@@ -213,7 +231,17 @@ def create_mt5_client(exchange_config: Dict[str, Any]):
|
||||
"Note: This library only works on Windows."
|
||||
)
|
||||
|
||||
login = int(exchange_config.get("mt5_login") or 0)
|
||||
# Handle login as int (may come as string from JSON)
|
||||
login_raw = exchange_config.get("mt5_login") or 0
|
||||
try:
|
||||
login = int(login_raw) if login_raw else 0
|
||||
except (ValueError, TypeError):
|
||||
# Try converting string to int
|
||||
try:
|
||||
login = int(str(login_raw).strip())
|
||||
except (ValueError, TypeError):
|
||||
login = 0
|
||||
|
||||
password = str(exchange_config.get("mt5_password") or "").strip()
|
||||
server = str(exchange_config.get("mt5_server") or "").strip()
|
||||
terminal_path = str(exchange_config.get("mt5_terminal_path") or "").strip()
|
||||
|
||||
@@ -20,6 +20,8 @@ from app.services.live_trading.symbols import to_okx_swap_inst_id, to_okx_spot_i
|
||||
|
||||
|
||||
class OkxClient(BaseRestClient):
|
||||
_DEFAULT_BROKER_CODE = "56fa80b0ce8cBCDE"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -28,11 +30,14 @@ class OkxClient(BaseRestClient):
|
||||
passphrase: str,
|
||||
base_url: str = "https://www.okx.com",
|
||||
timeout_sec: float = 15.0,
|
||||
broker_code: Optional[str] = None,
|
||||
):
|
||||
super().__init__(base_url=base_url, timeout_sec=timeout_sec)
|
||||
self.api_key = (api_key or "").strip()
|
||||
self.secret_key = (secret_key or "").strip()
|
||||
self.passphrase = (passphrase or "").strip()
|
||||
effective_broker = broker_code or self._DEFAULT_BROKER_CODE
|
||||
self.broker_code = str(effective_broker).strip() if effective_broker else None
|
||||
if not self.api_key or not self.secret_key or not self.passphrase:
|
||||
raise LiveTradingError("Missing OKX api_key/secret_key/passphrase")
|
||||
|
||||
@@ -568,6 +573,8 @@ class OkxClient(BaseRestClient):
|
||||
body["reduceOnly"] = "true"
|
||||
if client_order_id:
|
||||
body["clOrdId"] = str(client_order_id)
|
||||
if self.broker_code:
|
||||
body["tag"] = str(self.broker_code)
|
||||
|
||||
raw = self._signed_request("POST", "/api/v5/trade/order", json_body=body)
|
||||
data = (raw.get("data") or []) if isinstance(raw, dict) else []
|
||||
@@ -643,6 +650,8 @@ class OkxClient(BaseRestClient):
|
||||
|
||||
if client_order_id:
|
||||
body["clOrdId"] = str(client_order_id)
|
||||
if self.broker_code:
|
||||
body["tag"] = str(self.broker_code)
|
||||
|
||||
raw = self._signed_request("POST", "/api/v5/trade/order", json_body=body)
|
||||
data = (raw.get("data") or []) if isinstance(raw, dict) else []
|
||||
|
||||
@@ -183,6 +183,30 @@ class LLMService:
|
||||
data["response_format"] = {"type": "json_object"}
|
||||
|
||||
response = requests.post(url, headers=headers, json=data, timeout=timeout)
|
||||
|
||||
# Handle errors with detailed messages
|
||||
if response.status_code == 403:
|
||||
error_msg = "OpenRouter API 403 Forbidden"
|
||||
try:
|
||||
error_data = response.json()
|
||||
if "error" in error_data:
|
||||
error_detail = error_data["error"]
|
||||
if isinstance(error_detail, dict):
|
||||
error_msg = f"OpenRouter API 403: {error_detail.get('message', 'Forbidden')}"
|
||||
elif isinstance(error_detail, str):
|
||||
error_msg = f"OpenRouter API 403: {error_detail}"
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check if API key is configured
|
||||
from app.config.api_keys import APIKeys
|
||||
if not APIKeys.OPENROUTER_API_KEY:
|
||||
error_msg += ". OPENROUTER_API_KEY 未配置,请在 backend_api_python/.env 中设置"
|
||||
else:
|
||||
error_msg += ". 可能的原因:1) API 密钥无效或过期 2) 账户余额不足 3) 没有权限访问该模型。请检查 https://openrouter.ai/keys"
|
||||
|
||||
raise ValueError(error_msg)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
|
||||
@@ -211,6 +211,23 @@ class MT5Client:
|
||||
message=f"Symbol not found: {symbol}"
|
||||
)
|
||||
|
||||
# Validate volume against symbol constraints
|
||||
volume_float = float(volume)
|
||||
if volume_float < symbol_info.volume_min:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Volume {volume_float} is less than minimum {symbol_info.volume_min}"
|
||||
)
|
||||
if volume_float > symbol_info.volume_max:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Volume {volume_float} exceeds maximum {symbol_info.volume_max}"
|
||||
)
|
||||
# Round volume to lot step
|
||||
volume_step = symbol_info.volume_step
|
||||
if volume_step > 0:
|
||||
volume_float = round(volume_float / volume_step) * volume_step
|
||||
|
||||
if not symbol_info.visible:
|
||||
# Enable symbol in Market Watch
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
@@ -235,18 +252,28 @@ class MT5Client:
|
||||
order_type = mt5.ORDER_TYPE_SELL
|
||||
price = tick.bid
|
||||
|
||||
# Determine filling mode based on symbol properties
|
||||
# Different brokers support different filling modes
|
||||
filling_mode = mt5.ORDER_FILLING_IOC # Default
|
||||
if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
|
||||
filling_mode = mt5.ORDER_FILLING_IOC
|
||||
elif symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
|
||||
filling_mode = mt5.ORDER_FILLING_FOK
|
||||
elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
|
||||
filling_mode = mt5.ORDER_FILLING_RETURN
|
||||
|
||||
# Prepare order request
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": float(volume),
|
||||
"volume": volume_float, # Use validated and rounded volume
|
||||
"type": order_type,
|
||||
"price": price,
|
||||
"deviation": deviation,
|
||||
"magic": self.config.magic_number,
|
||||
"comment": comment,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": mt5.ORDER_FILLING_IOC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
# Send order
|
||||
@@ -419,6 +446,14 @@ class MT5Client:
|
||||
pos = position[0]
|
||||
symbol = pos.symbol
|
||||
|
||||
# Get symbol info for filling mode
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Symbol not found: {symbol}"
|
||||
)
|
||||
|
||||
# Get tick
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
if tick is None:
|
||||
@@ -437,6 +472,15 @@ class MT5Client:
|
||||
|
||||
close_volume = volume if volume else pos.volume
|
||||
|
||||
# Determine filling mode based on symbol properties
|
||||
filling_mode = mt5.ORDER_FILLING_IOC # Default
|
||||
if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
|
||||
filling_mode = mt5.ORDER_FILLING_IOC
|
||||
elif symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
|
||||
filling_mode = mt5.ORDER_FILLING_FOK
|
||||
elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
|
||||
filling_mode = mt5.ORDER_FILLING_RETURN
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
@@ -448,7 +492,7 @@ class MT5Client:
|
||||
"magic": self.config.magic_number,
|
||||
"comment": comment,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": mt5.ORDER_FILLING_IOC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
result = mt5.order_send(request)
|
||||
|
||||
@@ -2160,9 +2160,22 @@ class PendingOrderWorker:
|
||||
return
|
||||
|
||||
try:
|
||||
# Ensure client is connected before placing order
|
||||
if not client.connected:
|
||||
logger.warning(f"MT5 client not connected, attempting reconnect: strategy_id={strategy_id}, pending_id={order_id}")
|
||||
if not client.connect():
|
||||
self._mark_failed(order_id=order_id, error="mt5_connection_failed")
|
||||
_console_print(f"[worker] MT5 connection failed: strategy_id={strategy_id} pending_id={order_id}")
|
||||
_notify_live_best_effort(status="failed", error="mt5_connection_failed")
|
||||
return
|
||||
|
||||
# Normalize symbol before placing order (MT5 requires specific format)
|
||||
from app.services.mt5_trading.symbols import normalize_symbol
|
||||
normalized_symbol = normalize_symbol(symbol)
|
||||
|
||||
# Place market order via MT5
|
||||
result = client.place_market_order(
|
||||
symbol=symbol,
|
||||
symbol=normalized_symbol,
|
||||
side=action,
|
||||
volume=amount,
|
||||
comment="QuantDinger",
|
||||
|
||||
@@ -139,7 +139,22 @@ class PolymarketBatchAnalyzer:
|
||||
return analyzed_markets
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Batch analysis failed: {e}", exc_info=True)
|
||||
error_msg = str(e)
|
||||
# Provide more helpful error messages for common API errors
|
||||
if "403" in error_msg or "Forbidden" in error_msg:
|
||||
logger.error(
|
||||
f"Batch analysis failed: OpenRouter API 403 Forbidden. "
|
||||
f"请检查:1) OPENROUTER_API_KEY 是否正确配置 2) API 密钥是否有效 3) 账户余额是否充足。"
|
||||
f"错误详情: {error_msg}"
|
||||
)
|
||||
elif "401" in error_msg or "Unauthorized" in error_msg:
|
||||
logger.error(
|
||||
f"Batch analysis failed: OpenRouter API 401 Unauthorized. "
|
||||
f"OPENROUTER_API_KEY 无效或已过期。请检查 backend_api_python/.env 中的配置。"
|
||||
f"错误详情: {error_msg}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Batch analysis failed: {error_msg}", exc_info=True)
|
||||
return self._fallback_analysis(markets, max_opportunities)
|
||||
|
||||
def _build_markets_summary(self, markets: List[Dict]) -> str:
|
||||
|
||||
@@ -291,6 +291,84 @@ class StrategyService:
|
||||
if not exchange_id:
|
||||
return {'success': False, 'message': 'Missing exchange_id', 'data': None}
|
||||
|
||||
# Handle MT5 (Forex) connection test
|
||||
if exchange_id == 'mt5':
|
||||
# Validate that MT5 is only used for Forex market
|
||||
market_category = str(resolved.get("market_category") or exchange_config.get("market_category") or "").strip()
|
||||
if market_category and market_category != "Forex":
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'MT5 can only be used for Forex trading, but market_category is {market_category}. Please use MT5 only with Forex market.',
|
||||
'data': {'exchange': safe_cfg}
|
||||
}
|
||||
|
||||
try:
|
||||
from app.services.live_trading.factory import create_mt5_client
|
||||
mt5_client = create_mt5_client(resolved)
|
||||
if mt5_client and mt5_client.connected:
|
||||
# Get account info if available
|
||||
account_info = None
|
||||
try:
|
||||
account_info = mt5_client.get_account_info()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'MT5 connection successful',
|
||||
'data': {
|
||||
'exchange': safe_cfg,
|
||||
'account': account_info
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Failed to connect to MT5. Please check credentials and ensure terminal is running.',
|
||||
'data': {'exchange': safe_cfg}
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'MT5 connection failed: {error_msg}',
|
||||
'data': {'exchange': safe_cfg}
|
||||
}
|
||||
|
||||
# Handle IBKR (US Stocks) connection test
|
||||
if exchange_id == 'ibkr':
|
||||
try:
|
||||
from app.services.live_trading.factory import create_ibkr_client
|
||||
ibkr_client = create_ibkr_client(resolved)
|
||||
# create_ibkr_client already connects, so if it returns, connection is successful
|
||||
if ibkr_client and ibkr_client.connected():
|
||||
# Get account summary if available
|
||||
account_summary = None
|
||||
try:
|
||||
account_summary = ibkr_client.get_account_summary()
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'IBKR connection successful',
|
||||
'data': {
|
||||
'exchange': safe_cfg,
|
||||
'account': account_summary
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Failed to connect to IBKR. Please check TWS/Gateway is running and credentials are correct.',
|
||||
'data': {'exchange': safe_cfg}
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'IBKR connection failed: {error_msg}',
|
||||
'data': {'exchange': safe_cfg}
|
||||
}
|
||||
|
||||
# IMPORTANT:
|
||||
# Test connection should respect configured market_type (spot vs swap).
|
||||
# Otherwise Binance will default to futures endpoints (fapi) and spot-only keys will fail with -2015.
|
||||
@@ -549,6 +627,14 @@ class StrategyService:
|
||||
trading_config = payload.get('trading_config') or {}
|
||||
exchange_config = payload.get('exchange_config') or {}
|
||||
|
||||
# Validate MT5 can only be used for Forex trading
|
||||
exchange_id = (exchange_config.get('exchange_id') or '').strip().lower() if isinstance(exchange_config, dict) else ''
|
||||
if exchange_id == 'mt5' and market_category != 'Forex':
|
||||
raise ValueError(
|
||||
f"MT5 can only be used for Forex trading, but market_category is '{market_category}'. "
|
||||
f"MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
|
||||
)
|
||||
|
||||
# When credential_id is present, strip raw API keys to avoid
|
||||
# storing secrets in the strategy record — they live in qd_exchange_credentials.
|
||||
if isinstance(exchange_config, dict) and exchange_config.get('credential_id'):
|
||||
@@ -643,6 +729,16 @@ class StrategyService:
|
||||
if not base_name:
|
||||
raise ValueError("strategy_name is required")
|
||||
|
||||
# Validate MT5 can only be used for Forex trading
|
||||
market_category = payload.get('market_category') or 'Crypto'
|
||||
exchange_config = payload.get('exchange_config') or {}
|
||||
exchange_id = (exchange_config.get('exchange_id') or '').strip().lower() if isinstance(exchange_config, dict) else ''
|
||||
if exchange_id == 'mt5' and market_category != 'Forex':
|
||||
raise ValueError(
|
||||
f"MT5 can only be used for Forex trading, but market_category is '{market_category}'. "
|
||||
f"MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
|
||||
)
|
||||
|
||||
# Generate strategy group ID
|
||||
strategy_group_id = str(uuid.uuid4())[:8]
|
||||
|
||||
|
||||
@@ -283,16 +283,13 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
|
||||
if isinstance(node.args[1], ast.Constant) and node.args[1].value in dangerous_functions:
|
||||
return False, f"检测到通过 getattr 绕过限制: getattr({node.args[0].id}, '{node.args[1].value}')"
|
||||
|
||||
# 检查导入语句
|
||||
# 检查导入语句:用户脚本中一律禁止使用 import(统一由平台注入安全依赖)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name in dangerous_modules:
|
||||
return False, f"检测到危险模块导入: {alias.name}"
|
||||
return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象"
|
||||
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
if node.module and node.module.split('.')[0] in dangerous_modules:
|
||||
return False, f"检测到危险模块导入: {node.module}"
|
||||
return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象"
|
||||
|
||||
# 检查是否有访问 __builtins__ 的尝试
|
||||
for node in ast.walk(tree):
|
||||
|
||||
Reference in New Issue
Block a user