Initial commit: 可转债轮动分析系统

- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
2026-08-17 20:23:22 +08:00
commit e28bfec957
45 changed files with 7889 additions and 0 deletions

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