Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
186
internal/backtest/engine.go
Normal file
186
internal/backtest/engine.go
Normal 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 // 累计收益率
|
||||
}
|
||||
Reference in New Issue
Block a user