feat: improve order sync and add xyz dex trigger orders

- Add incremental sync for Binance trades using COMMISSION detection and fromId
- Add stop loss and take profit order support for xyz dex assets
- Add pagination for current positions and position history in UI
- Fix chart market type auto-selection based on exchange
This commit is contained in:
tinkle-community
2025-12-30 14:32:51 +08:00
parent 0408bf1f5f
commit ad04994d75
7 changed files with 983 additions and 283 deletions

View File

@@ -1174,3 +1174,73 @@ func (t *FuturesTrader) GetTradesForSymbol(symbol string, startTime time.Time, l
return trades, nil
}
// GetTradesForSymbolFromID retrieves trade history for a specific symbol starting from a given trade ID
// This is used for incremental sync - only fetch new trades since last sync
func (t *FuturesTrader) GetTradesForSymbolFromID(symbol string, fromID int64, limit int) ([]TradeRecord, error) {
if limit <= 0 {
limit = 100
}
if limit > 1000 {
limit = 1000
}
accountTrades, err := t.client.NewListAccountTradeService().
Symbol(symbol).
FromID(fromID).
Limit(limit).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get trade history for %s from ID %d: %w", symbol, fromID, err)
}
var trades []TradeRecord
for _, at := range accountTrades {
price, _ := strconv.ParseFloat(at.Price, 64)
qty, _ := strconv.ParseFloat(at.Quantity, 64)
fee, _ := strconv.ParseFloat(at.Commission, 64)
pnl, _ := strconv.ParseFloat(at.RealizedPnl, 64)
trade := TradeRecord{
TradeID: strconv.FormatInt(at.ID, 10),
Symbol: at.Symbol,
Side: string(at.Side),
PositionSide: string(at.PositionSide),
Price: price,
Quantity: qty,
RealizedPnL: pnl,
Fee: fee,
Time: time.UnixMilli(at.Time),
}
trades = append(trades, trade)
}
return trades, nil
}
// GetCommissionSymbols returns symbols that have new commission records since lastSyncTime
// COMMISSION income is generated for every trade, so this is more reliable than REALIZED_PNL
func (t *FuturesTrader) GetCommissionSymbols(lastSyncTime time.Time) ([]string, error) {
incomes, err := t.client.NewGetIncomeHistoryService().
IncomeType("COMMISSION").
StartTime(lastSyncTime.UnixMilli()).
Limit(1000).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get commission history: %w", err)
}
symbolMap := make(map[string]bool)
for _, income := range incomes {
if income.Symbol != "" {
symbolMap[income.Symbol] = true
}
}
var symbols []string
for symbol := range symbolMap {
symbols = append(symbols, symbol)
}
return symbols, nil
}

View File

@@ -7,46 +7,102 @@ import (
"nofx/store"
"sort"
"strings"
"sync"
"time"
)
// syncState stores the last sync time for incremental sync
var (
binanceSyncState = make(map[string]time.Time) // exchangeID -> lastSyncTime
binanceSyncStateMutex sync.RWMutex
)
// SyncOrdersFromBinance syncs Binance Futures trade history to local database
// Uses COMMISSION detection + fromId for efficient incremental sync
// Also creates/updates position records to ensure orders/fills/positions data consistency
func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string, exchangeType string, st *store.Store) error {
if st == nil {
return fmt.Errorf("store is nil")
}
// Get recent trades (last 24 hours)
startTime := time.Now().Add(-24 * time.Hour)
// Get last sync time (default to 24 hours ago for first sync)
binanceSyncStateMutex.RLock()
lastSyncTime, exists := binanceSyncState[exchangeID]
binanceSyncStateMutex.RUnlock()
logger.Infof("🔄 Syncing Binance trades from: %s", startTime.Format(time.RFC3339))
// Get list of symbols to sync from current positions and recent income
symbols, err := t.getActiveSymbols(startTime)
if err != nil {
return fmt.Errorf("failed to get active symbols: %w", err)
if !exists {
lastSyncTime = time.Now().Add(-24 * time.Hour)
}
if len(symbols) == 0 {
logger.Infof("📭 No active symbols to sync")
// Record current time BEFORE querying, to avoid missing trades during sync
// This prevents race condition where trades happen between query and lastSyncTime update
syncStartTime := time.Now()
logger.Infof("🔄 Syncing Binance trades from: %s", lastSyncTime.Format(time.RFC3339))
// Step 1: Get max trade IDs from local DB for incremental sync
orderStore := st.Order()
maxTradeIDs, err := orderStore.GetMaxTradeIDsByExchange(exchangeID)
if err != nil {
logger.Infof(" ⚠️ Failed to get max trade IDs: %v, will use time-based query", err)
maxTradeIDs = make(map[string]int64)
}
// Step 2: Use COMMISSION to detect which symbols have new trades (1 API call)
changedSymbols, err := t.GetCommissionSymbols(lastSyncTime)
if err != nil {
logger.Infof(" ⚠️ Failed to get commission symbols: %v, falling back to positions", err)
// Fallback: only sync symbols with active positions
changedSymbols = t.getPositionSymbols()
}
if len(changedSymbols) == 0 {
logger.Infof("📭 No symbols with new trades to sync")
// Update last sync time even if no changes
binanceSyncStateMutex.Lock()
binanceSyncState[exchangeID] = syncStartTime
binanceSyncStateMutex.Unlock()
return nil
}
logger.Infof("📊 Found %d symbols to sync: %v", len(symbols), symbols)
logger.Infof("📊 Found %d symbols with new trades: %v", len(changedSymbols), changedSymbols)
// Collect all trades from all symbols
// Step 3: Query trades for changed symbols using fromId (incremental) or time-based (new symbols)
var allTrades []TradeRecord
for _, symbol := range symbols {
trades, err := t.GetTradesForSymbol(symbol, startTime, 500)
if err != nil {
logger.Infof(" ⚠️ Failed to get trades for %s: %v", symbol, err)
var failedSymbols []string
apiCalls := 0
for _, symbol := range changedSymbols {
var trades []TradeRecord
var queryErr error
if lastID, ok := maxTradeIDs[symbol]; ok && lastID > 0 {
// Incremental sync: query from last known trade ID
trades, queryErr = t.GetTradesForSymbolFromID(symbol, lastID+1, 500)
} else {
// New symbol or first sync: query by time
trades, queryErr = t.GetTradesForSymbol(symbol, lastSyncTime, 500)
}
apiCalls++
if queryErr != nil {
logger.Infof(" ⚠️ Failed to get trades for %s: %v", symbol, queryErr)
failedSymbols = append(failedSymbols, symbol)
continue
}
allTrades = append(allTrades, trades...)
}
logger.Infof("📥 Received %d trades from Binance", len(allTrades))
logger.Infof("📥 Received %d trades from Binance (%d API calls)", len(allTrades), apiCalls)
// Only update last sync time if ALL symbols were successfully queried
// This prevents data loss when some symbols fail due to rate limit or network issues
if len(failedSymbols) == 0 {
binanceSyncStateMutex.Lock()
binanceSyncState[exchangeID] = syncStartTime
binanceSyncStateMutex.Unlock()
} else {
logger.Infof(" ⚠️ %d symbols failed, not updating lastSyncTime to retry next time: %v", len(failedSymbols), failedSymbols)
}
if len(allTrades) == 0 {
return nil
@@ -58,7 +114,6 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
})
// Process trades one by one
orderStore := st.Order()
positionStore := st.Position()
posBuilder := store.NewPositionBuilder(positionStore)
syncedCount := 0
@@ -163,36 +218,21 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
return nil
}
// getActiveSymbols returns list of symbols that have positions or recent trades
func (t *FuturesTrader) getActiveSymbols(startTime time.Time) ([]string, error) {
symbolMap := make(map[string]bool)
// Get symbols from current positions
// getPositionSymbols returns list of symbols that have active positions
// Used as fallback when COMMISSION detection fails
func (t *FuturesTrader) getPositionSymbols() []string {
positions, err := t.GetPositions()
if err == nil {
for _, pos := range positions {
if symbol, ok := pos["symbol"].(string); ok && symbol != "" {
symbolMap[symbol] = true
}
}
}
// Get symbols from recent income (REALIZED_PNL = closures)
incomes, err := t.GetTrades(startTime, 500)
if err == nil {
for _, income := range incomes {
if income.Symbol != "" {
symbolMap[income.Symbol] = true
}
}
if err != nil {
return nil
}
var symbols []string
for symbol := range symbolMap {
symbols = append(symbols, symbol)
for _, pos := range positions {
if symbol, ok := pos["symbol"].(string); ok && symbol != "" {
symbols = append(symbols, symbol)
}
}
return symbols, nil
return symbols
}
// determineOrderAction determines the order action based on trade data

View File

@@ -1035,7 +1035,15 @@ func (t *HyperliquidTrader) CancelTakeProfitOrders(symbol string) error {
func (t *HyperliquidTrader) CancelAllOrders(symbol string) error {
coin := convertSymbolToHyperliquid(symbol)
// Get all pending orders
// Check if this is an xyz dex asset
isXyz := strings.HasPrefix(coin, "xyz:")
if isXyz {
// xyz dex orders - use direct API call
return t.cancelXyzOrders(coin)
}
// Standard crypto orders
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
if err != nil {
return fmt.Errorf("failed to get pending orders: %w", err)
@@ -1059,7 +1067,15 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error {
func (t *HyperliquidTrader) CancelStopOrders(symbol string) error {
coin := convertSymbolToHyperliquid(symbol)
// Get all pending orders
// Check if this is an xyz dex asset
isXyz := strings.HasPrefix(coin, "xyz:")
if isXyz {
// xyz dex orders - use direct API call
return t.cancelXyzOrders(coin)
}
// Get all pending orders for standard crypto
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
if err != nil {
return fmt.Errorf("failed to get pending orders: %w", err)
@@ -1089,6 +1105,148 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error {
return nil
}
// cancelXyzOrders cancels all pending orders for xyz dex assets (stocks, forex, commodities)
func (t *HyperliquidTrader) cancelXyzOrders(coin string) error {
// Query xyz dex open orders
reqBody := map[string]interface{}{
"type": "openOrders",
"user": t.walletAddr,
"dex": "xyz",
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := "https://api.hyperliquid.xyz/info"
req, err := http.NewRequestWithContext(t.ctx, "POST", apiURL, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("xyz dex openOrders API error (status %d): %s", resp.StatusCode, string(body))
}
// Parse open orders
var openOrders []struct {
Coin string `json:"coin"`
Oid int64 `json:"oid"`
}
if err := json.Unmarshal(body, &openOrders); err != nil {
return fmt.Errorf("failed to parse open orders: %w", err)
}
// Filter orders for this coin and cancel them
canceledCount := 0
for _, order := range openOrders {
if order.Coin == coin {
if err := t.cancelXyzOrder(order.Oid); err != nil {
logger.Infof(" ⚠ Failed to cancel xyz dex order (oid=%d): %v", order.Oid, err)
continue
}
canceledCount++
}
}
if canceledCount == 0 {
logger.Infof(" No pending xyz dex orders to cancel for %s", coin)
} else {
logger.Infof(" ✓ Cancelled %d xyz dex orders for %s", canceledCount, coin)
}
return nil
}
// cancelXyzOrder cancels a single xyz dex order by oid
func (t *HyperliquidTrader) cancelXyzOrder(oid int64) error {
// Get asset index for this order (we need it for cancel action)
// For cancel, we construct a cancel action with the oid
action := map[string]interface{}{
"type": "cancel",
"cancels": []map[string]interface{}{
{
"a": oid, // asset index not needed for cancel by oid in xyz dex
"o": oid,
},
},
}
// Sign the action
nonce := time.Now().UnixMilli()
isMainnet := !t.isTestnet
vaultAddress := ""
sig, err := hyperliquid.SignL1Action(t.privateKey, action, vaultAddress, nonce, nil, isMainnet)
if err != nil {
return fmt.Errorf("failed to sign cancel action: %w", err)
}
payload := map[string]any{
"action": action,
"nonce": nonce,
"signature": sig,
}
apiURL := hyperliquid.MainnetAPIURL
if t.isTestnet {
apiURL = hyperliquid.TestnetAPIURL
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
req, err := http.NewRequestWithContext(t.ctx, http.MethodPost, apiURL+"/exchange", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
// Check response
var result struct {
Status string `json:"status"`
}
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("failed to parse response: %w", err)
}
if result.Status != "ok" {
return fmt.Errorf("cancel failed: %s", string(body))
}
return nil
}
// GetMarketPrice gets market price (supports both crypto and xyz dex assets)
func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) {
coin := convertSymbolToHyperliquid(symbol)
@@ -1377,37 +1535,206 @@ func (t *HyperliquidTrader) getXyzAssetIndex(baseCoin string) int {
return -1
}
// placeXyzTriggerOrder places a trigger order (stop loss / take profit) on the xyz dex
// tpsl: "sl" for stop loss, "tp" for take profit
func (t *HyperliquidTrader) placeXyzTriggerOrder(coin string, isBuy bool, size float64, triggerPrice float64, tpsl string) error {
// Fetch xyz meta if not cached
t.xyzMetaMutex.RLock()
hasMeta := t.xyzMeta != nil
t.xyzMetaMutex.RUnlock()
if !hasMeta {
if err := t.fetchXyzMeta(); err != nil {
return fmt.Errorf("failed to fetch xyz meta: %w", err)
}
}
// Get asset index from xyz meta (returns 0-based index)
metaIndex := t.getXyzAssetIndex(coin)
if metaIndex < 0 {
return fmt.Errorf("xyz asset %s not found in meta", coin)
}
// HIP-3 perp dex asset index formula: 100000 + perp_dex_index * 10000 + index_in_meta
// xyz dex is at perp_dex_index = 1
const xyzPerpDexIndex = 1
assetIndex := 100000 + xyzPerpDexIndex*10000 + metaIndex
// Round size to correct precision
szDecimals := t.getXyzSzDecimals(coin)
multiplier := 1.0
for i := 0; i < szDecimals; i++ {
multiplier *= 10.0
}
roundedSize := float64(int(size*multiplier+0.5)) / multiplier
// Round price to 5 significant figures
roundedPrice := t.roundPriceToSigfigs(triggerPrice)
logger.Infof("📝 Placing xyz dex %s order: %s %s size=%.4f triggerPrice=%.4f assetIndex=%d",
tpsl,
map[bool]string{true: "BUY", false: "SELL"}[isBuy],
coin, roundedSize, roundedPrice, assetIndex)
// Construct OrderWire with trigger type for stop loss / take profit
orderWire := hyperliquid.OrderWire{
Asset: assetIndex,
IsBuy: isBuy,
LimitPx: floatToWireStr(roundedPrice),
Size: floatToWireStr(roundedSize),
ReduceOnly: true, // TP/SL orders are always reduce-only
OrderType: hyperliquid.OrderWireType{
Trigger: &hyperliquid.OrderWireTypeTrigger{
TriggerPx: floatToWireStr(roundedPrice),
IsMarket: true,
Tpsl: hyperliquid.Tpsl(tpsl), // "sl" or "tp" - convert string to Tpsl type
},
},
}
// Create OrderAction with builder
action := hyperliquid.OrderAction{
Type: "order",
Orders: []hyperliquid.OrderWire{orderWire},
Grouping: "na",
Builder: &hyperliquid.BuilderInfo{
Builder: "0x891dc6f05ad47a3c1a05da55e7a7517971faaf0d",
Fee: 10,
},
}
// Sign the action
nonce := time.Now().UnixMilli()
isMainnet := !t.isTestnet
vaultAddress := ""
sig, err := hyperliquid.SignL1Action(t.privateKey, action, vaultAddress, nonce, nil, isMainnet)
if err != nil {
return fmt.Errorf("failed to sign xyz dex trigger order: %w", err)
}
// Construct payload for /exchange endpoint
payload := map[string]any{
"action": action,
"nonce": nonce,
"signature": sig,
}
// Determine API URL
apiURL := hyperliquid.MainnetAPIURL
if t.isTestnet {
apiURL = hyperliquid.TestnetAPIURL
}
// POST to /exchange
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
logger.Infof("📤 Sending xyz dex %s order to %s/exchange", tpsl, apiURL)
req, err := http.NewRequestWithContext(t.ctx, http.MethodPost, apiURL+"/exchange", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
// Parse response
var result struct {
Status string `json:"status"`
Response struct {
Type string `json:"type"`
Data struct {
Statuses []struct {
Resting *struct {
Oid int64 `json:"oid"`
} `json:"resting,omitempty"`
Error *string `json:"error,omitempty"`
} `json:"statuses"`
} `json:"data"`
} `json:"response"`
}
if err := json.Unmarshal(body, &result); err != nil {
logger.Infof("⚠️ Failed to parse response, raw body: %s", string(body))
return fmt.Errorf("xyz dex %s order failed, status=%d, body=%s", tpsl, resp.StatusCode, string(body))
}
// Check for errors in response
if result.Status != "ok" {
return fmt.Errorf("xyz dex %s order failed: status=%s, body=%s", tpsl, result.Status, string(body))
}
// Check order statuses
if len(result.Response.Data.Statuses) > 0 {
status := result.Response.Data.Statuses[0]
if status.Error != nil {
return fmt.Errorf("xyz dex %s order error: %s", tpsl, *status.Error)
}
if status.Resting != nil {
logger.Infof("✅ xyz dex %s order placed: oid=%d", tpsl, status.Resting.Oid)
}
}
logger.Infof("✅ xyz dex %s order placed successfully: %s", tpsl, coin)
return nil
}
// SetStopLoss sets stop loss order
func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
coin := convertSymbolToHyperliquid(symbol)
isBuy := positionSide == "SHORT" // Short position stop loss = buy, long position stop loss = sell
// ⚠️ Critical: Round quantity according to coin precision requirements
roundedQuantity := t.roundToSzDecimals(coin, quantity)
// ⚠️ Critical: Price also needs to be processed to 5 significant figures
// ⚠️ Critical: Price needs to be processed to 5 significant figures
roundedStopPrice := t.roundPriceToSigfigs(stopPrice)
// Create stop loss order (Trigger Order)
order := hyperliquid.CreateOrderRequest{
Coin: coin,
IsBuy: isBuy,
Size: roundedQuantity, // Use rounded quantity
Price: roundedStopPrice, // Use processed price
OrderType: hyperliquid.OrderType{
Trigger: &hyperliquid.TriggerOrderType{
TriggerPx: roundedStopPrice,
IsMarket: true,
Tpsl: "sl", // stop loss
},
},
ReduceOnly: true,
}
// Check if this is an xyz dex asset (stocks, forex, commodities)
isXyz := strings.HasPrefix(coin, "xyz:")
_, err := t.exchange.Order(t.ctx, order, defaultBuilder)
if err != nil {
return fmt.Errorf("failed to set stop loss: %w", err)
if isXyz {
// xyz dex stop loss order - use direct API call similar to placeXyzOrder
if err := t.placeXyzTriggerOrder(coin, isBuy, quantity, roundedStopPrice, "sl"); err != nil {
return fmt.Errorf("failed to set xyz dex stop loss: %w", err)
}
} else {
// Standard crypto stop loss order
// ⚠️ Critical: Round quantity according to coin precision requirements
roundedQuantity := t.roundToSzDecimals(coin, quantity)
// Create stop loss order (Trigger Order)
order := hyperliquid.CreateOrderRequest{
Coin: coin,
IsBuy: isBuy,
Size: roundedQuantity, // Use rounded quantity
Price: roundedStopPrice, // Use processed price
OrderType: hyperliquid.OrderType{
Trigger: &hyperliquid.TriggerOrderType{
TriggerPx: roundedStopPrice,
IsMarket: true,
Tpsl: "sl", // stop loss
},
},
ReduceOnly: true,
}
_, err := t.exchange.Order(t.ctx, order, defaultBuilder)
if err != nil {
return fmt.Errorf("failed to set stop loss: %w", err)
}
}
logger.Infof(" Stop loss price set: %.4f", roundedStopPrice)
@@ -1420,31 +1747,42 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu
isBuy := positionSide == "SHORT" // Short position take profit = buy, long position take profit = sell
// ⚠️ Critical: Round quantity according to coin precision requirements
roundedQuantity := t.roundToSzDecimals(coin, quantity)
// ⚠️ Critical: Price also needs to be processed to 5 significant figures
// ⚠️ Critical: Price needs to be processed to 5 significant figures
roundedTakeProfitPrice := t.roundPriceToSigfigs(takeProfitPrice)
// Create take profit order (Trigger Order)
order := hyperliquid.CreateOrderRequest{
Coin: coin,
IsBuy: isBuy,
Size: roundedQuantity, // Use rounded quantity
Price: roundedTakeProfitPrice, // Use processed price
OrderType: hyperliquid.OrderType{
Trigger: &hyperliquid.TriggerOrderType{
TriggerPx: roundedTakeProfitPrice,
IsMarket: true,
Tpsl: "tp", // take profit
},
},
ReduceOnly: true,
}
// Check if this is an xyz dex asset (stocks, forex, commodities)
isXyz := strings.HasPrefix(coin, "xyz:")
_, err := t.exchange.Order(t.ctx, order, defaultBuilder)
if err != nil {
return fmt.Errorf("failed to set take profit: %w", err)
if isXyz {
// xyz dex take profit order - use direct API call similar to placeXyzOrder
if err := t.placeXyzTriggerOrder(coin, isBuy, quantity, roundedTakeProfitPrice, "tp"); err != nil {
return fmt.Errorf("failed to set xyz dex take profit: %w", err)
}
} else {
// Standard crypto take profit order
// ⚠️ Critical: Round quantity according to coin precision requirements
roundedQuantity := t.roundToSzDecimals(coin, quantity)
// Create take profit order (Trigger Order)
order := hyperliquid.CreateOrderRequest{
Coin: coin,
IsBuy: isBuy,
Size: roundedQuantity, // Use rounded quantity
Price: roundedTakeProfitPrice, // Use processed price
OrderType: hyperliquid.OrderType{
Trigger: &hyperliquid.TriggerOrderType{
TriggerPx: roundedTakeProfitPrice,
IsMarket: true,
Tpsl: "tp", // take profit
},
},
ReduceOnly: true,
}
_, err := t.exchange.Order(t.ctx, order, defaultBuilder)
if err != nil {
return fmt.Errorf("failed to set take profit: %w", err)
}
}
logger.Infof(" Take profit price set: %.4f", roundedTakeProfitPrice)