128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package jobs
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.toki-labs.com/toki/alt/packages/domain/backtest"
|
|
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
|
)
|
|
|
|
// BacktestStarter turns a start command into a persisted pending run plus an
|
|
// asynchronous KindRunBacktest job. It reuses the existing Runner so the rail
|
|
// does not introduce a second execution engine: the start path only records the
|
|
// pending run and hands execution to the same handler the runner already owns.
|
|
type BacktestStarter struct {
|
|
runner *Runner
|
|
store storage.BacktestRunStore
|
|
newID func() string
|
|
now func() time.Time
|
|
// launch runs the execution goroutine. It is injectable so tests can run the
|
|
// job synchronously instead of racing a detached goroutine.
|
|
launch func(func())
|
|
}
|
|
|
|
// BacktestStarterOption customises a BacktestStarter for testing or wiring.
|
|
type BacktestStarterOption func(*BacktestStarter)
|
|
|
|
// WithStarterNow overrides the clock used for run timestamps.
|
|
func WithStarterNow(now func() time.Time) BacktestStarterOption {
|
|
return func(s *BacktestStarter) { s.now = now }
|
|
}
|
|
|
|
// WithStarterIDGenerator overrides the run id generator.
|
|
func WithStarterIDGenerator(newID func() string) BacktestStarterOption {
|
|
return func(s *BacktestStarter) { s.newID = newID }
|
|
}
|
|
|
|
// WithStarterLaunch overrides how the execution goroutine is launched. Tests use
|
|
// this to execute synchronously and observe the runner deterministically.
|
|
func WithStarterLaunch(launch func(func())) BacktestStarterOption {
|
|
return func(s *BacktestStarter) { s.launch = launch }
|
|
}
|
|
|
|
// NewBacktestStarter builds a starter bound to the given runner and run store.
|
|
func NewBacktestStarter(runner *Runner, store storage.BacktestRunStore, opts ...BacktestStarterOption) *BacktestStarter {
|
|
s := &BacktestStarter{
|
|
runner: runner,
|
|
store: store,
|
|
newID: newRunID,
|
|
now: time.Now,
|
|
launch: func(fn func()) { go fn() },
|
|
}
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// StartBacktest records a pending run for the spec and schedules execution. It
|
|
// returns the pending run so the caller can answer the start request immediately
|
|
// while the runner finishes the work in the background.
|
|
func (s *BacktestStarter) StartBacktest(ctx context.Context, spec backtest.RunSpec) (backtest.Run, error) {
|
|
now := s.now().UTC()
|
|
run := backtest.Run{
|
|
ID: backtest.RunID(s.newID()),
|
|
Spec: spec,
|
|
Status: backtest.RunStatusPending,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if err := s.store.UpsertRun(ctx, run); err != nil {
|
|
return backtest.Run{}, fmt.Errorf("failed to persist pending run: %w", err)
|
|
}
|
|
|
|
var instIDs []string
|
|
for _, id := range spec.Selector.InstrumentIDs {
|
|
instIDs = append(instIDs, string(id))
|
|
}
|
|
payload := RunBacktestPayload{
|
|
RunID: string(run.ID),
|
|
StrategyID: string(spec.StrategyID),
|
|
Market: string(spec.Market),
|
|
Timeframe: string(spec.Timeframe),
|
|
From: spec.From.UTC().Format(time.RFC3339),
|
|
To: spec.To.UTC().Format(time.RFC3339),
|
|
Selector: RunBacktestSelectorPayload{
|
|
InstrumentIDs: instIDs,
|
|
Symbols: spec.Selector.Symbols,
|
|
},
|
|
}
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return backtest.Run{}, fmt.Errorf("failed to encode run backtest payload: %w", err)
|
|
}
|
|
|
|
job := Job{
|
|
ID: string(run.ID),
|
|
Kind: KindRunBacktest,
|
|
Status: StatusPending,
|
|
Payload: raw,
|
|
}
|
|
|
|
// Execution is detached from the request lifecycle: the start response must
|
|
// not block on a long-running backtest, so the goroutine uses a background
|
|
// context instead of the request context.
|
|
s.launch(func() {
|
|
_ = s.runner.Execute(context.Background(), job)
|
|
})
|
|
|
|
return run, nil
|
|
}
|
|
|
|
// newRunID returns a random hex run identifier. crypto/rand keeps it collision
|
|
// resistant without pulling an external uuid dependency into the worker module.
|
|
func newRunID() string {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
// rand.Read failures are practically impossible; fall back to a
|
|
// timestamp-based id so a start never panics.
|
|
return fmt.Sprintf("run-%d", time.Now().UTC().UnixNano())
|
|
}
|
|
return "run-" + hex.EncodeToString(b[:])
|
|
}
|