Initial commit: 可转债轮动分析系统

- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
2026-08-17 20:23:22 +08:00
commit e28bfec957
45 changed files with 7889 additions and 0 deletions

186
internal/backtest/engine.go Normal file
View File

@@ -0,0 +1,186 @@
package backtest
import (
"context"
"fmt"
"sort"
"stock-analysis/internal/model"
"stock-analysis/internal/storage"
)
// Params 回测运行参数。
type Params struct {
InitialCash float64 // 初始资金(元)
Commission float64 // 单边佣金比例(含佣金,默认 0.00025)
Slippage float64 // 滑点比例(默认 0)
StartDate string // 回测起始日(含),空则使用数据首日
EndDate string // 回测结束日(含),空则使用数据末日
}
// DefaultParams 返回推荐默认参数。
func DefaultParams() Params {
return Params{
InitialCash: 100000,
Commission: 0.00025,
}
}
// DataFeed 从存储加载全量K线,构建按日期索引的行情数据。
type DataFeed struct {
quotes map[string][]Quote // date → 当日全部转债行情
dates []string // 升序交易日列表
hist *History
}
// NewDataFeed 从存储加载全部转债K线并构建回测数据源。
func NewDataFeed(ctx context.Context, store storage.KlineStore) (*DataFeed, error) {
all, err := store.LoadAllKlines(ctx)
if err != nil {
return nil, fmt.Errorf("load klines: %w", err)
}
quotes := make(map[string][]Quote)
for code, ks := range all {
for _, k := range ks {
quotes[k.Date] = append(quotes[k.Date], Quote{
Code: code,
Price: k.Close,
Open: k.Open,
High: k.High,
Low: k.Low,
Volume: k.Volume,
ConvertValue: k.ConvertValue,
PremiumRate: k.PremiumRate,
})
}
}
dates := make([]string, 0, len(quotes))
for d := range quotes {
dates = append(dates, d)
}
sort.Strings(dates)
return &DataFeed{quotes: quotes, dates: dates, hist: &History{klines: all}}, nil
}
// Dates 返回升序交易日列表。
func (d *DataFeed) Dates() []string {
return d.dates
}
// QuotesOn 返回指定交易日的全部转债行情。
func (d *DataFeed) QuotesOn(date string) []Quote {
return d.quotes[date]
}
// History 返回全量历史数据访问器。
func (d *DataFeed) History() *History {
return d.hist
}
// Filter 只保留指定代码的数据,用于按用户选择的转债子集回测。
func (d *DataFeed) Filter(codes []string) {
keep := make(map[string]bool, len(codes))
for _, c := range codes {
keep[c] = true
}
// 过滤 quotes。
for date, qs := range d.quotes {
var filtered []Quote
for _, q := range qs {
if keep[q.Code] {
filtered = append(filtered, q)
}
}
d.quotes[date] = filtered
}
// 过滤 history.klines。
filteredKlines := make(map[string][]model.Kline)
for code, ks := range d.hist.klines {
if keep[code] {
filteredKlines[code] = ks
}
}
d.hist.klines = filteredKlines
}
// Engine 事件驱动回测引擎。
type Engine struct {
params Params
strategy Strategy
account *Account
filter *FundamentalFilter
fundMap map[string]*model.StockFundamental
}
// NewEngine 创建回测引擎。
func NewEngine(params Params, strategy Strategy) *Engine {
return &Engine{params: params, strategy: strategy}
}
// SetFundamentalFilter 设置基本面过滤器(可选)。
// stock2bond 为正股代码到转债代码的映射;fundamentals 为正股代码到基本面数据的映射。
func (e *Engine) SetFundamentalFilter(filter *FundamentalFilter, fundamentals map[string]*model.StockFundamental) {
e.filter = filter
e.fundMap = fundamentals
}
// Run 执行回测,返回结果(含净值曲线与绩效指标)。
func (e *Engine) Run(ctx context.Context, feed *DataFeed) (*Result, error) {
e.account = NewAccount(e.params)
for _, date := range feed.dates {
if e.params.StartDate != "" && date < e.params.StartDate {
continue
}
if e.params.EndDate != "" && date > e.params.EndDate {
break
}
universe := feed.quotes[date]
if e.filter != nil {
universe = e.filter.Apply(ctx, universe, e.fundMap)
}
targets, err := e.strategy.OnBar(ctx, date, universe, feed.hist)
if err != nil {
return nil, fmt.Errorf("strategy %s on %s: %w", e.strategy.Name(), date, err)
}
if targets != nil {
e.account.Rebalance(date, targets, feed.hist.CloseOn)
}
e.account.MarkToMarket(date, feed.hist.CloseOn)
}
curve := e.account.Curve()
start, end := "", ""
if len(curve) > 0 {
start, end = curve[0].Date, curve[len(curve)-1].Date
}
return &Result{
Strategy: e.strategy.Name(),
Start: start,
End: end,
Metrics: ComputeMetrics(curve),
Curve: curve,
}, nil
}
// Result 回测输出。
type Result struct {
Strategy string
Start string
End string
Metrics Metrics
Curve []EquityPoint
}
// EquityPoint 净值曲线点。
type EquityPoint struct {
Date string
Value float64 // 组合总市值
Return float64 // 累计收益率
}