This commit is contained in:
薇薇安 2026-02-13 07:14:12 +08:00
parent 7550b707f4
commit fcbf702f71
3 changed files with 88 additions and 45 deletions

View File

@ -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
@ -360,6 +363,15 @@ class BinanceClient:
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]:
"""
获取所有USDT交易对
@ -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
@ -499,12 +523,13 @@ class BinanceClient:
优先从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:
@ -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)),
@ -785,11 +815,12 @@ 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
@ -1452,8 +1485,9 @@ class BinanceClient:
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
@ -1807,6 +1841,7 @@ class BinanceClient:
Returns:
是否成功
"""
symbol = self._resolve_api_symbol(symbol)
if not symbol or not symbol.isascii():
logger.error(f"❌ 设置杠杆请求包含非法 Symbol: '{symbol}'")
import traceback

View File

@ -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
if isinstance(r, dict)
and r.get('changePercent') is not None
and (r.get('symbol') or '').isascii()
]
# ⚠️ 优化2成交量验证 - 24H Volume低于1000万美金直接剔除

View File

@ -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'
# 更新数据库