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:
tinkle-community
2025-12-15 21:22:22 +08:00
parent aeede956e6
commit 3f084005e4
12 changed files with 699 additions and 270 deletions

View File

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

View File

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

View File

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

View File

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

View File

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