Files
stock-analysis/internal/strategy/doublelow_test.go
WarrenZhou e28bfec957 Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
2026-08-17 20:23:22 +08:00

152 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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