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