- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
// Package strategy 提供内置回测策略(MVC Service 层)。
|
||
// 用户可在 backtest.Strategy 接口基础上扩展自定义轮动/网格等策略。
|
||
package strategy
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sort"
|
||
|
||
"stock-analysis/internal/backtest"
|
||
)
|
||
|
||
// DoubleLowParams 双低轮动策略参数。
|
||
type DoubleLowParams struct {
|
||
TopN int // 持仓数量
|
||
Rebalance int // 调仓周期(交易日)
|
||
MinPrice float64 // 价格下限(过滤异常低价/退市风险债)
|
||
MaxPrice float64 // 价格上限
|
||
MaxPremium float64 // 溢价率上限(过滤高溢价债,0 表示不限制)
|
||
}
|
||
|
||
// DefaultDoubleLowParams 返回推荐默认参数。
|
||
func DefaultDoubleLowParams() DoubleLowParams {
|
||
return DoubleLowParams{TopN: 10, Rebalance: 21, MinPrice: 90, MaxPrice: 130, MaxPremium: 30}
|
||
}
|
||
|
||
// DoubleLow 双低轮动策略:每个调仓日选择"双低值 = 转债价 + 转股溢价率(%)"
|
||
// 最低的 TopN 只转债等权持有。该策略兼顾"低价"的债性保护与"低溢价"的股性弹性,
|
||
// 是经典的稳健型可转债轮动框架。
|
||
type DoubleLow struct {
|
||
params DoubleLowParams
|
||
day int // 已执行的交易日计数
|
||
}
|
||
|
||
// NewDoubleLow 创建双低轮动策略。
|
||
func NewDoubleLow(p DoubleLowParams) *DoubleLow {
|
||
return &DoubleLow{params: p}
|
||
}
|
||
|
||
// Name 返回策略名称。
|
||
func (s *DoubleLow) Name() string {
|
||
return fmt.Sprintf("双低轮动(top%d,每%d交易日)", s.params.TopN, s.params.Rebalance)
|
||
}
|
||
|
||
// OnBar 实现 backtest.Strategy:交易日递增,到调仓日时按双低值选股返回目标持仓。
|
||
func (s *DoubleLow) OnBar(ctx context.Context, date string, universe []backtest.Quote, hist *backtest.History) ([]backtest.Target, error) {
|
||
s.day++
|
||
if s.day > 1 && (s.day-1)%s.params.Rebalance != 0 {
|
||
return nil, nil // 非调仓日
|
||
}
|
||
|
||
type cand struct {
|
||
code string
|
||
doubleLow float64
|
||
}
|
||
candidates := make([]cand, 0, len(universe))
|
||
for _, q := range universe {
|
||
if q.Price < s.params.MinPrice || q.Price > s.params.MaxPrice {
|
||
continue
|
||
}
|
||
// 溢价率缺失(未入库)或非正时为 0/负值,不参与双低排序。
|
||
if q.PremiumRate <= 0 {
|
||
continue
|
||
}
|
||
if s.params.MaxPremium > 0 && q.PremiumRate > s.params.MaxPremium {
|
||
continue
|
||
}
|
||
candidates = append(candidates, cand{
|
||
code: q.Code,
|
||
doubleLow: q.Price + q.PremiumRate,
|
||
})
|
||
}
|
||
sort.Slice(candidates, func(i, j int) bool { return candidates[i].doubleLow < candidates[j].doubleLow })
|
||
if len(candidates) > s.params.TopN {
|
||
candidates = candidates[:s.params.TopN]
|
||
}
|
||
if len(candidates) == 0 {
|
||
return nil, nil // 无合格标的,保持现状
|
||
}
|
||
|
||
weight := 1.0 / float64(len(candidates))
|
||
targets := make([]backtest.Target, 0, len(candidates))
|
||
for _, c := range candidates {
|
||
targets = append(targets, backtest.Target{Code: c.code, Weight: weight})
|
||
}
|
||
return targets, nil
|
||
}
|