Files
nofx/nofxi/internal/interaction/kline.go
shinchan-zhai b72ef9e6df feat(nofxi): K-line chart with TradingView lightweight-charts
- Candlestick chart + volume histogram
- 8 trading pairs: BTC/ETH/SOL/BNB/XRP/DOGE/ADA/AVAX
- 5 timeframes: 15m/1H/4H/1D/1W
- Real-time price & 24h change display
- Auto-refresh every 30s
- Backend: /api/klines + /api/ticker (Binance futures API)
- Dark trading terminal theme, responsive resize
- Tab switching: Chat ↔ Chart
2026-03-22 22:18:48 +08:00

127 lines
3.3 KiB
Go

package interaction
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
// KlineBar represents a single candlestick.
type KlineBar struct {
Time int64 `json:"time"`
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Close float64 `json:"close"`
Volume float64 `json:"volume"`
}
// fetchKlines gets kline data from Binance public API (no auth needed).
func fetchKlines(symbol, interval string, limit int) ([]KlineBar, error) {
if limit <= 0 {
limit = 200
}
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=%d",
symbol, interval, limit)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("fetch klines: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var raw [][]interface{}
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parse klines: %w", err)
}
bars := make([]KlineBar, 0, len(raw))
for _, k := range raw {
if len(k) < 6 {
continue
}
bar := KlineBar{
Time: int64(k[0].(float64)) / 1000, // ms → seconds for lightweight-charts
}
bar.Open, _ = strconv.ParseFloat(k[1].(string), 64)
bar.High, _ = strconv.ParseFloat(k[2].(string), 64)
bar.Low, _ = strconv.ParseFloat(k[3].(string), 64)
bar.Close, _ = strconv.ParseFloat(k[4].(string), 64)
bar.Volume, _ = strconv.ParseFloat(k[5].(string), 64)
bars = append(bars, bar)
}
return bars, nil
}
// RegisterKlineRoutes adds kline API endpoints to the mux.
func RegisterKlineRoutes(mux *http.ServeMux) {
// GET /api/klines?symbol=BTCUSDT&interval=1h&limit=200
mux.HandleFunc("/api/klines", func(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Query().Get("symbol")
if symbol == "" {
symbol = "BTCUSDT"
}
interval := r.URL.Query().Get("interval")
if interval == "" {
interval = "1h"
}
limitStr := r.URL.Query().Get("limit")
limit := 200
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
if limit > 1000 {
limit = 1000
}
bars, err := fetchKlines(symbol, interval, limit)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
json.NewEncoder(w).Encode(bars)
})
// GET /api/ticker?symbol=BTCUSDT — current price + 24h change
mux.HandleFunc("/api/ticker", func(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Query().Get("symbol")
if symbol == "" {
symbol = "BTCUSDT"
}
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
w.WriteHeader(http.StatusBadGateway)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write(body)
})
}