1
This commit is contained in:
parent
e2c37e6d62
commit
4ea1c53813
|
|
@ -752,6 +752,7 @@ async def fetch_realtime_positions(account_id: int):
|
|||
entry_order_id = None
|
||||
entry_order_type = None
|
||||
id = None
|
||||
update_time_ms = pos.get('updateTime') # 币安持仓最后更新时间(ms),无 DB 时可用于展示
|
||||
try:
|
||||
from database.models import Trade
|
||||
db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open', account_id=account_id)
|
||||
|
|
@ -786,6 +787,14 @@ async def fetch_realtime_positions(account_id: int):
|
|||
info = await client.client.futures_get_order(symbol=pos.get('symbol'), orderId=int(entry_order_id))
|
||||
if isinstance(info, dict):
|
||||
entry_order_type = info.get("type")
|
||||
# 用开仓订单的下单时间补全开仓时间(当 DB 未提供时)
|
||||
order_time_ms = info.get("time")
|
||||
if (entry_time is None or created_at is None) and order_time_ms:
|
||||
order_time_sec = int(order_time_ms) // 1000
|
||||
if entry_time is None:
|
||||
entry_time = order_time_sec
|
||||
if created_at is None:
|
||||
created_at = order_time_sec
|
||||
except Exception:
|
||||
entry_order_type = None
|
||||
|
||||
|
|
@ -838,6 +847,7 @@ async def fetch_realtime_positions(account_id: int):
|
|||
"leverage": int(pos.get('leverage', 1)),
|
||||
"entry_time": entry_time,
|
||||
"created_at": created_at,
|
||||
"update_time": int(update_time_ms) // 1000 if update_time_ms else None,
|
||||
"stop_loss_price": stop_loss_price,
|
||||
"take_profit_price": take_profit_price,
|
||||
"take_profit_1": take_profit_1,
|
||||
|
|
|
|||
|
|
@ -304,13 +304,20 @@ const StatsDashboard = () => {
|
|||
const exportData = openTrades.map(trade => {
|
||||
const slOrders = trade.open_orders?.filter(o => o.type.includes('STOP')) || []
|
||||
const tpOrders = trade.open_orders?.filter(o => o.type.includes('TAKE_PROFIT')) || []
|
||||
|
||||
const formatTs = (ts) => {
|
||||
if (ts == null) return null
|
||||
try {
|
||||
const ms = typeof ts === 'number' ? (ts < 1e12 ? ts * 1000 : ts) : Date.parse(ts)
|
||||
return isNaN(ms) ? null : new Date(ms).toISOString().replace('T', ' ').slice(0, 19)
|
||||
} catch { return null }
|
||||
}
|
||||
return {
|
||||
...trade,
|
||||
entry_time_display: formatTs(trade.entry_time) || (trade.update_time ? `(仅最后更新 ${formatTs(trade.update_time)})` : null),
|
||||
update_time_display: formatTs(trade.update_time),
|
||||
// 添加易读的汇总字段
|
||||
active_sl_orders: slOrders.map(o => `${o.type} @ ${o.stopPrice} (${o.status})`).join('; '),
|
||||
active_tp_orders: tpOrders.map(o => `${o.type} @ ${o.stopPrice || o.price} (${o.status})`).join('; '),
|
||||
// 确保原始数据也在
|
||||
binance_open_orders_raw: trade.open_orders || []
|
||||
}
|
||||
})
|
||||
|
|
@ -739,7 +746,7 @@ const StatsDashboard = () => {
|
|||
</div>
|
||||
<div className="trade-info">
|
||||
<div className="position-time-row">
|
||||
开仓时间: {(trade.entry_time || trade.created_at) ? formatEntryTime(trade.entry_time || trade.created_at) : '—'}
|
||||
开仓时间: {(trade.entry_time || trade.created_at) ? formatEntryTime(trade.entry_time || trade.created_at) : (trade.update_time ? `最后更新 ${formatEntryTime(trade.update_time)}(非开仓时间)` : '—')}
|
||||
{' · '}
|
||||
创建时间: {(trade.created_at != null && trade.created_at !== '') ? formatEntryTime(trade.created_at) : '—'}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -932,36 +932,50 @@ class BinanceClient:
|
|||
}
|
||||
self._price_cache.pop(symbol, None)
|
||||
logger.debug(f"{symbol} 未在缓存中,使用REST API获取")
|
||||
try:
|
||||
ticker = await self._rate_limited_request(
|
||||
f'ticker_{symbol}',
|
||||
self.client.futures_symbol_ticker(symbol=symbol)
|
||||
)
|
||||
stats = await self._rate_limited_request(
|
||||
f'stats_{symbol}',
|
||||
self.client.futures_ticker(symbol=symbol)
|
||||
)
|
||||
result = {
|
||||
'symbol': symbol,
|
||||
'price': float(ticker['price']),
|
||||
'volume': float(stats.get('quoteVolume', 0)),
|
||||
'changePercent': float(stats.get('priceChangePercent', 0))
|
||||
}
|
||||
# 只写 Redis;仅当 Redis 写入失败时才写进程内存(降级)
|
||||
wrote_redis = await self.redis_cache.set(cache_key, result, ttl=30)
|
||||
if not wrote_redis:
|
||||
if len(self._price_cache) >= self._price_cache_max_size:
|
||||
oldest_key = min(self._price_cache.keys(), key=lambda k: self._price_cache[k].get('timestamp', 0))
|
||||
self._price_cache.pop(oldest_key, None)
|
||||
self._price_cache[symbol] = {**result, 'timestamp': time.time()}
|
||||
return result
|
||||
except BinanceAPIException as e:
|
||||
error_code = e.code if hasattr(e, 'code') else None
|
||||
if error_code == -1003:
|
||||
logger.warning(f"获取 {symbol} 24小时行情失败: API请求频率过高,建议使用WebSocket或增加扫描间隔")
|
||||
else:
|
||||
logger.error(f"获取 {symbol} 24小时行情失败: {e}")
|
||||
last_err = None
|
||||
for attempt in range(2):
|
||||
try:
|
||||
ticker = await self._rate_limited_request(
|
||||
f'ticker_{symbol}',
|
||||
self.client.futures_symbol_ticker(symbol=symbol)
|
||||
)
|
||||
stats = await self._rate_limited_request(
|
||||
f'stats_{symbol}',
|
||||
self.client.futures_ticker(symbol=symbol)
|
||||
)
|
||||
result = {
|
||||
'symbol': symbol,
|
||||
'price': float(ticker['price']),
|
||||
'volume': float(stats.get('quoteVolume', 0)),
|
||||
'changePercent': float(stats.get('priceChangePercent', 0))
|
||||
}
|
||||
# 只写 Redis;仅当 Redis 写入失败时才写进程内存(降级)
|
||||
wrote_redis = await self.redis_cache.set(cache_key, result, ttl=30)
|
||||
if not wrote_redis:
|
||||
if len(self._price_cache) >= self._price_cache_max_size:
|
||||
oldest_key = min(self._price_cache.keys(), key=lambda k: self._price_cache[k].get('timestamp', 0))
|
||||
self._price_cache.pop(oldest_key, None)
|
||||
self._price_cache[symbol] = {**result, 'timestamp': time.time()}
|
||||
return result
|
||||
except (TimeoutError, asyncio.TimeoutError) as e:
|
||||
last_err = e
|
||||
if attempt == 0:
|
||||
logger.warning(f"获取 {symbol} 24小时行情超时,2秒后重试: {e}")
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
logger.warning(f"获取 {symbol} 24小时行情再次超时: {e},调用方将使用入场价等兜底")
|
||||
return None
|
||||
except BinanceAPIException as e:
|
||||
error_code = e.code if hasattr(e, 'code') else None
|
||||
if error_code == -1003:
|
||||
logger.warning(f"获取 {symbol} 24小时行情失败: API请求频率过高,建议使用WebSocket或增加扫描间隔")
|
||||
else:
|
||||
logger.error(f"获取 {symbol} 24小时行情失败: {e}")
|
||||
return None
|
||||
if last_err:
|
||||
logger.warning(f"获取 {symbol} 24小时行情失败: {last_err}")
|
||||
return None
|
||||
return None
|
||||
|
||||
async def get_all_tickers_24h(self) -> Dict[str, Dict]:
|
||||
"""
|
||||
|
|
@ -1247,10 +1261,14 @@ class BinanceClient:
|
|||
'error_msg': error_msg,
|
||||
}
|
||||
|
||||
async def get_open_positions(self) -> List[Dict]:
|
||||
async def get_open_positions(self, include_low_notional: bool = False) -> List[Dict]:
|
||||
"""
|
||||
获取当前持仓
|
||||
|
||||
|
||||
Args:
|
||||
include_low_notional: 若为 True,则不过滤名义价值低于 POSITION_MIN_NOTIONAL_USDT 的仓位,
|
||||
返回所有非零持仓;用于 MAX_OPEN_POSITIONS 等需按“实际持仓数”校验的场景。默认 False 保持原行为。
|
||||
|
||||
Returns:
|
||||
持仓列表
|
||||
"""
|
||||
|
|
@ -1266,8 +1284,8 @@ class BinanceClient:
|
|||
self.client.futures_position_information(recvWindow=20000),
|
||||
timeout=read_timeout
|
||||
)
|
||||
# 只保留非零持仓,且名义价值 >= 配置阈值,避免灰尘持仓被当成“有仓”;与仪表板不一致时可调低 POSITION_MIN_NOTIONAL_USDT 或设为 0
|
||||
min_notional = getattr(config, 'POSITION_MIN_NOTIONAL_USDT', 1.0)
|
||||
# 只保留非零持仓;名义价值过滤:用于展示/风控时默认过滤灰尘仓,include_low_notional=True 时不过滤以准确计数
|
||||
min_notional = getattr(config, 'POSITION_MIN_NOTIONAL_USDT', 1.0) if not include_low_notional else 0
|
||||
open_positions = []
|
||||
skipped_low = []
|
||||
for pos in positions:
|
||||
|
|
@ -1285,7 +1303,8 @@ class BinanceClient:
|
|||
'entryPrice': entry_price,
|
||||
'markPrice': float(pos.get('markPrice', 0)),
|
||||
'unRealizedProfit': float(pos['unRealizedProfit']),
|
||||
'leverage': int(pos['leverage'])
|
||||
'leverage': int(pos['leverage']),
|
||||
'updateTime': pos.get('updateTime'), # 币安最后更新时间(ms),用于展示/兜底开仓时间
|
||||
})
|
||||
if skipped_low and logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -327,6 +327,17 @@ class PositionManager:
|
|||
# 判断是否应该交易
|
||||
if not await self.risk_manager.should_trade(symbol, change_percent):
|
||||
return None
|
||||
|
||||
# 开仓前二次校验持仓数(同一轮扫描多 symbol 通过 should_trade 后串行开仓,避免超限)
|
||||
try:
|
||||
max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0)
|
||||
if max_open > 0:
|
||||
positions = await self.client.get_open_positions(include_low_notional=True)
|
||||
if positions and len(positions) >= max_open:
|
||||
logger.info(f"{symbol} [入场] 开仓前复核:持仓数已达上限 {len(positions)}/{max_open},跳过")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"{symbol} 开仓前持仓数复核异常(继续): {e}")
|
||||
|
||||
# 设置杠杆(确保为 int,避免动态杠杆传入 float 导致 API/range 报错)
|
||||
actual_leverage = await self.client.set_leverage(symbol, int(leverage))
|
||||
|
|
|
|||
|
|
@ -876,16 +876,16 @@ class RiskManager:
|
|||
logger.debug(f"{symbol} 涨跌幅 {change_percent:.2f}% 小于阈值")
|
||||
return False
|
||||
|
||||
# 检查是否已有持仓 / 总持仓数量限制(优先 WS 缓存)
|
||||
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
|
||||
if positions is None:
|
||||
positions = await self.client.get_open_positions()
|
||||
# 检查是否已有持仓 / 总持仓数量限制(用 REST 全量持仓数,确保 MAX_OPEN_POSITIONS 按实际持仓数校验,不因 min_notional 过滤漏计)
|
||||
positions = await self.client.get_open_positions(include_low_notional=True)
|
||||
if not positions:
|
||||
positions = []
|
||||
try:
|
||||
max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0)
|
||||
except Exception:
|
||||
max_open = 0
|
||||
if max_open > 0 and len(positions) >= max_open:
|
||||
logger.debug(f"{symbol} 持仓数量已达上限:{len(positions)}/{max_open},跳过开仓")
|
||||
logger.info(f"{symbol} 持仓数量已达上限:{len(positions)}/{max_open},跳过开仓")
|
||||
return False
|
||||
|
||||
existing_position = next(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user