Files
nofx/provider/twelvedata/kline.go
T
tinkle-community 47bff87966 feat: add xyz dex balance calculation, market data providers, and UI improvements
- Fix xyz dex balance calculation (use marginSummary for isolated margin)
- Add Alpaca provider for US stocks market data
- Add TwelveData provider for forex & metals market data
- Add Hyperliquid kline provider
- Centralize API keys in config system
- Add builder fee for order routing
- Improve chart UI with compact design
- Fix position history fee display precision
- Add comprehensive balance calculation tests
2025-12-29 22:16:48 +08:00

272 lines
7.2 KiB
Go

package twelvedata
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"nofx/config"
"strconv"
"time"
)
const (
BaseURL = "https://api.twelvedata.com"
)
// Bar represents a single OHLCV bar from Twelve Data
type Bar struct {
Datetime string `json:"datetime"`
Open string `json:"open"`
High string `json:"high"`
Low string `json:"low"`
Close string `json:"close"`
Volume string `json:"volume,omitempty"`
}
// TimeSeriesResponse represents the response from Twelve Data time_series API
type TimeSeriesResponse struct {
Meta Meta `json:"meta"`
Values []Bar `json:"values"`
Status string `json:"status"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// Meta contains metadata about the time series
type Meta struct {
Symbol string `json:"symbol"`
Interval string `json:"interval"`
CurrencyBase string `json:"currency_base,omitempty"`
CurrencyQuote string `json:"currency_quote,omitempty"`
Type string `json:"type,omitempty"`
Exchange string `json:"exchange,omitempty"`
ExchangeTimezone string `json:"exchange_timezone,omitempty"`
}
// QuoteResponse represents the response from Twelve Data quote API
type QuoteResponse struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
Exchange string `json:"exchange"`
Open string `json:"open"`
High string `json:"high"`
Low string `json:"low"`
Close string `json:"close"`
PreviousClose string `json:"previous_close"`
Volume string `json:"volume,omitempty"`
Change string `json:"change"`
PercentChange string `json:"percent_change"`
AverageVolume string `json:"average_volume,omitempty"`
FiftyTwoWeekHigh string `json:"fifty_two_week_high,omitempty"`
FiftyTwoWeekLow string `json:"fifty_two_week_low,omitempty"`
Datetime string `json:"datetime"`
Status string `json:"status,omitempty"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// Client is the Twelve Data API client
type Client struct {
apiKey string
client *http.Client
}
// NewClient creates a new Twelve Data client from config
func NewClient() *Client {
return &Client{
apiKey: config.Get().TwelveDataKey,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// NewClientWithKey creates a new Twelve Data client with provided key
func NewClientWithKey(apiKey string) *Client {
return &Client{
apiKey: apiKey,
client: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// GetTimeSeries fetches historical bars for a symbol
// interval: 1min, 5min, 15min, 30min, 45min, 1h, 2h, 4h, 1day, 1week, 1month
func (c *Client) GetTimeSeries(ctx context.Context, symbol string, interval string, limit int) (*TimeSeriesResponse, error) {
if c.apiKey == "" {
return nil, fmt.Errorf("twelve data API key not configured")
}
// Build URL
endpoint := fmt.Sprintf("%s/time_series", BaseURL)
params := url.Values{}
params.Set("symbol", symbol)
params.Set("interval", interval)
params.Set("outputsize", fmt.Sprintf("%d", limit))
params.Set("apikey", c.apiKey)
fullURL := endpoint + "?" + params.Encode()
// Create request
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Execute request
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Parse response
var result TimeSeriesResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Check for API errors
if result.Status == "error" {
return nil, fmt.Errorf("twelve data API error: %s", result.Message)
}
return &result, nil
}
// GetQuote fetches real-time quote for a symbol
func (c *Client) GetQuote(ctx context.Context, symbol string) (*QuoteResponse, error) {
if c.apiKey == "" {
return nil, fmt.Errorf("twelve data API key not configured")
}
// Build URL
endpoint := fmt.Sprintf("%s/quote", BaseURL)
params := url.Values{}
params.Set("symbol", symbol)
params.Set("apikey", c.apiKey)
fullURL := endpoint + "?" + params.Encode()
// Create request
req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Execute request
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// Parse response
var result QuoteResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Check for API errors
if result.Status == "error" {
return nil, fmt.Errorf("twelve data API error: %s", result.Message)
}
return &result, nil
}
// MapTimeframe maps common timeframe strings to Twelve Data format
func MapTimeframe(interval string) string {
switch interval {
case "1m":
return "1min"
case "3m":
return "5min" // Twelve Data doesn't have 3m, use 5m
case "5m":
return "5min"
case "10m":
return "15min" // Twelve Data doesn't have 10m, use 15m
case "15m":
return "15min"
case "30m":
return "30min"
case "1h":
return "1h"
case "2h":
return "2h"
case "4h":
return "4h"
case "6h":
return "4h" // Twelve Data doesn't have 6h, use 4h
case "8h":
return "4h" // Twelve Data doesn't have 8h, use 4h
case "12h":
return "4h" // Twelve Data doesn't have 12h, use 4h
case "1d":
return "1day"
case "3d":
return "1day" // Twelve Data doesn't have 3d, use 1d
case "1w":
return "1week"
case "1M":
return "1month"
default:
return "5min" // Default to 5 minutes
}
}
// ParseBar converts a Twelve Data bar to numeric values
func ParseBar(bar Bar) (open, high, low, close, volume float64, timestamp int64, err error) {
open, err = strconv.ParseFloat(bar.Open, 64)
if err != nil {
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to parse open: %w", err)
}
high, err = strconv.ParseFloat(bar.High, 64)
if err != nil {
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to parse high: %w", err)
}
low, err = strconv.ParseFloat(bar.Low, 64)
if err != nil {
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to parse low: %w", err)
}
close, err = strconv.ParseFloat(bar.Close, 64)
if err != nil {
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to parse close: %w", err)
}
// Volume might be empty for forex
if bar.Volume != "" {
volume, _ = strconv.ParseFloat(bar.Volume, 64)
}
// Parse datetime - format is "2024-01-15 09:30:00" or "2024-01-15"
var t time.Time
if len(bar.Datetime) > 10 {
t, err = time.Parse("2006-01-02 15:04:05", bar.Datetime)
} else {
t, err = time.Parse("2006-01-02", bar.Datetime)
}
if err != nil {
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to parse datetime: %w", err)
}
timestamp = t.UnixMilli()
return open, high, low, close, volume, timestamp, nil
}