1
This commit is contained in:
parent
7550b707f4
commit
fcbf702f71
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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万美金,直接剔除
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
# 更新数据库
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user