Files
zeekayandHanzo Dev 1db9b4d974 the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 11:05:38 -07:00

172 lines
4.7 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package source
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// krakenDefaultURL is the public Kraken REST endpoint.
const krakenDefaultURL = "https://api.kraken.com/0/public"
// Kraken fetches ticker data from /Ticker?pair=... in a single request.
//
// Kraken pair names are bespoke (XBTUSD, XETHZUSD, ADAUSD, ...). The Source
// holds an ID->pair map seeded at construction time so the translation
// layer is explicit and testable. Pairs not present in the map are skipped
// silently — the source omits them rather than erroring.
type Kraken struct {
baseURL string
client *http.Client
idToPair map[string]string // canonical ID -> kraken pair code
enabled bool
}
// NewKraken constructs a Kraken source. Pass a custom baseURL ("") to use
// the public host. Pass a nil idToPair to use the default mapping.
func NewKraken(baseURL string, idToPair map[string]string) *Kraken {
if baseURL == "" {
baseURL = krakenDefaultURL
}
if idToPair == nil {
idToPair = DefaultKrakenPairs()
}
return &Kraken{
baseURL: baseURL,
client: &http.Client{Timeout: 15 * time.Second},
idToPair: idToPair,
enabled: true,
}
}
// Name returns the canonical source identifier.
func (k *Kraken) Name() string { return "kraken" }
// Enabled reports whether this source is wired up for the fetcher.
func (k *Kraken) Enabled() bool { return k != nil && k.enabled }
// Fetch returns quotes keyed by canonical token ID. Kraken returns multiple
// pairs in a single response, so we issue exactly one HTTP request.
func (k *Kraken) Fetch(ctx context.Context, ids []string) (map[string]Quote, error) {
if !k.Enabled() || len(ids) == 0 {
return map[string]Quote{}, nil
}
// Build the pair query. Track pair->id so we can demux the response.
var pairs []string
pairToID := make(map[string]string)
for _, id := range ids {
pair, ok := k.idToPair[strings.ToLower(id)]
if !ok {
continue
}
pairs = append(pairs, pair)
pairToID[pair] = id
}
if len(pairs) == 0 {
return map[string]Quote{}, nil
}
url := fmt.Sprintf("%s/Ticker?pair=%s", k.baseURL, strings.Join(pairs, ","))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
resp, err := k.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("kraken: status %d: %s", resp.StatusCode, string(body))
}
// Kraken response: {"error":[],"result":{"<PAIR>":{"c":["price","lot"], "v":["...","24h"], "p":["...","24h"], "o":"open"}}}
var payload struct {
Error []string `json:"error"`
Result map[string]json.RawMessage `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
if len(payload.Error) > 0 {
return nil, fmt.Errorf("kraken: %s", strings.Join(payload.Error, ","))
}
now := time.Now().UTC()
out := make(map[string]Quote, len(payload.Result))
for pair, raw := range payload.Result {
var t struct {
C []string `json:"c"` // last trade closed: [price, lot]
V []string `json:"v"` // volume: [today, 24h]
P []string `json:"p"` // VWAP: [today, 24h]
O string `json:"o"` // today open
}
if err := json.Unmarshal(raw, &t); err != nil {
continue
}
// Kraken sometimes returns a different pair key than the request
// (XBTUSD vs XXBTZUSD). Match by suffix as a fallback.
id, ok := pairToID[pair]
if !ok {
for reqPair, candidateID := range pairToID {
if strings.HasSuffix(pair, strings.TrimPrefix(reqPair, "X")) {
id = candidateID
ok = true
break
}
}
if !ok {
continue
}
}
price, err := parseKrakenFloat(t.C, 0)
if err != nil || price == 0 {
continue
}
volume, _ := parseKrakenFloat(t.V, 1)
vwap, _ := parseKrakenFloat(t.P, 1)
open, _ := strconv.ParseFloat(t.O, 64)
var change24h float64
if open > 0 {
change24h = ((price - open) / open) * 100
}
_ = vwap // reserved; kept for symmetry with other sources
out[id] = Quote{
ID: id,
PriceUSD: price,
Change24h: change24h,
Volume24h: volume * price, // Kraken volume is base units; convert to USD
UpdatedAt: now,
SourceName: k.Name(),
}
}
return out, nil
}
// parseKrakenFloat safely extracts the Nth element of a string array as a
// float. Kraken returns these arrays uniformly across endpoints.
func parseKrakenFloat(arr []string, idx int) (float64, error) {
if idx >= len(arr) {
return 0, fmt.Errorf("index out of range")
}
return strconv.ParseFloat(arr[idx], 64)
}