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

View File

@@ -0,0 +1,79 @@
package strategy
import (
"context"
"fmt"
"sort"
"stock-analysis/internal/backtest"
)
// PremiumParams 溢价率轮动策略参数。
type PremiumParams struct {
TopN int // 持仓数量
Rebalance int // 调仓周期(交易日)
MinPrice float64 // 价格下限
MaxPrice float64 // 价格上限
}
// DefaultPremiumParams 返回推荐默认参数(价格放宽到 90-150)。
func DefaultPremiumParams() PremiumParams {
return PremiumParams{TopN: 10, Rebalance: 21, MinPrice: 90, MaxPrice: 150}
}
// Premium 溢价率轮动策略:每 Rebalance 个交易日,选择转股溢价率最低的 TopN
// 只转债等权持有。溢价率低意味着转债价格与正股联动紧密,正股涨则转债跟涨,
// 适合正股上涨行情,避免"正股涨转债不涨"的尴尬。
type Premium struct {
params PremiumParams
day int
}
// NewPremium 创建溢价率轮动策略。
func NewPremium(p PremiumParams) *Premium {
return &Premium{params: p}
}
// Name 返回策略名称。
func (s *Premium) Name() string {
return fmt.Sprintf("溢价率轮动(top%d,每%d交易日)", s.params.TopN, s.params.Rebalance)
}
// OnBar 实现 backtest.Strategy:到调仓日时按溢价率升序选股。
// 注意:东财历史K线API不返回转股溢价率,当前数据库中的 PremiumRate 均为 0。
// 因此策略暂以"价格"作为溢价率的代理排序指标(低价债通常溢价率更低),
// 待后续接入正股K线数据后将改为按实际溢价率排序。
func (s *Premium) 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
}
candidates := make([]backtest.Quote, 0, len(universe))
for _, q := range universe {
if q.Price >= s.params.MinPrice && q.Price <= s.params.MaxPrice {
candidates = append(candidates, q)
}
}
// 按溢价率排序:有实际溢价率数据时优先使用,否则回退到价格排序。
sort.Slice(candidates, func(i, j int) bool {
pi, pj := candidates[i].PremiumRate, candidates[j].PremiumRate
if pi > 0 && pj > 0 {
return pi < pj
}
return candidates[i].Price < candidates[j].Price
})
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 _, q := range candidates {
targets = append(targets, backtest.Target{Code: q.Code, Weight: weight})
}
return targets, nil
}