mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
47bff87966
- Fix xyz dex balance calculation (use marginSummary for isolated margin) - Add Alpaca provider for US stocks market data - Add TwelveData provider for forex & metals market data - Add Hyperliquid kline provider - Centralize API keys in config system - Add builder fee for order routing - Improve chart UI with compact design - Fix position history fee display precision - Add comprehensive balance calculation tests
670 lines
20 KiB
Go
670 lines
20 KiB
Go
package trader
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testXyzDexAsset is a local copy of testXyzDexAsset for testing
|
|
type testXyzDexAsset struct {
|
|
Name string `json:"name"`
|
|
SzDecimals int `json:"szDecimals"`
|
|
MaxLeverage int `json:"maxLeverage"`
|
|
}
|
|
|
|
// testXyzDexMeta is a local copy of xyzDexMeta for testing
|
|
type testXyzDexMeta struct {
|
|
Universe []testXyzDexAsset `json:"universe"`
|
|
}
|
|
|
|
// TestXyzDexMetaFetch tests fetching xyz dex meta from Hyperliquid API
|
|
func TestXyzDexMetaFetch(t *testing.T) {
|
|
reqBody := map[string]string{
|
|
"type": "meta",
|
|
"dex": "xyz",
|
|
}
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal request: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read response: %v", err)
|
|
}
|
|
|
|
var meta testXyzDexMeta
|
|
if err := json.Unmarshal(body, &meta); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(meta.Universe) == 0 {
|
|
t.Fatal("xyz meta universe is empty")
|
|
}
|
|
|
|
t.Logf("✅ xyz dex meta contains %d assets", len(meta.Universe))
|
|
|
|
// Check that SILVER exists
|
|
// HIP-3 perp dex asset index formula: 100000 + perp_dex_index * 10000 + index_in_meta
|
|
// xyz dex is at perp_dex_index = 1
|
|
found := false
|
|
for i, asset := range meta.Universe {
|
|
if asset.Name == "xyz:SILVER" {
|
|
found = true
|
|
assetIndex := 100000 + 1*10000 + i // xyz dex index = 1
|
|
t.Logf("✅ Found xyz:SILVER at index %d (asset ID: %d)", i, assetIndex)
|
|
t.Logf(" SzDecimals: %d, MaxLeverage: %d", asset.SzDecimals, asset.MaxLeverage)
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("xyz:SILVER not found in meta")
|
|
}
|
|
}
|
|
|
|
// TestXyzDexPriceFetch tests fetching xyz dex prices from Hyperliquid API
|
|
func TestXyzDexPriceFetch(t *testing.T) {
|
|
reqBody := map[string]string{
|
|
"type": "allMids",
|
|
"dex": "xyz",
|
|
}
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal request: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read response: %v", err)
|
|
}
|
|
|
|
var mids map[string]string
|
|
if err := json.Unmarshal(body, &mids); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
// Check that prices have xyz: prefix
|
|
silverPrice, ok := mids["xyz:SILVER"]
|
|
if !ok {
|
|
t.Fatal("xyz:SILVER price not found (key should include xyz: prefix)")
|
|
}
|
|
t.Logf("✅ xyz:SILVER price: %s", silverPrice)
|
|
|
|
// Verify a few more assets
|
|
testAssets := []string{"xyz:GOLD", "xyz:TSLA", "xyz:NVDA"}
|
|
for _, asset := range testAssets {
|
|
if price, ok := mids[asset]; ok {
|
|
t.Logf("✅ %s price: %s", asset, price)
|
|
} else {
|
|
t.Logf("⚠️ %s not found in prices", asset)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestXyzAssetIndexLookup tests the asset index lookup for xyz dex assets
|
|
func TestXyzAssetIndexLookup(t *testing.T) {
|
|
// Fetch xyz meta
|
|
reqBody := map[string]string{
|
|
"type": "meta",
|
|
"dex": "xyz",
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to fetch meta: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var meta testXyzDexMeta
|
|
json.Unmarshal(body, &meta)
|
|
|
|
// Test lookup with different formats
|
|
testCases := []struct {
|
|
input string
|
|
expected string // expected match in meta
|
|
}{
|
|
{"SILVER", "xyz:SILVER"},
|
|
{"xyz:SILVER", "xyz:SILVER"},
|
|
{"GOLD", "xyz:GOLD"},
|
|
{"xyz:TSLA", "xyz:TSLA"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
lookupName := tc.input
|
|
if !strings.HasPrefix(lookupName, "xyz:") {
|
|
lookupName = "xyz:" + lookupName
|
|
}
|
|
|
|
found := false
|
|
for i, asset := range meta.Universe {
|
|
if asset.Name == lookupName {
|
|
found = true
|
|
assetIndex := 100000 + 1*10000 + i // HIP-3 formula: 100000 + xyz_dex_index(1) * 10000 + meta_index
|
|
t.Logf("✅ Lookup '%s' -> found at index %d (asset ID: %d)", tc.input, i, assetIndex)
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("❌ Lookup '%s' -> NOT FOUND (expected to match %s)", tc.input, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestXyzSzDecimalsLookup tests the szDecimals lookup for different xyz assets
|
|
func TestXyzSzDecimalsLookup(t *testing.T) {
|
|
reqBody := map[string]string{
|
|
"type": "meta",
|
|
"dex": "xyz",
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to fetch meta: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var meta testXyzDexMeta
|
|
json.Unmarshal(body, &meta)
|
|
|
|
// Check szDecimals for various assets
|
|
expectedDecimals := map[string]int{
|
|
"xyz:SILVER": 2,
|
|
"xyz:GOLD": 4,
|
|
"xyz:TSLA": 3,
|
|
}
|
|
|
|
for name, expected := range expectedDecimals {
|
|
for _, asset := range meta.Universe {
|
|
if asset.Name == name {
|
|
if asset.SzDecimals == expected {
|
|
t.Logf("✅ %s szDecimals: %d (expected %d)", name, asset.SzDecimals, expected)
|
|
} else {
|
|
t.Logf("⚠️ %s szDecimals: %d (expected %d, may have changed)", name, asset.SzDecimals, expected)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestXyzOrderParameters tests order parameter calculation
|
|
func TestXyzOrderParameters(t *testing.T) {
|
|
// Simulate order parameter calculation
|
|
testCases := []struct {
|
|
price float64
|
|
size float64
|
|
szDecimals int
|
|
expectedSz float64
|
|
}{
|
|
{75.33, 1.0, 2, 1.00},
|
|
{75.33, 1.234, 2, 1.23},
|
|
{75.33, 5.567, 2, 5.57},
|
|
{188.15, 0.5, 3, 0.500},
|
|
{188.15, 0.1234, 3, 0.123},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
multiplier := 1.0
|
|
for i := 0; i < tc.szDecimals; i++ {
|
|
multiplier *= 10.0
|
|
}
|
|
roundedSize := float64(int(tc.size*multiplier+0.5)) / multiplier
|
|
|
|
if roundedSize != tc.expectedSz {
|
|
t.Errorf("Size rounding failed: input=%v, decimals=%d, got=%v, expected=%v",
|
|
tc.size, tc.szDecimals, roundedSize, tc.expectedSz)
|
|
} else {
|
|
t.Logf("✅ Size rounding: %v (decimals=%d) -> %v", tc.size, tc.szDecimals, roundedSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestXyzAssetIndexCalculation tests the HIP-3 asset index calculation
|
|
// Formula: 100000 + perp_dex_index * 10000 + meta_index
|
|
// For xyz dex: perp_dex_index = 1, so asset_index = 110000 + meta_index
|
|
func TestXyzAssetIndexCalculation(t *testing.T) {
|
|
reqBody := map[string]string{
|
|
"type": "meta",
|
|
"dex": "xyz",
|
|
}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to fetch meta: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var meta testXyzDexMeta
|
|
json.Unmarshal(body, &meta)
|
|
|
|
// Test asset index calculation for SILVER
|
|
// HIP-3 perp dex asset index formula: 100000 + perp_dex_index * 10000 + index_in_meta
|
|
// xyz dex is at perp_dex_index = 1
|
|
const xyzPerpDexIndex = 1
|
|
for i, asset := range meta.Universe {
|
|
if asset.Name == "xyz:SILVER" {
|
|
assetIndex := 100000 + xyzPerpDexIndex*10000 + i
|
|
t.Logf("✅ xyz:SILVER: meta_index=%d, asset_index=%d", i, assetIndex)
|
|
|
|
if assetIndex < 110000 {
|
|
t.Errorf("Asset index should be >= 110000, got %d", assetIndex)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Log first few assets for reference
|
|
t.Log("\nFirst 5 xyz assets:")
|
|
for i := 0; i < 5 && i < len(meta.Universe); i++ {
|
|
asset := meta.Universe[i]
|
|
assetIndex := 100000 + xyzPerpDexIndex*10000 + i
|
|
t.Logf(" [%d] %s -> asset_index=%d, szDecimals=%d", i, asset.Name, assetIndex, asset.SzDecimals)
|
|
}
|
|
}
|
|
|
|
// TestIsXyzDexAsset tests the isXyzDexAsset function
|
|
func TestIsXyzDexAsset(t *testing.T) {
|
|
testCases := []struct {
|
|
symbol string
|
|
expected bool
|
|
}{
|
|
{"xyz:SILVER", true},
|
|
{"SILVER", true},
|
|
{"silver", true},
|
|
{"xyz:GOLD", true},
|
|
{"GOLD", true},
|
|
{"xyz:TSLA", true},
|
|
{"TSLA", true},
|
|
{"BTCUSDT", false},
|
|
{"BTC", false},
|
|
{"ETHUSDT", false},
|
|
{"SOLUSDT", false},
|
|
{"xyz:BTC", false}, // BTC is not an xyz asset
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := isXyzDexAsset(tc.symbol)
|
|
if result != tc.expected {
|
|
t.Errorf("isXyzDexAsset(%q) = %v, expected %v", tc.symbol, result, tc.expected)
|
|
} else {
|
|
t.Logf("✅ isXyzDexAsset(%q) = %v", tc.symbol, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConvertSymbolToHyperliquidXyz tests symbol conversion for xyz assets
|
|
func TestConvertSymbolToHyperliquidXyz(t *testing.T) {
|
|
testCases := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"SILVER", "xyz:SILVER"},
|
|
{"silver", "xyz:SILVER"},
|
|
{"xyz:SILVER", "xyz:SILVER"},
|
|
{"GOLD", "xyz:GOLD"},
|
|
{"TSLA", "xyz:TSLA"},
|
|
{"BTC", "BTC"},
|
|
{"BTCUSDT", "BTC"},
|
|
{"ETH", "ETH"},
|
|
{"ETHUSDT", "ETH"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := convertSymbolToHyperliquid(tc.input)
|
|
if result != tc.expected {
|
|
t.Errorf("convertSymbolToHyperliquid(%q) = %q, expected %q", tc.input, result, tc.expected)
|
|
} else {
|
|
t.Logf("✅ convertSymbolToHyperliquid(%q) = %q", tc.input, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestXyzDexOrderFlow tests the complete order flow (without actually placing an order)
|
|
func TestXyzDexOrderFlow(t *testing.T) {
|
|
t.Log("=== Testing xyz Dex Order Flow ===")
|
|
|
|
// Step 1: Fetch meta
|
|
t.Log("\nStep 1: Fetching xyz meta...")
|
|
reqBody := map[string]string{"type": "meta", "dex": "xyz"}
|
|
jsonBody, _ := json.Marshal(reqBody)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(jsonBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to fetch meta: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var meta testXyzDexMeta
|
|
json.Unmarshal(body, &meta)
|
|
t.Logf("✅ Fetched %d xyz assets", len(meta.Universe))
|
|
|
|
// Step 2: Find SILVER
|
|
t.Log("\nStep 2: Looking up xyz:SILVER...")
|
|
var silverIndex int = -1
|
|
var silverAsset *testXyzDexAsset
|
|
for i, asset := range meta.Universe {
|
|
if asset.Name == "xyz:SILVER" {
|
|
silverIndex = i
|
|
silverAsset = &meta.Universe[i]
|
|
break
|
|
}
|
|
}
|
|
if silverIndex < 0 {
|
|
t.Fatal("SILVER not found in xyz meta")
|
|
}
|
|
t.Logf("✅ Found at index %d", silverIndex)
|
|
|
|
// Step 3: Fetch price
|
|
t.Log("\nStep 3: Fetching price...")
|
|
priceReq := map[string]string{"type": "allMids", "dex": "xyz"}
|
|
priceBody, _ := json.Marshal(priceReq)
|
|
req2, _ := http.NewRequestWithContext(ctx, "POST", "https://api.hyperliquid.xyz/info", bytes.NewBuffer(priceBody))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
resp2, _ := client.Do(req2)
|
|
body2, _ := io.ReadAll(resp2.Body)
|
|
resp2.Body.Close()
|
|
|
|
var mids map[string]string
|
|
json.Unmarshal(body2, &mids)
|
|
priceStr := mids["xyz:SILVER"]
|
|
var price float64
|
|
fmt.Sscanf(priceStr, "%f", &price)
|
|
t.Logf("✅ Price: %s", priceStr)
|
|
|
|
// Step 4: Calculate order parameters
|
|
t.Log("\nStep 4: Calculating order parameters...")
|
|
orderSize := 1.0
|
|
multiplier := 1.0
|
|
for i := 0; i < silverAsset.SzDecimals; i++ {
|
|
multiplier *= 10.0
|
|
}
|
|
roundedSize := float64(int(orderSize*multiplier+0.5)) / multiplier
|
|
roundedPrice := price * 1.001 // 0.1% slippage
|
|
// HIP-3 perp dex asset index formula: 100000 + perp_dex_index * 10000 + index_in_meta
|
|
// xyz dex is at perp_dex_index = 1
|
|
assetIndex := 100000 + 1*10000 + silverIndex
|
|
|
|
t.Logf(" Asset Index: %d (110000 + %d)", assetIndex, silverIndex)
|
|
t.Logf(" Size: %.4f (szDecimals=%d)", roundedSize, silverAsset.SzDecimals)
|
|
t.Logf(" Price: %.4f (with slippage)", roundedPrice)
|
|
|
|
// Step 5: Summary
|
|
t.Log("\n=== Order Flow Test Summary ===")
|
|
t.Log("✅ Meta fetch: OK")
|
|
t.Log("✅ Asset lookup: OK")
|
|
t.Log("✅ Price fetch: OK")
|
|
t.Log("✅ Parameter calculation: OK")
|
|
t.Logf("\n📋 Order would be placed with:")
|
|
t.Logf(" coin: xyz:SILVER")
|
|
t.Logf(" assetIndex: %d", assetIndex)
|
|
t.Logf(" isBuy: true")
|
|
t.Logf(" size: %.4f", roundedSize)
|
|
t.Logf(" price: %.4f", roundedPrice)
|
|
}
|
|
|
|
// TestXyzDexLiveOrder tests placing a real order on xyz dex
|
|
// This test requires:
|
|
// - XYZ_DEX_LIVE_TEST=1 to enable
|
|
// - TEST_PRIVATE_KEY - the private key for signing
|
|
// - TEST_WALLET_ADDR - the wallet address with funds
|
|
func TestXyzDexLiveOrder(t *testing.T) {
|
|
// Skip unless explicitly enabled
|
|
if os.Getenv("XYZ_DEX_LIVE_TEST") != "1" {
|
|
t.Skip("Skipping live order test. Set XYZ_DEX_LIVE_TEST=1 to run")
|
|
}
|
|
|
|
// Get credentials from environment variables
|
|
privateKeyHex := os.Getenv("TEST_PRIVATE_KEY")
|
|
walletAddr := os.Getenv("TEST_WALLET_ADDR")
|
|
|
|
if privateKeyHex == "" || walletAddr == "" {
|
|
t.Skip("TEST_PRIVATE_KEY and TEST_WALLET_ADDR env vars required")
|
|
}
|
|
|
|
t.Logf("=== Live xyz Dex Order Test ===")
|
|
t.Logf("Wallet: %s", walletAddr)
|
|
|
|
// Create trader instance
|
|
trader, err := NewHyperliquidTrader(privateKeyHex, walletAddr, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create trader: %v", err)
|
|
}
|
|
|
|
// Check xyz dex balance first
|
|
xyzState, _ := trader.exchange.Info().UserState(trader.ctx, walletAddr, "xyz")
|
|
if xyzState != nil && xyzState.CrossMarginSummary.AccountValue == "0.0" {
|
|
t.Logf("⚠️ xyz dex account has no funds (balance: %s)", xyzState.CrossMarginSummary.AccountValue)
|
|
t.Logf(" To trade xyz dex, you need to transfer funds using perpDexClassTransfer")
|
|
t.Logf(" The test will still verify order signing and submission...")
|
|
}
|
|
|
|
// Fetch xyz meta first
|
|
if err := trader.fetchXyzMeta(); err != nil {
|
|
t.Fatalf("Failed to fetch xyz meta: %v", err)
|
|
}
|
|
|
|
// Get current price for xyz:SILVER
|
|
price, err := trader.getXyzMarketPrice("xyz:SILVER")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get price: %v", err)
|
|
}
|
|
t.Logf("Current xyz:SILVER price: %.4f", price)
|
|
|
|
// Place a test order (minimum $10 value = 0.14 SILVER at ~$75)
|
|
// With 5% slippage for IOC (market order)
|
|
testSize := 0.14 // ~$10.5 at current price
|
|
testPrice := price * 1.05 // 5% above market for IOC buy (market order)
|
|
|
|
t.Logf("Attempting to place order:")
|
|
t.Logf(" Symbol: xyz:SILVER")
|
|
t.Logf(" Side: BUY")
|
|
t.Logf(" Size: %.4f", testSize)
|
|
t.Logf(" Price: %.4f", testPrice)
|
|
|
|
// Place the order using the new direct method
|
|
err = trader.placeXyzOrder("xyz:SILVER", true, testSize, testPrice, false)
|
|
if err != nil {
|
|
t.Logf("⚠️ Order result: %v", err)
|
|
// Check if this is an expected error (e.g., insufficient margin, no matching orders for IOC)
|
|
if strings.Contains(err.Error(), "insufficient") || strings.Contains(err.Error(), "margin") || strings.Contains(err.Error(), "minimum value") {
|
|
t.Logf("This may be expected if the test wallet has no margin in xyz dex")
|
|
t.Logf("✅ Order was properly signed and submitted (API validated format/signature)")
|
|
} else if strings.Contains(err.Error(), "could not immediately match") {
|
|
// IOC order didn't fill - this is actually SUCCESS!
|
|
// It means the order was properly signed, submitted, and processed
|
|
t.Logf("✅ Order was properly submitted but didn't fill (IOC with no matching orders)")
|
|
t.Logf(" This confirms the asset index (%d) and signing are correct!", 110026)
|
|
} else if strings.Contains(err.Error(), "Order has invalid price") || strings.Contains(err.Error(), "95% away") {
|
|
t.Errorf("FAILED: Order has invalid price - asset index issue")
|
|
} else {
|
|
t.Errorf("FAILED: Unexpected error: %v", err)
|
|
}
|
|
} else {
|
|
t.Logf("✅ Order placed and filled successfully!")
|
|
}
|
|
}
|
|
|
|
// TestXyzDexClosePosition tests closing a position on xyz dex
|
|
// This test requires the XYZ_DEX_LIVE_TEST environment variable to be set
|
|
func TestXyzDexClosePosition(t *testing.T) {
|
|
// Skip unless explicitly enabled
|
|
if os.Getenv("XYZ_DEX_LIVE_TEST") != "1" {
|
|
t.Skip("Skipping live close position test. Set XYZ_DEX_LIVE_TEST=1 to run")
|
|
}
|
|
|
|
// Get credentials from environment variables
|
|
privateKeyHex := os.Getenv("TEST_PRIVATE_KEY")
|
|
walletAddr := os.Getenv("TEST_WALLET_ADDR")
|
|
|
|
if privateKeyHex == "" || walletAddr == "" {
|
|
t.Skip("TEST_PRIVATE_KEY and TEST_WALLET_ADDR env vars required")
|
|
}
|
|
|
|
t.Logf("=== Live xyz Dex Close Position Test ===")
|
|
t.Logf("Wallet: %s", walletAddr)
|
|
|
|
// Create trader instance
|
|
trader, err := NewHyperliquidTrader(privateKeyHex, walletAddr, false)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create trader: %v", err)
|
|
}
|
|
|
|
// Check current xyz dex position
|
|
xyzState, err := trader.exchange.Info().UserState(trader.ctx, walletAddr, "xyz")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get xyz state: %v", err)
|
|
}
|
|
|
|
if len(xyzState.AssetPositions) == 0 {
|
|
t.Logf("No xyz dex positions to close")
|
|
return
|
|
}
|
|
|
|
// Get the position details
|
|
pos := xyzState.AssetPositions[0].Position
|
|
entryPx := ""
|
|
if pos.EntryPx != nil {
|
|
entryPx = *pos.EntryPx
|
|
}
|
|
t.Logf("Current position: %s size=%s entryPx=%s", pos.Coin, pos.Szi, entryPx)
|
|
|
|
// Fetch xyz meta
|
|
if err := trader.fetchXyzMeta(); err != nil {
|
|
t.Fatalf("Failed to fetch xyz meta: %v", err)
|
|
}
|
|
|
|
// Get current price
|
|
price, err := trader.getXyzMarketPrice(pos.Coin)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get price: %v", err)
|
|
}
|
|
t.Logf("Current %s price: %.4f", pos.Coin, price)
|
|
|
|
// Parse position size
|
|
var posSize float64
|
|
fmt.Sscanf(pos.Szi, "%f", &posSize)
|
|
|
|
// Close position: if long (szi > 0), sell; if short (szi < 0), buy
|
|
isBuy := posSize < 0
|
|
closeSize := posSize
|
|
if closeSize < 0 {
|
|
closeSize = -closeSize
|
|
}
|
|
|
|
// Use aggressive slippage for close
|
|
closePrice := price * 0.95 // 5% below for sell
|
|
if isBuy {
|
|
closePrice = price * 1.05 // 5% above for buy
|
|
}
|
|
|
|
t.Logf("Closing position:")
|
|
t.Logf(" Side: %s", map[bool]string{true: "BUY", false: "SELL"}[isBuy])
|
|
t.Logf(" Size: %.4f", closeSize)
|
|
t.Logf(" Price: %.4f", closePrice)
|
|
|
|
// Place close order with reduceOnly=true
|
|
err = trader.placeXyzOrder(pos.Coin, isBuy, closeSize, closePrice, true)
|
|
if err != nil {
|
|
t.Logf("⚠️ Close order result: %v", err)
|
|
if strings.Contains(err.Error(), "could not immediately match") {
|
|
t.Logf("✅ Close order submitted but didn't fill (IOC)")
|
|
} else {
|
|
t.Errorf("FAILED: %v", err)
|
|
}
|
|
} else {
|
|
t.Logf("✅ Position closed successfully!")
|
|
}
|
|
|
|
// Verify position is closed
|
|
xyzState2, _ := trader.exchange.Info().UserState(trader.ctx, walletAddr, "xyz")
|
|
if len(xyzState2.AssetPositions) == 0 {
|
|
t.Logf("✅ Position confirmed closed (no positions remaining)")
|
|
} else {
|
|
newPos := xyzState2.AssetPositions[0].Position
|
|
t.Logf("Position after close: %s size=%s", newPos.Coin, newPos.Szi)
|
|
}
|
|
}
|