Files
stock-analysis/internal/controller/api.go
WarrenZhou e28bfec957 Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
2026-08-17 20:23:22 +08:00

340 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 接口。
type MarketController struct {
svc *service.MarketService
dbPath string
}
// NewMarketController 创建行情控制器。
func NewMarketController(svc *service.MarketService, dbPath string) *MarketController {
return &MarketController{svc: svc, dbPath: dbPath}
}
// 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, err := sqlite.Open(c.dbPath)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
defer store.Close()
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, err := sqlite.Open(c.dbPath)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
defer store.Close()
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()
status := DataStatus{DBPath: c.dbPath}
store, err := sqlite.Open(c.dbPath)
if err != nil {
writeJSON(w, http.StatusOK, status) // 返回部分信息
return
}
defer store.Close()
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()})
}