fix: HL balance now reads spot USDC via SpotUserState
HL testnet USDC lives in the spot account, not the perp clearinghouse. GetBalance() was calling UserState() (perp clearinghouseState), which returned /usr/bin/bash. Switched to SpotUserState() and parse USDC.total - USDC.hold. Also cleaned up .gitignore to exclude .env, binary, logs, data/.
This commit is contained in:
@@ -1 +1,6 @@
|
||||
hl_helper/node_modules/
|
||||
.env
|
||||
exchange-monitor
|
||||
exchange-monitor.log
|
||||
*.log
|
||||
data/
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
"trade_enabled": true,
|
||||
"arb_threshold": 0.03,
|
||||
"scan_interval_ms": 200,
|
||||
"trade_threshold": 0.20,
|
||||
"trade_amount_usd": 5,
|
||||
"trade_threshold": 0.10,
|
||||
"trade_amount_usd": 10,
|
||||
"trade_cooldown_ms": 30000,
|
||||
"alert_cooldown_sec": 300,
|
||||
"mock_slippage_pct": 0.05,
|
||||
|
||||
@@ -579,10 +579,22 @@ func (d *Dashboard) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
positions := d.trader.ReadSnapshot()
|
||||
converged, diverged, flat, total := d.trader.GetClosedStats()
|
||||
|
||||
// Format exchange funds (snake_case, like SSE)
|
||||
exFunds := d.trader.GetExchangeFunds()
|
||||
exFundsMap := make(map[string]map[string]float64, len(exFunds))
|
||||
for ex, ef := range exFunds {
|
||||
exFundsMap[ex] = map[string]float64{
|
||||
"balance": math.Round(ef.Balance*100) / 100,
|
||||
"total_fee": math.Round(ef.TotalFee*100) / 100,
|
||||
"total_pnl": math.Round(ef.TotalPnl*100) / 100,
|
||||
}
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"prices": snap,
|
||||
"positions": len(positions),
|
||||
"stats": map[string]int{"total": total, "converged": converged, "diverged": diverged, "flat": flat},
|
||||
"exchange_funds": exFundsMap,
|
||||
}
|
||||
writeJSON(w, resp)
|
||||
}
|
||||
|
||||
+53
-104
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BitgetTrade handles order placement on Bitget (live or paper).
|
||||
type BitgetTrade struct {
|
||||
APIKey string
|
||||
APISecret string
|
||||
@@ -23,8 +22,6 @@ type BitgetTrade struct {
|
||||
paperMode bool
|
||||
}
|
||||
|
||||
// NewBitgetTrade creates a BitgetTrade. Paper mode is auto-detected
|
||||
// from the API key prefix: "bg_" → Bitget paper trading.
|
||||
func NewBitgetTrade(apiKey, apiSecret, passphrase string) *BitgetTrade {
|
||||
return &BitgetTrade{
|
||||
APIKey: apiKey,
|
||||
@@ -35,49 +32,28 @@ func NewBitgetTrade(apiKey, apiSecret, passphrase string) *BitgetTrade {
|
||||
}
|
||||
}
|
||||
|
||||
// PlaceMarketOrder places a market order on Bitget.
|
||||
// side: "buy" or "sell"
|
||||
// symbol: "BTCUSDT" (always without _UMCBL suffix — appended internally for live)
|
||||
// size: contract size in coin units (e.g. 0.001 for BTC)
|
||||
// tradeSide: "open" or "close" — only used in paper mode
|
||||
func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (string, error) {
|
||||
ts := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
method := "POST"
|
||||
|
||||
var requestPath string
|
||||
var host string
|
||||
if b.paperMode {
|
||||
requestPath = "/api/v2/mix/order/place-order"
|
||||
host = "https://api.bitget.com"
|
||||
} else {
|
||||
requestPath = "/api/v2/mix/order/place"
|
||||
host = "https://api.bitget.com"
|
||||
}
|
||||
requestPath := "/api/v2/mix/order/place-order"
|
||||
host := "https://api.bitget.com"
|
||||
|
||||
body := map[string]interface{}{
|
||||
"marginCoin": "USDT",
|
||||
"symbol": symbol,
|
||||
"side": side,
|
||||
"orderType": "market",
|
||||
"timeInForce": "IOC", // immediate-or-cancel for market orders
|
||||
"timeInForce": "IOC",
|
||||
"marginMode": "crossed",
|
||||
"tradeSide": tradeSide,
|
||||
"size": size,
|
||||
}
|
||||
|
||||
if b.paperMode {
|
||||
body["symbol"] = symbol
|
||||
body["marginMode"] = "crossed"
|
||||
body["tradeSide"] = tradeSide
|
||||
} else {
|
||||
body["symbol"] = symbol + "_UMCBL"
|
||||
}
|
||||
|
||||
bodyJSON, _ := json.Marshal(body)
|
||||
|
||||
sign := b.sign(method, requestPath, ts, string(bodyJSON))
|
||||
|
||||
url := host + requestPath
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(string(bodyJSON)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, strings.NewReader(string(bodyJSON)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("ACCESS-KEY", b.APIKey)
|
||||
req.Header.Set("ACCESS-SIGN", sign)
|
||||
@@ -92,7 +68,6 @@ func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (st
|
||||
return "", fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result struct {
|
||||
@@ -103,7 +78,7 @@ func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (st
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return "", fmt.Errorf("parse response: %s", string(respBody))
|
||||
return "", fmt.Errorf("parse: %s", string(respBody))
|
||||
}
|
||||
if result.Code != "00000" {
|
||||
return "", fmt.Errorf("bitget error: %s - %s", result.Code, result.Msg)
|
||||
@@ -118,27 +93,15 @@ func (b *BitgetTrade) sign(method, requestPath, timestamp, body string) string {
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// GetBalance queries available balance.
|
||||
func (b *BitgetTrade) GetBalance() (float64, error) {
|
||||
ts := fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
method := "GET"
|
||||
|
||||
var host, requestPath string
|
||||
if b.paperMode {
|
||||
host = "https://api.bitget.com"
|
||||
requestPath = "/api/v2/mix/account/accounts?productType=USDT-FUTURES"
|
||||
} else {
|
||||
host = "https://api.bitget.com"
|
||||
requestPath = "/api/v2/mix/account/accounts?productType=UMCBL"
|
||||
}
|
||||
host := "https://api.bitget.com"
|
||||
requestPath := "/api/v2/mix/account/accounts?productType=USDT-FUTURES"
|
||||
|
||||
sign := b.sign(method, requestPath, ts, "")
|
||||
|
||||
url := host + requestPath
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, nil)
|
||||
req.Header.Set("ACCESS-KEY", b.APIKey)
|
||||
req.Header.Set("ACCESS-SIGN", sign)
|
||||
req.Header.Set("ACCESS-TIMESTAMP", ts)
|
||||
@@ -149,82 +112,68 @@ func (b *BitgetTrade) GetBalance() (float64, error) {
|
||||
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("http request: %w", err)
|
||||
return 0, fmt.Errorf("http: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
MarginCoin string `json:"marginCoin"`
|
||||
Available string `json:"available"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &raw); err != nil {
|
||||
return 0, fmt.Errorf("parse: %s", string(respBody))
|
||||
}
|
||||
if result.Code != "00000" {
|
||||
return 0, fmt.Errorf("bitget error: %s - %s", result.Code, result.Msg)
|
||||
code, _ := raw["code"].(string)
|
||||
if code != "00000" && code != "" {
|
||||
msg, _ := raw["msg"].(string)
|
||||
return 0, fmt.Errorf("bitget error: %s - %s", code, msg)
|
||||
}
|
||||
for _, acct := range result.Data {
|
||||
if acct.MarginCoin == "USDT" {
|
||||
bal, _ := strconv.ParseFloat(acct.Available, 64)
|
||||
|
||||
// Parse data as array of accounts
|
||||
dataRaw, ok := raw["data"]
|
||||
if !ok || dataRaw == nil {
|
||||
return 0, fmt.Errorf("no data in response")
|
||||
}
|
||||
dataArr, ok := dataRaw.([]interface{})
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unexpected data format")
|
||||
}
|
||||
for _, item := range dataArr {
|
||||
acct, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if acct["marginCoin"] == "USDT" {
|
||||
bal, _ := strconv.ParseFloat(fmt.Sprint(acct["available"]), 64)
|
||||
return bal, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no USDT margin account found")
|
||||
return 0, fmt.Errorf("no USDT account found")
|
||||
}
|
||||
|
||||
// GetBitgetSize calculates the contract size for a given USD amount.
|
||||
// Returns size as a decimal string complying with Bitget's USDT-FUTURES precision.
|
||||
// Enforces the exchange's minimum: minTradeNum contracts AND $5 min notional.
|
||||
// Uses math.Floor to round DOWN to the nearest valid step (B#5: prevent rounding up).
|
||||
func GetBitgetSize(symbol string, amountUSD, price float64) string {
|
||||
if amountUSD < 5 {
|
||||
amountUSD = 5 // Bitget minimum notional
|
||||
amountUSD = 5
|
||||
}
|
||||
sz := amountUSD / price // raw coin count
|
||||
|
||||
sz := amountUSD / price
|
||||
switch symbol {
|
||||
case "DOGEUSDT":
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
sz = math.Floor(sz) // step=1
|
||||
return fmt.Sprintf("%.0f", sz) // minTradeNum=1, sizeMultiplier=1
|
||||
case "LINKUSDT":
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
sz = math.Floor(sz) // step=1
|
||||
return fmt.Sprintf("%.0f", sz) // minTradeNum=1, sizeMultiplier=1
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", math.Floor(sz))
|
||||
case "ONDOUSDT":
|
||||
sz = math.Floor(sz*10) / 10 // step=0.1
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "OPUSDT":
|
||||
sz = math.Floor(sz*10) / 10 // step=0.1
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "WIFUSDT":
|
||||
sz = math.Floor(sz*10) / 10 // step=0.1
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "ARBUSDT":
|
||||
sz = math.Floor(sz*100) / 100 // step=0.01
|
||||
if sz < 0.01 {
|
||||
sz = 0.01
|
||||
}
|
||||
return fmt.Sprintf("%.2f", sz) // minTradeNum=0.01, sizeMultiplier=0.01
|
||||
sz = math.Floor(sz*100)/100
|
||||
if sz < 0.01 { sz = 0.01 }
|
||||
return fmt.Sprintf("%.2f", sz)
|
||||
default:
|
||||
return fmt.Sprintf("%.4f", sz)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
hl "github.com/sonirico/go-hyperliquid"
|
||||
)
|
||||
|
||||
// HyperLiquidTrade handles order placement on HyperLiquid using the SDK.
|
||||
type HyperLiquidTrade struct {
|
||||
exchange *hl.Exchange
|
||||
info *hl.Info
|
||||
@@ -40,24 +39,20 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL
|
||||
|
||||
privKey, err := crypto.ToECDSA(keyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert to ECDSA: %w", err)
|
||||
return nil, fmt.Errorf("to ECDSA: %w", err)
|
||||
}
|
||||
|
||||
// Initialize SDK Info (auto-fetches meta + spotMeta)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil)
|
||||
|
||||
t := &HyperLiquidTrade{
|
||||
return &HyperLiquidTrade{
|
||||
privateKey: privKey,
|
||||
mainAddress: mainAddress,
|
||||
info: info,
|
||||
configured: true,
|
||||
}
|
||||
|
||||
// Initialize exchange lazily on first order
|
||||
return t, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) initExchange() error {
|
||||
@@ -65,7 +60,7 @@ func (h *HyperLiquidTrade) initExchange() error {
|
||||
return nil
|
||||
}
|
||||
if !h.configured {
|
||||
return fmt.Errorf("HyperLiquid not configured")
|
||||
return fmt.Errorf("HL not configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
@@ -73,25 +68,14 @@ func (h *HyperLiquidTrade) initExchange() error {
|
||||
|
||||
meta, err := h.info.Meta(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch meta: %w", err)
|
||||
return fmt.Errorf("meta: %w", err)
|
||||
}
|
||||
|
||||
spotMeta, err := h.info.SpotMeta(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch spot meta: %w", err)
|
||||
return fmt.Errorf("spot meta: %w", err)
|
||||
}
|
||||
|
||||
h.exchange = hl.NewExchange(
|
||||
ctx,
|
||||
h.privateKey,
|
||||
hl.TestnetAPIURL,
|
||||
meta,
|
||||
"",
|
||||
h.mainAddress,
|
||||
spotMeta,
|
||||
nil,
|
||||
)
|
||||
|
||||
h.exchange = hl.NewExchange(ctx, h.privateKey, hl.TestnetAPIURL, meta, "", h.mainAddress, spotMeta, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -99,38 +83,30 @@ func (h *HyperLiquidTrade) IsConfigured() bool {
|
||||
return h.configured
|
||||
}
|
||||
|
||||
// PlaceMarketOrder places a market (IOC) order on HyperLiquid.
|
||||
func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, error) {
|
||||
if !h.configured {
|
||||
return "", fmt.Errorf("HyperLiquid not configured")
|
||||
return "", fmt.Errorf("HL not configured")
|
||||
}
|
||||
|
||||
if err := h.initExchange(); err != nil {
|
||||
return "", fmt.Errorf("init exchange: %w", err)
|
||||
return "", fmt.Errorf("init: %w", err)
|
||||
}
|
||||
|
||||
isBuy := side == "buy"
|
||||
size, err := strconv.ParseFloat(sz, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse size %s: %w", sz, err)
|
||||
}
|
||||
size, _ := strconv.ParseFloat(sz, 64)
|
||||
|
||||
// Get current price for slippage
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mids, err := h.info.AllMids(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch mids: %w", err)
|
||||
return "", fmt.Errorf("mids: %w", err)
|
||||
}
|
||||
|
||||
priceStr, ok := mids[coin]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("coin %s not found", coin)
|
||||
}
|
||||
midPx, _ := strconv.ParseFloat(priceStr, 64)
|
||||
|
||||
// Aggressive IOC: buy above market, sell below
|
||||
limitPx := midPx * 2.0
|
||||
if !isBuy {
|
||||
limitPx = midPx * 0.5
|
||||
@@ -140,51 +116,56 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("market open: %w", err)
|
||||
}
|
||||
|
||||
// Marshal response
|
||||
respJSON, _ := json.Marshal(result)
|
||||
return string(respJSON), nil
|
||||
}
|
||||
|
||||
// GetHLSize calculates size for a given USD amount on HyperLiquid.
|
||||
func (h *HyperLiquidTrade) GetBalance() (float64, error) {
|
||||
if !h.configured {
|
||||
return 0, fmt.Errorf("HL not configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// HL testnet USDC is on spot, not perp. Use SpotUserState.
|
||||
state, err := h.info.SpotUserState(ctx, h.mainAddress)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("spot user state: %w", err)
|
||||
}
|
||||
|
||||
for _, b := range state.Balances {
|
||||
if b.Coin == "USDC" {
|
||||
total, _ := strconv.ParseFloat(b.Total, 64)
|
||||
hold, _ := strconv.ParseFloat(b.Hold, 64)
|
||||
return total - hold, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("USDC balance not found in spot state")
|
||||
}
|
||||
|
||||
func GetHLSize(coin string, amountUSD, price float64) string {
|
||||
sz := amountUSD / price
|
||||
switch coin {
|
||||
case "DOGE":
|
||||
sz = math.Floor(sz)
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", sz)
|
||||
case "LINK":
|
||||
sz = math.Floor(sz*10) / 10
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "ONDO":
|
||||
sz = math.Floor(sz)
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", sz)
|
||||
case "OP":
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "WIF":
|
||||
sz = math.Floor(sz)
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", sz)
|
||||
case "ARB":
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
default:
|
||||
return fmt.Sprintf("%.4f", sz)
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ var takerFees = map[string]float64{
|
||||
// TickerCoins defines all coins we monitor.
|
||||
var TrackedCoins = []TrackedCoin{
|
||||
{Name: "DOGE", BN: "DOGEUSDT", BG: "DOGEUSDT", HL: "DOGE"},
|
||||
{Name: "LINK", BN: "LINKUSDT", BG: "LINKUSDT", HL: "LINK"},
|
||||
// LINK removed — not listed on HL testnet
|
||||
{Name: "ONDO", BN: "ONDOUSDT", BG: "ONDOUSDT", HL: "ONDO"},
|
||||
{Name: "OP", BN: "OPUSDT", BG: "OPUSDT", HL: "OP"},
|
||||
{Name: "WIF", BN: "WIFUSDT", BG: "WIFUSDT", HL: "WIF"},
|
||||
|
||||
@@ -248,9 +248,34 @@ func NewTrader(cfg *Config, database *db.DB) *Trader {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch real balances from exchanges
|
||||
t.fetchBalances()
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Trader) fetchBalances() {
|
||||
// Bitget
|
||||
if t.bitget != nil {
|
||||
if bal, err := t.bitget.GetBalance(); err == nil {
|
||||
t.exchangeFunds[ExBitget] = &ExchangeFund{Balance: bal}
|
||||
log.Printf("[Funds] Bitget balance: $%.2f", bal)
|
||||
} else {
|
||||
log.Printf("[Funds] Bitget balance fetch failed: %v (using default)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// HyperLiquid
|
||||
if t.hyperliquid != nil && t.hyperliquid.IsConfigured() {
|
||||
if bal, err := t.hyperliquid.GetBalance(); err == nil {
|
||||
t.exchangeFunds[ExHyperLiquid] = &ExchangeFund{Balance: bal}
|
||||
log.Printf("[Funds] HyperLiquid balance: $%.2f", bal)
|
||||
} else {
|
||||
log.Printf("[Funds] HyperLiquid balance fetch failed: %v (using default)", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trader) IsConfigured() bool {
|
||||
switch {
|
||||
case t.cfg.TestMode:
|
||||
|
||||
Reference in New Issue
Block a user