mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
cb31782be4
- Rename experience/ to telemetry/ for clarity - Split 15+ large Go files (800-2200 lines) into focused modules: kernel/engine.go, backtest/runner.go, market/data.go, store/position.go, api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders - Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx into domain-specific modules with barrel re-exports - Remove stale files: screenshots, .yml.old, pyproject.toml - Remove unused scripts/ and cmd/ directories - Remove broken/outdated test files (network-dependent, stale expectations)
759 lines
23 KiB
Go
759 lines
23 KiB
Go
package binance
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"nofx/logger"
|
||
"nofx/trader/types"
|
||
"strconv"
|
||
|
||
"github.com/adshao/go-binance/v2/futures"
|
||
)
|
||
|
||
// OpenLong opens a long position
|
||
func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||
// First cancel all pending orders for this symbol (clean up old stop-loss and take-profit orders)
|
||
if err := t.CancelAllOrders(symbol); err != nil {
|
||
logger.Infof(" ⚠ Failed to cancel old pending orders (may not have any): %v", err)
|
||
}
|
||
|
||
// Set leverage
|
||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Note: Margin mode should be set by the caller (AutoTrader) before opening position via SetMarginMode
|
||
|
||
// Format quantity to correct precision
|
||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Check if formatted quantity is 0 (prevent rounding errors)
|
||
quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64)
|
||
if parseErr != nil || quantityFloat <= 0 {
|
||
return nil, fmt.Errorf("position size too small, rounded to 0 (original: %.8f → formatted: %s). Suggest increasing position amount or selecting a lower-priced coin", quantity, quantityStr)
|
||
}
|
||
|
||
// Check minimum notional value (Binance requires at least 10 USDT)
|
||
if err := t.CheckMinNotional(symbol, quantityFloat); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Create market buy order (using br ID)
|
||
order, err := t.client.NewCreateOrderService().
|
||
Symbol(symbol).
|
||
Side(futures.SideTypeBuy).
|
||
PositionSide(futures.PositionSideTypeLong).
|
||
Type(futures.OrderTypeMarket).
|
||
Quantity(quantityStr).
|
||
NewClientOrderID(getBrOrderID()).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to open long position: %w", err)
|
||
}
|
||
|
||
logger.Infof("✓ Opened long position successfully: %s quantity: %s", symbol, quantityStr)
|
||
logger.Infof(" Order ID: %d", order.OrderID)
|
||
|
||
result := make(map[string]interface{})
|
||
result["orderId"] = order.OrderID
|
||
result["symbol"] = order.Symbol
|
||
result["status"] = order.Status
|
||
return result, nil
|
||
}
|
||
|
||
// OpenShort opens a short position
|
||
func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||
// First cancel all pending orders for this symbol (clean up old stop-loss and take-profit orders)
|
||
if err := t.CancelAllOrders(symbol); err != nil {
|
||
logger.Infof(" ⚠ Failed to cancel old pending orders (may not have any): %v", err)
|
||
}
|
||
|
||
// Set leverage
|
||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Note: Margin mode should be set by the caller (AutoTrader) before opening position via SetMarginMode
|
||
|
||
// Format quantity to correct precision
|
||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Check if formatted quantity is 0 (prevent rounding errors)
|
||
quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64)
|
||
if parseErr != nil || quantityFloat <= 0 {
|
||
return nil, fmt.Errorf("position size too small, rounded to 0 (original: %.8f → formatted: %s). Suggest increasing position amount or selecting a lower-priced coin", quantity, quantityStr)
|
||
}
|
||
|
||
// Check minimum notional value (Binance requires at least 10 USDT)
|
||
if err := t.CheckMinNotional(symbol, quantityFloat); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Create market sell order (using br ID)
|
||
order, err := t.client.NewCreateOrderService().
|
||
Symbol(symbol).
|
||
Side(futures.SideTypeSell).
|
||
PositionSide(futures.PositionSideTypeShort).
|
||
Type(futures.OrderTypeMarket).
|
||
Quantity(quantityStr).
|
||
NewClientOrderID(getBrOrderID()).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to open short position: %w", err)
|
||
}
|
||
|
||
logger.Infof("✓ Opened short position successfully: %s quantity: %s", symbol, quantityStr)
|
||
logger.Infof(" Order ID: %d", order.OrderID)
|
||
|
||
result := make(map[string]interface{})
|
||
result["orderId"] = order.OrderID
|
||
result["symbol"] = order.Symbol
|
||
result["status"] = order.Status
|
||
return result, nil
|
||
}
|
||
|
||
// CloseLong closes a long position
|
||
func (t *FuturesTrader) 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)
|
||
}
|
||
}
|
||
|
||
// Format quantity
|
||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Create market sell order (close long, using br ID)
|
||
order, err := t.client.NewCreateOrderService().
|
||
Symbol(symbol).
|
||
Side(futures.SideTypeSell).
|
||
PositionSide(futures.PositionSideTypeLong).
|
||
Type(futures.OrderTypeMarket).
|
||
Quantity(quantityStr).
|
||
NewClientOrderID(getBrOrderID()).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to close long position: %w", err)
|
||
}
|
||
|
||
logger.Infof("✓ Closed long position successfully: %s quantity: %s", symbol, quantityStr)
|
||
|
||
// After closing position, cancel all pending orders for this symbol (stop-loss and take-profit orders)
|
||
if err := t.CancelAllOrders(symbol); err != nil {
|
||
logger.Infof(" ⚠ Failed to cancel pending orders: %v", err)
|
||
}
|
||
|
||
result := make(map[string]interface{})
|
||
result["orderId"] = order.OrderID
|
||
result["symbol"] = order.Symbol
|
||
result["status"] = order.Status
|
||
return result, nil
|
||
}
|
||
|
||
// CloseShort closes a short position
|
||
func (t *FuturesTrader) 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" {
|
||
quantity = -pos["positionAmt"].(float64) // Short position quantity is negative, take absolute value
|
||
break
|
||
}
|
||
}
|
||
|
||
if quantity == 0 {
|
||
return nil, fmt.Errorf("no short position found for %s", symbol)
|
||
}
|
||
}
|
||
|
||
// Format quantity
|
||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Create market buy order (close short, using br ID)
|
||
order, err := t.client.NewCreateOrderService().
|
||
Symbol(symbol).
|
||
Side(futures.SideTypeBuy).
|
||
PositionSide(futures.PositionSideTypeShort).
|
||
Type(futures.OrderTypeMarket).
|
||
Quantity(quantityStr).
|
||
NewClientOrderID(getBrOrderID()).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to close short position: %w", err)
|
||
}
|
||
|
||
logger.Infof("✓ Closed short position successfully: %s quantity: %s", symbol, quantityStr)
|
||
|
||
// After closing position, cancel all pending orders for this symbol (stop-loss and take-profit orders)
|
||
if err := t.CancelAllOrders(symbol); err != nil {
|
||
logger.Infof(" ⚠ Failed to cancel pending orders: %v", err)
|
||
}
|
||
|
||
result := make(map[string]interface{})
|
||
result["orderId"] = order.OrderID
|
||
result["symbol"] = order.Symbol
|
||
result["status"] = order.Status
|
||
return result, nil
|
||
}
|
||
|
||
// CancelStopLossOrders cancels only stop-loss orders (doesn't affect take-profit orders)
|
||
// Now uses both legacy API and new Algo Order API
|
||
func (t *FuturesTrader) CancelStopLossOrders(symbol string) error {
|
||
canceledCount := 0
|
||
var cancelErrors []error
|
||
|
||
// 1. Cancel legacy stop-loss orders
|
||
orders, err := t.client.NewListOpenOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err == nil {
|
||
for _, order := range orders {
|
||
orderType := string(order.Type)
|
||
|
||
// Only cancel stop-loss orders (don't cancel take-profit orders)
|
||
// Use string comparison since OrderType constants were removed in v2.8.9
|
||
if orderType == "STOP_MARKET" || orderType == "STOP" {
|
||
_, err := t.client.NewCancelOrderService().
|
||
Symbol(symbol).
|
||
OrderID(order.OrderID).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||
logger.Infof(" ⚠ Failed to cancel legacy stop-loss order: %s", errMsg)
|
||
continue
|
||
}
|
||
|
||
canceledCount++
|
||
logger.Infof(" ✓ Canceled legacy stop-loss order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Cancel Algo stop-loss orders
|
||
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err == nil {
|
||
for _, algoOrder := range algoOrders {
|
||
// Only cancel stop-loss orders
|
||
if algoOrder.OrderType == futures.AlgoOrderTypeStopMarket || algoOrder.OrderType == futures.AlgoOrderTypeStop {
|
||
_, err := t.client.NewCancelAlgoOrderService().
|
||
AlgoID(algoOrder.AlgoId).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
errMsg := fmt.Sprintf("Algo ID %d: %v", algoOrder.AlgoId, err)
|
||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||
logger.Infof(" ⚠ Failed to cancel Algo stop-loss order: %s", errMsg)
|
||
continue
|
||
}
|
||
|
||
canceledCount++
|
||
logger.Infof(" ✓ Canceled Algo stop-loss order (Algo ID: %d, Type: %s)", algoOrder.AlgoId, algoOrder.OrderType)
|
||
}
|
||
}
|
||
}
|
||
|
||
if canceledCount == 0 && len(cancelErrors) == 0 {
|
||
logger.Infof(" ℹ %s has no stop-loss orders to cancel", symbol)
|
||
} else if canceledCount > 0 {
|
||
logger.Infof(" ✓ Canceled %d stop-loss order(s) for %s", canceledCount, symbol)
|
||
}
|
||
|
||
// If all cancellations failed, return error
|
||
if len(cancelErrors) > 0 && canceledCount == 0 {
|
||
return fmt.Errorf("failed to cancel stop-loss orders: %v", cancelErrors)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CancelTakeProfitOrders cancels only take-profit orders (doesn't affect stop-loss orders)
|
||
// Now uses both legacy API and new Algo Order API
|
||
func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error {
|
||
canceledCount := 0
|
||
var cancelErrors []error
|
||
|
||
// 1. Cancel legacy take-profit orders
|
||
orders, err := t.client.NewListOpenOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err == nil {
|
||
for _, order := range orders {
|
||
orderType := string(order.Type)
|
||
|
||
// Only cancel take-profit orders (don't cancel stop-loss orders)
|
||
// Use string comparison since OrderType constants were removed in v2.8.9
|
||
if orderType == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" {
|
||
_, err := t.client.NewCancelOrderService().
|
||
Symbol(symbol).
|
||
OrderID(order.OrderID).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||
logger.Infof(" ⚠ Failed to cancel legacy take-profit order: %s", errMsg)
|
||
continue
|
||
}
|
||
|
||
canceledCount++
|
||
logger.Infof(" ✓ Canceled legacy take-profit order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Cancel Algo take-profit orders
|
||
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err == nil {
|
||
for _, algoOrder := range algoOrders {
|
||
// Only cancel take-profit orders
|
||
if algoOrder.OrderType == futures.AlgoOrderTypeTakeProfitMarket || algoOrder.OrderType == futures.AlgoOrderTypeTakeProfit {
|
||
_, err := t.client.NewCancelAlgoOrderService().
|
||
AlgoID(algoOrder.AlgoId).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
errMsg := fmt.Sprintf("Algo ID %d: %v", algoOrder.AlgoId, err)
|
||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||
logger.Infof(" ⚠ Failed to cancel Algo take-profit order: %s", errMsg)
|
||
continue
|
||
}
|
||
|
||
canceledCount++
|
||
logger.Infof(" ✓ Canceled Algo take-profit order (Algo ID: %d, Type: %s)", algoOrder.AlgoId, algoOrder.OrderType)
|
||
}
|
||
}
|
||
}
|
||
|
||
if canceledCount == 0 && len(cancelErrors) == 0 {
|
||
logger.Infof(" ℹ %s has no take-profit orders to cancel", symbol)
|
||
} else if canceledCount > 0 {
|
||
logger.Infof(" ✓ Canceled %d take-profit order(s) for %s", canceledCount, symbol)
|
||
}
|
||
|
||
// If all cancellations failed, return error
|
||
if len(cancelErrors) > 0 && canceledCount == 0 {
|
||
return fmt.Errorf("failed to cancel take-profit orders: %v", cancelErrors)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CancelAllOrders cancels all pending orders for this symbol
|
||
// Now uses both legacy API and new Algo Order API
|
||
func (t *FuturesTrader) CancelAllOrders(symbol string) error {
|
||
// 1. Cancel all legacy orders
|
||
err := t.client.NewCancelAllOpenOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
logger.Infof(" ⚠ Failed to cancel legacy orders: %v", err)
|
||
} else {
|
||
logger.Infof(" ✓ Canceled all legacy pending orders for %s", symbol)
|
||
}
|
||
|
||
// 2. Cancel all Algo orders
|
||
err = t.client.NewCancelAllAlgoOpenOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
// Ignore "no algo orders" error
|
||
if !contains(err.Error(), "no algo") && !contains(err.Error(), "No algo") {
|
||
logger.Infof(" ⚠ Failed to cancel Algo orders: %v", err)
|
||
}
|
||
} else {
|
||
logger.Infof(" ✓ Canceled all Algo orders for %s", symbol)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// PlaceLimitOrder places a limit order for grid trading
|
||
// This implements the GridTrader interface for FuturesTrader
|
||
func (t *FuturesTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitOrderResult, error) {
|
||
// Format quantity to correct precision
|
||
quantityStr, err := t.FormatQuantity(req.Symbol, req.Quantity)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to format quantity: %w", err)
|
||
}
|
||
|
||
// Format price to correct precision
|
||
priceStr, err := t.FormatPrice(req.Symbol, req.Price)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to format price: %w", err)
|
||
}
|
||
|
||
// Set leverage if specified
|
||
if req.Leverage > 0 {
|
||
if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil {
|
||
logger.Warnf("Failed to set leverage: %v", err)
|
||
}
|
||
}
|
||
|
||
// Determine side and position side
|
||
var side futures.SideType
|
||
var positionSide futures.PositionSideType
|
||
|
||
if req.Side == "BUY" {
|
||
side = futures.SideTypeBuy
|
||
positionSide = futures.PositionSideTypeLong
|
||
} else {
|
||
side = futures.SideTypeSell
|
||
positionSide = futures.PositionSideTypeShort
|
||
}
|
||
|
||
// Build order service with broker ID
|
||
orderService := t.client.NewCreateOrderService().
|
||
Symbol(req.Symbol).
|
||
Side(side).
|
||
PositionSide(positionSide).
|
||
Type(futures.OrderTypeLimit).
|
||
TimeInForce(futures.TimeInForceTypeGTC).
|
||
Quantity(quantityStr).
|
||
Price(priceStr).
|
||
NewClientOrderID(getBrOrderID())
|
||
|
||
// Execute order
|
||
order, err := orderService.Do(context.Background())
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to place limit order: %w", err)
|
||
}
|
||
|
||
logger.Infof("✓ [Grid] Placed limit order: %s %s %s @ %s, qty=%s, orderID=%d",
|
||
req.Symbol, req.Side, positionSide, priceStr, quantityStr, order.OrderID)
|
||
|
||
return &types.LimitOrderResult{
|
||
OrderID: fmt.Sprintf("%d", order.OrderID),
|
||
ClientID: order.ClientOrderID,
|
||
Symbol: order.Symbol,
|
||
Side: string(order.Side),
|
||
PositionSide: string(order.PositionSide),
|
||
Price: req.Price,
|
||
Quantity: req.Quantity,
|
||
Status: string(order.Status),
|
||
}, nil
|
||
}
|
||
|
||
// CancelOrder cancels a specific order by ID
|
||
// This implements the GridTrader interface for FuturesTrader
|
||
func (t *FuturesTrader) CancelOrder(symbol, orderID string) error {
|
||
// Parse order ID to int64
|
||
orderIDInt, err := strconv.ParseInt(orderID, 10, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("invalid order ID: %w", err)
|
||
}
|
||
|
||
_, err = t.client.NewCancelOrderService().
|
||
Symbol(symbol).
|
||
OrderID(orderIDInt).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("failed to cancel order: %w", err)
|
||
}
|
||
|
||
logger.Infof("✓ [Grid] Cancelled order: %s/%s", symbol, orderID)
|
||
return nil
|
||
}
|
||
|
||
// GetOrderBook gets the order book for a symbol
|
||
// This implements the GridTrader interface for FuturesTrader
|
||
func (t *FuturesTrader) GetOrderBook(symbol string, depth int) (bids, asks [][]float64, err error) {
|
||
book, err := t.client.NewDepthService().
|
||
Symbol(symbol).
|
||
Limit(depth).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("failed to get order book: %w", err)
|
||
}
|
||
|
||
// Convert bids
|
||
bids = make([][]float64, len(book.Bids))
|
||
for i, bid := range book.Bids {
|
||
price, _ := strconv.ParseFloat(bid.Price, 64)
|
||
qty, _ := strconv.ParseFloat(bid.Quantity, 64)
|
||
bids[i] = []float64{price, qty}
|
||
}
|
||
|
||
// Convert asks
|
||
asks = make([][]float64, len(book.Asks))
|
||
for i, ask := range book.Asks {
|
||
price, _ := strconv.ParseFloat(ask.Price, 64)
|
||
qty, _ := strconv.ParseFloat(ask.Quantity, 64)
|
||
asks[i] = []float64{price, qty}
|
||
}
|
||
|
||
return bids, asks, nil
|
||
}
|
||
|
||
// CancelStopOrders cancels take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions)
|
||
// Now uses both legacy API and new Algo Order API (Binance migrated stop orders to Algo system)
|
||
func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||
canceledCount := 0
|
||
|
||
// 1. Cancel legacy stop orders (for backward compatibility)
|
||
orders, err := t.client.NewListOpenOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err == nil {
|
||
for _, order := range orders {
|
||
orderType := string(order.Type)
|
||
|
||
// Only cancel stop-loss and take-profit orders
|
||
// Use string comparison since OrderType constants were removed in v2.8.9
|
||
if orderType == "STOP_MARKET" ||
|
||
orderType == "TAKE_PROFIT_MARKET" ||
|
||
orderType == "STOP" ||
|
||
orderType == "TAKE_PROFIT" {
|
||
|
||
_, err := t.client.NewCancelOrderService().
|
||
Symbol(symbol).
|
||
OrderID(order.OrderID).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
logger.Infof(" ⚠ Failed to cancel legacy order %d: %v", order.OrderID, err)
|
||
continue
|
||
}
|
||
|
||
canceledCount++
|
||
logger.Infof(" ✓ Canceled legacy stop order for %s (Order ID: %d, Type: %s)",
|
||
symbol, order.OrderID, orderType)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Cancel Algo orders (new API)
|
||
err = t.client.NewCancelAllAlgoOpenOrdersService().
|
||
Symbol(symbol).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
// Ignore "no algo orders" error
|
||
if !contains(err.Error(), "no algo") && !contains(err.Error(), "No algo") {
|
||
logger.Infof(" ⚠ Failed to cancel Algo orders: %v", err)
|
||
}
|
||
} else {
|
||
logger.Infof(" ✓ Canceled all Algo orders for %s", symbol)
|
||
canceledCount++
|
||
}
|
||
|
||
if canceledCount == 0 {
|
||
logger.Infof(" ℹ %s has no take-profit/stop-loss orders to cancel", symbol)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetOpenOrders gets all open/pending orders for a symbol
|
||
func (t *FuturesTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, error) {
|
||
var result []types.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, types.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, types.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
|
||
}
|
||
|
||
// SetStopLoss sets stop-loss order using new Algo Order API
|
||
// Binance has migrated stop orders to Algo Order system (error -4120 STOP_ORDER_SWITCH_ALGO)
|
||
func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||
var side futures.SideType
|
||
var posSide futures.PositionSideType
|
||
|
||
if positionSide == "LONG" {
|
||
side = futures.SideTypeSell
|
||
posSide = futures.PositionSideTypeLong
|
||
} else {
|
||
side = futures.SideTypeBuy
|
||
posSide = futures.PositionSideTypeShort
|
||
}
|
||
|
||
// Use new Algo Order API
|
||
_, err := t.client.NewCreateAlgoOrderService().
|
||
Symbol(symbol).
|
||
Side(side).
|
||
PositionSide(posSide).
|
||
Type(futures.AlgoOrderTypeStopMarket).
|
||
TriggerPrice(fmt.Sprintf("%.8f", stopPrice)).
|
||
WorkingType(futures.WorkingTypeContractPrice).
|
||
ClosePosition(true).
|
||
ClientAlgoId(getBrOrderID()).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
||
}
|
||
|
||
logger.Infof(" Stop-loss price set (Algo Order): %.4f", stopPrice)
|
||
return nil
|
||
}
|
||
|
||
// SetTakeProfit sets take-profit order using new Algo Order API
|
||
// Binance has migrated stop orders to Algo Order system (error -4120 STOP_ORDER_SWITCH_ALGO)
|
||
func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||
var side futures.SideType
|
||
var posSide futures.PositionSideType
|
||
|
||
if positionSide == "LONG" {
|
||
side = futures.SideTypeSell
|
||
posSide = futures.PositionSideTypeLong
|
||
} else {
|
||
side = futures.SideTypeBuy
|
||
posSide = futures.PositionSideTypeShort
|
||
}
|
||
|
||
// Use new Algo Order API
|
||
_, err := t.client.NewCreateAlgoOrderService().
|
||
Symbol(symbol).
|
||
Side(side).
|
||
PositionSide(posSide).
|
||
Type(futures.AlgoOrderTypeTakeProfitMarket).
|
||
TriggerPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
||
WorkingType(futures.WorkingTypeContractPrice).
|
||
ClosePosition(true).
|
||
ClientAlgoId(getBrOrderID()).
|
||
Do(context.Background())
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("failed to set take-profit: %w", err)
|
||
}
|
||
|
||
logger.Infof(" Take-profit price set (Algo Order): %.4f", takeProfitPrice)
|
||
return nil
|
||
}
|
||
|
||
// GetOrderStatus gets order status
|
||
func (t *FuturesTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
|
||
// Convert orderID to int64
|
||
orderIDInt, err := strconv.ParseInt(orderID, 10, 64)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("invalid order ID: %s", orderID)
|
||
}
|
||
|
||
order, err := t.client.NewGetOrderService().
|
||
Symbol(symbol).
|
||
OrderID(orderIDInt).
|
||
Do(context.Background())
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to get order status: %w", err)
|
||
}
|
||
|
||
// Parse execution price
|
||
avgPrice, _ := strconv.ParseFloat(order.AvgPrice, 64)
|
||
executedQty, _ := strconv.ParseFloat(order.ExecutedQuantity, 64)
|
||
|
||
result := map[string]interface{}{
|
||
"orderId": order.OrderID,
|
||
"symbol": order.Symbol,
|
||
"status": string(order.Status),
|
||
"avgPrice": avgPrice,
|
||
"executedQty": executedQty,
|
||
"side": string(order.Side),
|
||
"type": string(order.Type),
|
||
"time": order.Time,
|
||
"updateTime": order.UpdateTime,
|
||
}
|
||
|
||
// Binance futures commission fee needs to be obtained through GetUserTrades, not retrieved here for now
|
||
// Can be obtained later through WebSocket or separate query
|
||
result["commission"] = 0.0
|
||
|
||
return result, nil
|
||
}
|