This commit is contained in:
薇薇安 2026-02-08 20:21:21 +08:00
parent bfae183e39
commit cc324eead5
3 changed files with 109 additions and 1 deletions

View File

@ -3,6 +3,7 @@
""" """
from fastapi import APIRouter, HTTPException, Header, Depends from fastapi import APIRouter, HTTPException, Header, Depends
from fastapi import Query from fastapi import Query
import asyncio
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
@ -640,6 +641,39 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
logger.info(f"获取到 {len(positions)} 个持仓") logger.info(f"获取到 {len(positions)} 个持仓")
# 并发获取所有持仓的挂单信息
open_orders_map = {}
try:
position_symbols = [p.get('symbol') for p in positions if float(p.get('positionAmt', 0)) != 0]
if position_symbols:
logger.info(f"正在获取挂单信息: {position_symbols}")
tasks = [client.get_open_orders(sym) for sym in position_symbols]
results = await asyncio.gather(*tasks, return_exceptions=True)
for sym, orders in zip(position_symbols, results):
if isinstance(orders, list):
# 过滤出止盈止损单
conditional_orders = []
for o in orders:
o_type = o.get('type')
if o_type in ['STOP_MARKET', 'TAKE_PROFIT_MARKET', 'STOP', 'TAKE_PROFIT']:
conditional_orders.append({
'orderId': o.get('orderId'),
'type': o_type,
'side': o.get('side'),
'stopPrice': float(o.get('stopPrice', 0)),
'price': float(o.get('price', 0)),
'origType': o.get('origType'),
'reduceOnly': o.get('reduceOnly'),
'status': o.get('status')
})
if conditional_orders:
open_orders_map[sym] = conditional_orders
else:
logger.warning(f"获取 {sym} 挂单失败: {orders}")
except Exception as e:
logger.error(f"批量获取挂单失败: {e}")
# 格式化持仓数据 # 格式化持仓数据
formatted_positions = [] formatted_positions = []
for pos in positions: for pos in positions:
@ -754,6 +788,7 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
"atr": atr_value, "atr": atr_value,
"entry_order_id": entry_order_id, "entry_order_id": entry_order_id,
"entry_order_type": entry_order_type, # LIMIT / MARKET用于仪表板展示“限价/市价”) "entry_order_type": entry_order_type, # LIMIT / MARKET用于仪表板展示“限价/市价”)
"open_orders": open_orders_map.get(pos.get('symbol'), []), # 实时挂单信息
}) })
logger.info(f"格式化后 {len(formatted_positions)} 个有效持仓") logger.info(f"格式化后 {len(formatted_positions)} 个有效持仓")

View File

@ -282,7 +282,23 @@ const StatsDashboard = () => {
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-') const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-')
const filename = `持仓记录_${timestamp}.json` const filename = `持仓记录_${timestamp}.json`
const dataStr = JSON.stringify(openTrades, null, 2)
//
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')) || []
return {
...trade,
//
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 || []
}
})
const dataStr = JSON.stringify(exportData, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' }) const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob) const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a') const link = document.createElement('a')
@ -736,6 +752,25 @@ const StatsDashboard = () => {
</div> </div>
)} )}
</div> </div>
{/* 显示币安实际挂单信息 */}
{trade.open_orders && trade.open_orders.length > 0 && (
<div className="open-orders-info" style={{ marginTop: '8px', borderTop: '1px dashed #eee', paddingTop: '6px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', fontWeight: 'bold' }}>条件单 (Binance):</div>
{trade.open_orders.map(order => (
<div key={order.orderId} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', marginBottom: '4px', paddingLeft: '8px' }}>
<span className={order.side === 'BUY' ? 'positive' : 'negative'} style={{ fontWeight: '500' }}>
{order.type.replace('_MARKET', '')}
</span>
<span style={{ fontFamily: 'monospace' }}>
{order.type.includes('STOP') ? `触发价: ${fmtPrice(order.stopPrice)}` : `价格: ${fmtPrice(order.price)}`}
</span>
<span style={{ color: '#999', fontSize: '11px' }}>{order.status}</span>
</div>
))}
</div>
)}
<div className="trade-actions"> <div className="trade-actions">
<div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}> <div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}>
{parseFloat(trade.pnl || 0).toFixed(2)} USDT {parseFloat(trade.pnl || 0).toFixed(2)} USDT

View File

@ -2635,6 +2635,44 @@ class PositionManager:
import traceback import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}") logger.error(f" 错误详情:\n{traceback.format_exc()}")
# 6. 同步挂单信息 (STOP_MARKET / TAKE_PROFIT_MARKET)
if self.active_positions:
logger.debug("正在同步持仓挂单信息...")
try:
tasks = []
symbols = list(self.active_positions.keys())
for symbol in symbols:
tasks.append(self.client.get_open_orders(symbol))
results = await asyncio.gather(*tasks, return_exceptions=True)
for symbol, orders in zip(symbols, results):
if isinstance(orders, list):
# Filter for relevant orders (SL/TP)
conditional_orders = []
for o in orders:
o_type = o.get('type')
# 关注止盈止损单
if o_type in ['STOP_MARKET', 'TAKE_PROFIT_MARKET', 'STOP', 'TAKE_PROFIT']:
conditional_orders.append({
'orderId': o.get('orderId'),
'type': o_type,
'side': o.get('side'),
'stopPrice': float(o.get('stopPrice', 0)),
'price': float(o.get('price', 0)),
'origType': o.get('origType'),
'reduceOnly': o.get('reduceOnly'),
'status': o.get('status')
})
if symbol in self.active_positions:
self.active_positions[symbol]['openOrders'] = conditional_orders
logger.debug(f"{symbol} 同步挂单: {len(conditional_orders)}")
else:
logger.warning(f"{symbol} 获取挂单失败: {orders}")
except Exception as e:
logger.error(f"同步挂单信息失败: {e}")
logger.info("持仓状态同步完成") logger.info("持仓状态同步完成")
except Exception as e: except Exception as e: