// backtest 是可转债轮动策略回测命令。 // // go run ./cmd/backtest [-db 库路径] [-from 2020-01-01] [-to 2025-12-31] \ // [-strategy lowprice|doublelow] [-topn 10] [-rebalance 21] \ // [-min-price 80] [-max-price 130] [-max-premium 30] [-cash 100000] [-out 曲线.csv] // // 从 SQLite 加载全量转债日K线,运行内置轮动策略(低价/双低), // 输出绩效指标并与等权全转债基准对比。 package main import ( "context" "encoding/csv" "flag" "fmt" "os" "path/filepath" "stock-analysis/internal/backtest" "stock-analysis/internal/strategy" "stock-analysis/internal/storage/sqlite" ) func main() { dbPath := flag.String("db", defaultDBPath(), "SQLite 数据库文件路径") from := flag.String("from", "2020-01-01", "回测起始日(含)") to := flag.String("to", "", "回测结束日(含,默认数据末日)") topn := flag.Int("topn", 10, "持仓数量") rebalance := flag.Int("rebalance", 21, "调仓周期(交易日)") minPrice := flag.Float64("min-price", 80, "价格下限") maxPrice := flag.Float64("max-price", 130, "价格上限") strategyName := flag.String("strategy", "lowprice", "策略: lowprice(低价轮动)|doublelow(双低轮动)|premium(溢价率轮动)") maxPremium := flag.Float64("max-premium", 30, "双低策略溢价率上限") fundamental := flag.Bool("fundamental", false, "启用正股基本面过滤器(排除ST/高负债/亏损标的)") cash := flag.Float64("cash", 100000, "初始资金(元)") out := flag.String("out", "", "净值曲线 CSV 输出路径(可选)") flag.Parse() ctx := context.Background() store, err := sqlite.Open(*dbPath) if err != nil { fatal("打开数据库失败: ", err) } defer store.Close() feed, err := backtest.NewDataFeed(ctx, store) if err != nil { fatal("加载K线数据失败: ", err) } fmt.Printf("数据加载完成:%d 个交易日(%s ~ %s)\n\n", len(feed.Dates()), feed.Dates()[0], feed.Dates()[len(feed.Dates())-1]) params := backtest.DefaultParams() params.InitialCash = *cash params.StartDate = *from params.EndDate = *to var strat backtest.Strategy switch *strategyName { case "lowprice": strat = strategy.NewLowPrice(strategy.LowPriceParams{ TopN: *topn, Rebalance: *rebalance, MinPrice: *minPrice, MaxPrice: *maxPrice, }) case "doublelow": strat = strategy.NewDoubleLow(strategy.DoubleLowParams{ TopN: *topn, Rebalance: *rebalance, MinPrice: *minPrice, MaxPrice: *maxPrice, MaxPremium: *maxPremium, }) case "premium": strat = strategy.NewPremium(strategy.PremiumParams{ TopN: *topn, Rebalance: *rebalance, MinPrice: *minPrice, MaxPrice: *maxPrice, }) default: fatal("未知策略 ", fmt.Errorf("应为 lowprice、doublelow 或 premium")) } engine := backtest.NewEngine(params, strat) if *fundamental { // 加载正股基本面数据 fundamentals, err := store.LoadAllFundamentals(ctx) if err != nil { fatal("加载基本面数据失败: ", err) } // 获取正股-转债映射 stock2bond, err := store.GetStockBondMap(ctx) if err != nil { fatal("获取正股-转债映射失败: ", err) } filter := backtest.NewFundamentalFilter(stock2bond) engine.SetFundamentalFilter(filter, fundamentals) fmt.Printf("基本面过滤器已启用:%d 只正股数据, %d 条正股-转债映射\n", len(fundamentals), len(stock2bond)) } res, err := engine.Run(ctx, feed) if err != nil { fatal("回测失败: ", err) } fmt.Printf("策略:%s(%s ~ %s)\n", res.Strategy, res.Start, res.End) printMetrics("策略", res.Metrics) bench := computeBenchmark(feed, *from, *to) fmt.Println() fmt.Printf("基准:等权全转债指数(%s ~ %s)\n", *from, *to) printMetrics("基准", bench) if *out != "" { if err := writeCSV(*out, res, feed); err != nil { fatal("写CSV失败: ", err) } fmt.Printf("\n净值曲线已输出到 %s\n", *out) } } // computeBenchmark 计算等权全转债指数:每日对全部存续转债取等权平均日收益并复利。 func computeBenchmark(feed *backtest.DataFeed, from, to string) backtest.Metrics { var ( idx float64 = 1 prev map[string]float64 curve []backtest.EquityPoint hasPrev bool ) for _, date := range feed.Dates() { if from != "" && date < from { continue } if to != "" && date > to { break } cur := make(map[string]float64) for _, q := range feed.QuotesOn(date) { cur[q.Code] = q.Price } if hasPrev { var sum, n float64 for code, p := range cur { if pp, ok := prev[code]; ok && pp > 0 { sum += p/pp - 1 n++ } } if n > 0 { idx *= 1 + sum/n } } curve = append(curve, backtest.EquityPoint{Date: date, Value: idx, Return: idx - 1}) prev = cur hasPrev = true } return backtest.ComputeMetrics(curve) } // printMetrics 打印绩效指标表格。 func printMetrics(label string, m backtest.Metrics) { fmt.Printf("%-14s 总收益 %9.2f%% 年化 %8.2f%% 最大回撤 %7.2f%% 夏普 %6.2f 波动率 %5.2f%% 胜率 %5.1f%%\n", label, m.TotalReturn*100, m.AnnualReturn*100, m.MaxDrawdown*100, m.Sharpe, m.Volatility*100, m.WinRatio*100) } // writeCSV 输出策略与基准的净值曲线(含累计收益)到 CSV 文件。 func writeCSV(path string, res *backtest.Result, feed *backtest.DataFeed) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() w := csv.NewWriter(f) defer w.Flush() if err := w.Write([]string{"date", "strategy_value", "strategy_return", "bench_value", "bench_return"}); err != nil { return err } // 基准曲线独立重算,与策略日期对齐。 benchCurve := benchmarkCurve(feed, res.Start, res.End) byDate := make(map[string]backtest.EquityPoint, len(benchCurve)) for _, p := range benchCurve { byDate[p.Date] = p } for _, p := range res.Curve { b, ok := byDate[p.Date] bv, br := 1.0, 0.0 if ok { bv, br = b.Value, b.Return } if err := w.Write([]string{ p.Date, fmt.Sprintf("%.2f", p.Value), fmt.Sprintf("%.6f", p.Return), fmt.Sprintf("%.6f", bv), fmt.Sprintf("%.6f", br), }); err != nil { return err } } return nil } // benchmarkCurve 返回 from~to 区间的基准净值曲线。 func benchmarkCurve(feed *backtest.DataFeed, from, to string) []backtest.EquityPoint { var ( idx float64 = 1 prev map[string]float64 curve []backtest.EquityPoint hasPrev bool ) for _, date := range feed.Dates() { if date < from || (to != "" && date > to) { continue } cur := make(map[string]float64) for _, q := range feed.QuotesOn(date) { cur[q.Code] = q.Price } if hasPrev { var sum, n float64 for code, p := range cur { if pp, ok := prev[code]; ok && pp > 0 { sum += p/pp - 1 n++ } } if n > 0 { idx *= 1 + sum/n } } curve = append(curve, backtest.EquityPoint{Date: date, Value: idx, Return: idx - 1}) prev = cur hasPrev = true } return curve } // defaultDBPath 与 ingest 保持一致,返回默认数据库路径。 func defaultDBPath() string { local := os.Getenv("LOCALAPPDATA") if local == "" { local = os.TempDir() } return filepath.Join(local, "stock-analysis", "stock.db") } // fatal 打印错误并退出。 func fatal(msg string, err error) { fmt.Fprintln(os.Stderr, msg, err) os.Exit(1) }