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
|
||||
}
|
||||
+12
-3
@@ -10,12 +10,21 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.yaml", "path to YAML configuration file")
|
||||
mode := flag.String("mode", "", "override APP_MODE: backtest|paper|sandbox|live_readonly|live_trade")
|
||||
unhalt := flag.Bool("unhalt", false, "manually clear HALT after reconciliation")
|
||||
reason := flag.String("reason", "", "audit reason for -unhalt")
|
||||
health := flag.Bool("healthcheck", false, "check local /health endpoint")
|
||||
healthURL := flag.String("healthcheck-url", "", "healthcheck URL; default http://127.0.0.1:3300/health")
|
||||
flag.Parse()
|
||||
|
||||
if err := app.Run(context.Background(), app.Options{
|
||||
ConfigPath: *configPath,
|
||||
Stdout: os.Stdout,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
ModeOverride: *mode,
|
||||
Unhalt: *unhalt,
|
||||
Reason: *reason,
|
||||
Healthcheck: *health,
|
||||
HealthcheckURL: *healthURL,
|
||||
}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bot failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"overnight-trading-bot/internal/repository/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
dsn := flag.String("dsn", os.Getenv("DB_DSN"), "MySQL/MariaDB DSN")
|
||||
direction := flag.String("direction", "up", "up or down")
|
||||
flag.Parse()
|
||||
if flag.NArg() > 0 {
|
||||
*direction = flag.Arg(0)
|
||||
}
|
||||
if *dsn == "" {
|
||||
return fmt.Errorf("DB_DSN is required")
|
||||
}
|
||||
db, err := sqlx.Open("mysql", *dsn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("ping db: %w", err)
|
||||
}
|
||||
switch *direction {
|
||||
case "up":
|
||||
err = mysql.ApplyMigrations(ctx, db.DB)
|
||||
case "down":
|
||||
err = mysql.RollbackAll(db.DB)
|
||||
default:
|
||||
err = fmt.Errorf("unknown direction %q", *direction)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
fmt.Println("migrations applied")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user