117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
package jobs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.toki-labs.com/toki/alt/packages/domain/backtest"
|
|
"git.toki-labs.com/toki/alt/packages/domain/market"
|
|
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
|
)
|
|
|
|
func sampleSpec() backtest.RunSpec {
|
|
return backtest.RunSpec{
|
|
StrategyID: "strat-abc",
|
|
Market: market.MarketKR,
|
|
Timeframe: market.TimeframeDaily,
|
|
From: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
|
|
To: time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC),
|
|
}
|
|
}
|
|
|
|
func TestBacktestStarterRecordsPendingAndLaunches(t *testing.T) {
|
|
runner := NewRunner()
|
|
store := &stubBacktestRunStore{runs: make(map[backtest.RunID]backtest.Run)}
|
|
|
|
var executedPayload json.RawMessage
|
|
runner.Register(KindRunBacktest, func(ctx context.Context, payload json.RawMessage) error {
|
|
executedPayload = payload
|
|
return nil
|
|
})
|
|
|
|
fixed := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC)
|
|
starter := NewBacktestStarter(runner, store,
|
|
WithStarterNow(func() time.Time { return fixed }),
|
|
WithStarterIDGenerator(func() string { return "run-fixed" }),
|
|
WithStarterLaunch(func(fn func()) { fn() }), // run synchronously for determinism
|
|
)
|
|
|
|
specWithSelector := sampleSpec()
|
|
specWithSelector.Selector = backtest.InputSelector{
|
|
InstrumentIDs: []market.InstrumentID{"KR:005930"},
|
|
Symbols: []string{"005930"},
|
|
}
|
|
run, err := starter.StartBacktest(context.Background(), specWithSelector)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if run.ID != "run-fixed" {
|
|
t.Errorf("expected generated id run-fixed, got %q", run.ID)
|
|
}
|
|
if run.Status != backtest.RunStatusPending {
|
|
t.Errorf("expected pending status, got %q", run.Status)
|
|
}
|
|
if !run.CreatedAt.Equal(fixed) || !run.UpdatedAt.Equal(fixed) {
|
|
t.Errorf("expected timestamps %v, got created=%v updated=%v", fixed, run.CreatedAt, run.UpdatedAt)
|
|
}
|
|
|
|
// Pending run is persisted before execution so it is immediately listable.
|
|
if len(store.upsertCalls) != 1 || store.upsertCalls[0].Status != backtest.RunStatusPending {
|
|
t.Fatalf("expected one pending upsert, got %+v", store.upsertCalls)
|
|
}
|
|
|
|
// The launched job carries a decodable RunBacktestPayload for the same run.
|
|
if executedPayload == nil {
|
|
t.Fatal("expected the runner to be invoked with a payload")
|
|
}
|
|
decoded, err := DecodeRunBacktestPayload(executedPayload)
|
|
if err != nil {
|
|
t.Fatalf("payload should decode: %v", err)
|
|
}
|
|
if decoded.RunID != "run-fixed" {
|
|
t.Errorf("payload run id mismatch: %q", decoded.RunID)
|
|
}
|
|
if decoded.StrategyID != "strat-abc" || decoded.Market != "KR" || decoded.Timeframe != "1d" {
|
|
t.Errorf("payload spec mismatch: %+v", decoded)
|
|
}
|
|
if len(decoded.Selector.InstrumentIDs) != 1 || decoded.Selector.InstrumentIDs[0] != "KR:005930" {
|
|
t.Errorf("payload selector instrument_ids mismatch: %+v", decoded.Selector.InstrumentIDs)
|
|
}
|
|
if len(decoded.Selector.Symbols) != 1 || decoded.Selector.Symbols[0] != "005930" {
|
|
t.Errorf("payload selector symbols mismatch: %+v", decoded.Selector.Symbols)
|
|
}
|
|
}
|
|
|
|
func TestBacktestStarterPersistErrorStops(t *testing.T) {
|
|
runner := NewRunner()
|
|
launched := false
|
|
runner.Register(KindRunBacktest, func(ctx context.Context, payload json.RawMessage) error {
|
|
launched = true
|
|
return nil
|
|
})
|
|
|
|
store := &failingRunStore{err: errors.New("db down")}
|
|
starter := NewBacktestStarter(runner, store,
|
|
WithStarterLaunch(func(fn func()) { fn() }),
|
|
)
|
|
|
|
if _, err := starter.StartBacktest(context.Background(), sampleSpec()); err == nil {
|
|
t.Fatal("expected persist error to propagate")
|
|
}
|
|
if launched {
|
|
t.Error("execution must not launch when the pending run cannot be persisted")
|
|
}
|
|
}
|
|
|
|
type failingRunStore struct {
|
|
err error
|
|
}
|
|
|
|
func (f *failingRunStore) UpsertRun(ctx context.Context, run backtest.Run) error { return f.err }
|
|
func (f *failingRunStore) GetRun(ctx context.Context, id backtest.RunID) (backtest.Run, error) {
|
|
return backtest.Run{}, storage.ErrRunNotFound
|
|
}
|