Files
cbpred/cmd/cbpred-export/main.go
2026-10-07 21:23:21 +08:00

166 lines
4.6 KiB
Go

package main
import (
"encoding/csv"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/banbox/banbot/biz"
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/orm"
"github.com/banbox/banexg/errs"
)
var columns = []string{
"timestamp", "open", "high", "low", "close", "volume", "quote", "buy_volume", "trade_num",
}
func main() {
configPath := flag.String("config", "", "banbot config path")
dataDir := flag.String("data-dir", "", "banbot data directory")
pairs := flag.String("pairs", "", "comma-separated symbols")
startMS := flag.Int64("start-ms", 0, "inclusive start timestamp in milliseconds")
endMS := flag.Int64("end-ms", 0, "exclusive end timestamp in milliseconds")
timeframe := flag.String("timeframe", "8h", "bar timeframe (8h or 1d)")
outDir := flag.String("out-dir", "", "CSV output directory")
skipUnavailable := flag.Bool("skip-unavailable", true, "skip pairs unavailable on the exchange and absent from the local cache")
flag.Parse()
if (*timeframe != "8h" && *timeframe != "1d") || *configPath == "" || *dataDir == "" || *pairs == "" || *outDir == "" || *startMS <= 0 || *endMS <= *startMS {
flag.Usage()
os.Exit(2)
}
args := &config.CmdArgs{
Configs: config.ArrString{*configPath},
DataDir: *dataDir,
NoDefault: true,
RawPairs: *pairs,
TimeStart: strconv.FormatInt(*startMS, 10),
TimeEnd: strconv.FormatInt(*endMS, 10),
LogLevel: "error",
}
if err := biz.SetupComs(args); err != nil {
fail(err)
}
defer core.RunExitCalls()
if err := os.MkdirAll(*outDir, 0o755); err != nil {
fail(err)
}
for _, rawPair := range strings.Split(*pairs, ",") {
pair := strings.TrimSpace(rawPair)
if pair == "" {
continue
}
exs, err := orm.GetExSymbolCur(pair)
if err != nil {
if *skipUnavailable && isUnavailablePair(err) {
continue
}
fail(err)
}
_, rows, err := orm.GetSeries(exs, "1h", *startMS, *endMS, 0, false)
if err != nil {
fail(err)
}
bars := make([]*orm.SeriesOHLCV, 0, len(rows))
for _, row := range rows {
bar, err := row.OHLCV(exs)
if err != nil {
fail(err)
}
bars = append(bars, bar)
}
bars = aggregateHourly(bars, *timeframe)
name := fmt.Sprintf("%d_%s.csv", exs.ID, strings.NewReplacer("/", "_", ":", "_").Replace(pair))
if err := writeCSV(filepath.Join(*outDir, name), bars); err != nil {
fail(err)
}
}
}
func isUnavailablePair(err *errs.Error) bool {
return err != nil && err.Code == errs.CodeNoMarketForPair
}
// aggregateHourly emits only complete UTC buckets with exactly one bar per hour.
func aggregateHourly(rows []*orm.SeriesOHLCV, timeframe string) []*orm.SeriesOHLCV {
const hourMS int64 = 60 * 60 * 1000
hours := 8
if timeframe == "1d" {
hours = 24
}
stepMS := int64(hours) * hourMS
ordered := append([]*orm.SeriesOHLCV(nil), rows...)
sort.Slice(ordered, func(i, j int) bool { return ordered[i].Time < ordered[j].Time })
var output []*orm.SeriesOHLCV
for first := 0; first < len(ordered); {
bucket := ordered[first].Time / stepMS * stepMS
end := first + 1
for end < len(ordered) && ordered[end].Time/stepMS*stepMS == bucket {
end++
}
complete := end-first == hours
for i := first; complete && i < end; i++ {
complete = ordered[i].Time == bucket+int64(i-first)*hourMS
}
if complete {
bar := *ordered[first]
bar.Time, bar.EndMS, bar.TimeFrame, bar.Closed = bucket, bucket+stepMS, timeframe, true
for _, row := range ordered[first+1 : end] {
bar.High = max(bar.High, row.High)
bar.Low = min(bar.Low, row.Low)
bar.Close = row.Close
bar.Volume += row.Volume
bar.Quote += row.Quote
bar.BuyVolume += row.BuyVolume
bar.TradeNum += row.TradeNum
}
output = append(output, &bar)
}
first = end
}
return output
}
func writeCSV(path string, rows []*orm.SeriesOHLCV) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
if err := writer.Write(columns); err != nil {
return err
}
for _, bar := range rows {
values := []string{
strconv.FormatInt(bar.Time, 10),
strconv.FormatFloat(bar.Open, 'f', -1, 64),
strconv.FormatFloat(bar.High, 'f', -1, 64),
strconv.FormatFloat(bar.Low, 'f', -1, 64),
strconv.FormatFloat(bar.Close, 'f', -1, 64),
strconv.FormatFloat(bar.Volume, 'f', -1, 64),
strconv.FormatFloat(bar.Quote, 'f', -1, 64),
strconv.FormatFloat(bar.BuyVolume, 'f', -1, 64),
strconv.FormatInt(bar.TradeNum, 10),
}
if err := writer.Write(values); err != nil {
return err
}
}
writer.Flush()
return writer.Error()
}
func fail(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}