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