fix(trader): harden API calls with timeouts, strict balance parsing, error context

- binance/bybit/gate: SDK default http.DefaultClient has no timeout; use a
  dedicated 30s-timeout client so a hung connection cannot stall the loop
- bybit: stop mutating http.DefaultClient.Transport, which leaked the
  referer header into every other HTTP request in the process
- add types.ParseFloatField: empty exchange fields stay zero, but malformed
  numeric values now surface as errors instead of silently becoming zero
  balances (applied to GetBalance across 8 exchanges)
- wrap order/market-data errors in auto_trader_orders and okx cancel paths
  with symbol context; log per-order cancel failures in okx CancelAllOrders
This commit is contained in:
tinkle-community
2026-06-11 00:30:34 +08:00
parent 094ab45476
commit 9ea9bd705f
15 changed files with 206 additions and 50 deletions

22
trader/types/parse.go Normal file
View File

@@ -0,0 +1,22 @@
package types
import (
"fmt"
"strconv"
)
// ParseFloatField parses a numeric string field from an exchange API response.
// An empty string is treated as zero, since exchanges commonly omit fields
// that have no value (e.g. unrealized PnL with no open position). A non-empty
// value that fails to parse returns an error naming the field, so a malformed
// API response surfaces instead of silently becoming a zero balance.
func ParseFloatField(field, value string) (float64, error) {
if value == "" {
return 0, nil
}
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse %s value %q: %w", field, value, err)
}
return v, nil
}

View File

@@ -0,0 +1,44 @@
package types
import (
"strings"
"testing"
)
func TestParseFloatField(t *testing.T) {
tests := []struct {
name string
field string
input string
want float64
wantErr bool
}{
{name: "normal value", field: "totalEq", input: "1234.56", want: 1234.56},
{name: "zero", field: "totalEq", input: "0", want: 0},
{name: "negative", field: "upl", input: "-12.5", want: -12.5},
{name: "empty string treated as zero", field: "upl", input: "", want: 0},
{name: "garbage returns error", field: "totalEq", input: "abc", wantErr: true},
{name: "null literal returns error", field: "totalEq", input: "null", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseFloatField(tt.field, tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("ParseFloatField(%q, %q) expected error, got nil", tt.field, tt.input)
}
if !strings.Contains(err.Error(), tt.field) {
t.Errorf("error %q should mention field name %q", err.Error(), tt.field)
}
return
}
if err != nil {
t.Fatalf("ParseFloatField(%q, %q) unexpected error: %v", tt.field, tt.input, err)
}
if got != tt.want {
t.Errorf("ParseFloatField(%q, %q) = %v, want %v", tt.field, tt.input, got, tt.want)
}
})
}
}