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