// 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 }