Files
nofx/trader/lighter_order_sync.go
tinkle-community 1744e7f38e feat: migrate to CoinAnk API and improve chart UI
- Chart improvements: professional styling, popular symbols quick selection, simplified B/S legend
- Data source migration: use CoinAnk API exclusively for all kline data
- Code cleanup: remove Binance WebSocket cache and related code (websocket_client.go, combined_streams.go, monitor.go)
- Log optimization: reduce hook spam, suppress 404 errors, increase P&L diff threshold
- Lighter integration: add order sync functionality, fix market order precision
- Remove ticker merge logic for simplicity
2025-12-26 00:58:12 +08:00

210 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package trader
import (
"encoding/json"
"fmt"
"io"
"nofx/logger"
"nofx/store"
"net/http"
"strings"
"time"
)
// LighterOrderHistory 订单历史记录
type LighterOrderHistory struct {
OrderID string `json:"order_id"`
Symbol string `json:"symbol"`
Side string `json:"side"` // "buy" or "sell"
Type string `json:"type"` // "limit" or "market"
Price string `json:"price"`
Size string `json:"size"`
FilledSize string `json:"filled_size"`
Status string `json:"status"` // "filled", "cancelled", etc.
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
FilledAt int64 `json:"filled_at"`
}
// SyncOrdersFromLighter 同步 Lighter 交易所的订单历史到本地数据库
func (t *LighterTraderV2) SyncOrdersFromLighter(traderID string, orderStore *store.OrderStore) error {
// 确保有 account index
if t.accountIndex == 0 {
if err := t.initializeAccount(); err != nil {
return fmt.Errorf("failed to get account index: %w", err)
}
}
// 获取最近的订单过去24小时
startTime := time.Now().Add(-24 * time.Hour).Unix()
endpoint := fmt.Sprintf("%s/api/v1/orders?account_index=%d&start_time=%d&limit=100",
t.baseURL, t.accountIndex, startTime)
logger.Infof("🔄 Syncing Lighter orders from: %s", endpoint)
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// 添加认证头
if err := t.ensureAuthToken(); err != nil {
return fmt.Errorf("failed to get auth token: %w", err)
}
req.Header.Set("Authorization", t.authToken)
resp, err := t.client.Do(req)
if err != nil {
return fmt.Errorf("failed to get orders: %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 {
// Don't spam logs for 404 errors (API endpoint might not be available)
if resp.StatusCode != http.StatusNotFound {
logger.Infof("⚠️ Lighter orders API returned %d: %s", resp.StatusCode, string(body))
}
return fmt.Errorf("API returned status %d", resp.StatusCode)
}
// 解析响应
var apiResp struct {
Code int `json:"code"`
Orders []LighterOrderHistory `json:"orders"`
}
if err := json.Unmarshal(body, &apiResp); err != nil {
logger.Infof("⚠️ Failed to parse orders response: %v, body: %s", err, string(body))
return fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 200 {
return fmt.Errorf("API returned code %d", apiResp.Code)
}
logger.Infof("📥 Received %d orders from Lighter", len(apiResp.Orders))
// 同步每个订单
syncedCount := 0
for _, order := range apiResp.Orders {
// 只同步已成交的订单
if order.Status != "filled" {
continue
}
// 检查订单是否已存在
existing, err := orderStore.GetOrderByExchangeID("lighter", order.OrderID)
if err == nil && existing != nil {
continue // 订单已存在,跳过
}
// 解析价格和数量
price, _ := parseFloat(order.Price)
size, _ := parseFloat(order.Size)
filledSize, _ := parseFloat(order.FilledSize)
if filledSize == 0 {
filledSize = size
}
// 确定订单方向和动作
var positionSide, orderAction, side string
if order.Side == "buy" {
side = "BUY"
// 买入可能是开多或平空,这里假设是开多
positionSide = "LONG"
orderAction = "open_long"
} else {
side = "SELL"
// 卖出可能是平多或开空,这里假设是平多
positionSide = "LONG"
orderAction = "close_long"
}
// 估算手续费
fee := price * filledSize * 0.0004
// 创建订单记录
filledAt := time.Unix(order.FilledAt, 0)
if order.FilledAt == 0 {
filledAt = time.Unix(order.UpdatedAt, 0)
}
orderRecord := &store.TraderOrder{
TraderID: traderID,
ExchangeID: "lighter",
ExchangeOrderID: order.OrderID,
Symbol: order.Symbol,
Side: side,
PositionSide: positionSide,
Type: "MARKET",
OrderAction: orderAction,
Quantity: filledSize,
Price: price,
Status: "FILLED",
FilledQuantity: filledSize,
AvgFillPrice: price,
Commission: fee,
FilledAt: filledAt,
CreatedAt: time.Unix(order.CreatedAt, 0),
UpdatedAt: time.Unix(order.UpdatedAt, 0),
}
// 插入订单记录
if err := orderStore.CreateOrder(orderRecord); err != nil {
logger.Infof(" ⚠️ Failed to sync order %s: %v", order.OrderID, err)
continue
}
// 创建成交记录
fillRecord := &store.TraderFill{
TraderID: traderID,
ExchangeID: "lighter",
OrderID: orderRecord.ID,
ExchangeOrderID: order.OrderID,
ExchangeTradeID: fmt.Sprintf("%s-%d", order.OrderID, time.Now().UnixNano()),
Symbol: order.Symbol,
Side: side,
Price: price,
Quantity: filledSize,
QuoteQuantity: price * filledSize,
Commission: fee,
CommissionAsset: "USDT",
RealizedPnL: 0,
IsMaker: order.Type == "limit",
CreatedAt: filledAt,
}
if err := orderStore.CreateFill(fillRecord); err != nil {
logger.Infof(" ⚠️ Failed to sync fill for order %s: %v", order.OrderID, err)
}
syncedCount++
logger.Infof(" ✅ Synced order: %s %s %s qty=%.6f price=%.6f", order.OrderID, order.Symbol, side, filledSize, price)
}
logger.Infof("✅ Order sync completed: %d new orders synced", syncedCount)
return nil
}
// StartOrderSync 启动订单同步后台任务
func (t *LighterTraderV2) StartOrderSync(traderID string, orderStore *store.OrderStore, interval time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
if err := t.SyncOrdersFromLighter(traderID, orderStore); 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 sync started (interval: %v)", interval)
}