mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 22:36:58 +08:00
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:
22
trader/types/parse.go
Normal file
22
trader/types/parse.go
Normal 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
|
||||
}
|
||||
44
trader/types/parse_test.go
Normal file
44
trader/types/parse_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user