Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
87
internal/strategy/doublelow.go
Normal file
87
internal/strategy/doublelow.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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
|
||||
}
|
||||
152
internal/strategy/doublelow_test.go
Normal file
152
internal/strategy/doublelow_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"stock-analysis/internal/backtest"
|
||||
)
|
||||
|
||||
func TestDoubleLow_Name(t *testing.T) {
|
||||
s := NewDoubleLow(DoubleLowParams{TopN: 10, Rebalance: 21, MinPrice: 90, MaxPrice: 130, MaxPremium: 30})
|
||||
name := s.Name()
|
||||
if name == "" {
|
||||
t.Error("strategy name should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoubleLow_SelectByDoubleLowValue(t *testing.T) {
|
||||
s := NewDoubleLow(DoubleLowParams{TopN: 3, Rebalance: 5, MinPrice: 90, MaxPrice: 130, MaxPremium: 30})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 100, PremiumRate: 10}, // 双低 = 110
|
||||
{Code: "B", Price: 95, PremiumRate: 5}, // 双低 = 100 (最低)
|
||||
{Code: "C", Price: 110, PremiumRate: 20}, // 双低 = 130
|
||||
{Code: "D", Price: 105, PremiumRate: 8}, // 双低 = 113
|
||||
{Code: "E", Price: 120, PremiumRate: 15}, // 双低 = 135
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(targets) != 3 {
|
||||
t.Fatalf("expected 3 targets, got %d", len(targets))
|
||||
}
|
||||
|
||||
// 按双低值排序:B(100), A(110), D(113)
|
||||
codes := make(map[string]bool)
|
||||
for _, tg := range targets {
|
||||
codes[tg.Code] = true
|
||||
}
|
||||
if !codes["B"] || !codes["A"] || !codes["D"] {
|
||||
t.Errorf("expected codes B, A, D, got %v", codes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoubleLow_FilterByPriceRange(t *testing.T) {
|
||||
s := NewDoubleLow(DoubleLowParams{TopN: 5, Rebalance: 5, MinPrice: 90, MaxPrice: 130, MaxPremium: 30})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 85, PremiumRate: 5}, // 价格过低,过滤
|
||||
{Code: "B", Price: 100, PremiumRate: 10}, // 合格
|
||||
{Code: "C", Price: 135, PremiumRate: 5}, // 价格过高,过滤
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(targets) != 1 {
|
||||
t.Fatalf("expected 1 target, got %d", len(targets))
|
||||
}
|
||||
if targets[0].Code != "B" {
|
||||
t.Errorf("expected code B, got %s", targets[0].Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoubleLow_FilterByPremiumRate(t *testing.T) {
|
||||
s := NewDoubleLow(DoubleLowParams{TopN: 5, Rebalance: 5, MinPrice: 90, MaxPrice: 130, MaxPremium: 20})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 100, PremiumRate: 15}, // 合格
|
||||
{Code: "B", Price: 100, PremiumRate: 25}, // 溢价过高,过滤
|
||||
{Code: "C", Price: 100, PremiumRate: 0}, // 溢价为0,过滤
|
||||
{Code: "D", Price: 100, PremiumRate: -5}, // 负溢价,过滤
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(targets) != 1 {
|
||||
t.Fatalf("expected 1 target, got %d", len(targets))
|
||||
}
|
||||
if targets[0].Code != "A" {
|
||||
t.Errorf("expected code A, got %s", targets[0].Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoubleLow_NoQualifiedCandidates(t *testing.T) {
|
||||
s := NewDoubleLow(DoubleLowParams{TopN: 3, Rebalance: 5, MinPrice: 90, MaxPrice: 130, MaxPremium: 30})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 50, PremiumRate: 5},
|
||||
{Code: "B", Price: 200, PremiumRate: 10},
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if targets != nil {
|
||||
t.Error("expected nil targets when no qualified candidates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoubleLow_RebalanceCycle(t *testing.T) {
|
||||
s := NewDoubleLow(DoubleLowParams{TopN: 3, Rebalance: 3, MinPrice: 90, MaxPrice: 130, MaxPremium: 30})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 100, PremiumRate: 10},
|
||||
}
|
||||
|
||||
// 第 1 天:调仓
|
||||
targets, _ := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if targets == nil {
|
||||
t.Error("day 1 should rebalance")
|
||||
}
|
||||
|
||||
// 第 2-3 天:不调仓
|
||||
for i := 0; i < 2; i++ {
|
||||
targets, _ = s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if targets != nil {
|
||||
t.Errorf("day %d should not rebalance", i+2)
|
||||
}
|
||||
}
|
||||
|
||||
// 第 4 天:调仓
|
||||
targets, _ = s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if targets == nil {
|
||||
t.Error("day 4 should rebalance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDoubleLowParams(t *testing.T) {
|
||||
p := DefaultDoubleLowParams()
|
||||
if p.TopN != 10 {
|
||||
t.Errorf("TopN: expected 10, got %d", p.TopN)
|
||||
}
|
||||
if p.Rebalance != 21 {
|
||||
t.Errorf("Rebalance: expected 21, got %d", p.Rebalance)
|
||||
}
|
||||
if p.MinPrice != 90 {
|
||||
t.Errorf("MinPrice: expected 90, got %f", p.MinPrice)
|
||||
}
|
||||
if p.MaxPrice != 130 {
|
||||
t.Errorf("MaxPrice: expected 130, got %f", p.MaxPrice)
|
||||
}
|
||||
if p.MaxPremium != 30 {
|
||||
t.Errorf("MaxPremium: expected 30, got %f", p.MaxPremium)
|
||||
}
|
||||
}
|
||||
71
internal/strategy/lowprice.go
Normal file
71
internal/strategy/lowprice.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Package strategy 提供内置回测策略(MVC Service 层)。
|
||||
// 用户可在 backtest.Strategy 接口基础上扩展自定义轮动/网格等策略。
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"stock-analysis/internal/backtest"
|
||||
)
|
||||
|
||||
// LowPriceParams 低价轮动策略参数。
|
||||
type LowPriceParams struct {
|
||||
TopN int // 持仓数量
|
||||
Rebalance int // 调仓周期(交易日)
|
||||
MinPrice float64 // 价格下限(过滤异常低价/退市风险债)
|
||||
MaxPrice float64 // 价格上限(低价策略通常低于 130)
|
||||
}
|
||||
|
||||
// DefaultLowPriceParams 返回推荐默认参数。
|
||||
func DefaultLowPriceParams() LowPriceParams {
|
||||
return LowPriceParams{TopN: 10, Rebalance: 21, MinPrice: 80, MaxPrice: 130}
|
||||
}
|
||||
|
||||
// LowPrice 低价轮动策略:每 Rebalance 个交易日,选择全市场价格最低的 TopN
|
||||
// 只转债等权持有。基于可转债"下有保底"特性,低价格通常意味着较低的下行风险,
|
||||
// 是经典的可转债轮动框架(演示用,可替换为双低/溢价等因子)。
|
||||
type LowPrice struct {
|
||||
params LowPriceParams
|
||||
day int // 已执行的交易日计数
|
||||
}
|
||||
|
||||
// NewLowPrice 创建低价轮动策略。
|
||||
func NewLowPrice(p LowPriceParams) *LowPrice {
|
||||
return &LowPrice{params: p}
|
||||
}
|
||||
|
||||
// Name 返回策略名称。
|
||||
func (s *LowPrice) Name() string {
|
||||
return fmt.Sprintf("低价轮动(top%d,每%d交易日)", s.params.TopN, s.params.Rebalance)
|
||||
}
|
||||
|
||||
// OnBar 实现 backtest.Strategy:交易日递增,到调仓日时选股返回目标持仓。
|
||||
func (s *LowPrice) 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 { 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
|
||||
}
|
||||
139
internal/strategy/lowprice_test.go
Normal file
139
internal/strategy/lowprice_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package strategy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"stock-analysis/internal/backtest"
|
||||
)
|
||||
|
||||
func TestLowPrice_Name(t *testing.T) {
|
||||
s := NewLowPrice(LowPriceParams{TopN: 10, Rebalance: 21, MinPrice: 80, MaxPrice: 130})
|
||||
name := s.Name()
|
||||
if name == "" {
|
||||
t.Error("strategy name should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowPrice_FirstDaySelectsTopN(t *testing.T) {
|
||||
s := NewLowPrice(LowPriceParams{TopN: 3, Rebalance: 5, MinPrice: 80, MaxPrice: 130})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 100},
|
||||
{Code: "B", Price: 90},
|
||||
{Code: "C", Price: 110},
|
||||
{Code: "D", Price: 85},
|
||||
{Code: "E", Price: 120},
|
||||
{Code: "F", Price: 75}, // 低于 MinPrice,应被过滤
|
||||
{Code: "G", Price: 135}, // 高于 MaxPrice,应被过滤
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(targets) != 3 {
|
||||
t.Fatalf("expected 3 targets, got %d", len(targets))
|
||||
}
|
||||
|
||||
// 按价格升序,应选 D(85), B(90), A(100)
|
||||
codes := make(map[string]bool)
|
||||
for _, tg := range targets {
|
||||
codes[tg.Code] = true
|
||||
}
|
||||
if !codes["D"] || !codes["B"] || !codes["A"] {
|
||||
t.Errorf("expected codes D, B, A, got %v", codes)
|
||||
}
|
||||
|
||||
// 权重应为等权 1/3
|
||||
for _, tg := range targets {
|
||||
if tg.Weight != 1.0/3.0 {
|
||||
t.Errorf("weight for %s: expected %f, got %f", tg.Code, 1.0/3.0, tg.Weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowPrice_NonRebalanceDay(t *testing.T) {
|
||||
s := NewLowPrice(LowPriceParams{TopN: 3, Rebalance: 5, MinPrice: 80, MaxPrice: 130})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 100},
|
||||
}
|
||||
|
||||
// 第 1 天:调仓
|
||||
targets, _ := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if targets == nil {
|
||||
t.Error("day 1 should rebalance")
|
||||
}
|
||||
|
||||
// 第 2-5 天:不调仓
|
||||
for i := 0; i < 4; i++ {
|
||||
targets, _ = s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if targets != nil {
|
||||
t.Errorf("day %d should not rebalance", i+2)
|
||||
}
|
||||
}
|
||||
|
||||
// 第 6 天:调仓
|
||||
targets, _ = s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if targets == nil {
|
||||
t.Error("day 6 should rebalance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowPrice_NoQualifiedCandidates(t *testing.T) {
|
||||
s := NewLowPrice(LowPriceParams{TopN: 3, Rebalance: 5, MinPrice: 80, MaxPrice: 130})
|
||||
|
||||
// 所有标的价格都不在范围内
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 50},
|
||||
{Code: "B", Price: 200},
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if targets != nil {
|
||||
t.Error("expected nil targets when no qualified candidates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowPrice_FewerThanTopN(t *testing.T) {
|
||||
s := NewLowPrice(LowPriceParams{TopN: 10, Rebalance: 5, MinPrice: 80, MaxPrice: 130})
|
||||
|
||||
universe := []backtest.Quote{
|
||||
{Code: "A", Price: 100},
|
||||
{Code: "B", Price: 110},
|
||||
}
|
||||
|
||||
targets, err := s.OnBar(context.Background(), "2020-01-02", universe, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(targets) != 2 {
|
||||
t.Fatalf("expected 2 targets, got %d", len(targets))
|
||||
}
|
||||
// 权重应为等权 1/2
|
||||
for _, tg := range targets {
|
||||
if tg.Weight != 0.5 {
|
||||
t.Errorf("weight for %s: expected 0.5, got %f", tg.Code, tg.Weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLowPriceParams(t *testing.T) {
|
||||
p := DefaultLowPriceParams()
|
||||
if p.TopN != 10 {
|
||||
t.Errorf("TopN: expected 10, got %d", p.TopN)
|
||||
}
|
||||
if p.Rebalance != 21 {
|
||||
t.Errorf("Rebalance: expected 21, got %d", p.Rebalance)
|
||||
}
|
||||
if p.MinPrice != 80 {
|
||||
t.Errorf("MinPrice: expected 80, got %f", p.MinPrice)
|
||||
}
|
||||
if p.MaxPrice != 130 {
|
||||
t.Errorf("MaxPrice: expected 130, got %f", p.MaxPrice)
|
||||
}
|
||||
}
|
||||
79
internal/strategy/premium.go
Normal file
79
internal/strategy/premium.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user