package bitget import ( "encoding/json" "fmt" "nofx/logger" "nofx/trader/types" "strconv" "strings" ) // OpenLong opens long position func (t *BitgetTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) // Cancel old orders first t.CancelAllOrders(symbol) // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { logger.Infof(" ⚠️ Failed to set leverage: %v", err) } // Format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "side": "buy", "orderType": "market", "size": qtyStr, "clientOid": genBitgetClientOid(), } logger.Infof(" 📊 Bitget OpenLong: symbol=%s, qty=%s, leverage=%d", symbol, qtyStr, leverage) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { return nil, fmt.Errorf("failed to open long position: %w", err) } var order struct { OrderId string `json:"orderId"` ClientOid string `json:"clientOid"` } if err := json.Unmarshal(data, &order); err != nil { return nil, fmt.Errorf("failed to parse order response: %w", err) } // Clear cache t.clearCache() logger.Infof("✓ Bitget opened long position successfully: %s", symbol) return map[string]interface{}{ "orderId": order.OrderId, "symbol": symbol, "status": "FILLED", }, nil } // OpenShort opens short position func (t *BitgetTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) // Cancel old orders first t.CancelAllOrders(symbol) // Set leverage if err := t.SetLeverage(symbol, leverage); err != nil { logger.Infof(" ⚠️ Failed to set leverage: %v", err) } // Format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "side": "sell", "orderType": "market", "size": qtyStr, "clientOid": genBitgetClientOid(), } logger.Infof(" 📊 Bitget OpenShort: symbol=%s, qty=%s, leverage=%d", symbol, qtyStr, leverage) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { return nil, fmt.Errorf("failed to open short position: %w", err) } var order struct { OrderId string `json:"orderId"` ClientOid string `json:"clientOid"` } if err := json.Unmarshal(data, &order); err != nil { return nil, fmt.Errorf("failed to parse order response: %w", err) } // Clear cache t.clearCache() logger.Infof("✓ Bitget opened short position successfully: %s", symbol) return map[string]interface{}{ "orderId": order.OrderId, "symbol": symbol, "status": "FILLED", }, nil } // CloseLong closes long position func (t *BitgetTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) // If quantity is 0, get current position if quantity == 0 { positions, err := t.GetPositions() if err != nil { return nil, err } for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "long" { quantity = pos["positionAmt"].(float64) break } } if quantity == 0 { return nil, fmt.Errorf("long position not found for %s", symbol) } } // Format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "side": "sell", "orderType": "market", "size": qtyStr, "reduceOnly": "YES", "clientOid": genBitgetClientOid(), } logger.Infof(" 📊 Bitget CloseLong: symbol=%s, qty=%s", symbol, qtyStr) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { return nil, fmt.Errorf("failed to close long position: %w", err) } var order struct { OrderId string `json:"orderId"` } if err := json.Unmarshal(data, &order); err != nil { return nil, err } // Clear cache t.clearCache() logger.Infof("✓ Bitget closed long position successfully: %s", symbol) return map[string]interface{}{ "orderId": order.OrderId, "symbol": symbol, "status": "FILLED", }, nil } // CloseShort closes short position func (t *BitgetTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) // If quantity is 0, get current position if quantity == 0 { positions, err := t.GetPositions() if err != nil { return nil, err } for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "short" { quantity = pos["positionAmt"].(float64) break } } if quantity == 0 { return nil, fmt.Errorf("short position not found for %s", symbol) } } // Ensure quantity is positive if quantity < 0 { quantity = -quantity } // Format quantity qtyStr, _ := t.FormatQuantity(symbol, quantity) body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "side": "buy", "orderType": "market", "size": qtyStr, "reduceOnly": "YES", "clientOid": genBitgetClientOid(), } logger.Infof(" 📊 Bitget CloseShort: symbol=%s, qty=%s", symbol, qtyStr) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { return nil, fmt.Errorf("failed to close short position: %w", err) } var order struct { OrderId string `json:"orderId"` } if err := json.Unmarshal(data, &order); err != nil { return nil, err } // Clear cache t.clearCache() logger.Infof("✓ Bitget closed short position successfully: %s", symbol) return map[string]interface{}{ "orderId": order.OrderId, "symbol": symbol, "status": "FILLED", }, nil } // SetStopLoss sets stop loss order func (t *BitgetTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { // Bitget V2 uses plan order for stop loss symbol = t.convertSymbol(symbol) side := "sell" holdSide := "long" if strings.ToUpper(positionSide) == "SHORT" { side = "buy" holdSide = "short" } qtyStr, _ := t.FormatQuantity(symbol, quantity) body := map[string]interface{}{ "planType": "loss_plan", "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "triggerPrice": fmt.Sprintf("%.8f", stopPrice), "triggerType": "mark_price", "side": side, "tradeSide": "close", "orderType": "market", "size": qtyStr, "holdSide": holdSide, "clientOid": genBitgetClientOid(), } _, err := t.doRequest("POST", "/api/v2/mix/order/place-plan-order", body) if err != nil { return fmt.Errorf("failed to set stop loss: %w", err) } logger.Infof(" ✓ [Bitget] Stop loss set: %s @ %.4f", symbol, stopPrice) return nil } // SetTakeProfit sets take profit order func (t *BitgetTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { // Bitget V2 uses plan order for take profit symbol = t.convertSymbol(symbol) side := "sell" holdSide := "long" if strings.ToUpper(positionSide) == "SHORT" { side = "buy" holdSide = "short" } qtyStr, _ := t.FormatQuantity(symbol, quantity) body := map[string]interface{}{ "planType": "profit_plan", "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "triggerPrice": fmt.Sprintf("%.8f", takeProfitPrice), "triggerType": "mark_price", "side": side, "tradeSide": "close", "orderType": "market", "size": qtyStr, "holdSide": holdSide, "clientOid": genBitgetClientOid(), } _, err := t.doRequest("POST", "/api/v2/mix/order/place-plan-order", body) if err != nil { return fmt.Errorf("failed to set take profit: %w", err) } logger.Infof(" ✓ [Bitget] Take profit set: %s @ %.4f", symbol, takeProfitPrice) return nil } // CancelStopLossOrders cancels stop loss orders func (t *BitgetTrader) CancelStopLossOrders(symbol string) error { return t.cancelPlanOrders(symbol, "loss_plan") } // CancelTakeProfitOrders cancels take profit orders func (t *BitgetTrader) CancelTakeProfitOrders(symbol string) error { return t.cancelPlanOrders(symbol, "profit_plan") } // cancelPlanOrders cancels plan orders func (t *BitgetTrader) cancelPlanOrders(symbol string, planType string) error { symbol = t.convertSymbol(symbol) // Get pending plan orders params := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "planType": planType, } data, err := t.doRequest("GET", "/api/v2/mix/order/orders-plan-pending", params) if err != nil { return err } var orders struct { EntrustedList []struct { OrderId string `json:"orderId"` } `json:"entrustedList"` } if err := json.Unmarshal(data, &orders); err != nil { return err } // Cancel each order for _, order := range orders.EntrustedList { body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginCoin": "USDT", "orderId": order.OrderId, } t.doRequest("POST", "/api/v2/mix/order/cancel-plan-order", body) } return nil } // CancelAllOrders cancels all pending orders func (t *BitgetTrader) CancelAllOrders(symbol string) error { symbol = t.convertSymbol(symbol) // Get pending orders params := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", } data, err := t.doRequest("GET", bitgetPendingPath, params) if err != nil { return err } var orders struct { EntrustedList []struct { OrderId string `json:"orderId"` } `json:"entrustedList"` } if err := json.Unmarshal(data, &orders); err != nil { return err } // Cancel each order for _, order := range orders.EntrustedList { body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginCoin": "USDT", "orderId": order.OrderId, } t.doRequest("POST", bitgetCancelOrderPath, body) } // Also cancel plan orders t.cancelPlanOrders(symbol, "loss_plan") t.cancelPlanOrders(symbol, "profit_plan") return nil } // CancelStopOrders cancels stop loss and take profit orders func (t *BitgetTrader) CancelStopOrders(symbol string) error { t.CancelStopLossOrders(symbol) t.CancelTakeProfitOrders(symbol) return nil } // GetOrderStatus gets order status func (t *BitgetTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) params := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "orderId": orderID, } data, err := t.doRequest("GET", "/api/v2/mix/order/detail", params) if err != nil { return nil, fmt.Errorf("failed to get order status: %w", err) } var order struct { OrderId string `json:"orderId"` State string `json:"state"` // filled, canceled, partially_filled, new PriceAvg string `json:"priceAvg"` // Average fill price BaseVolume string `json:"baseVolume"` // Filled quantity Fee string `json:"fee"` // Fee Side string `json:"side"` OrderType string `json:"orderType"` CTime string `json:"cTime"` UTime string `json:"uTime"` } if err := json.Unmarshal(data, &order); err != nil { return nil, err } avgPrice, _ := strconv.ParseFloat(order.PriceAvg, 64) fillQty, _ := strconv.ParseFloat(order.BaseVolume, 64) fee, _ := strconv.ParseFloat(order.Fee, 64) cTime, _ := strconv.ParseInt(order.CTime, 10, 64) uTime, _ := strconv.ParseInt(order.UTime, 10, 64) // Status mapping statusMap := map[string]string{ "filled": "FILLED", "new": "NEW", "partially_filled": "PARTIALLY_FILLED", "canceled": "CANCELED", } status := statusMap[order.State] if status == "" { status = order.State } return map[string]interface{}{ "orderId": order.OrderId, "symbol": symbol, "status": status, "avgPrice": avgPrice, "executedQty": fillQty, "side": order.Side, "type": order.OrderType, "time": cTime, "updateTime": uTime, "commission": -fee, }, nil } // GetOpenOrders gets all open/pending orders for a symbol func (t *BitgetTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, error) { symbol = t.convertSymbol(symbol) var result []types.OpenOrder // 1. Get pending limit orders params := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", } data, err := t.doRequest("GET", bitgetPendingPath, params) if err != nil { logger.Warnf("[Bitget] Failed to get pending orders: %v", err) } if err == nil && data != nil { var orders struct { EntrustedList []struct { OrderId string `json:"orderId"` Symbol string `json:"symbol"` Side string `json:"side"` // buy/sell TradeSide string `json:"tradeSide"` // open/close PosSide string `json:"posSide"` // long/short OrderType string `json:"orderType"` // limit/market Price string `json:"price"` Size string `json:"size"` State string `json:"state"` } `json:"entrustedList"` } if err := json.Unmarshal(data, &orders); err == nil { for _, order := range orders.EntrustedList { price, _ := strconv.ParseFloat(order.Price, 64) quantity, _ := strconv.ParseFloat(order.Size, 64) // Convert side to standard format side := strings.ToUpper(order.Side) positionSide := strings.ToUpper(order.PosSide) result = append(result, types.OpenOrder{ OrderID: order.OrderId, Symbol: symbol, Side: side, PositionSide: positionSide, Type: strings.ToUpper(order.OrderType), Price: price, StopPrice: 0, Quantity: quantity, Status: "NEW", }) } } } // 2. Get pending plan orders (stop-loss/take-profit) // Bitget V2 API requires planType parameter: profit_loss for SL/TP orders planParams := map[string]interface{}{ "productType": "USDT-FUTURES", "planType": "profit_loss", } planData, err := t.doRequest("GET", "/api/v2/mix/order/orders-plan-pending", planParams) if err != nil { logger.Warnf("[Bitget] Failed to get plan orders: %v", err) } if err == nil && planData != nil { var planOrders struct { EntrustedList []struct { OrderId string `json:"orderId"` Symbol string `json:"symbol"` Side string `json:"side"` PosSide string `json:"posSide"` PlanType string `json:"planType"` // pos_loss, pos_profit TriggerPrice string `json:"triggerPrice"` StopLossTriggerPrice string `json:"stopLossTriggerPrice"` StopSurplusTriggerPrice string `json:"stopSurplusTriggerPrice"` Size string `json:"size"` PlanStatus string `json:"planStatus"` } `json:"entrustedList"` } if err := json.Unmarshal(planData, &planOrders); err == nil { for _, order := range planOrders.EntrustedList { // Filter by symbol if specified if symbol != "" && order.Symbol != symbol { continue } // Determine trigger price based on plan type var triggerPrice float64 orderType := "STOP_MARKET" if order.PlanType == "pos_profit" { // Take profit order orderType = "TAKE_PROFIT_MARKET" if order.StopSurplusTriggerPrice != "" { triggerPrice, _ = strconv.ParseFloat(order.StopSurplusTriggerPrice, 64) } else { triggerPrice, _ = strconv.ParseFloat(order.TriggerPrice, 64) } } else { // Stop loss order (pos_loss) if order.StopLossTriggerPrice != "" { triggerPrice, _ = strconv.ParseFloat(order.StopLossTriggerPrice, 64) } else { triggerPrice, _ = strconv.ParseFloat(order.TriggerPrice, 64) } } quantity, _ := strconv.ParseFloat(order.Size, 64) side := strings.ToUpper(order.Side) positionSide := strings.ToUpper(order.PosSide) result = append(result, types.OpenOrder{ OrderID: order.OrderId, Symbol: order.Symbol, Side: side, PositionSide: positionSide, Type: orderType, Price: 0, StopPrice: triggerPrice, Quantity: quantity, Status: "NEW", }) } } } logger.Infof("✓ BITGET GetOpenOrders: found %d open orders for %s", len(result), symbol) return result, nil } // PlaceLimitOrder places a limit order for grid trading // Implements GridTrader interface func (t *BitgetTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitOrderResult, error) { symbol := t.convertSymbol(req.Symbol) // Set leverage if specified if req.Leverage > 0 { if err := t.SetLeverage(symbol, req.Leverage); err != nil { logger.Warnf("[Bitget] Failed to set leverage: %v", err) } } // Format quantity qtyStr, _ := t.FormatQuantity(symbol, req.Quantity) // Determine side side := "buy" if req.Side == "SELL" { side = "sell" } body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "marginMode": "crossed", "marginCoin": "USDT", "side": side, "orderType": "limit", "size": qtyStr, "price": fmt.Sprintf("%.8f", req.Price), "force": "GTC", // Good Till Cancel "clientOid": genBitgetClientOid(), } // Add reduce only if specified if req.ReduceOnly { body["reduceOnly"] = "YES" } logger.Infof("[Bitget] PlaceLimitOrder: %s %s @ %.4f, qty=%s", symbol, side, req.Price, qtyStr) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { return nil, fmt.Errorf("failed to place limit order: %w", err) } var order struct { OrderId string `json:"orderId"` ClientOid string `json:"clientOid"` } if err := json.Unmarshal(data, &order); err != nil { return nil, fmt.Errorf("failed to parse order response: %w", err) } logger.Infof("✓ [Bitget] Limit order placed: %s %s @ %.4f, orderID=%s", symbol, side, req.Price, order.OrderId) return &types.LimitOrderResult{ OrderID: order.OrderId, ClientID: order.ClientOid, Symbol: req.Symbol, Side: req.Side, PositionSide: req.PositionSide, Price: req.Price, Quantity: req.Quantity, Status: "NEW", }, nil } // CancelOrder cancels a specific order by ID // Implements GridTrader interface func (t *BitgetTrader) CancelOrder(symbol, orderID string) error { symbol = t.convertSymbol(symbol) body := map[string]interface{}{ "symbol": symbol, "productType": "USDT-FUTURES", "orderId": orderID, } _, err := t.doRequest("POST", "/api/v2/mix/order/cancel-order", body) if err != nil { return fmt.Errorf("failed to cancel order: %w", err) } logger.Infof("✓ [Bitget] Order cancelled: %s %s", symbol, orderID) return nil }