// Package controller 定义 MVC 架构中的 Controller 层, // 负责接收 HTTP 请求、调用 Service 层业务逻辑、返回响应。 package controller import ( "encoding/json" "fmt" "log" "net/http" "stock-analysis/internal/backtest" "stock-analysis/internal/service" "stock-analysis/internal/storage/sqlite" "stock-analysis/internal/strategy" ) // MarketController 提供行情与回测相关的 HTTP 接口。 // store 为共享 SQLite 存储,与 job.Manager 共用同一连接池,避免每次请求重复开关连接。 type MarketController struct { svc *service.MarketService store *sqlite.Store } // NewMarketController 创建行情控制器。 func NewMarketController(svc *service.MarketService, store *sqlite.Store) *MarketController { return &MarketController{svc: svc, store: store} } // ListBonds 处理 GET /api/bonds,返回全量可转债实时行情。 func (c *MarketController) ListBonds(w http.ResponseWriter, r *http.Request) { bonds, err := c.svc.ListConvertibles(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, bonds) } // GetKline 处理 GET /api/bonds/{code}/kline,返回指定可转债日K线。 func (c *MarketController) GetKline(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") klines, err := c.svc.GetDailyKline(r.Context(), code, "20200101") if err != nil { writeError(w, http.StatusBadRequest, err) return } writeJSON(w, http.StatusOK, klines) } // BacktestRequest 回测请求参数。 type BacktestRequest struct { Strategy string `json:"strategy"` StartDate string `json:"startDate"` EndDate string `json:"endDate"` TopN int `json:"topN"` Rebalance int `json:"rebalance"` MinPrice float64 `json:"minPrice"` MaxPrice float64 `json:"maxPrice"` MaxPremium float64 `json:"maxPremium"` Cash float64 `json:"cash"` Codes []string `json:"codes"` // 指定参与回测的转债代码,空表示全部 Fundamental bool `json:"fundamental"` // 启用正股基本面过滤器 } // RunBacktest 处理 POST /api/backtest,执行回测并返回结果。 func (c *MarketController) RunBacktest(w http.ResponseWriter, r *http.Request) { var req BacktestRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, err) return } // 默认值填充。 if req.Strategy == "" { req.Strategy = "lowprice" } if req.TopN <= 0 { req.TopN = 10 } if req.Rebalance <= 0 { req.Rebalance = 21 } if req.Cash <= 0 { req.Cash = 100000 } if req.MaxPrice <= 0 { req.MaxPrice = 130 } if req.MinPrice <= 0 { req.MinPrice = 80 } if req.MaxPremium <= 0 { req.MaxPremium = 30 } ctx := r.Context() store := c.store feed, err := backtest.NewDataFeed(ctx, store) if err != nil { writeError(w, http.StatusInternalServerError, err) return } // 如果指定了 codes,过滤数据源。 if len(req.Codes) > 0 { feed.Filter(req.Codes) } params := backtest.DefaultParams() params.InitialCash = req.Cash params.StartDate = req.StartDate params.EndDate = req.EndDate var strat backtest.Strategy switch req.Strategy { case "lowprice": strat = strategy.NewLowPrice(strategy.LowPriceParams{ TopN: req.TopN, Rebalance: req.Rebalance, MinPrice: req.MinPrice, MaxPrice: req.MaxPrice, }) case "doublelow": strat = strategy.NewDoubleLow(strategy.DoubleLowParams{ TopN: req.TopN, Rebalance: req.Rebalance, MinPrice: req.MinPrice, MaxPrice: req.MaxPrice, MaxPremium: req.MaxPremium, }) case "premium": strat = strategy.NewPremium(strategy.PremiumParams{ TopN: req.TopN, Rebalance: req.Rebalance, MinPrice: req.MinPrice, MaxPrice: req.MaxPrice, }) default: writeError(w, http.StatusBadRequest, err) return } engine := backtest.NewEngine(params, strat) if req.Fundamental { fundamentals, err := store.LoadAllFundamentals(ctx) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Errorf("加载基本面数据: %w", err)) return } stock2bond, err := store.GetStockBondMap(ctx) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Errorf("获取正股映射: %w", err)) return } filter := backtest.NewFundamentalFilter(stock2bond) engine.SetFundamentalFilter(filter, fundamentals) log.Printf("基本面过滤器已启用:%d 只正股数据", len(fundamentals)) } res, err := engine.Run(ctx, feed) if err != nil { writeError(w, http.StatusInternalServerError, err) return } log.Printf("backtest done: %s, start=%s, end=%s, totalReturn=%.2f%%, curvePoints=%d, tradeDays=%d", res.Strategy, res.Start, res.End, res.Metrics.TotalReturn*100, len(res.Curve), res.Metrics.TradeDays) bench := computeBenchmarkJSON(feed, req.StartDate, req.EndDate) writeJSON(w, http.StatusOK, map[string]any{ "strategy": res.Strategy, "start": res.Start, "end": res.End, "metrics": res.Metrics, "curve": res.Curve, "benchMetrics": bench.Metrics, "benchCurve": bench.Curve, }) } // benchmarkResult 基准回测结果。 type benchmarkResult struct { Metrics backtest.Metrics Curve []backtest.EquityPoint } // computeBenchmarkJSON 在 Web API 中计算基准指数。 func computeBenchmarkJSON(feed *backtest.DataFeed, from, to string) benchmarkResult { 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 benchmarkResult{ Metrics: backtest.ComputeMetrics(curve), Curve: curve, } } // DataStatus 数据状态信息。 type DataStatus struct { DBPath string `json:"dbPath"` BondCount int `json:"bondCount"` KlineCount int `json:"klineCount"` DateRange string `json:"dateRange"` } // DBBond 数据库中转债简要信息。 type DBBond struct { Code string `json:"Code"` Name string `json:"Name"` KlineCount int `json:"KlineCount"` } // ListDBBonds 处理 GET /api/db/bonds,返回数据库中已有的转债列表。 func (c *MarketController) ListDBBonds(w http.ResponseWriter, r *http.Request) { ctx := r.Context() store := c.store codes, err := store.ListCodes(ctx) if err != nil { writeError(w, http.StatusInternalServerError, err) return } // 加载转债名称映射。 allBonds, _ := store.ListBonds(ctx) nameMap := make(map[string]string, len(allBonds)) for _, b := range allBonds { nameMap[b.Code] = b.Name } bonds := make([]DBBond, 0, len(codes)) for _, code := range codes { n, _ := store.CountKlines(ctx, code) bonds = append(bonds, DBBond{Code: code, Name: nameMap[code], KlineCount: n}) } writeJSON(w, http.StatusOK, bonds) } // GetDataStatus 处理 GET /api/status,返回数据库状态信息。 func (c *MarketController) GetDataStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() store := c.store status := DataStatus{DBPath: c.store.Path()} codes, err := store.ListCodes(ctx) if err == nil { status.BondCount = len(codes) } all, err := store.LoadAllKlines(ctx) if err == nil { for _, ks := range all { status.KlineCount += len(ks) } } // 取日期范围。 if len(all) > 0 { var minDate, maxDate string for _, ks := range all { if len(ks) > 0 { if minDate == "" || ks[0].Date < minDate { minDate = ks[0].Date } last := ks[len(ks)-1].Date if maxDate == "" || last > maxDate { maxDate = last } } } status.DateRange = minDate + " ~ " + maxDate } writeJSON(w, http.StatusOK, status) } // writeJSON 写入 JSON 响应。 func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { log.Printf("write json: %v", err) } } // writeError 写入 JSON 错误响应。 func writeError(w http.ResponseWriter, status int, err error) { log.Printf("http %d: %v", status, err) writeJSON(w, status, map[string]string{"error": err.Error()}) }