diff --git a/api/server.go b/api/server.go index 0bc6ebb4..e21cda6c 100644 --- a/api/server.go +++ b/api/server.go @@ -202,6 +202,7 @@ func (s *Server) setupRoutes() { protected.GET("/trades", s.handleTrades) protected.GET("/orders", s.handleOrders) // Order list (all orders) protected.GET("/orders/:id/fills", s.handleOrderFills) // Order fill details + protected.GET("/open-orders", s.handleOpenOrders) // Open orders from exchange (pending SL/TP) protected.GET("/decisions", s.handleDecisions) protected.GET("/decisions/latest", s.handleLatestDecisions) protected.GET("/statistics", s.handleStatistics) @@ -2341,6 +2342,40 @@ func (s *Server) handleOrderFills(c *gin.Context) { c.JSON(http.StatusOK, fills) } +// handleOpenOrders Get open orders (pending SL/TP) from exchange +func (s *Server) handleOpenOrders(c *gin.Context) { + _, traderID, err := s.getTraderFromQuery(c) + if err != nil { + SafeBadRequest(c, "Invalid trader ID") + return + } + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + SafeNotFound(c, "Trader") + return + } + + // Get symbol parameter (required for exchange query) + symbol := c.Query("symbol") + if symbol == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "symbol parameter is required"}) + return + } + + // Normalize symbol + symbol = market.Normalize(symbol) + + // Get open orders from exchange + openOrders, err := trader.GetOpenOrders(symbol) + if err != nil { + SafeInternalError(c, "Get open orders", err) + return + } + + c.JSON(http.StatusOK, openOrders) +} + // handleKlines K-line data (supports multiple exchanges via coinank) func (s *Server) handleKlines(c *gin.Context) { // Get query parameters diff --git a/trader/aster_trader.go b/trader/aster_trader.go index b75c9579..2c1bbe7d 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -1414,3 +1414,9 @@ func (t *AsterTrader) GetTrades(startTime time.Time, limit int) ([]TradeRecord, return result, nil } + +// GetOpenOrders gets all open/pending orders for a symbol +func (t *AsterTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + // TODO: Implement Aster open orders + return []OpenOrder{}, nil +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 72ee4d35..2f0daee0 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -2218,3 +2218,8 @@ func getSideFromAction(action string) string { } } +// GetOpenOrders returns open orders (pending SL/TP) from exchange +func (at *AutoTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + return at.trader.GetOpenOrders(symbol) +} + diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 6a9ca2ff..5a54db04 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -776,6 +776,64 @@ func (t *FuturesTrader) CancelStopOrders(symbol string) error { return nil } +// GetOpenOrders gets all open/pending orders for a symbol +func (t *FuturesTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + var result []OpenOrder + + // 1. Get legacy open orders + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return nil, fmt.Errorf("failed to get open orders: %w", err) + } + + for _, order := range orders { + price, _ := strconv.ParseFloat(order.Price, 64) + stopPrice, _ := strconv.ParseFloat(order.StopPrice, 64) + quantity, _ := strconv.ParseFloat(order.OrigQuantity, 64) + + result = append(result, OpenOrder{ + OrderID: fmt.Sprintf("%d", order.OrderID), + Symbol: order.Symbol, + Side: string(order.Side), + PositionSide: string(order.PositionSide), + Type: string(order.Type), + Price: price, + StopPrice: stopPrice, + Quantity: quantity, + Status: string(order.Status), + }) + } + + // 2. Get Algo orders (new API for stop-loss/take-profit) + algoOrders, err := t.client.NewListOpenAlgoOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err == nil { + for _, algoOrder := range algoOrders { + triggerPrice, _ := strconv.ParseFloat(algoOrder.TriggerPrice, 64) + quantity, _ := strconv.ParseFloat(algoOrder.Quantity, 64) + + result = append(result, OpenOrder{ + OrderID: fmt.Sprintf("%d", algoOrder.AlgoId), + Symbol: algoOrder.Symbol, + Side: string(algoOrder.Side), + PositionSide: string(algoOrder.PositionSide), + Type: string(algoOrder.OrderType), + Price: 0, // Algo orders use stop price + StopPrice: triggerPrice, + Quantity: quantity, + Status: "NEW", + }) + } + } + + return result, nil +} + // GetMarketPrice gets market price func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) { prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background()) diff --git a/trader/bitget_trader.go b/trader/bitget_trader.go index 19a7d891..41f42f4a 100644 --- a/trader/bitget_trader.go +++ b/trader/bitget_trader.go @@ -1096,3 +1096,9 @@ func genBitgetClientOid() string { rand := time.Now().Nanosecond() % 100000 return fmt.Sprintf("nofx%d%05d", timestamp, rand) } + +// GetOpenOrders gets all open/pending orders for a symbol +func (t *BitgetTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + // TODO: Implement Bitget open orders + return []OpenOrder{}, nil +} diff --git a/trader/bybit_trader.go b/trader/bybit_trader.go index 46c5ff50..d40de870 100644 --- a/trader/bybit_trader.go +++ b/trader/bybit_trader.go @@ -1044,3 +1044,64 @@ func (t *BybitTrader) parseClosedPnLResult(resultData interface{}) ([]ClosedPnLR return records, nil } + +// GetOpenOrders gets all open/pending orders for a symbol +func (t *BybitTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + var result []OpenOrder + + // Get conditional orders (stop-loss, take-profit) + params := map[string]interface{}{ + "category": "linear", + "symbol": symbol, + "orderFilter": "StopOrder", + } + + resp, err := t.client.NewUtaBybitServiceWithParams(params).GetOpenOrders(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to get open orders: %w", err) + } + + if resp.RetCode == 0 { + resultData, ok := resp.Result.(map[string]interface{}) + if ok { + list, _ := resultData["list"].([]interface{}) + for _, item := range list { + order, ok := item.(map[string]interface{}) + if !ok { + continue + } + + orderId, _ := order["orderId"].(string) + sym, _ := order["symbol"].(string) + side, _ := order["side"].(string) + orderType, _ := order["orderType"].(string) + stopOrderType, _ := order["stopOrderType"].(string) + triggerPrice, _ := order["triggerPrice"].(string) + qty, _ := order["qty"].(string) + + price, _ := strconv.ParseFloat(triggerPrice, 64) + quantity, _ := strconv.ParseFloat(qty, 64) + + // Determine type based on stopOrderType + displayType := orderType + if stopOrderType != "" { + displayType = stopOrderType + } + + result = append(result, OpenOrder{ + OrderID: orderId, + Symbol: sym, + Side: side, + PositionSide: "", // Bybit doesn't use positionSide for UTA + Type: displayType, + Price: 0, + StopPrice: price, + Quantity: quantity, + Status: "NEW", + }) + } + } + } + + return result, nil +} diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 87074c3c..b6a1e6bf 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2085,3 +2085,37 @@ func (t *HyperliquidTrader) GetTrades(startTime time.Time, limit int) ([]TradeRe // Fee: 10, // } var defaultBuilder *hyperliquid.BuilderInfo = nil + +// GetOpenOrders gets all open/pending orders for a symbol +func (t *HyperliquidTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) + if err != nil { + return nil, fmt.Errorf("failed to get open orders: %w", err) + } + + var result []OpenOrder + for _, order := range openOrders { + if order.Coin != symbol { + continue + } + + side := "BUY" + if order.Side == "A" { + side = "SELL" + } + + result = append(result, OpenOrder{ + OrderID: fmt.Sprintf("%d", order.Oid), + Symbol: order.Coin, + Side: side, + PositionSide: "", + Type: "LIMIT", + Price: order.LimitPx, + StopPrice: 0, + Quantity: order.Size, + Status: "NEW", + }) + } + + return result, nil +} diff --git a/trader/interface.go b/trader/interface.go index a18e176e..35618633 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -94,4 +94,21 @@ type Trader interface { // limit: max number of records to return // Returns accurate exit price, fees, and close reason for positions closed externally GetClosedPnL(startTime time.Time, limit int) ([]ClosedPnLRecord, error) + + // GetOpenOrders Get open/pending orders from exchange + // Returns stop-loss, take-profit, and limit orders that haven't been filled + GetOpenOrders(symbol string) ([]OpenOrder, error) +} + +// OpenOrder represents a pending order on the exchange +type OpenOrder struct { + OrderID string `json:"order_id"` + Symbol string `json:"symbol"` + Side string `json:"side"` // BUY/SELL + PositionSide string `json:"position_side"` // LONG/SHORT + Type string `json:"type"` // LIMIT/STOP_MARKET/TAKE_PROFIT_MARKET + Price float64 `json:"price"` // Order price (for limit orders) + StopPrice float64 `json:"stop_price"` // Trigger price (for stop orders) + Quantity float64 `json:"quantity"` + Status string `json:"status"` // NEW } diff --git a/trader/lighter_trader_v2_trading.go b/trader/lighter_trader_v2_trading.go index 726b1323..ff5a7341 100644 --- a/trader/lighter_trader_v2_trading.go +++ b/trader/lighter_trader_v2_trading.go @@ -686,3 +686,9 @@ func pow10(n int) int64 { } return result } + +// GetOpenOrders gets all open/pending orders for a symbol +func (t *LighterTraderV2) GetOpenOrders(symbol string) ([]OpenOrder, error) { + // TODO: Implement Lighter open orders + return []OpenOrder{}, nil +} diff --git a/trader/okx_trader.go b/trader/okx_trader.go index 26120aa8..2d4b8b89 100644 --- a/trader/okx_trader.go +++ b/trader/okx_trader.go @@ -1387,3 +1387,9 @@ func (t *OKXTrader) GetClosedPnL(startTime time.Time, limit int) ([]ClosedPnLRec return records, nil } + +// GetOpenOrders gets all open/pending orders for a symbol +func (t *OKXTrader) GetOpenOrders(symbol string) ([]OpenOrder, error) { + // TODO: Implement OKX open orders + return []OpenOrder{}, nil +} diff --git a/web/src/components/AdvancedChart.tsx b/web/src/components/AdvancedChart.tsx index 4a6cd758..5eb372cd 100644 --- a/web/src/components/AdvancedChart.tsx +++ b/web/src/components/AdvancedChart.tsx @@ -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(null) // Markers primitive for v5 const currentMarkersDataRef = useRef([]) // 存储当前的标记数据 const klineDataRef = useRef>(new Map()) // 存储 kline 额外数据 + const priceLinesRef = useRef([]) // 存储挂单价格线 const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -307,6 +321,26 @@ export function AdvancedChart({ } } + // 获取交易所挂单 (止盈止损订单) + const fetchOpenOrders = async (traderID: string, symbol: string): Promise => { + 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