mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
093d2a329d
Gate.io Integration: - Add Gate trader with full Trader interface implementation - Add order_sync.go for background trade synchronization - Fix quantity display (convert contracts to actual tokens via quanto_multiplier) - Fix fill price return in OpenLong/OpenShort/CloseLong/CloseShort - Add Gate-specific CoinAnk K-line data source support - Add Gate to supported exchanges in frontend and backend - Add Gate/KuCoin logo SVG icons Trader Package Refactoring: - Move exchange-specific code into subdirectories (binance/, bybit/, okx/, bitget/, hyperliquid/, aster/, lighter/, gate/) - Create types/ package for shared types to avoid circular dependencies - Move TraderTestSuite to trader/testutil package to avoid import cycles - Update market.GetWithExchange to support exchange-specific data
1109 lines
28 KiB
Go
1109 lines
28 KiB
Go
package lighter
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
tradertypes "nofx/trader/types"
|
|
)
|
|
|
|
// Test configuration - uses environment variables for security
|
|
// Run with:
|
|
// LIGHTER_TEST=1 LIGHTER_WALLET=0x... LIGHTER_API_KEY=... LIGHTER_API_KEY_INDEX=2 go test -v ./trader -run TestLighter -timeout 300s
|
|
// Run with trading:
|
|
// LIGHTER_TEST=1 LIGHTER_TRADE_TEST=1 LIGHTER_WALLET=0x... LIGHTER_API_KEY=... go test -v ./trader -run TestLighter -timeout 300s
|
|
|
|
// getTestConfig returns test configuration from environment variables
|
|
func getTestConfig() (walletAddr, apiKey string, apiKeyIndex int) {
|
|
walletAddr = os.Getenv("LIGHTER_WALLET")
|
|
apiKey = os.Getenv("LIGHTER_API_KEY")
|
|
// All credentials must be provided via environment variables for security
|
|
apiKeyIndex = 2 // Default to index 2 (more stable than index 0)
|
|
if idx := os.Getenv("LIGHTER_API_KEY_INDEX"); idx != "" {
|
|
fmt.Sscanf(idx, "%d", &apiKeyIndex)
|
|
}
|
|
return
|
|
}
|
|
|
|
func skipIfNoEnv(t *testing.T) {
|
|
if os.Getenv("LIGHTER_TEST") != "1" {
|
|
t.Skip("Skipping Lighter integration test. Set LIGHTER_TEST=1 to run")
|
|
}
|
|
if os.Getenv("LIGHTER_WALLET") == "" {
|
|
t.Skip("Skipping: LIGHTER_WALLET environment variable not set")
|
|
}
|
|
if os.Getenv("LIGHTER_API_KEY") == "" {
|
|
t.Skip("Skipping: LIGHTER_API_KEY environment variable not set")
|
|
}
|
|
}
|
|
|
|
// skipIfJurisdictionRestricted checks if error is due to geographic restriction
|
|
// and skips the test if so (this is expected when running from restricted regions)
|
|
func skipIfJurisdictionRestricted(t *testing.T, err error) {
|
|
if err != nil && strings.Contains(err.Error(), "restricted jurisdiction") {
|
|
t.Skip("Skipping: API blocked due to geographic restriction (IP-based). Use VPN to allowed region.")
|
|
}
|
|
}
|
|
|
|
func createTestTrader(t *testing.T) *LighterTraderV2 {
|
|
walletAddr, apiKey, apiKeyIndex := getTestConfig()
|
|
trader, err := NewLighterTraderV2(walletAddr, apiKey, apiKeyIndex, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create trader: %v", err)
|
|
}
|
|
return trader
|
|
}
|
|
|
|
// ==================== Account Tests ====================
|
|
|
|
func TestLighterAccountInit(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Verify account index is valid (non-zero)
|
|
if trader.accountIndex <= 0 {
|
|
t.Errorf("Expected valid account index, got %d", trader.accountIndex)
|
|
}
|
|
|
|
t.Logf("✅ Account initialized: index=%d", trader.accountIndex)
|
|
}
|
|
|
|
func TestLighterAPIKeyVerification(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Verify API key
|
|
err := trader.checkClient()
|
|
if err != nil {
|
|
t.Errorf("API key verification failed: %v", err)
|
|
} else {
|
|
t.Log("✅ API key verified successfully")
|
|
}
|
|
}
|
|
|
|
func TestLighterGetBalance(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
balance, err := trader.GetBalance()
|
|
if err != nil {
|
|
t.Fatalf("GetBalance failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Balance retrieved:")
|
|
if te, ok := balance["total_equity"].(float64); ok {
|
|
t.Logf(" Total Equity: %.2f", te)
|
|
}
|
|
if ab, ok := balance["available_balance"].(float64); ok {
|
|
t.Logf(" Available Balance: %.2f", ab)
|
|
}
|
|
if mu, ok := balance["margin_used"].(float64); ok {
|
|
t.Logf(" Margin Used: %.2f", mu)
|
|
}
|
|
if up, ok := balance["unrealized_pnl"].(float64); ok {
|
|
t.Logf(" Unrealized PnL: %.2f", up)
|
|
}
|
|
|
|
if len(balance) == 0 {
|
|
t.Error("Expected balance data")
|
|
}
|
|
}
|
|
|
|
// ==================== Position Tests ====================
|
|
|
|
func TestLighterGetPositions(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
positions, err := trader.GetPositions()
|
|
if err != nil {
|
|
t.Fatalf("GetPositions failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Positions retrieved: %d positions", len(positions))
|
|
for i, pos := range positions {
|
|
symbol, _ := pos["symbol"].(string)
|
|
side, _ := pos["side"].(string)
|
|
size, _ := pos["size"].(float64)
|
|
entryPrice, _ := pos["entry_price"].(float64)
|
|
unrealizedPnl, _ := pos["unrealized_pnl"].(float64)
|
|
|
|
t.Logf(" [%d] %s %s: size=%.4f, entry=%.2f, pnl=%.2f",
|
|
i+1, symbol, side, size, entryPrice, unrealizedPnl)
|
|
}
|
|
}
|
|
|
|
// ==================== Market Data Tests ====================
|
|
|
|
func TestLighterGetMarketPrice(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
symbols := []string{"ETH", "BTC", "SOL"}
|
|
|
|
for _, symbol := range symbols {
|
|
price, err := trader.GetMarketPrice(symbol)
|
|
if err != nil {
|
|
t.Errorf("GetMarketPrice(%s) failed: %v", symbol, err)
|
|
continue
|
|
}
|
|
t.Logf("✅ %s price: %.2f", symbol, price)
|
|
|
|
if price <= 0 {
|
|
t.Errorf("Expected positive price for %s, got %.2f", symbol, price)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLighterFetchMarketList(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
markets, err := trader.fetchMarketList()
|
|
if err != nil {
|
|
t.Fatalf("fetchMarketList failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Markets retrieved: %d markets", len(markets))
|
|
for i, m := range markets {
|
|
if i >= 10 {
|
|
t.Logf(" ... and %d more", len(markets)-10)
|
|
break
|
|
}
|
|
t.Logf(" [%d] %s (market_id=%d, size_decimals=%d, price_decimals=%d)",
|
|
m.MarketID, m.Symbol, m.MarketID, m.SizeDecimals, m.PriceDecimals)
|
|
}
|
|
|
|
if len(markets) == 0 {
|
|
t.Error("Expected at least one market")
|
|
}
|
|
}
|
|
|
|
// ==================== Trades API Tests ====================
|
|
|
|
func TestLighterGetTrades(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Get trades from last 7 days
|
|
startTime := time.Now().Add(-7 * 24 * time.Hour)
|
|
trades, err := trader.GetTrades(startTime, 100)
|
|
if err != nil {
|
|
t.Fatalf("GetTrades failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Trades retrieved: %d trades", len(trades))
|
|
for i, trade := range trades {
|
|
if i >= 5 {
|
|
t.Logf(" ... and %d more", len(trades)-5)
|
|
break
|
|
}
|
|
t.Logf(" [%d] %s %s: qty=%.4f @ %.2f, fee=%.6f, time=%s",
|
|
i+1, trade.Symbol, trade.Side, trade.Quantity, trade.Price, trade.Fee,
|
|
trade.Time.Format("2006-01-02 15:04:05"))
|
|
}
|
|
}
|
|
|
|
func TestLighterGetClosedPnL(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
startTime := time.Now().Add(-7 * 24 * time.Hour)
|
|
records, err := trader.GetClosedPnL(startTime, 100)
|
|
if err != nil {
|
|
t.Fatalf("GetClosedPnL failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Closed PnL records: %d records", len(records))
|
|
for i, r := range records {
|
|
if i >= 5 {
|
|
t.Logf(" ... and %d more", len(records)-5)
|
|
break
|
|
}
|
|
t.Logf(" [%d] %s %s: qty=%.4f, entry=%.2f, exit=%.2f, pnl=%.2f",
|
|
i+1, r.Symbol, r.Side, r.Quantity, r.EntryPrice, r.ExitPrice, r.RealizedPnL)
|
|
}
|
|
}
|
|
|
|
// ==================== Order Tests ====================
|
|
|
|
func TestLighterCreateAndCancelLimitOrder(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Get current market price
|
|
marketPrice, err := trader.GetMarketPrice("ETH")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get market price: %v", err)
|
|
}
|
|
t.Logf("Current ETH price: %.2f", marketPrice)
|
|
|
|
// Create a limit order far from market (won't fill)
|
|
// Buy order at 80% of market price
|
|
limitPrice := marketPrice * 0.80
|
|
quantity := 0.01 // Minimum quantity
|
|
|
|
t.Logf("Creating limit buy order: %.4f ETH @ %.2f", quantity, limitPrice)
|
|
|
|
result, err := trader.CreateOrder("ETH", false, quantity, limitPrice, "limit", false)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("CreateOrder failed: %v", err)
|
|
}
|
|
|
|
orderID, _ := result["orderId"].(string)
|
|
t.Logf("✅ Order created: %s", orderID)
|
|
|
|
if orderID == "" {
|
|
t.Fatal("Expected orderId in response")
|
|
}
|
|
|
|
// Wait a moment for order to be processed
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Cancel the order
|
|
t.Logf("Cancelling order: %s", orderID)
|
|
err = trader.CancelOrder("ETH", orderID)
|
|
if err != nil {
|
|
t.Errorf("CancelOrder failed: %v", err)
|
|
} else {
|
|
t.Log("✅ Order cancelled successfully")
|
|
}
|
|
}
|
|
|
|
func TestLighterCancelAllOrders(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// First create a few test orders
|
|
marketPrice, err := trader.GetMarketPrice("ETH")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get market price: %v", err)
|
|
}
|
|
|
|
// Create 2 limit orders
|
|
for i := 0; i < 2; i++ {
|
|
limitPrice := marketPrice * (0.75 - float64(i)*0.05) // 75%, 70% of market
|
|
_, err := trader.CreateOrder("ETH", false, 0.01, limitPrice, "limit", false)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Logf("Failed to create test order %d: %v", i+1, err)
|
|
} else {
|
|
t.Logf("Created test order %d @ %.2f", i+1, limitPrice)
|
|
}
|
|
}
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Cancel all
|
|
err = trader.CancelAllOrders("ETH")
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Errorf("CancelAllOrders failed: %v", err)
|
|
} else {
|
|
t.Log("✅ CancelAllOrders executed")
|
|
}
|
|
}
|
|
|
|
// ==================== Trading Flow Tests ====================
|
|
|
|
func TestLighterOpenCloseLongFlow(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
// This test actually trades - be careful!
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping actual trade test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
symbol := "ETH"
|
|
quantity := 0.01 // Minimum quantity
|
|
leverage := 10
|
|
|
|
// Get initial positions
|
|
positionsBefore, _ := trader.GetPositions()
|
|
t.Logf("Positions before: %d", len(positionsBefore))
|
|
|
|
// Open long
|
|
t.Logf("Opening long: %s qty=%.4f leverage=%d", symbol, quantity, leverage)
|
|
result, err := trader.OpenLong(symbol, quantity, leverage)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("OpenLong failed: %v", err)
|
|
}
|
|
t.Logf("✅ OpenLong result: %v", result)
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Verify position
|
|
positions, _ := trader.GetPositions()
|
|
t.Logf("Positions after open: %d", len(positions))
|
|
|
|
// Close long
|
|
t.Logf("Closing long: %s qty=%.4f", symbol, quantity)
|
|
result, err = trader.CloseLong(symbol, quantity)
|
|
if err != nil {
|
|
t.Errorf("CloseLong failed: %v", err)
|
|
} else {
|
|
t.Logf("✅ CloseLong result: %v", result)
|
|
}
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Verify position closed
|
|
positions, _ = trader.GetPositions()
|
|
t.Logf("Positions after close: %d", len(positions))
|
|
}
|
|
|
|
func TestLighterOpenCloseShortFlow(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping actual trade test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
symbol := "ETH"
|
|
quantity := 0.01
|
|
leverage := 10
|
|
|
|
// Open short
|
|
t.Logf("Opening short: %s qty=%.4f leverage=%d", symbol, quantity, leverage)
|
|
result, err := trader.OpenShort(symbol, quantity, leverage)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("OpenShort failed: %v", err)
|
|
}
|
|
t.Logf("✅ OpenShort result: %v", result)
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Close short
|
|
t.Logf("Closing short: %s qty=%.4f", symbol, quantity)
|
|
result, err = trader.CloseShort(symbol, quantity)
|
|
if err != nil {
|
|
t.Errorf("CloseShort failed: %v", err)
|
|
} else {
|
|
t.Logf("✅ CloseShort result: %v", result)
|
|
}
|
|
}
|
|
|
|
// ==================== Leverage Tests ====================
|
|
|
|
func TestLighterSetLeverage(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test setting leverage
|
|
leverages := []int{5, 10, 20}
|
|
|
|
for _, lev := range leverages {
|
|
err := trader.SetLeverage("ETH", lev)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Errorf("SetLeverage(%d) failed: %v", lev, err)
|
|
} else {
|
|
t.Logf("✅ SetLeverage(%d) succeeded", lev)
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
// ==================== Auth Token Tests ====================
|
|
|
|
func TestLighterAuthTokenRefresh(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Get initial token
|
|
err := trader.ensureAuthToken()
|
|
if err != nil {
|
|
t.Fatalf("ensureAuthToken failed: %v", err)
|
|
}
|
|
t.Logf("✅ Initial auth token obtained")
|
|
|
|
// Force refresh
|
|
err = trader.refreshAuthToken()
|
|
if err != nil {
|
|
t.Errorf("refreshAuthToken failed: %v", err)
|
|
} else {
|
|
t.Log("✅ Auth token refreshed successfully")
|
|
}
|
|
|
|
// Verify token works by making API call
|
|
_, err = trader.GetBalance()
|
|
if err != nil {
|
|
t.Errorf("GetBalance after refresh failed: %v", err)
|
|
} else {
|
|
t.Log("✅ Token verified working after refresh")
|
|
}
|
|
}
|
|
|
|
// ==================== Error Handling Tests ====================
|
|
|
|
func TestLighterInvalidSymbol(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test with invalid symbol
|
|
_, err := trader.GetMarketPrice("INVALID_SYMBOL_XYZ")
|
|
if err == nil {
|
|
t.Error("Expected error for invalid symbol, got nil")
|
|
} else {
|
|
t.Logf("✅ Got expected error for invalid symbol: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLighterCancelNonExistentOrder(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Try to cancel non-existent order
|
|
err := trader.CancelOrder("ETH", "999999999999")
|
|
if err == nil {
|
|
t.Log("⚠️ No error for cancelling non-existent order (may be expected)")
|
|
} else {
|
|
t.Logf("✅ Got error for non-existent order: %v", err)
|
|
}
|
|
}
|
|
|
|
// ==================== OrderSync Tests ====================
|
|
|
|
func TestLighterOrderSync(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Get trades to simulate order sync
|
|
startTime := time.Now().Add(-24 * time.Hour)
|
|
trades, err := trader.GetTrades(startTime, 50)
|
|
if err != nil {
|
|
t.Fatalf("GetTrades failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ OrderSync simulation: retrieved %d trades", len(trades))
|
|
|
|
// Analyze trades
|
|
openTrades := 0
|
|
closeTrades := 0
|
|
for _, trade := range trades {
|
|
if trade.OrderAction == "open_long" || trade.OrderAction == "open_short" {
|
|
openTrades++
|
|
} else if trade.OrderAction == "close_long" || trade.OrderAction == "close_short" {
|
|
closeTrades++
|
|
}
|
|
}
|
|
|
|
t.Logf(" Open trades: %d, Close trades: %d", openTrades, closeTrades)
|
|
}
|
|
|
|
// ==================== Benchmark Tests ====================
|
|
|
|
func BenchmarkLighterGetBalance(b *testing.B) {
|
|
if os.Getenv("LIGHTER_TEST") != "1" || os.Getenv("LIGHTER_API_KEY") == "" {
|
|
b.Skip("Skipping benchmark. Set LIGHTER_TEST=1 and LIGHTER_API_KEY to run")
|
|
}
|
|
|
|
walletAddr, apiKey, apiKeyIndex := getTestConfig()
|
|
trader, err := NewLighterTraderV2(walletAddr, apiKey, apiKeyIndex, false)
|
|
if err != nil {
|
|
b.Fatalf("Failed to create trader: %v", err)
|
|
}
|
|
defer trader.Cleanup()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := trader.GetBalance()
|
|
if err != nil {
|
|
b.Fatalf("GetBalance failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkLighterGetMarketPrice(b *testing.B) {
|
|
if os.Getenv("LIGHTER_TEST") != "1" || os.Getenv("LIGHTER_API_KEY") == "" {
|
|
b.Skip("Skipping benchmark. Set LIGHTER_TEST=1 and LIGHTER_API_KEY to run")
|
|
}
|
|
|
|
walletAddr, apiKey, apiKeyIndex := getTestConfig()
|
|
trader, err := NewLighterTraderV2(walletAddr, apiKey, apiKeyIndex, false)
|
|
if err != nil {
|
|
b.Fatalf("Failed to create trader: %v", err)
|
|
}
|
|
defer trader.Cleanup()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := trader.GetMarketPrice("ETH")
|
|
if err != nil {
|
|
b.Fatalf("GetMarketPrice failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== GetOpenOrders Tests ====================
|
|
|
|
func TestLighterGetOpenOrders(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test GetOpenOrders
|
|
orders, err := trader.GetOpenOrders("ETH")
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("GetOpenOrders failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ GetOpenOrders: found %d open orders", len(orders))
|
|
for i, order := range orders {
|
|
if i >= 5 {
|
|
t.Logf(" ... and %d more", len(orders)-5)
|
|
break
|
|
}
|
|
t.Logf(" [%d] %s %s %s: qty=%.4f @ %.2f, status=%s",
|
|
i+1, order.Symbol, order.Side, order.Type, order.Quantity, order.Price, order.Status)
|
|
}
|
|
}
|
|
|
|
func TestLighterGetActiveOrders(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test GetActiveOrders (internal API)
|
|
orders, err := trader.GetActiveOrders("ETH")
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("GetActiveOrders failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ GetActiveOrders: found %d active orders", len(orders))
|
|
for i, order := range orders {
|
|
if i >= 5 {
|
|
t.Logf(" ... and %d more", len(orders)-5)
|
|
break
|
|
}
|
|
t.Logf(" [%d] OrderID=%s, Type=%s, Price=%s, RemainingAmount=%s",
|
|
i+1, order.OrderID, order.Type, order.Price, order.RemainingBaseAmount)
|
|
}
|
|
}
|
|
|
|
// ==================== OrderBook Tests ====================
|
|
|
|
func TestLighterGetOrderBook(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test GetOrderBook
|
|
bids, asks, err := trader.GetOrderBook("ETH", 10)
|
|
if err != nil {
|
|
// OrderBook API may not be available in all regions or require special permissions
|
|
if strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "restricted") {
|
|
t.Skipf("Skipping: OrderBook API not available: %v", err)
|
|
}
|
|
t.Fatalf("GetOrderBook failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ GetOrderBook: %d bids, %d asks", len(bids), len(asks))
|
|
|
|
if len(bids) > 0 {
|
|
t.Logf(" Best Bid: %.2f @ %.4f", bids[0][0], bids[0][1])
|
|
}
|
|
if len(asks) > 0 {
|
|
t.Logf(" Best Ask: %.2f @ %.4f", asks[0][0], asks[0][1])
|
|
}
|
|
|
|
// Verify spread makes sense
|
|
if len(bids) > 0 && len(asks) > 0 {
|
|
spread := asks[0][0] - bids[0][0]
|
|
spreadPct := spread / bids[0][0] * 100
|
|
t.Logf(" Spread: %.2f (%.4f%%)", spread, spreadPct)
|
|
|
|
if spread < 0 {
|
|
t.Error("Invalid spread: ask < bid")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== PlaceLimitOrder (GridTrader) Tests ====================
|
|
|
|
func TestLighterPlaceLimitOrder(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Get current market price
|
|
marketPrice, err := trader.GetMarketPrice("ETH")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get market price: %v", err)
|
|
}
|
|
t.Logf("Current ETH price: %.2f", marketPrice)
|
|
|
|
// Create a limit order using PlaceLimitOrder (GridTrader interface)
|
|
// Buy order at 75% of market price (won't fill)
|
|
limitPrice := marketPrice * 0.75
|
|
quantity := 0.01
|
|
|
|
req := &tradertypes.LimitOrderRequest{
|
|
Symbol: "ETH",
|
|
Side: "BUY",
|
|
PositionSide: "LONG",
|
|
Price: limitPrice,
|
|
Quantity: quantity,
|
|
Leverage: 10,
|
|
ClientID: "test-order-001",
|
|
ReduceOnly: false,
|
|
}
|
|
|
|
t.Logf("Placing limit order via PlaceLimitOrder: %s %.4f @ %.2f", req.Side, req.Quantity, req.Price)
|
|
|
|
result, err := trader.PlaceLimitOrder(req)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("PlaceLimitOrder failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ PlaceLimitOrder result: OrderID=%s, Status=%s", result.OrderID, result.Status)
|
|
|
|
if result.OrderID == "" {
|
|
t.Fatal("Expected OrderID in result")
|
|
}
|
|
|
|
// Wait and cancel
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Cancel the order
|
|
err = trader.CancelOrder("ETH", result.OrderID)
|
|
if err != nil {
|
|
t.Logf("⚠️ Failed to cancel order: %v", err)
|
|
} else {
|
|
t.Log("✅ Order cancelled successfully")
|
|
}
|
|
}
|
|
|
|
// ==================== SetMarginMode Tests ====================
|
|
|
|
func TestLighterSetMarginMode(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test setting cross margin
|
|
t.Log("Setting margin mode to CROSS...")
|
|
err := trader.SetMarginMode("ETH", true)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Errorf("SetMarginMode(cross) failed: %v", err)
|
|
} else {
|
|
t.Log("✅ SetMarginMode(cross) succeeded")
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Note: Isolated margin may fail if there's an open position
|
|
// Just test cross margin for safety
|
|
}
|
|
|
|
// ==================== Stop-Loss/Take-Profit Tests ====================
|
|
|
|
func TestLighterStopLossOrder(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping stop-loss test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Check if we have a position first
|
|
pos, err := trader.GetPosition("ETH")
|
|
if err != nil {
|
|
t.Fatalf("GetPosition failed: %v", err)
|
|
}
|
|
|
|
if pos == nil || pos.Size == 0 {
|
|
t.Skip("No ETH position to set stop-loss for")
|
|
}
|
|
|
|
// Calculate stop-loss price (5% below entry for long, 5% above for short)
|
|
var stopPrice float64
|
|
if pos.Side == "long" {
|
|
stopPrice = pos.EntryPrice * 0.95
|
|
} else {
|
|
stopPrice = pos.EntryPrice * 1.05
|
|
}
|
|
|
|
t.Logf("Position: %s %s, size=%.4f, entry=%.2f", pos.Symbol, pos.Side, pos.Size, pos.EntryPrice)
|
|
t.Logf("Setting stop-loss at %.2f", stopPrice)
|
|
|
|
err = trader.SetStopLoss("ETH", strings.ToUpper(pos.Side), pos.Size, stopPrice)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Errorf("SetStopLoss failed: %v", err)
|
|
} else {
|
|
t.Log("✅ SetStopLoss succeeded")
|
|
}
|
|
}
|
|
|
|
func TestLighterTakeProfitOrder(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping take-profit test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Check if we have a position first
|
|
pos, err := trader.GetPosition("ETH")
|
|
if err != nil {
|
|
t.Fatalf("GetPosition failed: %v", err)
|
|
}
|
|
|
|
if pos == nil || pos.Size == 0 {
|
|
t.Skip("No ETH position to set take-profit for")
|
|
}
|
|
|
|
// Calculate take-profit price (10% above entry for long, 10% below for short)
|
|
var takeProfitPrice float64
|
|
if pos.Side == "long" {
|
|
takeProfitPrice = pos.EntryPrice * 1.10
|
|
} else {
|
|
takeProfitPrice = pos.EntryPrice * 0.90
|
|
}
|
|
|
|
t.Logf("Position: %s %s, size=%.4f, entry=%.2f", pos.Symbol, pos.Side, pos.Size, pos.EntryPrice)
|
|
t.Logf("Setting take-profit at %.2f", takeProfitPrice)
|
|
|
|
err = trader.SetTakeProfit("ETH", strings.ToUpper(pos.Side), pos.Size, takeProfitPrice)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Errorf("SetTakeProfit failed: %v", err)
|
|
} else {
|
|
t.Log("✅ SetTakeProfit succeeded")
|
|
}
|
|
}
|
|
|
|
// ==================== Full Trading Flow Tests ====================
|
|
|
|
func TestLighterFullTradingFlow(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping full trading flow test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
symbol := "ETH"
|
|
quantity := 0.01 // Minimum quantity
|
|
leverage := 10
|
|
|
|
// Step 1: Get initial state
|
|
t.Log("=== Step 1: Get Initial State ===")
|
|
balance, _ := trader.GetBalance()
|
|
if equity, ok := balance["total_equity"].(float64); ok {
|
|
t.Logf(" Initial equity: %.2f", equity)
|
|
}
|
|
|
|
marketPrice, err := trader.GetMarketPrice(symbol)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get market price: %v", err)
|
|
}
|
|
t.Logf(" Market price: %.2f", marketPrice)
|
|
|
|
// Step 2: Set leverage
|
|
t.Log("=== Step 2: Set Leverage ===")
|
|
err = trader.SetLeverage(symbol, leverage)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("SetLeverage failed: %v", err)
|
|
}
|
|
t.Logf(" Leverage set to %dx", leverage)
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Step 3: Open Long Position
|
|
t.Log("=== Step 3: Open Long Position ===")
|
|
result, err := trader.OpenLong(symbol, quantity, leverage)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("OpenLong failed: %v", err)
|
|
}
|
|
t.Logf(" OpenLong result: %v", result)
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Step 4: Verify position
|
|
t.Log("=== Step 4: Verify Position ===")
|
|
pos, err := trader.GetPosition(symbol)
|
|
if err != nil {
|
|
t.Errorf("GetPosition failed: %v", err)
|
|
} else if pos != nil {
|
|
t.Logf(" Position: %s %s, size=%.4f, entry=%.2f, pnl=%.2f",
|
|
pos.Symbol, pos.Side, pos.Size, pos.EntryPrice, pos.UnrealizedPnL)
|
|
}
|
|
|
|
// Step 5: Place limit order (sell at higher price)
|
|
t.Log("=== Step 5: Place Limit Sell Order ===")
|
|
limitPrice := marketPrice * 1.05 // 5% above market
|
|
limitResult, err := trader.CreateOrder(symbol, true, quantity, limitPrice, "limit", true)
|
|
if err != nil {
|
|
t.Logf(" Failed to place limit order: %v", err)
|
|
} else {
|
|
t.Logf(" Limit order placed: %v", limitResult)
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Step 6: Get open orders
|
|
t.Log("=== Step 6: Get Open Orders ===")
|
|
orders, err := trader.GetOpenOrders(symbol)
|
|
if err != nil {
|
|
t.Logf(" Failed to get open orders: %v", err)
|
|
} else {
|
|
t.Logf(" Open orders: %d", len(orders))
|
|
for _, o := range orders {
|
|
t.Logf(" - %s %s: qty=%.4f @ %.2f", o.Side, o.Type, o.Quantity, o.Price)
|
|
}
|
|
}
|
|
|
|
// Step 7: Cancel all orders
|
|
t.Log("=== Step 7: Cancel All Orders ===")
|
|
err = trader.CancelAllOrders(symbol)
|
|
if err != nil {
|
|
t.Logf(" Failed to cancel orders: %v", err)
|
|
} else {
|
|
t.Log(" All orders cancelled")
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Step 8: Close position
|
|
t.Log("=== Step 8: Close Position ===")
|
|
closeResult, err := trader.CloseLong(symbol, 0) // 0 = close all
|
|
if err != nil {
|
|
t.Errorf("CloseLong failed: %v", err)
|
|
} else {
|
|
t.Logf(" CloseLong result: %v", closeResult)
|
|
}
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Step 9: Verify position closed
|
|
t.Log("=== Step 9: Verify Position Closed ===")
|
|
pos, _ = trader.GetPosition(symbol)
|
|
if pos == nil || pos.Size == 0 {
|
|
t.Log(" ✅ Position closed successfully")
|
|
} else {
|
|
t.Logf(" ⚠️ Position still exists: size=%.4f", pos.Size)
|
|
}
|
|
|
|
// Step 10: Get final balance
|
|
t.Log("=== Step 10: Get Final State ===")
|
|
balance, _ = trader.GetBalance()
|
|
if equity, ok := balance["total_equity"].(float64); ok {
|
|
t.Logf(" Final equity: %.2f", equity)
|
|
}
|
|
|
|
t.Log("=== Full Trading Flow Completed ===")
|
|
}
|
|
|
|
// ==================== API Key Validation Tests ====================
|
|
|
|
func TestLighterAPIKeyValid(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Check if API key is valid
|
|
if trader.apiKeyValid {
|
|
t.Log("✅ API key is VALID and matches server")
|
|
} else {
|
|
t.Error("❌ API key is INVALID - does not match server")
|
|
}
|
|
|
|
// Verify by checking the actual API key
|
|
err := trader.checkClient()
|
|
if err != nil {
|
|
t.Errorf("API key verification error: %v", err)
|
|
} else {
|
|
t.Log("✅ API key verification passed")
|
|
}
|
|
}
|
|
|
|
// ==================== Market Order Tests ====================
|
|
|
|
func TestLighterMarketOrderBuy(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping market order test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Create a small market buy order
|
|
quantity := 0.01
|
|
t.Logf("Creating market buy order: %.4f ETH", quantity)
|
|
|
|
result, err := trader.CreateOrder("ETH", false, quantity, 0, "market", false)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("Market buy failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Market buy result: %v", result)
|
|
|
|
// Wait and close
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Close the position
|
|
_, err = trader.CloseLong("ETH", quantity)
|
|
if err != nil {
|
|
t.Logf("⚠️ Failed to close position: %v", err)
|
|
} else {
|
|
t.Log("✅ Position closed")
|
|
}
|
|
}
|
|
|
|
func TestLighterMarketOrderSell(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
if os.Getenv("LIGHTER_TRADE_TEST") != "1" {
|
|
t.Skip("Skipping market order test. Set LIGHTER_TRADE_TEST=1 to run")
|
|
}
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Create a small market sell order (short)
|
|
quantity := 0.01
|
|
t.Logf("Creating market sell order (short): %.4f ETH", quantity)
|
|
|
|
result, err := trader.CreateOrder("ETH", true, quantity, 0, "market", false)
|
|
skipIfJurisdictionRestricted(t, err)
|
|
if err != nil {
|
|
t.Fatalf("Market sell failed: %v", err)
|
|
}
|
|
|
|
t.Logf("✅ Market sell result: %v", result)
|
|
|
|
// Wait and close
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Close the position
|
|
_, err = trader.CloseShort("ETH", quantity)
|
|
if err != nil {
|
|
t.Logf("⚠️ Failed to close position: %v", err)
|
|
} else {
|
|
t.Log("✅ Position closed")
|
|
}
|
|
}
|
|
|
|
// ==================== GetPosition Tests ====================
|
|
|
|
func TestLighterGetPosition(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test GetPosition for ETH
|
|
pos, err := trader.GetPosition("ETH")
|
|
if err != nil {
|
|
t.Fatalf("GetPosition failed: %v", err)
|
|
}
|
|
|
|
if pos == nil {
|
|
t.Log("✅ No ETH position (pos is nil)")
|
|
} else if pos.Size == 0 {
|
|
t.Log("✅ No ETH position (size is 0)")
|
|
} else {
|
|
t.Logf("✅ ETH position found:")
|
|
t.Logf(" Symbol: %s", pos.Symbol)
|
|
t.Logf(" Side: %s", pos.Side)
|
|
t.Logf(" Size: %.4f", pos.Size)
|
|
t.Logf(" Entry Price: %.2f", pos.EntryPrice)
|
|
t.Logf(" Mark Price: %.2f", pos.MarkPrice)
|
|
t.Logf(" Liquidation Price: %.2f", pos.LiquidationPrice)
|
|
t.Logf(" Unrealized PnL: %.2f", pos.UnrealizedPnL)
|
|
t.Logf(" Leverage: %.1fx", pos.Leverage)
|
|
}
|
|
}
|
|
|
|
// ==================== Symbol Normalization Tests ====================
|
|
|
|
func TestLighterSymbolNormalization(t *testing.T) {
|
|
skipIfNoEnv(t)
|
|
|
|
trader := createTestTrader(t)
|
|
defer trader.Cleanup()
|
|
|
|
// Test different symbol formats
|
|
testCases := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"ETH", "ETH"},
|
|
{"ETH-PERP", "ETH"},
|
|
{"ETHUSDT", "ETH"},
|
|
{"ETH/USDT", "ETH"},
|
|
{"BTC", "BTC"},
|
|
{"BTCUSDT", "BTC"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
// Try to get market price with different formats
|
|
price, err := trader.GetMarketPrice(tc.input)
|
|
if err != nil {
|
|
t.Logf("⚠️ GetMarketPrice(%s) failed: %v", tc.input, err)
|
|
} else {
|
|
t.Logf("✅ GetMarketPrice(%s) = %.2f", tc.input, price)
|
|
}
|
|
}
|
|
}
|