package lighter import ( "fmt" "nofx/logger" "nofx/market" "nofx/store" "sort" "strings" "time" ) // SyncOrdersFromLighter syncs Lighter exchange trade history to local database // Also creates/updates position records to ensure orders/fills/positions data consistency // exchangeID: Exchange account UUID (from exchanges.id) // exchangeType: Exchange type ("lighter") func (t *LighterTraderV2) SyncOrdersFromLighter(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) logger.Infof("🔄 Syncing Lighter trades from: %s", startTime.Format(time.RFC3339)) // Use GetTrades method to fetch trade records (same as other exchanges) trades, err := t.GetTrades(startTime, 100) if err != nil { return fmt.Errorf("failed to get trades: %w", err) } logger.Infof("📥 Received %d trades from Lighter", len(trades)) // Sort trades by time ASC (oldest first) for proper position building sort.Slice(trades, func(i, j int) bool { return trades[i].Time.UnixMilli() < trades[j].Time.UnixMilli() }) // Process trades one by one (no transaction to avoid deadlock) orderStore := st.Order() positionStore := st.Position() posBuilder := store.NewPositionBuilder(positionStore) syncedCount := 0 for _, trade := range trades { // Check if trade already exists (use exchangeID which is UUID, not exchange type) existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID) if err == nil && existing != nil { continue // Trade already exists, skip } // Normalize symbol (add USDT suffix) symbol := market.Normalize(trade.Symbol) // Use OrderAction from TradeRecord (determined by position change in GetTrades) // This is more accurate than guessing based on database state positionSide := trade.PositionSide orderAction := trade.OrderAction side := trade.Side // Fallback if OrderAction is empty (shouldn't happen with updated GetTrades) if orderAction == "" { if strings.ToUpper(side) == "BUY" { positionSide = "LONG" orderAction = "open_long" } else { positionSide = "SHORT" orderAction = "open_short" } } // Create order record - use Unix milliseconds UTC tradeTimeMs := trade.Time.UTC().UnixMilli() orderRecord := &store.TraderOrder{ TraderID: traderID, ExchangeID: exchangeID, // UUID ExchangeType: exchangeType, // Exchange type ExchangeOrderID: trade.TradeID, Symbol: symbol, Side: strings.ToUpper(side), PositionSide: positionSide, Type: "MARKET", OrderAction: orderAction, Quantity: trade.Quantity, Price: trade.Price, Status: "FILLED", FilledQuantity: trade.Quantity, AvgFillPrice: trade.Price, Commission: trade.Fee, FilledAt: tradeTimeMs, CreatedAt: tradeTimeMs, UpdatedAt: tradeTimeMs, } // Insert order record if err := orderStore.CreateOrder(orderRecord); err != nil { logger.Infof(" ⚠️ Failed to sync trade %s: %v", trade.TradeID, err) continue } // Create fill record - use Unix milliseconds UTC fillRecord := &store.TraderFill{ TraderID: traderID, ExchangeID: exchangeID, // UUID ExchangeType: exchangeType, // Exchange type OrderID: orderRecord.ID, ExchangeOrderID: trade.TradeID, ExchangeTradeID: trade.TradeID, Symbol: symbol, Side: strings.ToUpper(side), Price: trade.Price, Quantity: trade.Quantity, QuoteQuantity: trade.Price * trade.Quantity, Commission: trade.Fee, CommissionAsset: "USDT", RealizedPnL: trade.RealizedPnL, IsMaker: false, CreatedAt: tradeTimeMs, } if err := orderStore.CreateFill(fillRecord); err != nil { logger.Infof(" ⚠️ Failed to sync fill for trade %s: %v", trade.TradeID, err) } // Create/update position record using PositionBuilder if err := posBuilder.ProcessTrade( traderID, exchangeID, exchangeType, symbol, positionSide, orderAction, trade.Quantity, trade.Price, trade.Fee, trade.RealizedPnL, tradeTimeMs, trade.TradeID, ); err != nil { logger.Infof(" ⚠️ Failed to sync position for trade %s: %v", trade.TradeID, err) } else { logger.Infof(" 📍 Position updated for trade: %s (action: %s, qty: %.6f)", trade.TradeID, orderAction, trade.Quantity) } syncedCount++ logger.Infof(" ✅ Synced trade: %s %s %s qty=%.6f price=%.6f pnl=%.2f fee=%.6f action=%s", trade.TradeID, symbol, side, trade.Quantity, trade.Price, trade.RealizedPnL, trade.Fee, orderAction) } logger.Infof("✅ Order sync completed: %d new trades synced", syncedCount) return nil } // StartOrderSync starts background order sync task func (t *LighterTraderV2) StartOrderSync(traderID string, exchangeID string, exchangeType string, st *store.Store, interval time.Duration) { ticker := time.NewTicker(interval) go func() { for range ticker.C { if err := t.SyncOrdersFromLighter(traderID, exchangeID, exchangeType, st); err != nil { // Only log non-404 errors to reduce log spam if !strings.Contains(err.Error(), "status 404") { logger.Infof("⚠️ Order sync failed: %v", err) } } } }() logger.Infof("🔄 Lighter order+position sync started (interval: %v)", interval) }