181 lines
4.2 KiB
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")
|
|
}
|