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,287 @@
package sqlite
import (
"context"
"os"
"path/filepath"
"testing"
"stock-analysis/internal/model"
)
func tempDB(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "test.db")
}
func TestOpenAndClose(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
if err := store.Close(); err != nil {
t.Fatalf("close: %v", err)
}
}
func TestSaveAndGetKlines(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
klines := []model.Kline{
{Date: "2020-01-02", Open: 100, Close: 101, High: 102, Low: 99, Volume: 1000, Amount: 100000, ConvertPrice: 10, ConvertValue: 1010, PremiumRate: 5},
{Date: "2020-01-03", Open: 101, Close: 102, High: 103, Low: 100, Volume: 2000, Amount: 200000, ConvertPrice: 10, ConvertValue: 1020, PremiumRate: 4},
{Date: "2020-01-06", Open: 102, Close: 100, High: 104, Low: 99, Volume: 1500, Amount: 150000, ConvertPrice: 10, ConvertValue: 1000, PremiumRate: 6},
}
if err := store.SaveKlines(ctx, "113001", klines); err != nil {
t.Fatalf("save: %v", err)
}
got, err := store.GetKlines(ctx, "113001")
if err != nil {
t.Fatalf("get: %v", err)
}
if len(got) != 3 {
t.Fatalf("expected 3 klines, got %d", len(got))
}
// 验证按日期升序
if got[0].Date != "2020-01-02" {
t.Errorf("first date: expected 2020-01-02, got %s", got[0].Date)
}
if got[2].Date != "2020-01-06" {
t.Errorf("last date: expected 2020-01-06, got %s", got[2].Date)
}
if got[1].Close != 102 {
t.Errorf("second close: expected 102, got %f", got[1].Close)
}
}
func TestSaveKlines_Idempotent(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
klines := []model.Kline{
{Date: "2020-01-02", Open: 100, Close: 101, High: 102, Low: 99, Volume: 1000},
}
// 第一次写入
if err := store.SaveKlines(ctx, "113001", klines); err != nil {
t.Fatalf("first save: %v", err)
}
// 第二次写入(幂等)
if err := store.SaveKlines(ctx, "113001", klines); err != nil {
t.Fatalf("second save: %v", err)
}
got, err := store.GetKlines(ctx, "113001")
if err != nil {
t.Fatalf("get: %v", err)
}
if len(got) != 1 {
t.Fatalf("expected 1 kline, got %d", len(got))
}
}
func TestCountKlines(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
// 空库
n, err := store.CountKlines(ctx, "113001")
if err != nil {
t.Fatalf("count empty: %v", err)
}
if n != 0 {
t.Errorf("expected 0, got %d", n)
}
klines := []model.Kline{
{Date: "2020-01-02", Open: 100, Close: 101, High: 102, Low: 99},
{Date: "2020-01-03", Open: 101, Close: 102, High: 103, Low: 100},
}
if err := store.SaveKlines(ctx, "113001", klines); err != nil {
t.Fatalf("save: %v", err)
}
n, err = store.CountKlines(ctx, "113001")
if err != nil {
t.Fatalf("count: %v", err)
}
if n != 2 {
t.Errorf("expected 2, got %d", n)
}
}
func TestListCodes(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
klines := []model.Kline{
{Date: "2020-01-02", Open: 100, Close: 101, High: 102, Low: 99},
}
if err := store.SaveKlines(ctx, "113001", klines); err != nil {
t.Fatalf("save 113001: %v", err)
}
if err := store.SaveKlines(ctx, "113002", klines); err != nil {
t.Fatalf("save 113002: %v", err)
}
codes, err := store.ListCodes(ctx)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(codes) != 2 {
t.Fatalf("expected 2 codes, got %d", len(codes))
}
if codes[0] != "113001" || codes[1] != "113002" {
t.Errorf("expected [113001, 113002], got %v", codes)
}
}
func TestLoadAllKlines(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
if err := store.SaveKlines(ctx, "A", []model.Kline{
{Date: "2020-01-02", Open: 100, Close: 101, High: 102, Low: 99},
{Date: "2020-01-03", Open: 101, Close: 102, High: 103, Low: 100},
}); err != nil {
t.Fatalf("save A: %v", err)
}
if err := store.SaveKlines(ctx, "B", []model.Kline{
{Date: "2020-01-02", Open: 200, Close: 201, High: 202, Low: 199},
}); err != nil {
t.Fatalf("save B: %v", err)
}
all, err := store.LoadAllKlines(ctx)
if err != nil {
t.Fatalf("load all: %v", err)
}
if len(all) != 2 {
t.Fatalf("expected 2 codes, got %d", len(all))
}
if len(all["A"]) != 2 {
t.Errorf("expected 2 klines for A, got %d", len(all["A"]))
}
if len(all["B"]) != 1 {
t.Errorf("expected 1 kline for B, got %d", len(all["B"]))
}
}
func TestOpen_NonExistentDir(t *testing.T) {
path := filepath.Join(t.TempDir(), "nonexistent", "test.db")
// 确保父目录存在
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
store.Close()
}
func TestSaveBonds(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
bonds := []model.ConvertibleBond{
{Code: "113001", Name: "测试转债1", StockCode: "600001", InitialConvertPrice: 10, ListDate: "2020-01-01"},
{Code: "113002", Name: "测试转债2", StockCode: "600002", InitialConvertPrice: 15, ListDate: "2020-06-01"},
}
if err := store.SaveBonds(ctx, bonds); err != nil {
t.Fatalf("save bonds: %v", err)
}
got, err := store.ListBonds(ctx)
if err != nil {
t.Fatalf("list bonds: %v", err)
}
if len(got) != 2 {
t.Fatalf("expected 2 bonds, got %d", len(got))
}
if got[0].Name != "测试转债1" {
t.Errorf("first bond name: expected 测试转债1, got %s", got[0].Name)
}
}
func TestUpdatePremiumBatch(t *testing.T) {
path := tempDB(t)
store, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer store.Close()
ctx := context.Background()
// 先写入K线
klines := []model.Kline{
{Date: "2020-01-02", Open: 100, Close: 101, High: 102, Low: 99},
}
if err := store.SaveKlines(ctx, "113001", klines); err != nil {
t.Fatalf("save: %v", err)
}
// 更新溢价率
rows := []PremiumRow{
{Date: "2020-01-02", ConvertPrice: 10, ConvertValue: 1010, PremiumRate: 5.5},
}
if err := store.UpdatePremiumBatch(ctx, "113001", rows); err != nil {
t.Fatalf("update premium: %v", err)
}
// 验证
got, err := store.GetKlines(ctx, "113001")
if err != nil {
t.Fatalf("get: %v", err)
}
if got[0].ConvertPrice != 10 {
t.Errorf("convert price: expected 10, got %f", got[0].ConvertPrice)
}
if got[0].ConvertValue != 1010 {
t.Errorf("convert value: expected 1010, got %f", got[0].ConvertValue)
}
if got[0].PremiumRate != 5.5 {
t.Errorf("premium rate: expected 5.5, got %f", got[0].PremiumRate)
}
}