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:
tinkle-community
2026-01-07 00:50:29 +08:00
parent 5e65ae7077
commit b36ab27b65
11 changed files with 349 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}