mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
fix: OKX trading issues and improve position tracking
- Add maxMktSz check for OKX market orders to prevent exceeding limits - Increase margin safety buffer (0.1% fee + 1% buffer) for all exchanges - Fix Binance position closure detection with direct trade queries - Move Recent Completed Trades before Current Positions in AI prompt - Update README screenshots with table layout for better alignment
This commit is contained in:
@@ -214,11 +214,173 @@ func (t *LighterTrader) Run() error {
|
||||
return fmt.Errorf("please use AutoTrader to manage trader lifecycle")
|
||||
}
|
||||
|
||||
// GetClosedPnL gets closed position PnL records from exchange
|
||||
// LIGHTER does not have a direct closed PnL API, returns empty slice
|
||||
// GetClosedPnL gets recent closing trades from Lighter
|
||||
// Note: Lighter does NOT have a position history API, only trade history.
|
||||
// This returns individual closing trades for real-time position closure detection.
|
||||
func (t *LighterTrader) GetClosedPnL(startTime time.Time, limit int) ([]ClosedPnLRecord, error) {
|
||||
// LIGHTER does not provide a closed PnL history API
|
||||
// Position closure data needs to be tracked locally via position sync
|
||||
logger.Infof("⚠️ LIGHTER GetClosedPnL not supported, returning empty")
|
||||
return []ClosedPnLRecord{}, nil
|
||||
trades, err := t.GetTrades(startTime, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter only closing trades (realizedPnl != 0)
|
||||
var records []ClosedPnLRecord
|
||||
for _, trade := range trades {
|
||||
if trade.RealizedPnL == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine side (Lighter uses one-way mode)
|
||||
side := "long"
|
||||
if trade.Side == "SELL" || trade.Side == "Sell" {
|
||||
side = "long"
|
||||
} else {
|
||||
side = "short"
|
||||
}
|
||||
|
||||
// Calculate entry price from PnL
|
||||
var entryPrice float64
|
||||
if trade.Quantity > 0 {
|
||||
if side == "long" {
|
||||
entryPrice = trade.Price - trade.RealizedPnL/trade.Quantity
|
||||
} else {
|
||||
entryPrice = trade.Price + trade.RealizedPnL/trade.Quantity
|
||||
}
|
||||
}
|
||||
|
||||
records = append(records, ClosedPnLRecord{
|
||||
Symbol: trade.Symbol,
|
||||
Side: side,
|
||||
EntryPrice: entryPrice,
|
||||
ExitPrice: trade.Price,
|
||||
Quantity: trade.Quantity,
|
||||
RealizedPnL: trade.RealizedPnL,
|
||||
Fee: trade.Fee,
|
||||
ExitTime: trade.Time,
|
||||
EntryTime: trade.Time,
|
||||
OrderID: trade.TradeID,
|
||||
ExchangeID: trade.TradeID,
|
||||
CloseType: "unknown",
|
||||
})
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// LighterTradeResponse represents the response from Lighter trades API
|
||||
type LighterTradeResponse struct {
|
||||
Trades []LighterTrade `json:"trades"`
|
||||
}
|
||||
|
||||
// LighterTrade represents a single trade from Lighter
|
||||
type LighterTrade struct {
|
||||
TradeID string `json:"trade_id"`
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
MarketIndex int `json:"market_index"`
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"` // "buy" or "sell"
|
||||
Price string `json:"price"`
|
||||
Size string `json:"size"`
|
||||
RealizedPnl string `json:"realized_pnl"`
|
||||
Fee string `json:"fee"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
IsMaker bool `json:"is_maker"`
|
||||
}
|
||||
|
||||
// GetTrades retrieves trade history from Lighter
|
||||
func (t *LighterTrader) GetTrades(startTime time.Time, limit int) ([]TradeRecord, error) {
|
||||
// Ensure we have account index
|
||||
if t.accountIndex == 0 {
|
||||
accountInfo, err := t.getAccountByL1Address()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get account index: %w", err)
|
||||
}
|
||||
if idx, ok := accountInfo["index"].(int); ok {
|
||||
t.accountIndex = idx
|
||||
} else if idx, ok := accountInfo["index"].(float64); ok {
|
||||
t.accountIndex = int(idx)
|
||||
}
|
||||
}
|
||||
|
||||
// Build request URL
|
||||
// API: GET /api/v1/trades?account_index=X&start_time=Y&limit=Z
|
||||
startTimeMs := startTime.UnixMilli()
|
||||
endpoint := fmt.Sprintf("%s/api/v1/trades?account_index=%d&start_time=%d",
|
||||
t.baseURL, t.accountIndex, startTimeMs)
|
||||
if limit > 0 {
|
||||
endpoint = fmt.Sprintf("%s&limit=%d", endpoint, limit)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get trades: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Infof("⚠️ Lighter trades API returned %d: %s", resp.StatusCode, string(body))
|
||||
return []TradeRecord{}, nil // Return empty on error
|
||||
}
|
||||
|
||||
var response LighterTradeResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
// Try parsing as array directly
|
||||
var trades []LighterTrade
|
||||
if err := json.Unmarshal(body, &trades); err != nil {
|
||||
logger.Infof("⚠️ Failed to parse Lighter trades response: %v", err)
|
||||
return []TradeRecord{}, nil
|
||||
}
|
||||
response.Trades = trades
|
||||
}
|
||||
|
||||
// Convert to unified TradeRecord format
|
||||
var result []TradeRecord
|
||||
for _, lt := range response.Trades {
|
||||
price, _ := parseFloat(lt.Price)
|
||||
qty, _ := parseFloat(lt.Size)
|
||||
fee, _ := parseFloat(lt.Fee)
|
||||
pnl, _ := parseFloat(lt.RealizedPnl)
|
||||
|
||||
var side string
|
||||
if strings.ToLower(lt.Side) == "buy" {
|
||||
side = "BUY"
|
||||
} else {
|
||||
side = "SELL"
|
||||
}
|
||||
|
||||
trade := TradeRecord{
|
||||
TradeID: lt.TradeID,
|
||||
Symbol: lt.Symbol,
|
||||
Side: side,
|
||||
PositionSide: "BOTH", // Lighter uses one-way mode
|
||||
Price: price,
|
||||
Quantity: qty,
|
||||
RealizedPnL: pnl,
|
||||
Fee: fee,
|
||||
Time: time.UnixMilli(lt.Timestamp),
|
||||
}
|
||||
result = append(result, trade)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseFloat safely parses a float string
|
||||
func parseFloat(s string) (float64, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var f float64
|
||||
_, err := fmt.Sscanf(s, "%f", &f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user