package main import ( "flag" "fmt" "os" "github.com/shopspring/decimal" "overnight-trading-bot/internal/backtest" "overnight-trading-bot/internal/domain" ) func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } func run() error { candlesPath := flag.String("candles", "", "CSV with columns instrument_uid,trade_date,open,high,low,close,volume_lots") minuteCandlesPath := flag.String("minute-candles", "", "optional minute CSV with the same columns; trade_date may be RFC3339") outputDir := flag.String("out", "./backtest_out", "output directory") useMinuteModel := flag.Bool("use-minute-model", false, "require minute candles for conservative limit-fill simulation") entrySlip := flag.String("entry-slippage-bps", "8", "entry slippage in bps") exitSlip := flag.String("exit-slippage-bps", "8", "exit slippage in bps") commission := flag.String("commission-roundtrip-bps", "0", "roundtrip commission in bps") rollingShort := flag.Int("rolling-short", 60, "short rolling window") rollingLong := flag.Int("rolling-long", 252, "long rolling window") ewmaLambda := flag.Float64("ewma-lambda", 0.08, "EWMA lambda") minTStat := flag.String("min-tstat-60", "1.25", "minimum short-window t-stat") minWinRate := flag.String("min-win-rate-60", "0.55", "minimum short-window win rate") minNetEdge := flag.String("min-net-edge-bps", "10", "minimum net edge in bps") minADV := flag.String("min-adv-rub", "5000000", "minimum ADV in RUB") maxSpread := flag.String("max-spread-bps", "20", "maximum spread in bps") maxTick := flag.String("max-tick-bps", "10", "maximum tick size in bps") requireZeroCommission := flag.Bool("require-zero-commission", true, "reject trades when roundtrip commission is non-zero") flag.Parse() if *candlesPath == "" { return fmt.Errorf("-candles is required") } file, err := os.Open(*candlesPath) if err != nil { return fmt.Errorf("open candles: %w", err) } defer func() { _ = file.Close() }() candles, err := backtest.LoadCandlesCSV(file) if err != nil { return fmt.Errorf("load candles: %w", err) } var minuteCandles map[string][]domain.Candle if *minuteCandlesPath != "" { minuteFile, err := os.Open(*minuteCandlesPath) if err != nil { return fmt.Errorf("open minute candles: %w", err) } defer func() { _ = minuteFile.Close() }() minuteCandles, err = backtest.LoadCandlesCSV(minuteFile) if err != nil { return fmt.Errorf("load minute candles: %w", err) } } if *useMinuteModel && len(minuteCandles) == 0 { return fmt.Errorf("-minute-candles is required when -use-minute-model=true") } entry, err := decimal.NewFromString(*entrySlip) if err != nil { return fmt.Errorf("entry slippage: %w", err) } exit, err := decimal.NewFromString(*exitSlip) if err != nil { return fmt.Errorf("exit slippage: %w", err) } comm, err := decimal.NewFromString(*commission) if err != nil { return fmt.Errorf("commission: %w", err) } tstat, err := decimal.NewFromString(*minTStat) if err != nil { return fmt.Errorf("min tstat: %w", err) } winRate, err := decimal.NewFromString(*minWinRate) if err != nil { return fmt.Errorf("min win rate: %w", err) } netEdge, err := decimal.NewFromString(*minNetEdge) if err != nil { return fmt.Errorf("min net edge: %w", err) } adv, err := decimal.NewFromString(*minADV) if err != nil { return fmt.Errorf("min adv: %w", err) } spread, err := decimal.NewFromString(*maxSpread) if err != nil { return fmt.Errorf("max spread: %w", err) } tick, err := decimal.NewFromString(*maxTick) if err != nil { return fmt.Errorf("max tick: %w", err) } engine := backtest.New(backtest.Config{ EntrySlippageBps: entry, ExitSlippageBps: exit, CommissionRoundtripBps: comm, OutputDir: *outputDir, RollingShort: *rollingShort, RollingLong: *rollingLong, EWMALambda: *ewmaLambda, MinTStat60: tstat, MinWinRate60: winRate, MinNetEdgeBps: netEdge, MinADVRUB: adv, MaxSpreadBps: spread, MaxTickBps: tick, RequireZeroCommission: *requireZeroCommission, UseMinuteModel: *useMinuteModel, }) result, err := engine.RunWithMinuteCandles(candles, minuteCandles) if err != nil { return fmt.Errorf("run backtest: %w", err) } if err := result.Write(*outputDir); err != nil { return fmt.Errorf("write result: %w", err) } fmt.Printf("backtest complete: trades=%d total_return=%.6f\n", result.Metrics.NumberOfTrades, result.Metrics.TotalReturn) return nil }