- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
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)
|
||
}
|
||
} |