package okx import ( "bytes" "encoding/json" "fmt" "github.com/google/go-querystring/query" // 用于将 struct 转为 URL 参数,需要安装:go get github.com/google/go-querystring "io" "net/http" "net/url" "strconv" "time" ) // getPeriodDuration 根据时间周期字符串返回对应的 duration func getPeriodDuration(period string) (time.Duration, error) { switch period { case "1m": return time.Minute, nil case "3m": return 3 * time.Minute, nil case "5m": return 5 * time.Minute, nil case "15m": return 15 * time.Minute, nil case "30m": return 30 * time.Minute, nil case "1H": return time.Hour, nil case "2H": return 2 * time.Hour, nil case "4H": return 4 * time.Hour, nil case "6H": return 6 * time.Hour, nil case "12H": return 12 * time.Hour, nil case "1D": return 24 * time.Hour, nil case "2D": return 2 * 24 * time.Hour, nil case "3D": return 3 * 24 * time.Hour, nil case "5D": return 5 * 24 * time.Hour, nil case "1W": return 7 * 24 * time.Hour, nil default: return 0, fmt.Errorf("unsupported bar period: %s", period) } } type OkxPublicDataService struct { BaseURL string client *http.Client } // NewOkxPublicDataService 创建服务实例 func NewOkxPublicDataService() *OkxPublicDataService { return &OkxPublicDataService{ BaseURL: "https://aws.okx.com/api/v5", client: &http.Client{ Timeout: 10 * time.Second, // 设置10秒超时 }, } } // GetInstruments 获取交易对信息 func (s *OkxPublicDataService) GetInstruments(params InstrumentsRequest) ([]Instrument, error) { u, err := url.Parse(s.BaseURL + "/public/instruments") if err != nil { return nil, err } // 将 struct 转为查询参数 q, err := query.Values(params) if err != nil { return nil, err } u.RawQuery = q.Encode() resp, err := s.client.Get(u.String()) if err != nil { return nil, err } defer resp.Body.Close() var apiResp ApiResponse apiResp.Data = new([]Instrument) // 预分配 Data 字段为 Instrument 切片 if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { return nil, err } if apiResp.Code != "0" { return nil, fmt.Errorf("API error: code=%s, msg=%s, request_url=%s", apiResp.Code, apiResp.Msg, u.String()) } // 进行类型断言并检查长度 switch data := apiResp.Data.(type) { case []Instrument: if len(data) == 0 { return nil, fmt.Errorf("no data returned from API") } case [][]string: if len(data) == 0 { return nil, fmt.Errorf("no data returned from API") } case []*Ticker: if len(data) == 0 { return nil, fmt.Errorf("no data returned from API") } default: return nil, fmt.Errorf("unexpected data type from API: %T", apiResp.Data) } return *apiResp.Data.(*[]Instrument), nil } // GetCandles 获取K线数据 func (s *OkxPublicDataService) GetCandles(params CandlesRequest) ([]*Candle, error) { // 根据时间范围选择不同的API端点 endpoint := "/market/candles" // 计算时间范围 now := time.Now() var startTime time.Time var err error // 优先使用 After 参数,如果都提供了 if params.After != "" { // 将毫秒时间戳转换为 time.Time timestamp, err := strconv.ParseInt(params.After, 10, 64) if err != nil { return nil, fmt.Errorf("invalid After timestamp: %v", err) } startTime = time.Unix(0, timestamp*int64(time.Millisecond)) } else if params.Before != "" { // 将毫秒时间戳转换为 time.Time timestamp, err := strconv.ParseInt(params.Before, 10, 64) if err != nil { return nil, fmt.Errorf("invalid Before timestamp: %v", err) } startTime = time.Unix(0, timestamp*int64(time.Millisecond)) } else { return nil, fmt.Errorf("either After or Before parameter is required") } // 根据时间维度计算20个周期 unitDuration, err := getPeriodDuration(params.Bar) if err != nil { return nil, err } periodDuration := 20 * unitDuration // 如果数据超过20个周期,使用历史数据接口 if now.Sub(startTime) > periodDuration { endpoint = "/market/history-candles" } u, err := url.Parse(s.BaseURL + endpoint) if err != nil { return nil, err } // 将 struct 转为查询参数 q, err := query.Values(params) if err != nil { return nil, err } u.RawQuery = q.Encode() // 添加调试日志 fmt.Printf("Making request to: %s\n", u.String()) resp, err := http.Get(u.String()) if err != nil { return nil, err } defer resp.Body.Close() // 记录响应状态 fmt.Printf("Response Status: %s\n", resp.Status) // 读取并记录响应体 body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } //fmt.Printf("Response Body: %s\n", string(body)) // 重新设置resp.Body以便后续解码 resp.Body = io.NopCloser(bytes.NewBuffer(body)) var apiResp struct { Code string `json:"code"` Msg string `json:"msg"` Data [][]string `json:"data"` // K线数据是二维数组 } if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { return nil, err } if apiResp.Code != "0" { return nil, fmt.Errorf("API error: %s", apiResp.Msg) } // 将二维数组转为 Candle 切片 candles := make([]*Candle, len(apiResp.Data)) for i, item := range apiResp.Data { candles[i] = &Candle{ Timestamp: item[0], Open: item[1], High: item[2], Low: item[3], Close: item[4], Volume: item[5], VolumeCcy: item[6], } } return candles, nil } // GetTicker 获取单个交易对的 ticker 数据 func (s *OkxPublicDataService) GetTicker(params TickerRequest) (*Ticker, error) { u, err := url.Parse(s.BaseURL + "/market/ticker") if err != nil { return nil, err } // 将 struct 转为查询参数 q, err := query.Values(params) if err != nil { return nil, err } u.RawQuery = q.Encode() resp, err := http.Get(u.String()) if err != nil { return nil, err } defer resp.Body.Close() var apiResp struct { Code string `json:"code"` Msg string `json:"msg"` Data []*Ticker `json:"data"` // 返回的是指针数组 } if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { return nil, err } if apiResp.Code != "0" { return nil, fmt.Errorf("API error: %s", apiResp.Msg) } if len(apiResp.Data) == 0 { return nil, fmt.Errorf("no ticker data returned") } // 返回第一个 ticker(通常只返回一个) return apiResp.Data[0], nil }