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") }