mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
refactor: split large files and clean up project structure
- 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)
This commit is contained in:
565
api/handler_trader_status.go
Normal file
565
api/handler_trader_status.go
Normal file
@@ -0,0 +1,565 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nofx/logger"
|
||||
"nofx/store"
|
||||
"nofx/trader"
|
||||
"nofx/trader/aster"
|
||||
"nofx/trader/binance"
|
||||
"nofx/trader/bitget"
|
||||
"nofx/trader/bybit"
|
||||
"nofx/trader/gate"
|
||||
hyperliquidtrader "nofx/trader/hyperliquid"
|
||||
"nofx/trader/kucoin"
|
||||
"nofx/trader/lighter"
|
||||
"nofx/trader/okx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// handleGetGridRiskInfo returns current risk information for a grid trader
|
||||
func (s *Server) handleGetGridRiskInfo(c *gin.Context) {
|
||||
traderID := c.Param("id")
|
||||
|
||||
autoTrader, err := s.traderManager.GetTrader(traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "trader not found"})
|
||||
return
|
||||
}
|
||||
|
||||
riskInfo := autoTrader.GetGridRiskInfo()
|
||||
c.JSON(http.StatusOK, riskInfo)
|
||||
}
|
||||
|
||||
// handleSyncBalance Sync exchange balance to initial_balance (Option B: Manual Sync + Option C: Smart Detection)
|
||||
func (s *Server) handleSyncBalance(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
traderID := c.Param("id")
|
||||
|
||||
logger.Infof("🔄 User %s requested balance sync for trader %s", userID, traderID)
|
||||
|
||||
// Get trader configuration from database (including exchange info)
|
||||
fullConfig, err := s.store.Trader().GetFullConfig(userID, traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
traderConfig := fullConfig.Trader
|
||||
exchangeCfg := fullConfig.Exchange
|
||||
|
||||
if exchangeCfg == nil || !exchangeCfg.Enabled {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Exchange not configured or not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create temporary trader to query balance
|
||||
var tempTrader trader.Trader
|
||||
var createErr error
|
||||
|
||||
// Use ExchangeType (e.g., "binance") instead of ExchangeID (which is now UUID)
|
||||
// Convert EncryptedString fields to string
|
||||
switch exchangeCfg.ExchangeType {
|
||||
case "binance":
|
||||
tempTrader = binance.NewFuturesTrader(string(exchangeCfg.APIKey), string(exchangeCfg.SecretKey), userID)
|
||||
case "hyperliquid":
|
||||
tempTrader, createErr = hyperliquidtrader.NewHyperliquidTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
exchangeCfg.HyperliquidWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
exchangeCfg.HyperliquidUnifiedAcct,
|
||||
)
|
||||
case "aster":
|
||||
tempTrader, createErr = aster.NewAsterTrader(
|
||||
exchangeCfg.AsterUser,
|
||||
exchangeCfg.AsterSigner,
|
||||
string(exchangeCfg.AsterPrivateKey),
|
||||
)
|
||||
case "bybit":
|
||||
tempTrader = bybit.NewBybitTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
)
|
||||
case "okx":
|
||||
tempTrader = okx.NewOKXTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
string(exchangeCfg.Passphrase),
|
||||
)
|
||||
case "bitget":
|
||||
tempTrader = bitget.NewBitgetTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
string(exchangeCfg.Passphrase),
|
||||
)
|
||||
case "gate":
|
||||
tempTrader = gate.NewGateTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
)
|
||||
case "kucoin":
|
||||
tempTrader = kucoin.NewKuCoinTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
string(exchangeCfg.Passphrase),
|
||||
)
|
||||
case "lighter":
|
||||
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
|
||||
// Lighter only supports mainnet
|
||||
tempTrader, createErr = lighter.NewLighterTraderV2(
|
||||
exchangeCfg.LighterWalletAddr,
|
||||
string(exchangeCfg.LighterAPIKeyPrivateKey),
|
||||
exchangeCfg.LighterAPIKeyIndex,
|
||||
false, // Always use mainnet for Lighter
|
||||
)
|
||||
} else {
|
||||
createErr = fmt.Errorf("Lighter requires wallet address and API Key private key")
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"})
|
||||
return
|
||||
}
|
||||
|
||||
if createErr != nil {
|
||||
logger.Infof("⚠️ Failed to create temporary trader: %v", createErr)
|
||||
SafeInternalError(c, "Failed to connect to exchange", createErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Query actual balance
|
||||
balanceInfo, balanceErr := tempTrader.GetBalance()
|
||||
if balanceErr != nil {
|
||||
logger.Infof("⚠️ Failed to query exchange balance: %v", balanceErr)
|
||||
SafeInternalError(c, "Failed to query balance", balanceErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract total equity (for P&L calculation, we need total account value, not available balance)
|
||||
var actualBalance float64
|
||||
// Priority: total_equity > totalWalletBalance > wallet_balance > totalEq > balance
|
||||
balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"}
|
||||
for _, key := range balanceKeys {
|
||||
if balance, ok := balanceInfo[key].(float64); ok && balance > 0 {
|
||||
actualBalance = balance
|
||||
break
|
||||
}
|
||||
}
|
||||
if actualBalance <= 0 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to get total equity"})
|
||||
return
|
||||
}
|
||||
|
||||
oldBalance := traderConfig.InitialBalance
|
||||
|
||||
// Smart balance change detection
|
||||
changePercent := ((actualBalance - oldBalance) / oldBalance) * 100
|
||||
changeType := "increase"
|
||||
if changePercent < 0 {
|
||||
changeType = "decrease"
|
||||
}
|
||||
|
||||
logger.Infof("✓ Queried actual exchange balance: %.2f USDT (current config: %.2f USDT, change: %.2f%%)",
|
||||
actualBalance, oldBalance, changePercent)
|
||||
|
||||
// Update initial_balance in database
|
||||
err = s.store.Trader().UpdateInitialBalance(userID, traderID, actualBalance)
|
||||
if err != nil {
|
||||
logger.Infof("❌ Failed to update initial_balance: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update balance"})
|
||||
return
|
||||
}
|
||||
|
||||
// Reload traders into memory
|
||||
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ Failed to reload user traders into memory: %v", err)
|
||||
}
|
||||
|
||||
logger.Infof("✅ Synced balance: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Balance synced successfully",
|
||||
"old_balance": oldBalance,
|
||||
"new_balance": actualBalance,
|
||||
"change_percent": changePercent,
|
||||
"change_type": changeType,
|
||||
})
|
||||
}
|
||||
|
||||
// handleClosePosition One-click close position
|
||||
func (s *Server) handleClosePosition(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
traderID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Symbol string `json:"symbol" binding:"required"`
|
||||
Side string `json:"side" binding:"required"` // "LONG" or "SHORT"
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Parameter error: symbol and side are required"})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("🔻 User %s requested position close: trader=%s, symbol=%s, side=%s", userID, traderID, req.Symbol, req.Side)
|
||||
|
||||
// Get trader configuration from database (including exchange info)
|
||||
fullConfig, err := s.store.Trader().GetFullConfig(userID, traderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Trader does not exist"})
|
||||
return
|
||||
}
|
||||
|
||||
exchangeCfg := fullConfig.Exchange
|
||||
|
||||
if exchangeCfg == nil || !exchangeCfg.Enabled {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Exchange not configured or not enabled"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create temporary trader to execute close position
|
||||
var tempTrader trader.Trader
|
||||
var createErr error
|
||||
|
||||
// Use ExchangeType (e.g., "binance") instead of ExchangeID (which is now UUID)
|
||||
// Convert EncryptedString fields to string
|
||||
switch exchangeCfg.ExchangeType {
|
||||
case "binance":
|
||||
tempTrader = binance.NewFuturesTrader(string(exchangeCfg.APIKey), string(exchangeCfg.SecretKey), userID)
|
||||
case "hyperliquid":
|
||||
tempTrader, createErr = hyperliquidtrader.NewHyperliquidTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
exchangeCfg.HyperliquidWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
exchangeCfg.HyperliquidUnifiedAcct,
|
||||
)
|
||||
case "aster":
|
||||
tempTrader, createErr = aster.NewAsterTrader(
|
||||
exchangeCfg.AsterUser,
|
||||
exchangeCfg.AsterSigner,
|
||||
string(exchangeCfg.AsterPrivateKey),
|
||||
)
|
||||
case "bybit":
|
||||
tempTrader = bybit.NewBybitTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
)
|
||||
case "okx":
|
||||
tempTrader = okx.NewOKXTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
string(exchangeCfg.Passphrase),
|
||||
)
|
||||
case "bitget":
|
||||
tempTrader = bitget.NewBitgetTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
string(exchangeCfg.Passphrase),
|
||||
)
|
||||
case "gate":
|
||||
tempTrader = gate.NewGateTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
)
|
||||
case "kucoin":
|
||||
tempTrader = kucoin.NewKuCoinTrader(
|
||||
string(exchangeCfg.APIKey),
|
||||
string(exchangeCfg.SecretKey),
|
||||
string(exchangeCfg.Passphrase),
|
||||
)
|
||||
case "lighter":
|
||||
if exchangeCfg.LighterWalletAddr != "" && string(exchangeCfg.LighterAPIKeyPrivateKey) != "" {
|
||||
// Lighter only supports mainnet
|
||||
tempTrader, createErr = lighter.NewLighterTraderV2(
|
||||
exchangeCfg.LighterWalletAddr,
|
||||
string(exchangeCfg.LighterAPIKeyPrivateKey),
|
||||
exchangeCfg.LighterAPIKeyIndex,
|
||||
false, // Always use mainnet for Lighter
|
||||
)
|
||||
} else {
|
||||
createErr = fmt.Errorf("Lighter requires wallet address and API Key private key")
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"})
|
||||
return
|
||||
}
|
||||
|
||||
if createErr != nil {
|
||||
logger.Infof("⚠️ Failed to create temporary trader: %v", createErr)
|
||||
SafeInternalError(c, "Failed to connect to exchange", createErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Get current position info BEFORE closing (to get quantity and price)
|
||||
positions, err := tempTrader.GetPositions()
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ Failed to get positions: %v", err)
|
||||
}
|
||||
|
||||
var posQty float64
|
||||
var entryPrice float64
|
||||
for _, pos := range positions {
|
||||
if pos["symbol"] == req.Symbol && pos["side"] == strings.ToLower(req.Side) {
|
||||
if amt, ok := pos["positionAmt"].(float64); ok {
|
||||
posQty = amt
|
||||
if posQty < 0 {
|
||||
posQty = -posQty // Make positive
|
||||
}
|
||||
}
|
||||
if price, ok := pos["entryPrice"].(float64); ok {
|
||||
entryPrice = price
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Execute close position operation
|
||||
var result map[string]interface{}
|
||||
var closeErr error
|
||||
|
||||
if req.Side == "LONG" {
|
||||
result, closeErr = tempTrader.CloseLong(req.Symbol, 0) // 0 means close all
|
||||
} else if req.Side == "SHORT" {
|
||||
result, closeErr = tempTrader.CloseShort(req.Symbol, 0) // 0 means close all
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "side must be LONG or SHORT"})
|
||||
return
|
||||
}
|
||||
|
||||
if closeErr != nil {
|
||||
logger.Infof("❌ Close position failed: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr)
|
||||
SafeInternalError(c, "Close position", closeErr)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("✅ Position closed successfully: symbol=%s, side=%s, qty=%.6f, result=%v", req.Symbol, req.Side, posQty, result)
|
||||
|
||||
// Record order to database (for chart markers and history)
|
||||
s.recordClosePositionOrder(traderID, exchangeCfg.ID, exchangeCfg.ExchangeType, req.Symbol, req.Side, posQty, entryPrice, result)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Position closed successfully",
|
||||
"symbol": req.Symbol,
|
||||
"side": req.Side,
|
||||
"result": result,
|
||||
})
|
||||
}
|
||||
|
||||
// recordClosePositionOrder Record close position order to database (Lighter version - direct FILLED status)
|
||||
func (s *Server) recordClosePositionOrder(traderID, exchangeID, exchangeType, symbol, side string, quantity, exitPrice float64, result map[string]interface{}) {
|
||||
// Skip for exchanges with OrderSync - let the background sync handle it to avoid duplicates
|
||||
switch exchangeType {
|
||||
case "binance", "lighter", "hyperliquid", "bybit", "okx", "bitget", "aster", "gate":
|
||||
logger.Infof(" 📝 Close order will be synced by OrderSync, skipping immediate record")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if order was placed (skip if NO_POSITION)
|
||||
status, _ := result["status"].(string)
|
||||
if status == "NO_POSITION" {
|
||||
logger.Infof(" ⚠️ No position to close, skipping order record")
|
||||
return
|
||||
}
|
||||
|
||||
// Get order ID from result
|
||||
var orderID string
|
||||
switch v := result["orderId"].(type) {
|
||||
case int64:
|
||||
orderID = fmt.Sprintf("%d", v)
|
||||
case float64:
|
||||
orderID = fmt.Sprintf("%.0f", v)
|
||||
case string:
|
||||
orderID = v
|
||||
default:
|
||||
orderID = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
if orderID == "" || orderID == "0" {
|
||||
logger.Infof(" ⚠️ Order ID is empty, skipping record")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine order action based on side
|
||||
var orderAction string
|
||||
if side == "LONG" {
|
||||
orderAction = "close_long"
|
||||
} else {
|
||||
orderAction = "close_short"
|
||||
}
|
||||
|
||||
// Use entry price if exit price not available
|
||||
if exitPrice == 0 {
|
||||
exitPrice = quantity * 100 // Rough estimate if we don't have price
|
||||
}
|
||||
|
||||
// Estimate fee (0.04% for Lighter taker)
|
||||
fee := exitPrice * quantity * 0.0004
|
||||
|
||||
// Create order record - DIRECTLY as FILLED (Lighter market orders fill immediately)
|
||||
orderRecord := &store.TraderOrder{
|
||||
TraderID: traderID,
|
||||
ExchangeID: exchangeID,
|
||||
ExchangeType: exchangeType,
|
||||
ExchangeOrderID: orderID,
|
||||
Symbol: symbol,
|
||||
PositionSide: side,
|
||||
OrderAction: orderAction,
|
||||
Type: "MARKET",
|
||||
Side: getSideFromAction(orderAction),
|
||||
Quantity: quantity,
|
||||
Price: 0, // Market order
|
||||
Status: "FILLED",
|
||||
FilledQuantity: quantity,
|
||||
AvgFillPrice: exitPrice,
|
||||
Commission: fee,
|
||||
FilledAt: time.Now().UTC().UnixMilli(),
|
||||
CreatedAt: time.Now().UTC().UnixMilli(),
|
||||
UpdatedAt: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
|
||||
if err := s.store.Order().CreateOrder(orderRecord); err != nil {
|
||||
logger.Infof(" ⚠️ Failed to record order: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(" ✅ Order recorded as FILLED: %s [%s] %s qty=%.6f price=%.6f", orderID, orderAction, symbol, quantity, exitPrice)
|
||||
|
||||
// Create fill record immediately
|
||||
tradeID := fmt.Sprintf("%s-%d", orderID, time.Now().UnixNano())
|
||||
fillRecord := &store.TraderFill{
|
||||
TraderID: traderID,
|
||||
ExchangeID: exchangeID,
|
||||
ExchangeType: exchangeType,
|
||||
OrderID: orderRecord.ID,
|
||||
ExchangeOrderID: orderID,
|
||||
ExchangeTradeID: tradeID,
|
||||
Symbol: symbol,
|
||||
Side: getSideFromAction(orderAction),
|
||||
Price: exitPrice,
|
||||
Quantity: quantity,
|
||||
QuoteQuantity: exitPrice * quantity,
|
||||
Commission: fee,
|
||||
CommissionAsset: "USDT",
|
||||
RealizedPnL: 0,
|
||||
IsMaker: false,
|
||||
CreatedAt: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
|
||||
if err := s.store.Order().CreateFill(fillRecord); err != nil {
|
||||
logger.Infof(" ⚠️ Failed to record fill: %v", err)
|
||||
} else {
|
||||
logger.Infof(" ✅ Fill record created: price=%.6f qty=%.6f", exitPrice, quantity)
|
||||
}
|
||||
}
|
||||
|
||||
// pollAndUpdateOrderStatus Poll order status and update with fill data
|
||||
func (s *Server) pollAndUpdateOrderStatus(orderRecordID int64, traderID, exchangeID, exchangeType, orderID, symbol, orderAction string, tempTrader trader.Trader) {
|
||||
var actualPrice float64
|
||||
var actualQty float64
|
||||
var fee float64
|
||||
|
||||
// Wait a bit for order to be filled
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// For Lighter, use GetTrades instead of GetOrderStatus (market orders are filled immediately)
|
||||
if exchangeType == "lighter" {
|
||||
s.pollLighterTradeHistory(orderRecordID, traderID, exchangeID, exchangeType, orderID, symbol, orderAction, tempTrader)
|
||||
return
|
||||
}
|
||||
|
||||
// For other exchanges, poll GetOrderStatus
|
||||
for i := 0; i < 5; i++ {
|
||||
status, err := tempTrader.GetOrderStatus(symbol, orderID)
|
||||
if err != nil {
|
||||
logger.Infof(" ⚠️ GetOrderStatus failed (attempt %d/5): %v", i+1, err)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if err == nil {
|
||||
statusStr, _ := status["status"].(string)
|
||||
if statusStr == "FILLED" {
|
||||
// Get actual fill price
|
||||
if avgPrice, ok := status["avgPrice"].(float64); ok && avgPrice > 0 {
|
||||
actualPrice = avgPrice
|
||||
}
|
||||
// Get actual executed quantity
|
||||
if execQty, ok := status["executedQty"].(float64); ok && execQty > 0 {
|
||||
actualQty = execQty
|
||||
}
|
||||
// Get commission/fee
|
||||
if commission, ok := status["commission"].(float64); ok {
|
||||
fee = commission
|
||||
}
|
||||
|
||||
logger.Infof(" ✅ Order filled: avgPrice=%.6f, qty=%.6f, fee=%.6f", actualPrice, actualQty, fee)
|
||||
|
||||
// Update order status to FILLED
|
||||
if err := s.store.Order().UpdateOrderStatus(orderRecordID, "FILLED", actualQty, actualPrice, fee); err != nil {
|
||||
logger.Infof(" ⚠️ Failed to update order status: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Record fill details
|
||||
tradeID := fmt.Sprintf("%s-%d", orderID, time.Now().UnixNano())
|
||||
fillRecord := &store.TraderFill{
|
||||
TraderID: traderID,
|
||||
ExchangeID: exchangeID,
|
||||
ExchangeType: exchangeType,
|
||||
OrderID: orderRecordID,
|
||||
ExchangeOrderID: orderID,
|
||||
ExchangeTradeID: tradeID,
|
||||
Symbol: symbol,
|
||||
Side: getSideFromAction(orderAction),
|
||||
Price: actualPrice,
|
||||
Quantity: actualQty,
|
||||
QuoteQuantity: actualPrice * actualQty,
|
||||
Commission: fee,
|
||||
CommissionAsset: "USDT",
|
||||
RealizedPnL: 0,
|
||||
IsMaker: false,
|
||||
CreatedAt: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
|
||||
if err := s.store.Order().CreateFill(fillRecord); err != nil {
|
||||
logger.Infof(" ⚠️ Failed to record fill: %v", err)
|
||||
} else {
|
||||
logger.Infof(" 📝 Fill recorded: price=%.6f, qty=%.6f", actualPrice, actualQty)
|
||||
}
|
||||
|
||||
return
|
||||
} else if statusStr == "CANCELED" || statusStr == "EXPIRED" || statusStr == "REJECTED" {
|
||||
logger.Infof(" ⚠️ Order %s, updating status", statusStr)
|
||||
s.store.Order().UpdateOrderStatus(orderRecordID, statusStr, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
logger.Infof(" ⚠️ Failed to confirm order fill after polling, order may still be pending")
|
||||
}
|
||||
|
||||
// pollLighterTradeHistory No longer used - Lighter orders are marked as FILLED immediately
|
||||
// Keeping this function stub for compatibility with other exchanges
|
||||
func (s *Server) pollLighterTradeHistory(orderRecordID int64, traderID, exchangeID, exchangeType, orderID, symbol, orderAction string, tempTrader trader.Trader) {
|
||||
// For Lighter, orders are now recorded as FILLED immediately in recordClosePositionOrder
|
||||
// This function is no longer called for Lighter exchange
|
||||
logger.Infof(" ℹ️ pollLighterTradeHistory called but not needed (order already marked FILLED)")
|
||||
}
|
||||
|
||||
// getSideFromAction Get order side (BUY/SELL) from order action
|
||||
func getSideFromAction(action string) string {
|
||||
switch action {
|
||||
case "open_long", "close_short":
|
||||
return "BUY"
|
||||
case "open_short", "close_long":
|
||||
return "SELL"
|
||||
default:
|
||||
return "BUY"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user