package integration import ( "context" "encoding/json" "errors" "fmt" "git.sundynix.cn/Blizzard/Veyra/internal/apperror" "git.sundynix.cn/Blizzard/Veyra/internal/model" "git.sundynix.cn/Blizzard/Veyra/internal/repository" "git.sundynix.cn/Blizzard/Veyra/internal/research" "git.sundynix.cn/Blizzard/Veyra/internal/service" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "testing" ) func testStrategyBacktest(t *testing.T, ctx context.Context, pool *pgxpool.Pool, admin, user model.User) { t.Helper() strategies := service.NewStrategyService(repository.NewStrategyRepository(pool), true) input := service.StrategyInput{Code: "sma_research", Name: "均线研究", Description: "fixture parameters", Parameters: model.StrategyParameters{ShortWindow: 2, LongWindow: 3, FeeBPS: 10, SlippageBPS: 0, LotSize: 100}} if _, e := strategies.Save(ctx, user, "", input, uuid.NewString()); !errors.Is(e, apperror.ErrForbidden) { t.Fatal("ordinary user can edit research strategy") } created, e := strategies.Save(ctx, admin, "", input, uuid.NewString()) if e != nil { t.Fatal(e) } deployed, e := strategies.Deploy(ctx, admin, created.ID, created.Versions[0].ID, "ACTIVE", created.Version, "研究选用", uuid.NewString()) if e != nil { t.Fatal(e) } input.ExpectedVersion = deployed.Version input.Parameters.ShortWindow = 3 input.Parameters.LongWindow = 4 revised, e := strategies.Save(ctx, admin, created.ID, input, uuid.NewString()) if e != nil { t.Fatal(e) } if len(revised.Versions) != 2 || *revised.ActiveVersionID != created.Versions[0].ID || revised.Versions[1].Parameters.ShortWindow != 2 { t.Fatal("revision changed selected or immutable parameters") } if _, e := strategies.Save(ctx, admin, created.ID, input, uuid.NewString()); !errors.Is(e, apperror.ErrConflict) { t.Fatal("stale strategy edit succeeded") } if _, e := pool.Exec(ctx, `UPDATE veyra_strategy_version SET description='overwrite' WHERE id=$1`, created.Versions[0].ID); e == nil { t.Fatal("strategy parameters can be overwritten") } repo := repository.NewBacktestRepository(pool) svc := service.NewBacktestService(repo, research.New("python3"), true) bars := []model.HistoricalBar{} for i, v := range []string{"10.00", "9.00", "8.00", "20.00", "21.00", "7.00", "6.00", "5.00"} { bars = append(bars, model.HistoricalBar{Date: fmt.Sprintf("2025-01-%02d", i+1), Open: v, High: v, Low: v, Close: v}) } bi := model.BacktestInput{StrategyVersionID: created.Versions[0].ID, DatasetName: "integration fixture", Symbol: "600000", InitialCash: "10000.00", Bars: bars} bad := bi bad.Bars = append([]model.HistoricalBar(nil), bars...) bad.Bars[1].Date = bad.Bars[0].Date if _, e := svc.Create(ctx, admin, bad, uuid.NewString()); !errors.Is(e, apperror.ErrInvalidInput) { t.Fatal("duplicate day allowed") } job, e := svc.Create(ctx, admin, bi, uuid.NewString()) if e != nil { t.Fatal(e) } var snapshot model.ResearchInput if json.Unmarshal(job.Input, &snapshot) != nil || snapshot.Parameters.ShortWindow != 2 { t.Fatal("backtest lost immutable version parameters") } claim, e := repo.Claim(ctx) if e != nil || claim.ID != job.ID || claim.LeaseID == nil { t.Fatalf("claim: %v", e) } if _, e := repo.Claim(ctx); !errors.Is(e, apperror.ErrNotFound) { t.Fatal("active lease claimed twice") } if _, e := pool.Exec(ctx, `UPDATE veyra_backtest_run SET lease_expires_at=now()-interval '1 second' WHERE id=$1`, job.ID); e != nil { t.Fatal(e) } renewed, e := repo.Claim(ctx) if e != nil || *renewed.LeaseID == *claim.LeaseID { t.Fatal("expired job cannot recover") } result, e := research.New("python3").Execute(ctx, renewed.Input) if e != nil { t.Fatal(e) } if e := repo.Finish(ctx, claim, result, ""); !errors.Is(e, apperror.ErrConflict) { t.Fatal("stale worker can overwrite result") } if e := repo.Finish(ctx, renewed, result, ""); e != nil { t.Fatal(e) } done, e := svc.Detail(ctx, admin, job.ID) if e != nil || done.Status != "COMPLETED" || done.FinishedAt == nil || len(done.Result) == 0 { t.Fatalf("completed job: %v", e) } if _, e := pool.Exec(ctx, `UPDATE veyra_backtest_run SET result='{}'::jsonb WHERE id=$1`, job.ID); e == nil { t.Fatal("completed result can be overwritten") } if _, e := svc.Detail(ctx, user, job.ID); !errors.Is(e, apperror.ErrForbidden) { t.Fatal("app user can access research inputs") } failed, e := svc.Create(ctx, admin, bi, uuid.NewString()) if e != nil { t.Fatal(e) } failing := service.NewBacktestService(repo, research.New("/missing/python"), true) if _, e := failing.ProcessNext(ctx); e != nil { t.Fatal(e) } failed, e = svc.Detail(ctx, admin, failed.ID) if e != nil || failed.Status != "FAILED" || failed.Result != nil || failed.Failure == "" { t.Fatal("execution failure presented a result") } }