Files
nofx/trader/alpaca/trader_test.go
shinchan-zhai f1a0725130 feat: route nofxos data requests through claw402 x402 payment
When CLAW402_WALLET_KEY env var is set, all nofxos.ai data API calls
(AI500, OI rankings, NetFlow, price rankings, etc.) are automatically
routed through claw402.ai with x402 USDC payment instead of direct
free access.

- New Claw402DataClient for GET requests with x402 payment flow
- Endpoint mapping: /api/xxx -> /api/v1/nofx/xxx
- MakeClaw402SignFunc helper for reusable payment signing
- Auto-detection: if CLAW402_WALLET_KEY is set, claw402 mode activates
- Fallback: without wallet key, direct nofxos.ai access (backward compatible)

Env vars:
  CLAW402_WALLET_KEY=0x...  # wallet private key for payment
  CLAW402_URL=https://claw402.ai  # optional, defaults to claw402.ai
2026-03-25 01:07:22 +08:00

836 lines
21 KiB
Go

package alpaca
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// --- Mock Alpaca Server ---
type mockAlpacaServer struct {
server *httptest.Server
positions []AlpacaPosition
orders []AlpacaOrder
account AlpacaAccount
nextOrderID int
}
func newMockAlpacaServer() *mockAlpacaServer {
m := &mockAlpacaServer{
account: AlpacaAccount{
ID: "test-account-id",
AccountNumber: "PA1234567",
Status: "ACTIVE",
Currency: "USD",
Cash: "100000.00",
Equity: "100000.00",
BuyingPower: "100000.00",
},
nextOrderID: 1000,
}
mux := http.NewServeMux()
// Account
mux.HandleFunc("/v2/account", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "method not allowed", 405)
return
}
json.NewEncoder(w).Encode(m.account)
})
// Positions
mux.HandleFunc("/v2/positions", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && !strings.Contains(r.URL.Path, "/v2/positions/") {
json.NewEncoder(w).Encode(m.positions)
return
}
})
// Position close by symbol (DELETE /v2/positions/AAPL)
mux.HandleFunc("/v2/positions/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "DELETE" {
symbol := strings.TrimPrefix(r.URL.Path, "/v2/positions/")
// Remove position
var remaining []AlpacaPosition
found := false
for _, p := range m.positions {
if p.Symbol == symbol {
found = true
continue
}
remaining = append(remaining, p)
}
if !found {
http.Error(w, `{"code":40410000,"message":"position does not exist"}`, 404)
return
}
m.positions = remaining
order := m.createOrder(symbol, "sell", "0", "market", "filled")
json.NewEncoder(w).Encode(order)
return
}
// GET single position
if r.Method == "GET" {
symbol := strings.TrimPrefix(r.URL.Path, "/v2/positions/")
for _, p := range m.positions {
if p.Symbol == symbol {
json.NewEncoder(w).Encode(p)
return
}
}
http.Error(w, `{"code":40410000,"message":"position does not exist"}`, 404)
}
})
// Orders
mux.HandleFunc("/v2/orders", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
status := r.URL.Query().Get("status")
symbolsFilter := r.URL.Query().Get("symbols")
var result []AlpacaOrder
for _, o := range m.orders {
// Alpaca's "open" status filter matches: new, partially_filled, accepted
if status == "open" {
if o.Status != "new" && o.Status != "partially_filled" && o.Status != "accepted" {
continue
}
} else if status == "closed" {
if o.Status != "filled" && o.Status != "canceled" && o.Status != "expired" {
continue
}
} else if status != "" && o.Status != status {
continue
}
if symbolsFilter != "" && o.Symbol != symbolsFilter {
continue
}
result = append(result, o)
}
json.NewEncoder(w).Encode(result)
case "POST":
// Parse as generic map to capture all fields including client_order_id
var raw map[string]string
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
http.Error(w, `{"message":"invalid body"}`, 400)
return
}
reqType := raw["type"]
reqSide := raw["side"]
reqSymbol := raw["symbol"]
reqQty := raw["qty"]
// Simulate order fill for market orders
fillStatus := "accepted"
if reqType == "market" {
fillStatus = "filled"
if reqSide == "buy" {
m.addPosition(reqSymbol, reqQty, "150.00")
}
}
order := m.createOrder(reqSymbol, reqSide, reqQty, reqType, fillStatus)
if v := raw["stop_price"]; v != "" {
order.StopPrice = v
}
if v := raw["limit_price"]; v != "" {
order.LimitPrice = v
}
if v := raw["client_order_id"]; v != "" {
order.ClientOrderID = v
}
if fillStatus != "filled" {
order.Status = "new" // stop/limit orders stay open
}
m.orders = append(m.orders, order)
w.WriteHeader(200)
json.NewEncoder(w).Encode(order)
case "DELETE":
// Cancel all orders
m.orders = nil
w.WriteHeader(207)
json.NewEncoder(w).Encode([]interface{}{})
}
})
// Single order operations
mux.HandleFunc("/v2/orders/", func(w http.ResponseWriter, r *http.Request) {
orderID := strings.TrimPrefix(r.URL.Path, "/v2/orders/")
switch r.Method {
case "GET":
for _, o := range m.orders {
if o.ID == orderID {
json.NewEncoder(w).Encode(o)
return
}
}
http.Error(w, `{"code":40410000,"message":"order not found"}`, 404)
case "DELETE":
var remaining []AlpacaOrder
found := false
for _, o := range m.orders {
if o.ID == orderID {
found = true
continue
}
remaining = append(remaining, o)
}
if !found {
http.Error(w, `{"code":40410000,"message":"order not found"}`, 404)
return
}
m.orders = remaining
w.WriteHeader(204)
}
})
// Clock
mux.HandleFunc("/v2/clock", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{
"timestamp": time.Now().Format(time.RFC3339),
"is_open": false,
"next_open": time.Now().Add(12 * time.Hour).Format(time.RFC3339),
"next_close": time.Now().Add(18 * time.Hour).Format(time.RFC3339),
})
})
m.server = httptest.NewServer(mux)
return m
}
func (m *mockAlpacaServer) createOrder(symbol, side, qty, orderType, status string) AlpacaOrder {
m.nextOrderID++
now := time.Now().Format(time.RFC3339Nano)
o := AlpacaOrder{
ID: fmt.Sprintf("order-%d", m.nextOrderID),
ClientOrderID: fmt.Sprintf("client-%d", m.nextOrderID),
Symbol: symbol,
Side: side,
Type: orderType,
Qty: qty,
Status: status,
CreatedAt: now,
UpdatedAt: now,
SubmittedAt: now,
TimeInForce: "day",
}
if status == "filled" {
o.FilledQty = qty
o.FilledAvgPrice = "150.25"
o.FilledAt = now
}
return o
}
func (m *mockAlpacaServer) addPosition(symbol, qty, price string) {
for i, p := range m.positions {
if p.Symbol == symbol {
// Update existing
existQty := parseFloatStr(p.Qty)
addQty := parseFloatStr(qty)
m.positions[i].Qty = fmt.Sprintf("%.4f", existQty+addQty)
return
}
}
m.positions = append(m.positions, AlpacaPosition{
AssetID: "asset-" + symbol,
Symbol: symbol,
Exchange: "NASDAQ",
AssetClass: "us_equity",
AvgEntryPrice: price,
Qty: qty,
Side: "long",
MarketValue: fmt.Sprintf("%.2f", parseFloatStr(qty)*parseFloatStr(price)),
CostBasis: fmt.Sprintf("%.2f", parseFloatStr(qty)*parseFloatStr(price)),
UnrealizedPL: "0.00",
CurrentPrice: price,
})
}
func (m *mockAlpacaServer) close() {
m.server.Close()
}
func newTestTrader(serverURL string) *AlpacaTrader {
t := NewAlpacaTrader("test-key", "test-secret", true)
t.baseURL = serverURL
return t
}
// --- Tests ---
func TestGetBalance(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
balance, err := trader.GetBalance()
if err != nil {
t.Fatalf("GetBalance() error: %v", err)
}
if balance["totalEquity"].(float64) != 100000.0 {
t.Errorf("totalEquity = %v, want 100000.0", balance["totalEquity"])
}
if balance["availableBalance"].(float64) != 100000.0 {
t.Errorf("availableBalance = %v, want 100000.0", balance["availableBalance"])
}
if balance["currency"].(string) != "USD" {
t.Errorf("currency = %v, want USD", balance["currency"])
}
if balance["status"].(string) != "ACTIVE" {
t.Errorf("status = %v, want ACTIVE", balance["status"])
}
}
func TestGetBalance_Caching(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
// First call
b1, err := trader.GetBalance()
if err != nil {
t.Fatalf("first GetBalance() error: %v", err)
}
// Modify server-side data
mock.account.Cash = "50000.00"
// Second call should return cached
b2, err := trader.GetBalance()
if err != nil {
t.Fatalf("second GetBalance() error: %v", err)
}
if b1["availableBalance"] != b2["availableBalance"] {
t.Errorf("cache miss: got different balances %v vs %v", b1["availableBalance"], b2["availableBalance"])
}
}
func TestGetPositions_Empty(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
positions, err := trader.GetPositions()
if err != nil {
t.Fatalf("GetPositions() error: %v", err)
}
if len(positions) != 0 {
t.Errorf("expected 0 positions, got %d", len(positions))
}
}
func TestOpenLong_Buy(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
result, err := trader.OpenLong("AAPL", 10.0, 1)
if err != nil {
t.Fatalf("OpenLong() error: %v", err)
}
if result["symbol"].(string) != "AAPL" {
t.Errorf("symbol = %v, want AAPL", result["symbol"])
}
if result["side"].(string) != "buy" {
t.Errorf("side = %v, want buy", result["side"])
}
if result["status"].(string) != "filled" {
t.Errorf("status = %v, want filled", result["status"])
}
// Verify position was created
positions, err := trader.GetPositions()
if err != nil {
t.Fatalf("GetPositions() after buy error: %v", err)
}
if len(positions) != 1 {
t.Fatalf("expected 1 position, got %d", len(positions))
}
if positions[0]["symbol"].(string) != "AAPL" {
t.Errorf("position symbol = %v, want AAPL", positions[0]["symbol"])
}
}
func TestOpenLong_FractionalShares(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
result, err := trader.OpenLong("AAPL", 0.5, 1)
if err != nil {
t.Fatalf("OpenLong(0.5) error: %v", err)
}
if result["qty"].(string) != "0.5000" {
t.Errorf("qty = %v, want 0.5000", result["qty"])
}
}
func TestCloseLong_FullPosition(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
mock.addPosition("TSLA", "10.0000", "250.00")
trader := newTestTrader(mock.server.URL)
// Close full position (quantity=0)
result, err := trader.CloseLong("TSLA", 0)
if err != nil {
t.Fatalf("CloseLong() error: %v", err)
}
if result["symbol"].(string) != "TSLA" {
t.Errorf("symbol = %v, want TSLA", result["symbol"])
}
// Position should be removed
positions, err := trader.GetPositions()
if err != nil {
t.Fatalf("GetPositions() after close error: %v", err)
}
if len(positions) != 0 {
t.Errorf("expected 0 positions after close, got %d", len(positions))
}
}
func TestCloseLong_PartialSell(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
mock.addPosition("AAPL", "10.0000", "150.00")
trader := newTestTrader(mock.server.URL)
result, err := trader.CloseLong("AAPL", 5.0)
if err != nil {
t.Fatalf("CloseLong(partial) error: %v", err)
}
if result["side"].(string) != "sell" {
t.Errorf("side = %v, want sell", result["side"])
}
}
func TestOpenShort_NotSupported(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
_, err := trader.OpenShort("AAPL", 10, 1)
if err == nil {
t.Fatal("OpenShort() should return error")
}
if !strings.Contains(err.Error(), "not supported") {
t.Errorf("unexpected error: %v", err)
}
}
func TestCloseShort_NotSupported(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
_, err := trader.CloseShort("AAPL", 10)
if err == nil {
t.Fatal("CloseShort() should return error")
}
}
func TestSetStopLoss(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
err := trader.SetStopLoss("AAPL", "LONG", 10, 140.0)
if err != nil {
t.Fatalf("SetStopLoss() error: %v", err)
}
// Verify stop order was created
orders, err := trader.GetOpenOrders("AAPL")
if err != nil {
t.Fatalf("GetOpenOrders() error: %v", err)
}
if len(orders) != 1 {
t.Fatalf("expected 1 open order, got %d", len(orders))
}
if orders[0].Type != "STOP" {
t.Errorf("order type = %v, want STOP", orders[0].Type)
}
if orders[0].Side != "SELL" {
t.Errorf("order side = %v, want SELL", orders[0].Side)
}
}
func TestSetTakeProfit(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
err := trader.SetTakeProfit("AAPL", "LONG", 10, 200.0)
if err != nil {
t.Fatalf("SetTakeProfit() error: %v", err)
}
orders, err := trader.GetOpenOrders("AAPL")
if err != nil {
t.Fatalf("GetOpenOrders() error: %v", err)
}
if len(orders) != 1 {
t.Fatalf("expected 1 open order, got %d", len(orders))
}
if orders[0].Type != "LIMIT" {
t.Errorf("order type = %v, want LIMIT", orders[0].Type)
}
}
func TestCancelStopOrders(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
// Create stop and limit orders
trader.SetStopLoss("AAPL", "LONG", 10, 140.0)
trader.SetTakeProfit("AAPL", "LONG", 10, 200.0)
orders, _ := trader.GetOpenOrders("AAPL")
if len(orders) != 2 {
t.Fatalf("expected 2 orders before cancel, got %d", len(orders))
}
// Cancel stop orders (both stop and limit/TP)
err := trader.CancelStopOrders("AAPL")
if err != nil {
t.Fatalf("CancelStopOrders() error: %v", err)
}
orders, _ = trader.GetOpenOrders("AAPL")
if len(orders) != 0 {
t.Errorf("expected 0 orders after cancel, got %d", len(orders))
}
}
func TestCancelAllOrders(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
// Create multiple orders
trader.SetStopLoss("AAPL", "LONG", 10, 140.0)
trader.SetStopLoss("TSLA", "LONG", 5, 200.0)
// Cancel all (no symbol filter)
err := trader.CancelAllOrders("")
if err != nil {
t.Fatalf("CancelAllOrders() error: %v", err)
}
// Mock server DELETE /v2/orders clears all
if len(mock.orders) != 0 {
t.Errorf("expected 0 orders after cancel all, got %d", len(mock.orders))
}
}
func TestIsMarketOpen(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
isOpen, status, err := trader.IsMarketOpen()
if err != nil {
t.Fatalf("IsMarketOpen() error: %v", err)
}
if isOpen {
t.Errorf("expected market closed (mock)")
}
if !strings.Contains(status, "closed") {
t.Errorf("status = %v, want contains 'closed'", status)
}
}
func TestFormatQuantity(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
qty, err := trader.FormatQuantity("AAPL", 10.123456789)
if err != nil {
t.Fatalf("FormatQuantity() error: %v", err)
}
if qty != "10.1235" {
t.Errorf("FormatQuantity = %v, want 10.1235", qty)
}
}
func TestSetLeverage_NoOp(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
if err := trader.SetLeverage("AAPL", 10); err != nil {
t.Errorf("SetLeverage should be no-op, got error: %v", err)
}
}
func TestSetMarginMode_NoOp(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
if err := trader.SetMarginMode("AAPL", true); err != nil {
t.Errorf("SetMarginMode should be no-op, got error: %v", err)
}
}
func TestGetOrderStatus(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
// Create an open order (stop order stays "new")
trader.SetStopLoss("AAPL", "LONG", 10, 140.0)
orders, _ := trader.GetOpenOrders("AAPL")
if len(orders) == 0 {
t.Fatal("no open orders to check status")
}
status, err := trader.GetOrderStatus("AAPL", orders[0].OrderID)
if err != nil {
t.Fatalf("GetOrderStatus() error: %v", err)
}
if status["status"].(string) != "NEW" {
t.Errorf("status = %v, want NEW", status["status"])
}
if status["commission"].(float64) != 0.0 {
t.Errorf("commission = %v, want 0.0 (commission-free)", status["commission"])
}
}
func TestFullTradeFlow(t *testing.T) {
// Simulate: check balance → buy AAPL → check positions → set SL/TP → close position
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
// 1. Check balance
balance, err := trader.GetBalance()
if err != nil {
t.Fatalf("Step 1 GetBalance: %v", err)
}
if balance["totalEquity"].(float64) != 100000.0 {
t.Fatalf("unexpected equity: %v", balance["totalEquity"])
}
// 2. Buy AAPL
buyResult, err := trader.OpenLong("AAPL", 10, 1)
if err != nil {
t.Fatalf("Step 2 OpenLong: %v", err)
}
if buyResult["status"].(string) != "filled" {
t.Fatalf("buy not filled: %v", buyResult["status"])
}
// 3. Check positions
positions, err := trader.GetPositions()
if err != nil {
t.Fatalf("Step 3 GetPositions: %v", err)
}
if len(positions) != 1 {
t.Fatalf("expected 1 position, got %d", len(positions))
}
pos := positions[0]
if pos["symbol"].(string) != "AAPL" {
t.Errorf("position symbol = %v, want AAPL", pos["symbol"])
}
if pos["exchange"].(string) != "alpaca" {
t.Errorf("position exchange = %v, want alpaca", pos["exchange"])
}
// 4. Set stop loss and take profit
err = trader.SetStopLoss("AAPL", "LONG", 10, 140.0)
if err != nil {
t.Fatalf("Step 4a SetStopLoss: %v", err)
}
err = trader.SetTakeProfit("AAPL", "LONG", 10, 200.0)
if err != nil {
t.Fatalf("Step 4b SetTakeProfit: %v", err)
}
orders, _ := trader.GetOpenOrders("AAPL")
if len(orders) != 2 {
t.Errorf("expected 2 open orders (SL+TP), got %d", len(orders))
}
// 5. Cancel SL/TP before closing
err = trader.CancelStopOrders("AAPL")
if err != nil {
t.Fatalf("Step 5 CancelStopOrders: %v", err)
}
// 6. Close position
closeResult, err := trader.CloseLong("AAPL", 0)
if err != nil {
t.Fatalf("Step 6 CloseLong: %v", err)
}
if closeResult["symbol"].(string) != "AAPL" {
t.Errorf("close symbol = %v, want AAPL", closeResult["symbol"])
}
// 7. Verify no positions left
positions, err = trader.GetPositions()
if err != nil {
t.Fatalf("Step 7 GetPositions: %v", err)
}
if len(positions) != 0 {
t.Errorf("expected 0 positions after close, got %d", len(positions))
}
}
func TestCloseLong_NoPosition(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
trader := newTestTrader(mock.server.URL)
_, err := trader.CloseLong("AAPL", 0) // No AAPL position exists
if err == nil {
t.Fatal("CloseLong should fail when no position exists")
}
}
func TestGetClosedPnL(t *testing.T) {
mock := newMockAlpacaServer()
defer mock.close()
// Create some filled sell orders in mock
now := time.Now().Format(time.RFC3339Nano)
mock.orders = append(mock.orders, AlpacaOrder{
ID: "order-closed-1",
Symbol: "AAPL",
Side: "sell",
Type: "market",
Qty: "10",
FilledQty: "10",
FilledAvgPrice: "155.50",
Status: "filled",
FilledAt: now,
UpdatedAt: now,
TimeInForce: "day",
})
mock.orders = append(mock.orders, AlpacaOrder{
ID: "order-closed-2",
Symbol: "TSLA",
Side: "buy", // buy orders should be excluded
Type: "market",
Qty: "5",
Status: "filled",
FilledQty: "5",
FilledAvgPrice: "250.00",
FilledAt: now,
UpdatedAt: now,
})
trader := newTestTrader(mock.server.URL)
records, err := trader.GetClosedPnL(time.Now().Add(-24*time.Hour), 50)
if err != nil {
t.Fatalf("GetClosedPnL() error: %v", err)
}
// Should only include the sell order
if len(records) != 1 {
t.Fatalf("expected 1 closed PnL record, got %d", len(records))
}
if records[0].Symbol != "AAPL" {
t.Errorf("record symbol = %v, want AAPL", records[0].Symbol)
}
if records[0].ExitPrice != 155.50 {
t.Errorf("exit price = %v, want 155.50", records[0].ExitPrice)
}
if records[0].Fee != 0 {
t.Errorf("fee = %v, want 0 (commission-free)", records[0].Fee)
}
}
func TestParseFloatStr(t *testing.T) {
tests := []struct {
input string
want float64
}{
{"100.50", 100.50},
{"0", 0},
{"", 0},
{"99999.9999", 99999.9999},
{"-50.25", -50.25},
}
for _, tt := range tests {
got := parseFloatStr(tt.input)
if got != tt.want {
t.Errorf("parseFloatStr(%q) = %v, want %v", tt.input, got, tt.want)
}
}
}
func TestTruncate(t *testing.T) {
if truncate("hello", 10) != "hello" {
t.Error("short string should not be truncated")
}
if truncate("hello world", 5) != "hello..." {
t.Errorf("got %v", truncate("hello world", 5))
}
}
// --- Live Integration Test (skip unless ALPACA_API_KEY is set) ---
func TestLiveAlpacaPaper(t *testing.T) {
apiKey := ""
apiSecret := ""
// Check env vars at runtime
if apiKey == "" {
t.Skip("Skipping live test: set ALPACA_API_KEY and ALPACA_API_SECRET env vars")
}
trader := NewAlpacaTrader(apiKey, apiSecret, true)
// Test 1: Get balance
balance, err := trader.GetBalance()
if err != nil {
t.Fatalf("Live GetBalance: %v", err)
}
t.Logf("Balance: equity=%.2f, cash=%.2f, buying_power=%.2f",
balance["totalEquity"], balance["availableBalance"], balance["buying_power"])
// Test 2: Check market status
isOpen, status, err := trader.IsMarketOpen()
if err != nil {
t.Fatalf("Live IsMarketOpen: %v", err)
}
t.Logf("Market: open=%v, status=%s", isOpen, status)
// Test 3: Get positions
positions, err := trader.GetPositions()
if err != nil {
t.Fatalf("Live GetPositions: %v", err)
}
t.Logf("Positions: %d", len(positions))
// Test 4: Get open orders
orders, err := trader.GetOpenOrders("")
if err != nil {
t.Fatalf("Live GetOpenOrders: %v", err)
}
t.Logf("Open orders: %d", len(orders))
}