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