- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
187 lines
4.6 KiB
Go
187 lines
4.6 KiB
Go
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 // 累计收益率
|
||
}
|