diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 6f1e4b6..c2345b6 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -103,6 +103,9 @@ class BinanceClient: self._price_cache: Dict[str, Dict] = {} # WebSocket价格缓存 {symbol: {price, volume, changePercent, timestamp}} self._price_cache_ttl = 60 # 价格缓存有效期(秒) + # 显示名 -> API symbol 映射(当交易所返回中文/非 ASCII 的 symbol 时,用 baseAsset+quoteAsset 作为下单用 symbol) + self._display_to_api_symbol: Dict[str, str] = {} + # 持仓模式缓存(币安 U本位合约):dualSidePosition=True => 对冲模式;False => 单向模式 self._dual_side_position: Optional[bool] = None self._position_mode_checked_at: float = 0.0 @@ -359,6 +362,15 @@ class BinanceClient: if self.client: await self.client.close_connection() logger.info("币安客户端已断开连接") + + def _resolve_api_symbol(self, symbol: str) -> str: + """ + 将显示名(含中文等非 ASCII)解析为交易所 API 使用的英文 symbol。 + 若未建立映射或已是 ASCII,则原样返回。 + """ + if not symbol: + return symbol + return self._display_to_api_symbol.get(symbol, symbol) async def get_all_usdt_pairs(self, max_retries: int = 3, timeout: int = 30) -> List[str]: """ @@ -384,13 +396,25 @@ class BinanceClient: timeout=timeout ) - usdt_pairs = [ - symbol['symbol'] - for symbol in exchange_info['symbols'] - if symbol['symbol'].endswith('USDT') - and symbol['status'] == 'TRADING' - and symbol.get('contractType') == 'PERPETUAL' # U本位永续合约 - ] + # 支持中文名交易对:若 symbol 含非 ASCII,用 baseAsset+quoteAsset 作为 API 下单用 symbol,并建立映射 + self._display_to_api_symbol.clear() + usdt_pairs = [] + for s in exchange_info['symbols']: + if not (s['symbol'].endswith('USDT') and s['status'] == 'TRADING' and s.get('contractType') == 'PERPETUAL'): + continue + sym = s['symbol'] + if sym.isascii(): + usdt_pairs.append(sym) + continue + api_sym = (s.get('baseAsset') or '') + (s.get('quoteAsset') or '') + if api_sym and api_sym.isascii(): + usdt_pairs.append(api_sym) + self._display_to_api_symbol[sym] = api_sym + logger.debug(f"交易对显示名 -> API symbol: '{sym}' -> '{api_sym}'") + else: + logger.warning(f"跳过无法解析为英文 symbol 的交易对: {sym!r} (baseAsset={s.get('baseAsset')!r}, quoteAsset={s.get('quoteAsset')!r})") + if self._display_to_api_symbol: + logger.info(f"已映射 {len(self._display_to_api_symbol)} 个中文/非ASCII交易对到英文 symbol,均可正常下单") logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对") return usdt_pairs @@ -497,15 +521,16 @@ class BinanceClient: """ 获取24小时行情数据(合约市场) 优先从WebSocket缓存读取,其次从Redis缓存读取,最后使用REST API - + Args: - symbol: 交易对 - + symbol: 交易对(支持中文名,会解析为 API symbol) + Returns: 24小时行情数据 """ import time - + symbol = self._resolve_api_symbol(symbol) + # 1. 优先从WebSocket缓存读取 if symbol in self._price_cache: cached = self._price_cache[symbol] @@ -589,10 +614,15 @@ class BinanceClient: result = {} now_ms = int(__import__("time").time() * 1000) for ticker in tickers: - symbol = ticker['symbol'] - if symbol.endswith('USDT'): - result[symbol] = { - 'symbol': symbol, + sym = ticker['symbol'] + if not sym.endswith('USDT'): + continue + # 支持中文名:用映射后的 API symbol 作为 key,便于扫描与下单一致 + api_sym = self._display_to_api_symbol.get(sym, sym) if not sym.isascii() else sym + if not api_sym.isascii(): + continue + result[api_sym] = { + 'symbol': api_sym, 'price': float(ticker.get('lastPrice', 0)), 'volume': float(ticker.get('quoteVolume', 0)), 'changePercent': float(ticker.get('priceChangePercent', 0)), @@ -783,13 +813,14 @@ class BinanceClient: """ 获取交易对的精度和限制信息 优先从 Redis 缓存读取,如果缓存不可用或过期则使用 REST API - + Args: - symbol: 交易对 - + symbol: 交易对(支持中文名,会解析为 API symbol) + Returns: 交易对信息字典,包含 quantityPrecision, minQty, stepSize 等 """ + symbol = self._resolve_api_symbol(symbol) # 1. 先检查内存缓存 if symbol in self._symbol_info_cache: cached_mem = self._symbol_info_cache[symbol] @@ -1150,7 +1181,9 @@ class BinanceClient: Returns: 订单信息 """ - # 0. 校验 symbol 合法性(防止非法字符导致 -1022 签名错误) + # 支持中文名:将显示名解析为 API 使用的英文 symbol + symbol = self._resolve_api_symbol(symbol) + # 校验 symbol 合法性(API 仅接受 ASCII) if not symbol or not symbol.isascii(): logger.error(f"❌ 下单请求包含非法 Symbol: '{symbol}' (包含非ASCII字符)") import traceback @@ -1444,16 +1477,17 @@ class BinanceClient: async def cancel_order(self, symbol: str, order_id: int) -> bool: """ 取消订单 - + Args: symbol: 交易对 order_id: 订单ID - + Returns: 是否成功 """ + symbol = self._resolve_api_symbol(symbol) if not symbol or not symbol.isascii(): - logger.error(f"❌ 设置杠杆请求包含非法 Symbol: '{symbol}'") + logger.error(f"❌ 取消订单请求包含非法 Symbol: '{symbol}'") import traceback logger.error(f" 调用堆栈:\n{traceback.format_exc()}") return False @@ -1799,14 +1833,15 @@ class BinanceClient: """ 设置杠杆倍数 如果设置失败(比如超过交易对支持的最大杠杆),会自动降低杠杆重试 - + Args: symbol: 交易对 leverage: 杠杆倍数 - + Returns: 是否成功 """ + symbol = self._resolve_api_symbol(symbol) if not symbol or not symbol.isascii(): logger.error(f"❌ 设置杠杆请求包含非法 Symbol: '{symbol}'") import traceback diff --git a/trading_system/market_scanner.py b/trading_system/market_scanner.py index 5281912..e760d9c 100644 --- a/trading_system/market_scanner.py +++ b/trading_system/market_scanner.py @@ -52,8 +52,11 @@ class MarketScanner: logger.info("开始扫描市场...") - # 获取所有USDT交易对 - all_symbols = await self.client.get_all_usdt_pairs() + # 获取所有USDT交易对(仅使用纯 ASCII symbol,避免本地化名称导致下单报错) + raw_symbols = await self.client.get_all_usdt_pairs() + all_symbols = [s for s in (raw_symbols or []) if s and s.isascii()] + if raw_symbols and len(all_symbols) < len(raw_symbols): + logger.info(f"已过滤 {len(raw_symbols) - len(all_symbols)} 个非ASCII交易对,保留 {len(all_symbols)} 个") if not all_symbols: logger.warning("未获取到交易对") return [] @@ -131,10 +134,12 @@ class MarketScanner: tasks = [get_symbol_change_with_limit(symbol) for symbol in pre_filtered_symbols] results = await asyncio.gather(*tasks, return_exceptions=True) - # 过滤有效结果 + # 过滤有效结果(排除非 ASCII symbol,防止下单时报错) valid_results = [ - r for r in results - if isinstance(r, dict) and r.get('changePercent') is not None + r for r in results + if isinstance(r, dict) + and r.get('changePercent') is not None + and (r.get('symbol') or '').isascii() ] # ⚠️ 优化2:成交量验证 - 24H Volume低于1000万美金,直接剔除 diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 04fa62d..89034d6 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1952,24 +1952,27 @@ class PositionManager: closed_positions.append(symbol) continue else: - # 如果未部分止盈,但达到止盈目标,直接全部平仓(基于保证金收益比) - # ✅ 已移除时间锁限制,可以立即执行 - take_profit = position_info.get('takeProfit') - if take_profit is not None: - # ⚠️ 关键修复:直接使用配置的 TAKE_PROFIT_PERCENT,而不是从止盈价格反推 - # 因为止盈价格可能使用了ATR(更远),反推会导致阈值过大,难以触发 - take_profit_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10) - # 兼容百分比形式和比例形式 - if take_profit_pct_margin_config > 1: - take_profit_pct_margin_config = take_profit_pct_margin_config / 100.0 - take_profit_pct_margin = take_profit_pct_margin_config * 100 # 转换为百分比 + # 如果未部分止盈,但达到【第二目标】止盈价对应的收益比时,才全部平仓 + # ⚠️ 修复:不再使用 TAKE_PROFIT_PERCENT(10%) 作为全平条件,否则会“刚赚一点就整仓止盈” + # 改为使用 take_profit_2 价格对应的保证金收益%,与第一目标(20%) 取较大者,避免盈利过少 + take_profit_2 = position_info.get('takeProfit2', position_info.get('takeProfit')) + if take_profit_2 is not None and margin and margin > 0: + if position_info['side'] == 'BUY': + take_profit_2_amount = (take_profit_2 - entry_price) * quantity + else: + take_profit_2_amount = (entry_price - take_profit_2) * quantity + take_profit_2_pct_margin = (take_profit_2_amount / margin * 100) if margin > 0 else 0 + # 至少要求达到第一目标对应的收益%(如 20%),避免过早全平 + take_profit_1_pct = (config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.20) or 0.20) + if take_profit_1_pct > 1: + take_profit_1_pct = take_profit_1_pct / 100.0 + min_pct_for_full_tp = max(take_profit_2_pct_margin, take_profit_1_pct * 100) - # 直接比较当前盈亏百分比与止盈目标(基于保证金) - if pnl_percent_margin >= take_profit_pct_margin: + if pnl_percent_margin >= min_pct_for_full_tp: logger.info( - f"{symbol} 触发止盈(基于保证金): " - f"当前盈亏={pnl_percent_margin:.2f}% of margin >= 目标={take_profit_pct_margin:.2f}% of margin | " - f"当前价={current_price:.4f}, 止盈价={take_profit:.4f}" + f"{symbol} 触发止盈(第二目标/保证金): " + f"当前盈亏={pnl_percent_margin:.2f}% of margin >= 目标={min_pct_for_full_tp:.2f}% | " + f"当前价={current_price:.4f}, 第二目标价={take_profit_2:.4f}" ) exit_reason = 'take_profit' # 更新数据库