mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 09:54:35 +08:00
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
This commit is contained in:
271
provider/twelvedata/kline.go
Normal file
271
provider/twelvedata/kline.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user