package aster import ( "encoding/json" "fmt" "nofx/logger" "nofx/trader/types" "strconv" "strings" ) // Aggressive limit prices simulate market orders: buy slightly above and sell // slightly below the current price so limit orders fill immediately while // capping slippage at 1%. const ( aggressiveBuyPriceFactor = 1.01 aggressiveSellPriceFactor = 0.99 ) // OpenLong Open long position func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { // Cancel all pending orders before opening position to prevent position stacking from residual orders if err := t.CancelAllOrders(symbol); err != nil { logger.Infof(" ⚠ Failed to cancel pending orders (continuing to open position): %v", err) } // Set leverage first (non-fatal if position already exists) if err := t.SetLeverage(symbol, leverage); err != nil { // Error -2030: Cannot adjust leverage when position exists // This is expected when adding to an existing position, continue with current leverage if strings.Contains(err.Error(), "-2030") { logger.Infof(" ⚠ Cannot change leverage (position exists), using current leverage: %v", err) } else { return nil, fmt.Errorf("failed to set leverage: %w", err) } } // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } // Use limit order to simulate market order (price set slightly higher to ensure execution) limitPrice := price * aggressiveBuyPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err } formattedQty, err := t.formatQuantity(symbol, quantity) if err != nil { return nil, err } // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ "symbol": symbol, "positionSide": "BOTH", "type": "LIMIT", "side": "BUY", "timeInForce": "GTC", "quantity": qtyStr, "price": priceStr, } body, err := t.request("POST", "/fapi/v3/order", params) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } return result, nil } // OpenShort Open short position func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { // Cancel all pending orders before opening position to prevent position stacking from residual orders if err := t.CancelAllOrders(symbol); err != nil { logger.Infof(" ⚠ Failed to cancel pending orders (continuing to open position): %v", err) } // Set leverage first (non-fatal if position already exists) if err := t.SetLeverage(symbol, leverage); err != nil { // Error -2030: Cannot adjust leverage when position exists // This is expected when adding to an existing position, continue with current leverage if strings.Contains(err.Error(), "-2030") { logger.Infof(" ⚠ Cannot change leverage (position exists), using current leverage: %v", err) } else { return nil, fmt.Errorf("failed to set leverage: %w", err) } } // Get current price price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } // Use limit order to simulate market order (price set slightly lower to ensure execution) limitPrice := price * aggressiveSellPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err } formattedQty, err := t.formatQuantity(symbol, quantity) if err != nil { return nil, err } // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ "symbol": symbol, "positionSide": "BOTH", "type": "LIMIT", "side": "SELL", "timeInForce": "GTC", "quantity": qtyStr, "price": priceStr, } body, err := t.request("POST", "/fapi/v3/order", params) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } return result, nil } // CloseLong Close long position func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { // If quantity is 0, get current position quantity 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("no long position found for %s", symbol) } logger.Infof(" 📊 Retrieved long position quantity: %.8f", quantity) } price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } limitPrice := price * aggressiveSellPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err } formattedQty, err := t.formatQuantity(symbol, quantity) if err != nil { return nil, err } // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ "symbol": symbol, "positionSide": "BOTH", "type": "LIMIT", "side": "SELL", "timeInForce": "GTC", "quantity": qtyStr, "price": priceStr, } body, err := t.request("POST", "/fapi/v3/order", params) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } logger.Infof("✓ Successfully closed long position: %s quantity: %s", symbol, qtyStr) // Cancel all pending orders for this symbol after closing position (stop-loss/take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } return result, nil } // CloseShort Close short position func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { // If quantity is 0, get current position quantity if quantity == 0 { positions, err := t.GetPositions() if err != nil { return nil, err } for _, pos := range positions { if pos["symbol"] == symbol && pos["side"] == "short" { // Aster's GetPositions has already converted short position quantity to positive, use directly quantity = pos["positionAmt"].(float64) break } } if quantity == 0 { return nil, fmt.Errorf("no short position found for %s", symbol) } logger.Infof(" 📊 Retrieved short position quantity: %.8f", quantity) } price, err := t.GetMarketPrice(symbol) if err != nil { return nil, err } limitPrice := price * aggressiveBuyPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) if err != nil { return nil, err } formattedQty, err := t.formatQuantity(symbol, quantity) if err != nil { return nil, err } // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return nil, err } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) logger.Infof(" 📏 Precision handling: price %.8f -> %s (precision=%d), quantity %.8f -> %s (precision=%d)", limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) params := map[string]interface{}{ "symbol": symbol, "positionSide": "BOTH", "type": "LIMIT", "side": "BUY", "timeInForce": "GTC", "quantity": qtyStr, "price": priceStr, } body, err := t.request("POST", "/fapi/v3/order", params) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } logger.Infof("✓ Successfully closed short position: %s quantity: %s", symbol, qtyStr) // Cancel all pending orders for this symbol after closing position (stop-loss/take-profit orders) if err := t.CancelAllOrders(symbol); err != nil { logger.Infof(" ⚠ Failed to cancel pending orders: %v", err) } return result, nil } // SetStopLoss Set stop loss func (t *AsterTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { side := "SELL" if positionSide == "SHORT" { side = "BUY" } // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, stopPrice) if err != nil { return err } formattedQty, err := t.formatQuantity(symbol, quantity) if err != nil { return err } // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return err } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) params := map[string]interface{}{ "symbol": symbol, "positionSide": "BOTH", "type": "STOP_MARKET", "side": side, "stopPrice": priceStr, "quantity": qtyStr, "timeInForce": "GTC", } _, err = t.request("POST", "/fapi/v3/order", params) return err } // SetTakeProfit Set take profit func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { side := "SELL" if positionSide == "SHORT" { side = "BUY" } // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, takeProfitPrice) if err != nil { return err } formattedQty, err := t.formatQuantity(symbol, quantity) if err != nil { return err } // Get precision information prec, err := t.getPrecision(symbol) if err != nil { return err } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) params := map[string]interface{}{ "symbol": symbol, "positionSide": "BOTH", "type": "TAKE_PROFIT_MARKET", "side": side, "stopPrice": priceStr, "quantity": qtyStr, "timeInForce": "GTC", } _, err = t.request("POST", "/fapi/v3/order", params) return err } // CancelStopLossOrders Cancel stop-loss orders only (does not affect take-profit orders) func (t *AsterTrader) CancelStopLossOrders(symbol string) error { // Get all open orders for this symbol params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { return fmt.Errorf("failed to get open orders: %w", err) } var orders []map[string]interface{} if err := json.Unmarshal(body, &orders); err != nil { return fmt.Errorf("failed to parse order data: %w", err) } // Filter and cancel stop-loss orders (cancel all directions including LONG and SHORT) canceledCount := 0 var cancelErrors []error for _, order := range orders { orderType, _ := order["type"].(string) // Only cancel stop-loss orders (don't cancel take-profit orders) if orderType == "STOP_MARKET" || orderType == "STOP" { orderID, _ := order["orderId"].(float64) positionSide, _ := order["positionSide"].(string) cancelParams := map[string]interface{}{ "symbol": symbol, "orderId": int64(orderID), } _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) if err != nil { errMsg := fmt.Sprintf("order ID %d: %v", int64(orderID), err) cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg)) logger.Infof(" ⚠ Failed to cancel stop-loss order: %s", errMsg) continue } canceledCount++ logger.Infof(" ✓ Canceled stop-loss order (order ID: %d, type: %s, direction: %s)", int64(orderID), orderType, positionSide) } } if canceledCount == 0 && len(cancelErrors) == 0 { logger.Infof(" ℹ %s no stop-loss orders to cancel", symbol) } else if canceledCount > 0 { logger.Infof(" ✓ Canceled %d stop-loss order(s) for %s", canceledCount, symbol) } // Return error if all cancellations failed if len(cancelErrors) > 0 && canceledCount == 0 { return fmt.Errorf("failed to cancel stop-loss orders: %v", cancelErrors) } return nil } // CancelTakeProfitOrders Cancel take-profit orders only (does not affect stop-loss orders) func (t *AsterTrader) CancelTakeProfitOrders(symbol string) error { // Get all open orders for this symbol params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { return fmt.Errorf("failed to get open orders: %w", err) } var orders []map[string]interface{} if err := json.Unmarshal(body, &orders); err != nil { return fmt.Errorf("failed to parse order data: %w", err) } // Filter and cancel take-profit orders (cancel all directions including LONG and SHORT) canceledCount := 0 var cancelErrors []error for _, order := range orders { orderType, _ := order["type"].(string) // Only cancel take-profit orders (don't cancel stop-loss orders) if orderType == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" { orderID, _ := order["orderId"].(float64) positionSide, _ := order["positionSide"].(string) cancelParams := map[string]interface{}{ "symbol": symbol, "orderId": int64(orderID), } _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) if err != nil { errMsg := fmt.Sprintf("order ID %d: %v", int64(orderID), err) cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg)) logger.Infof(" ⚠ Failed to cancel take-profit order: %s", errMsg) continue } canceledCount++ logger.Infof(" ✓ Canceled take-profit order (order ID: %d, type: %s, direction: %s)", int64(orderID), orderType, positionSide) } } if canceledCount == 0 && len(cancelErrors) == 0 { logger.Infof(" ℹ %s no take-profit orders to cancel", symbol) } else if canceledCount > 0 { logger.Infof(" ✓ Canceled %d take-profit order(s) for %s", canceledCount, symbol) } // Return error if all cancellations failed if len(cancelErrors) > 0 && canceledCount == 0 { return fmt.Errorf("failed to cancel take-profit orders: %v", cancelErrors) } return nil } // CancelAllOrders Cancel all orders func (t *AsterTrader) CancelAllOrders(symbol string) error { params := map[string]interface{}{ "symbol": symbol, } _, err := t.request("DELETE", "/fapi/v3/allOpenOrders", params) return err } // CancelStopOrders Cancel take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions) func (t *AsterTrader) CancelStopOrders(symbol string) error { // Get all open orders for this symbol params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { return fmt.Errorf("failed to get open orders: %w", err) } var orders []map[string]interface{} if err := json.Unmarshal(body, &orders); err != nil { return fmt.Errorf("failed to parse order data: %w", err) } // Filter and cancel take-profit/stop-loss orders canceledCount := 0 for _, order := range orders { orderType, _ := order["type"].(string) // Only cancel stop-loss and take-profit orders if orderType == "STOP_MARKET" || orderType == "TAKE_PROFIT_MARKET" || orderType == "STOP" || orderType == "TAKE_PROFIT" { orderID, _ := order["orderId"].(float64) cancelParams := map[string]interface{}{ "symbol": symbol, "orderId": int64(orderID), } _, err := t.request("DELETE", "/fapi/v3/order", cancelParams) if err != nil { logger.Infof(" ⚠ Failed to cancel order %d: %v", int64(orderID), err) continue } canceledCount++ logger.Infof(" ✓ Canceled take-profit/stop-loss order for %s (order ID: %d, type: %s)", symbol, int64(orderID), orderType) } } if canceledCount == 0 { logger.Infof(" ℹ %s no take-profit/stop-loss orders to cancel", symbol) } else { logger.Infof(" ✓ Canceled %d take-profit/stop-loss order(s) for %s", canceledCount, symbol) } return nil } // FormatQuantity Format quantity (implements Trader interface) func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { formatted, err := t.formatQuantity(symbol, quantity) if err != nil { return "", err } return fmt.Sprintf("%v", formatted), nil } // GetOrderStatus Get order status func (t *AsterTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { params := map[string]interface{}{ "symbol": symbol, "orderId": orderID, } body, err := t.request("GET", "/fapi/v3/order", params) if err != nil { return nil, fmt.Errorf("failed to get order status: %w", err) } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse order response: %w", err) } // Standardize return fields response := map[string]interface{}{ "orderId": result["orderId"], "symbol": result["symbol"], "status": result["status"], "side": result["side"], "type": result["type"], "time": result["time"], "updateTime": result["updateTime"], "commission": 0.0, // Aster may require separate query } // Parse numeric fields if avgPrice, ok := result["avgPrice"].(string); ok { if v, err := strconv.ParseFloat(avgPrice, 64); err == nil { response["avgPrice"] = v } } else if avgPrice, ok := result["avgPrice"].(float64); ok { response["avgPrice"] = avgPrice } if executedQty, ok := result["executedQty"].(string); ok { if v, err := strconv.ParseFloat(executedQty, 64); err == nil { response["executedQty"] = v } } else if executedQty, ok := result["executedQty"].(float64); ok { response["executedQty"] = executedQty } return response, nil } // GetOpenOrders gets all open/pending orders for a symbol func (t *AsterTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, error) { params := map[string]interface{}{ "symbol": symbol, } body, err := t.request("GET", "/fapi/v3/openOrders", params) if err != nil { return nil, fmt.Errorf("failed to get open orders: %w", err) } var orders []struct { OrderID int64 `json:"orderId"` Symbol string `json:"symbol"` Side string `json:"side"` PositionSide string `json:"positionSide"` Type string `json:"type"` Price string `json:"price"` StopPrice string `json:"stopPrice"` OrigQty string `json:"origQty"` Status string `json:"status"` } if err := json.Unmarshal(body, &orders); err != nil { return nil, fmt.Errorf("failed to parse open orders: %w", err) } var result []types.OpenOrder for _, order := range orders { price, _ := strconv.ParseFloat(order.Price, 64) stopPrice, _ := strconv.ParseFloat(order.StopPrice, 64) quantity, _ := strconv.ParseFloat(order.OrigQty, 64) result = append(result, types.OpenOrder{ OrderID: fmt.Sprintf("%d", order.OrderID), Symbol: order.Symbol, Side: order.Side, PositionSide: order.PositionSide, Type: order.Type, Price: price, StopPrice: stopPrice, Quantity: quantity, Status: order.Status, }) } logger.Infof("✓ ASTER GetOpenOrders: found %d open orders for %s", len(result), symbol) return result, nil } // PlaceLimitOrder places a limit order for grid trading func (t *AsterTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitOrderResult, error) { // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(req.Symbol, req.Price) if err != nil { return nil, fmt.Errorf("failed to format price: %w", err) } formattedQty, err := t.formatQuantity(req.Symbol, req.Quantity) if err != nil { return nil, fmt.Errorf("failed to format quantity: %w", err) } // Get precision information prec, err := t.getPrecision(req.Symbol) if err != nil { return nil, fmt.Errorf("failed to get precision: %w", err) } // Convert to string with correct precision format priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) // Determine side side := "BUY" if req.Side == "SELL" || req.Side == "Sell" || req.Side == "sell" { side = "SELL" } params := map[string]interface{}{ "symbol": req.Symbol, "positionSide": "BOTH", "type": "LIMIT", "side": side, "timeInForce": "GTC", "quantity": qtyStr, "price": priceStr, } // Add reduceOnly if specified if req.ReduceOnly { params["reduceOnly"] = "true" } body, err := t.request("POST", "/fapi/v3/order", params) if err != nil { return nil, fmt.Errorf("failed to place limit order: %w", err) } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse order response: %w", err) } // Extract order ID orderID := "" if id, ok := result["orderId"].(float64); ok { orderID = fmt.Sprintf("%.0f", id) } else if id, ok := result["orderId"].(string); ok { orderID = id } // Extract client order ID clientOrderID := "" if cid, ok := result["clientOrderId"].(string); ok { clientOrderID = cid } return &types.LimitOrderResult{ OrderID: orderID, ClientID: clientOrderID, Symbol: req.Symbol, Side: side, Price: formattedPrice, Quantity: formattedQty, Status: "NEW", }, nil } // CancelOrder cancels a specific order by order ID func (t *AsterTrader) CancelOrder(symbol, orderID string) error { params := map[string]interface{}{ "symbol": symbol, "orderId": orderID, } _, err := t.request("DELETE", "/fapi/v3/order", params) if err != nil { return fmt.Errorf("failed to cancel order %s: %w", orderID, err) } return nil }