Files
Veyra/internal/integration/strategy_backtest_test.go
T

121 lines
4.9 KiB
Go

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)
}
listed, e := repo.List(ctx)
if e != nil || len(listed) != 1 || listed[0].Symbol != bi.Symbol || listed[0].Input != nil || listed[0].Result != nil {
t.Fatal("backtest list must retain the symbol without returning large inputs")
}
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")
}
}