money/data.go

181 lines
4.2 KiB
Go

package main
import (
"context"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"sort"
"strconv"
"time"
)
// ================== Binance ==================
type Kline struct {
OpenTime time.Time
Open float64
High float64
Low float64
Close float64
Volume float64
CloseTime time.Time
}
func fetchBinanceKlines(ctx context.Context, symbol string, limit int) ([]Kline, error) {
url := fmt.Sprintf("https://api.binance.com/api/v3/klines?symbol=%s&interval=1d&limit=%d", symbol, limit)
var raw [][]any
if err := httpGetJSON(ctx, url, &raw); err != nil {
return nil, err
}
out := make([]Kline, 0, len(raw))
for _, row := range raw {
if len(row) < 7 {
continue
}
ot := time.UnixMilli(int64(toF64(row[0])))
ct := time.UnixMilli(int64(toF64(row[6])))
open := toF64(row[1])
high := toF64(row[2])
low := toF64(row[3])
closep := toF64(row[4])
vol := toF64(row[5])
out = append(out, Kline{OpenTime: ot, Open: open, High: high, Low: low, Close: closep, Volume: vol, CloseTime: ct})
}
return out, nil
}
func toF64(v any) float64 {
switch t := v.(type) {
case float64:
return t
case string:
f, _ := strconv.ParseFloat(t, 64)
return f
default:
f, _ := strconv.ParseFloat(fmt.Sprintf("%v", v), 64)
return f
}
}
func loadLocalCSV(path string) ([]Kline, error) {
f, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, err
}
defer f.Close()
r := csv.NewReader(f)
_, _ = r.Read()
var out []Kline
for {
rec, err := r.Read()
if err != nil {
if err == io.EOF {
break
}
return out, nil
}
if len(rec) < 6 {
break
}
t, _ := time.Parse("2006-01-02", rec[0])
open, _ := strconv.ParseFloat(rec[1], 64)
high, _ := strconv.ParseFloat(rec[2], 64)
low, _ := strconv.ParseFloat(rec[3], 64)
closep, _ := strconv.ParseFloat(rec[4], 64)
vol, _ := strconv.ParseFloat(rec[5], 64)
out = append(out, Kline{OpenTime: t, CloseTime: t, Open: open, High: high, Low: low, Close: closep, Volume: vol})
}
return out, nil
}
func saveLocalCSV(path string, kl []Kline) error {
tmp := path + ".tmp"
f, err := os.Create(tmp)
if err != nil {
return err
}
defer f.Close()
w := csv.NewWriter(f)
defer w.Flush()
if err := w.Write([]string{"date", "open", "high", "low", "close", "volume"}); err != nil {
return err
}
for _, k := range kl {
rec := []string{
k.CloseTime.Format("2006-01-02"),
fmt.Sprintf("%.12f", k.Open),
fmt.Sprintf("%.12f", k.High),
fmt.Sprintf("%.12f", k.Low),
fmt.Sprintf("%.12f", k.Close),
fmt.Sprintf("%.8f", k.Volume),
}
if err := w.Write(rec); err != nil {
return err
}
}
return w.Error()
}
func mergeKlines(old, fresh []Kline) []Kline {
m := make(map[string]Kline, len(old)+len(fresh))
for _, k := range old {
m[k.CloseTime.Format("2006-01-02")] = k
}
for _, k := range fresh {
m[k.CloseTime.Format("2006-01-02")] = k
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
out := make([]Kline, 0, len(keys))
for _, k := range keys {
out = append(out, m[k])
}
return out
}
// ================== THORNode Fee (debug + sanity) ==================
func fetchThorSwapFeeBpsDebug(ctx context.Context, fromAsset, toAsset string, amountUnits int64) (float64, map[string]any, error) {
base := "https://thornode.ninerealms.com"
url := fmt.Sprintf("%s/thorchain/quote/swap?from_asset=%s&to_asset=%s&amount=%d", base, fromAsset, toAsset, amountUnits)
var v map[string]any
if err := httpGetJSON(ctx, url, &v); err != nil {
return 0, nil, err
}
raw, _ := json.Marshal(v)
log.Printf("thorquote url=%s resp=%s", url, sanitize(string(raw)))
feeBps := 0.0
if fees, ok := v["fees"].(map[string]any); ok {
if tb, ok2 := fees["total_bps"]; ok2 {
feeBps = toF64(tb)
return feeBps, v, nil
}
total := toF64(fees["total"])
amt := toF64(v["amount"])
if feeBps == 0 && total > 0 && amountUnits > 0 {
feeBps = (total / float64(amountUnits)) * 1e4
return feeBps, v, nil
}
if feeBps == 0 && total > 0 && amt > 0 {
feeBps = (total / amt) * 1e4
return feeBps, v, nil
}
}
if tb, ok := v["fees_total_bps"]; ok {
feeBps = toF64(tb)
return feeBps, v, nil
}
return 0, v, fmt.Errorf("quote priva di fees interpretabili")
}