Files
nofx/market/historical.go
shinchan-zhai 1f8493c0cf fix: goroutine leak on shutdown, unsafe type assertions, redundant API calls
- Agent: add stopCh to cleanly stop chat-history-cleanup goroutine on Agent.Stop()
  (previously leaked a goroutine+ticker forever on every restart)
- Agent: add looksLikeStockQuery() guard to avoid hitting Sina search API on every
  single message — only calls external API when text contains stock-related content
  (saves ~200ms latency + API call on crypto-only queries)
- market/historical.go: safe type assertions in kline parsing (was bare .(float64)
  which panics on unexpected API responses), reuse HTTP client for connection pooling
- market/api_client.go: safe comma-ok type assertions for all kline field parsing
  (11 bare assertions → all guarded)
- trader/bybit: fix unsafe type assertion in CloseShort — pos["positionAmt"].(float64)
  could panic if field is nil/wrong type (critical: handles real money)
2026-03-25 01:05:54 +08:00

124 lines
2.7 KiB
Go

package market
import (
"nofx/safe"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
binanceFuturesKlinesURL = "https://fapi.binance.com/fapi/v1/klines"
binanceMaxKlineLimit = 1500
)
// GetKlinesRange fetches K-line series within specified time range (closed interval), returns data sorted by time in ascending order.
func GetKlinesRange(symbol string, timeframe string, start, end time.Time) ([]Kline, error) {
symbol = Normalize(symbol)
normTF, err := NormalizeTimeframe(timeframe)
if err != nil {
return nil, err
}
if !end.After(start) {
return nil, fmt.Errorf("end time must be after start time")
}
startMs := start.UnixMilli()
endMs := end.UnixMilli()
var all []Kline
cursor := startMs
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 5,
MaxIdleConnsPerHost: 5,
IdleConnTimeout: 90 * time.Second,
},
}
for cursor < endMs {
req, err := http.NewRequest("GET", binanceFuturesKlinesURL, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("symbol", symbol)
q.Set("interval", normTF)
q.Set("limit", fmt.Sprintf("%d", binanceMaxKlineLimit))
q.Set("startTime", fmt.Sprintf("%d", cursor))
q.Set("endTime", fmt.Sprintf("%d", endMs))
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, err := safe.ReadAllLimited(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("binance klines api returned status %d: %s", resp.StatusCode, string(body))
}
var raw [][]interface{}
if err := json.Unmarshal(body, &raw); err != nil {
return nil, err
}
if len(raw) == 0 {
break
}
batch := make([]Kline, 0, len(raw))
for _, item := range raw {
if len(item) < 7 {
continue // skip malformed entries
}
openTimeF, ok := item[0].(float64)
if !ok {
continue
}
closeTimeF, ok := item[6].(float64)
if !ok {
continue
}
open, _ := parseFloat(item[1])
high, _ := parseFloat(item[2])
low, _ := parseFloat(item[3])
cls, _ := parseFloat(item[4])
volume, _ := parseFloat(item[5])
batch = append(batch, Kline{
OpenTime: int64(openTimeF),
Open: open,
High: high,
Low: low,
Close: cls,
Volume: volume,
CloseTime: int64(closeTimeF),
})
}
if len(batch) == 0 {
break
}
all = append(all, batch...)
last := batch[len(batch)-1]
cursor = last.CloseTime + 1
// If returned quantity is less than request limit, reached the end, can exit early.
if len(batch) < binanceMaxKlineLimit {
break
}
}
return all, nil
}