first version
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user