mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
feat: add pending orders (SL/TP) display on chart
- Add GetOpenOrders method to Trader interface - Implement for Binance (legacy + Algo), Bybit, Hyperliquid - Add stub implementations for OKX, Bitget, Aster, Lighter - Add /api/open-orders endpoint - Display price lines for SL (red) and TP (green) orders - Refresh open orders every 60 seconds (separate from 5s kline refresh)
This commit is contained in:
@@ -31,6 +31,19 @@ interface OrderMarker {
|
||||
symbol: string
|
||||
}
|
||||
|
||||
// 挂单接口定义 (交易所的止盈止损订单)
|
||||
interface OpenOrder {
|
||||
order_id: string
|
||||
symbol: string
|
||||
side: string // BUY/SELL
|
||||
position_side: string // LONG/SHORT
|
||||
type: string // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET
|
||||
price: number // 限价单价格
|
||||
stop_price: number // 触发价格 (止损/止盈)
|
||||
quantity: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface AdvancedChartProps {
|
||||
symbol: string
|
||||
interval?: string
|
||||
@@ -101,6 +114,7 @@ export function AdvancedChart({
|
||||
const seriesMarkersRef = useRef<any>(null) // Markers primitive for v5
|
||||
const currentMarkersDataRef = useRef<any[]>([]) // 存储当前的标记数据
|
||||
const klineDataRef = useRef<Map<number, { volume: number; quoteVolume: number }>>(new Map()) // 存储 kline 额外数据
|
||||
const priceLinesRef = useRef<any[]>([]) // 存储挂单价格线
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -307,6 +321,26 @@ export function AdvancedChart({
|
||||
}
|
||||
}
|
||||
|
||||
// 获取交易所挂单 (止盈止损订单)
|
||||
const fetchOpenOrders = async (traderID: string, symbol: string): Promise<OpenOrder[]> => {
|
||||
try {
|
||||
console.log('[AdvancedChart] Fetching open orders for trader:', traderID, 'symbol:', symbol)
|
||||
const result = await httpClient.get(`/api/open-orders?trader_id=${traderID}&symbol=${symbol}`)
|
||||
|
||||
console.log('[AdvancedChart] Open orders API response:', result)
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
console.warn('[AdvancedChart] No open orders found')
|
||||
return []
|
||||
}
|
||||
|
||||
return result.data as OpenOrder[]
|
||||
} catch (err) {
|
||||
console.error('[AdvancedChart] Error fetching open orders:', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current) return
|
||||
@@ -706,6 +740,87 @@ export function AdvancedChart({
|
||||
return () => clearInterval(refreshInterval)
|
||||
}, [symbol, interval, traderID, exchange])
|
||||
|
||||
// 单独刷新挂单价格线 (60秒刷新一次,避免频繁调用交易所API)
|
||||
useEffect(() => {
|
||||
if (!traderID || !candlestickSeriesRef.current) return
|
||||
|
||||
// 加载挂单并显示价格线
|
||||
const loadOpenOrders = async () => {
|
||||
try {
|
||||
// 先清除旧的价格线
|
||||
priceLinesRef.current.forEach(line => {
|
||||
try {
|
||||
candlestickSeriesRef.current?.removePriceLine(line)
|
||||
} catch (e) {
|
||||
// 忽略清除错误
|
||||
}
|
||||
})
|
||||
priceLinesRef.current = []
|
||||
|
||||
const openOrders = await fetchOpenOrders(traderID, symbol)
|
||||
console.log('[AdvancedChart] Open orders for price lines:', openOrders)
|
||||
|
||||
if (openOrders.length > 0 && candlestickSeriesRef.current) {
|
||||
openOrders.forEach(order => {
|
||||
// 获取触发价格 (止损/止盈用 stop_price,限价单用 price)
|
||||
const linePrice = order.stop_price > 0 ? order.stop_price : order.price
|
||||
if (linePrice <= 0) return
|
||||
|
||||
// 判断订单类型
|
||||
const isStopLoss = order.type.includes('STOP') || order.type.includes('SL')
|
||||
const isTakeProfit = order.type.includes('TAKE_PROFIT') || order.type.includes('TP')
|
||||
const isLimit = order.type === 'LIMIT'
|
||||
|
||||
// 设置价格线样式
|
||||
let lineColor = '#F0B90B' // 默认黄色
|
||||
const lineStyle = 2 // 虚线
|
||||
let title = ''
|
||||
|
||||
if (isStopLoss) {
|
||||
lineColor = '#F6465D' // 红色 - 止损
|
||||
title = `SL ${order.quantity}`
|
||||
} else if (isTakeProfit) {
|
||||
lineColor = '#0ECB81' // 绿色 - 止盈
|
||||
title = `TP ${order.quantity}`
|
||||
} else if (isLimit) {
|
||||
lineColor = '#F0B90B' // 黄色 - 限价单
|
||||
title = `Limit ${order.side} ${order.quantity}`
|
||||
} else {
|
||||
title = `${order.type} ${order.quantity}`
|
||||
}
|
||||
|
||||
const priceLine = candlestickSeriesRef.current?.createPriceLine({
|
||||
price: linePrice,
|
||||
color: lineColor,
|
||||
lineWidth: 1,
|
||||
lineStyle: lineStyle,
|
||||
axisLabelVisible: true,
|
||||
title: title,
|
||||
})
|
||||
|
||||
if (priceLine) {
|
||||
priceLinesRef.current.push(priceLine)
|
||||
}
|
||||
})
|
||||
console.log('[AdvancedChart] ✅ Created', priceLinesRef.current.length, 'price lines for pending orders')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[AdvancedChart] Error loading open orders:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载 (延迟1秒等待图表初始化完成)
|
||||
const initialTimeout = setTimeout(loadOpenOrders, 1000)
|
||||
|
||||
// 60秒刷新一次挂单
|
||||
const openOrdersInterval = setInterval(loadOpenOrders, 60000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(initialTimeout)
|
||||
clearInterval(openOrdersInterval)
|
||||
}
|
||||
}, [symbol, traderID])
|
||||
|
||||
// 单独处理订单标记的显示/隐藏,避免重新加载数据
|
||||
useEffect(() => {
|
||||
if (!seriesMarkersRef.current) return
|
||||
|
||||
Reference in New Issue
Block a user