mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 23:36:55 +08:00
feat: upgrade Binance to Algo Order API and improve trading flow
- Upgrade go-binance to v2.8.9 with new Algo Order API - Migrate SetStopLoss/SetTakeProfit to use AlgoOrderTypeStopMarket/TakeProfitMarket - Update cancel functions to handle both legacy and Algo orders - Fix Lighter stop orders using correct order types (type=2/4) with TriggerPrice - Add CancelAllOrders before opening positions for Bybit and Lighter - Fix decision limit selector in API handler - Add stop_loss/take_profit/confidence fields to DecisionAction - Store decisions array in database with proper serialization - Redesign DecisionCard with beautiful entry/SL/TP display
This commit is contained in:
@@ -528,13 +528,17 @@ func (at *AutoTrader) runCycle() error {
|
||||
// Execute decisions and record results
|
||||
for _, d := range sortedDecisions {
|
||||
actionRecord := store.DecisionAction{
|
||||
Action: d.Action,
|
||||
Symbol: d.Symbol,
|
||||
Quantity: 0,
|
||||
Leverage: d.Leverage,
|
||||
Price: 0,
|
||||
Timestamp: time.Now(),
|
||||
Success: false,
|
||||
Action: d.Action,
|
||||
Symbol: d.Symbol,
|
||||
Quantity: 0,
|
||||
Leverage: d.Leverage,
|
||||
Price: 0,
|
||||
StopLoss: d.StopLoss,
|
||||
TakeProfit: d.TakeProfit,
|
||||
Confidence: d.Confidence,
|
||||
Reasoning: d.Reasoning,
|
||||
Timestamp: time.Now(),
|
||||
Success: false,
|
||||
}
|
||||
|
||||
if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil {
|
||||
@@ -816,9 +820,13 @@ func (at *AutoTrader) ExecuteDecision(d *decision.Decision) error {
|
||||
|
||||
// Create a minimal action record for tracking
|
||||
actionRecord := &store.DecisionAction{
|
||||
Symbol: d.Symbol,
|
||||
Action: d.Action,
|
||||
Leverage: d.Leverage,
|
||||
Symbol: d.Symbol,
|
||||
Action: d.Action,
|
||||
Leverage: d.Leverage,
|
||||
StopLoss: d.StopLoss,
|
||||
TakeProfit: d.TakeProfit,
|
||||
Confidence: d.Confidence,
|
||||
Reasoning: d.Reasoning,
|
||||
}
|
||||
|
||||
// Execute the decision
|
||||
|
||||
@@ -534,38 +534,64 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Get all open orders for this symbol
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
|
||||
// 1. Cancel legacy stop-loss orders
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get open orders: %w", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out stop-loss orders and cancel them (cancel all directions including LONG and SHORT)
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
// 2. Cancel Algo stop-loss orders
|
||||
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
// Only cancel stop-loss orders (don't cancel take-profit orders)
|
||||
if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeStop {
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
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("Order ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel stop-loss order: %s", errMsg)
|
||||
continue
|
||||
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)
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled stop-loss order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,38 +610,64 @@ func (t *FuturesTrader) CancelStopLossOrders(symbol string) error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Get all open orders for this symbol
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
|
||||
// 1. Cancel legacy take-profit orders
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get open orders: %w", err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out take-profit orders and cancel them (cancel all directions including LONG and SHORT)
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
// 2. Cancel Algo take-profit orders
|
||||
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
// Only cancel take-profit orders (don't cancel stop-loss orders)
|
||||
if orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeTakeProfit {
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
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("Order ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel take-profit order: %s", errMsg)
|
||||
continue
|
||||
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)
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled take-profit order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,61 +686,91 @@ func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to cancel pending orders: %w", err)
|
||||
logger.Infof(" ⚠ Failed to cancel legacy orders: %v", err)
|
||||
} else {
|
||||
logger.Infof(" ✓ Canceled all legacy pending orders for %s", symbol)
|
||||
}
|
||||
|
||||
logger.Infof(" ✓ Canceled all pending orders for %s", symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopOrders cancels take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions)
|
||||
func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||||
// Get all open orders for this symbol
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
// 2. Cancel all Algo orders
|
||||
err = t.client.NewCancelAllAlgoOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get open orders: %w", err)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Filter out take-profit and stop-loss orders and cancel them
|
||||
return 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
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
|
||||
// Only cancel stop-loss and take-profit orders
|
||||
if orderType == futures.OrderTypeStopMarket ||
|
||||
orderType == futures.OrderTypeTakeProfitMarket ||
|
||||
orderType == futures.OrderTypeStop ||
|
||||
orderType == futures.OrderTypeTakeProfit {
|
||||
// 1. Cancel legacy stop orders (for backward compatibility)
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
if err == nil {
|
||||
for _, order := range orders {
|
||||
orderType := string(order.Type)
|
||||
|
||||
if err != nil {
|
||||
logger.Infof(" ⚠ Failed to cancel order %d: %v", order.OrderID, err)
|
||||
continue
|
||||
// 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)
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled take-profit/stop-loss 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)
|
||||
} else {
|
||||
logger.Infof(" ✓ Canceled %d take-profit/stop-loss order(s) for %s", canceledCount, symbol)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -721,7 +803,8 @@ func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float6
|
||||
return quantity
|
||||
}
|
||||
|
||||
// SetStopLoss sets stop-loss order
|
||||
// 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
|
||||
@@ -734,33 +817,28 @@ func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity
|
||||
posSide = futures.PositionSideTypeShort
|
||||
}
|
||||
|
||||
// Format quantity
|
||||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.client.NewCreateOrderService().
|
||||
// Use new Algo Order API
|
||||
_, err := t.client.NewCreateAlgoOrderService().
|
||||
Symbol(symbol).
|
||||
Side(side).
|
||||
PositionSide(posSide).
|
||||
Type(futures.OrderTypeStopMarket).
|
||||
StopPrice(fmt.Sprintf("%.8f", stopPrice)).
|
||||
Quantity(quantityStr).
|
||||
Type(futures.AlgoOrderTypeStopMarket).
|
||||
TriggerPrice(fmt.Sprintf("%.8f", stopPrice)).
|
||||
WorkingType(futures.WorkingTypeContractPrice).
|
||||
ClosePosition(true).
|
||||
NewClientOrderID(getBrOrderID()).
|
||||
ClientAlgoId(getBrOrderID()).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof(" Stop-loss price set: %.4f", stopPrice)
|
||||
logger.Infof(" Stop-loss price set (Algo Order): %.4f", stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTakeProfit sets take-profit order
|
||||
// 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
|
||||
@@ -773,29 +851,23 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti
|
||||
posSide = futures.PositionSideTypeShort
|
||||
}
|
||||
|
||||
// Format quantity
|
||||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.client.NewCreateOrderService().
|
||||
// Use new Algo Order API
|
||||
_, err := t.client.NewCreateAlgoOrderService().
|
||||
Symbol(symbol).
|
||||
Side(side).
|
||||
PositionSide(posSide).
|
||||
Type(futures.OrderTypeTakeProfitMarket).
|
||||
StopPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
||||
Quantity(quantityStr).
|
||||
Type(futures.AlgoOrderTypeTakeProfitMarket).
|
||||
TriggerPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
||||
WorkingType(futures.WorkingTypeContractPrice).
|
||||
ClosePosition(true).
|
||||
NewClientOrderID(getBrOrderID()).
|
||||
ClientAlgoId(getBrOrderID()).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set take-profit: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof(" Take-profit price set: %.4f", takeProfitPrice)
|
||||
logger.Infof(" Take-profit price set (Algo Order): %.4f", takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,15 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
logger.Infof("[Bybit] ===== OpenLong called: symbol=%s, qty=%.6f, leverage=%d =====", symbol, quantity, leverage)
|
||||
|
||||
// First cancel all pending orders for this symbol (clean up old orders)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old pending orders: %v", err)
|
||||
}
|
||||
// Also cancel conditional orders (stop-loss/take-profit) - Bybit keeps them separate
|
||||
if err := t.CancelStopOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old stop orders: %v", err)
|
||||
}
|
||||
|
||||
// Set leverage first
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
||||
@@ -314,6 +323,15 @@ func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (m
|
||||
func (t *BybitTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
logger.Infof("[Bybit] ===== OpenShort called: symbol=%s, qty=%.6f, leverage=%d =====", symbol, quantity, leverage)
|
||||
|
||||
// First cancel all pending orders for this symbol (clean up old orders)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old pending orders: %v", err)
|
||||
}
|
||||
// Also cancel conditional orders (stop-loss/take-profit) - Bybit keeps them separate
|
||||
if err := t.CancelStopOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old stop orders: %v", err)
|
||||
}
|
||||
|
||||
// Set leverage first
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
||||
|
||||
@@ -14,44 +14,46 @@ import (
|
||||
)
|
||||
|
||||
// SetStopLoss Set stop-loss order (implements Trader interface)
|
||||
// IMPORTANT: Uses StopLossOrder type (type=2) with TriggerPrice, NOT regular limit order
|
||||
func (t *LighterTraderV2) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||
if t.txClient == nil {
|
||||
return fmt.Errorf("TxClient not initialized")
|
||||
}
|
||||
|
||||
logger.Infof("🛑 LIGHTER Setting stop-loss: %s %s qty=%.4f, stop=%.2f", symbol, positionSide, quantity, stopPrice)
|
||||
logger.Infof("🛑 LIGHTER Setting stop-loss: %s %s qty=%.4f, trigger=%.2f", symbol, positionSide, quantity, stopPrice)
|
||||
|
||||
// Determine order direction (short position uses buy order, long position uses sell order)
|
||||
// Determine order direction (long position uses sell order, short position uses buy order)
|
||||
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||
|
||||
// Create limit stop-loss order
|
||||
_, err := t.CreateOrder(symbol, isAsk, quantity, stopPrice, "limit")
|
||||
// Create stop-loss order with TriggerPrice (type=2: StopLossOrder)
|
||||
_, err := t.CreateStopOrder(symbol, isAsk, quantity, stopPrice, "stop_loss")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("✓ LIGHTER stop-loss set: %.2f", stopPrice)
|
||||
logger.Infof("✓ LIGHTER stop-loss set: trigger=%.2f", stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTakeProfit Set take-profit order (implements Trader interface)
|
||||
// IMPORTANT: Uses TakeProfitOrder type (type=4) with TriggerPrice, NOT regular limit order
|
||||
func (t *LighterTraderV2) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||
if t.txClient == nil {
|
||||
return fmt.Errorf("TxClient not initialized")
|
||||
}
|
||||
|
||||
logger.Infof("🎯 LIGHTER Setting take-profit: %s %s qty=%.4f, tp=%.2f", symbol, positionSide, quantity, takeProfitPrice)
|
||||
logger.Infof("🎯 LIGHTER Setting take-profit: %s %s qty=%.4f, trigger=%.2f", symbol, positionSide, quantity, takeProfitPrice)
|
||||
|
||||
// Determine order direction (short position uses buy order, long position uses sell order)
|
||||
// Determine order direction (long position uses sell order, short position uses buy order)
|
||||
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||
|
||||
// Create limit take-profit order
|
||||
_, err := t.CreateOrder(symbol, isAsk, quantity, takeProfitPrice, "limit")
|
||||
// Create take-profit order with TriggerPrice (type=4: TakeProfitOrder)
|
||||
_, err := t.CreateStopOrder(symbol, isAsk, quantity, takeProfitPrice, "take_profit")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set take-profit: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("✓ LIGHTER take-profit set: %.2f", takeProfitPrice)
|
||||
logger.Infof("✓ LIGHTER take-profit set: trigger=%.2f", takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,23 @@ func (t *LighterTraderV2) OpenLong(symbol string, quantity float64, leverage int
|
||||
|
||||
logger.Infof("📈 LIGHTER opening long: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
|
||||
// 1. Set leverage (if needed)
|
||||
// 1. 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: %v", err)
|
||||
}
|
||||
|
||||
// 2. Set leverage (if needed)
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
||||
}
|
||||
|
||||
// 2. Get market price
|
||||
// 3. Get market price
|
||||
marketPrice, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market price: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create market buy order (open long)
|
||||
// 4. Create market buy order (open long)
|
||||
orderResult, err := t.CreateOrder(symbol, false, quantity, 0, "market")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open long: %w", err)
|
||||
@@ -59,18 +64,23 @@ func (t *LighterTraderV2) OpenShort(symbol string, quantity float64, leverage in
|
||||
|
||||
logger.Infof("📉 LIGHTER opening short: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
|
||||
// 1. Set leverage
|
||||
// 1. 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: %v", err)
|
||||
}
|
||||
|
||||
// 2. Set leverage
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
||||
}
|
||||
|
||||
// 2. Get market price
|
||||
// 3. Get market price
|
||||
marketPrice, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market price: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create market sell order (open short)
|
||||
// 4. Create market sell order (open short)
|
||||
orderResult, err := t.CreateOrder(symbol, true, quantity, 0, "market")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open short: %w", err)
|
||||
@@ -563,6 +573,107 @@ func (t *LighterTraderV2) SetMarginMode(symbol string, isCrossMargin bool) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStopOrder Create stop-loss or take-profit order with TriggerPrice
|
||||
// Order types: "stop_loss" (type=2), "take_profit" (type=4)
|
||||
func (t *LighterTraderV2) CreateStopOrder(symbol string, isAsk bool, quantity float64, triggerPrice float64, orderType string) (map[string]interface{}, error) {
|
||||
if t.txClient == nil {
|
||||
return nil, fmt.Errorf("TxClient not initialized")
|
||||
}
|
||||
|
||||
// Get market index
|
||||
marketIndexU16, err := t.getMarketIndex(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market index: %w", err)
|
||||
}
|
||||
marketIndex := uint8(marketIndexU16)
|
||||
|
||||
// Build order request
|
||||
clientOrderIndex := time.Now().UnixMilli() % 281474976710655
|
||||
|
||||
// Order type: StopLossOrder=2, TakeProfitOrder=4
|
||||
var orderTypeValue uint8 = 2 // Default: StopLossOrder
|
||||
if orderType == "take_profit" {
|
||||
orderTypeValue = 4 // TakeProfitOrder
|
||||
}
|
||||
|
||||
// Convert quantity to base amount
|
||||
sizeDecimals := 4
|
||||
normalizedSymbol := normalizeSymbol(symbol)
|
||||
switch normalizedSymbol {
|
||||
case "BTC":
|
||||
sizeDecimals = 5
|
||||
case "SOL":
|
||||
sizeDecimals = 3
|
||||
case "ETH":
|
||||
sizeDecimals = 4
|
||||
}
|
||||
baseAmount := int64(quantity * float64(pow10(sizeDecimals)))
|
||||
|
||||
// TriggerPrice: price precision is 2 decimals (multiply by 100)
|
||||
triggerPriceValue := uint32(triggerPrice * 1e2)
|
||||
|
||||
// For stop orders, Price should be set to a reasonable execution price
|
||||
// Stop-loss sell: price slightly below trigger (95% of trigger)
|
||||
// Take-profit sell: price slightly below trigger (95% of trigger)
|
||||
// Stop-loss buy: price slightly above trigger (105% of trigger)
|
||||
// Take-profit buy: price slightly above trigger (105% of trigger)
|
||||
var priceValue uint32
|
||||
if isAsk {
|
||||
// Sell order - set price at 95% of trigger to ensure execution
|
||||
priceValue = uint32(triggerPrice * 0.95 * 1e2)
|
||||
} else {
|
||||
// Buy order - set price at 105% of trigger to ensure execution
|
||||
priceValue = uint32(triggerPrice * 1.05 * 1e2)
|
||||
}
|
||||
|
||||
// Stop orders use GoodTillTime with expiry
|
||||
orderExpiry := time.Now().Add(30 * 24 * time.Hour).UnixMilli() // 30 days
|
||||
|
||||
txReq := &types.CreateOrderTxReq{
|
||||
MarketIndex: marketIndex,
|
||||
ClientOrderIndex: clientOrderIndex,
|
||||
BaseAmount: baseAmount,
|
||||
Price: priceValue,
|
||||
IsAsk: boolToUint8(isAsk),
|
||||
Type: orderTypeValue,
|
||||
TimeInForce: 1, // GoodTillTime
|
||||
ReduceOnly: 1, // Stop orders should be reduce-only
|
||||
TriggerPrice: triggerPriceValue,
|
||||
OrderExpiry: orderExpiry,
|
||||
}
|
||||
|
||||
// Sign transaction
|
||||
nonce := int64(-1)
|
||||
tx, err := t.txClient.GetCreateOrderTransaction(txReq, &types.TransactOpts{
|
||||
Nonce: &nonce,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign stop order: %w", err)
|
||||
}
|
||||
|
||||
// Get tx_info
|
||||
txInfo, err := tx.GetTxInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tx info: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("DEBUG stop order - type: %d, trigger: %.2f, price: %.2f, isAsk: %v", orderTypeValue, triggerPrice, float64(priceValue)/100, isAsk)
|
||||
|
||||
// Submit order
|
||||
orderResp, err := t.submitOrder(int(tx.GetTxType()), txInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to submit stop order: %w", err)
|
||||
}
|
||||
|
||||
side := "buy"
|
||||
if isAsk {
|
||||
side = "sell"
|
||||
}
|
||||
logger.Infof("✓ LIGHTER %s order created: %s %s qty=%.4f trigger=%.2f", orderType, symbol, side, quantity, triggerPrice)
|
||||
|
||||
return orderResp, nil
|
||||
}
|
||||
|
||||
// boolToUint8 Convert boolean to uint8
|
||||
func boolToUint8(b bool) uint8 {
|
||||
if b {
|
||||
|
||||
Reference in New Issue
Block a user