package socket import ( "fmt" "time" altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1" "git.toki-labs.com/toki/alt/packages/domain/backtest" "git.toki-labs.com/toki/alt/packages/domain/market" ) // backtest_mapping.go owns backtest-specific conversions plus shared scalar // enum helpers. Keeping domain-to-proto mapping in worker socket code (and out // of the API) honours the rail design where the worker owns persistence shapes // while the API stays a thin pass-through. // runSpecFromProto validates an inbound BacktestRunSpec and converts it into the // worker-owned domain RunSpec. Validation lives next to the conversion so a // malformed start request never reaches the runner. func runSpecFromProto(spec *altv1.BacktestRunSpec) (backtest.RunSpec, error) { if spec == nil { return backtest.RunSpec{}, fmt.Errorf("backtest spec is required") } if spec.GetStrategyId() == "" { return backtest.RunSpec{}, fmt.Errorf("backtest spec: strategy_id is required") } mkt, err := marketFromProto(spec.GetMarket()) if err != nil { return backtest.RunSpec{}, fmt.Errorf("backtest spec: %w", err) } timeframe, err := timeframeFromProto(spec.GetTimeframe()) if err != nil { return backtest.RunSpec{}, fmt.Errorf("backtest spec: %w", err) } fromMs := spec.GetFromUnixMs() toMs := spec.GetToUnixMs() if fromMs == 0 { return backtest.RunSpec{}, fmt.Errorf("backtest spec: from_unix_ms is required") } if toMs == 0 { return backtest.RunSpec{}, fmt.Errorf("backtest spec: to_unix_ms is required") } from := time.UnixMilli(fromMs).UTC() to := time.UnixMilli(toMs).UTC() if from.After(to) { return backtest.RunSpec{}, fmt.Errorf("backtest spec: from cannot be after to") } return backtest.RunSpec{ StrategyID: backtest.StrategyID(spec.GetStrategyId()), Market: mkt, Timeframe: timeframe, From: from, To: to, }, nil } func marketFromProto(m altv1.Market) (market.Market, error) { switch m { case altv1.Market_MARKET_KR: return market.MarketKR, nil case altv1.Market_MARKET_US: return market.MarketUS, nil default: return "", fmt.Errorf("unsupported market %q", m.String()) } } func marketToProto(m market.Market) altv1.Market { switch m { case market.MarketKR: return altv1.Market_MARKET_KR case market.MarketUS: return altv1.Market_MARKET_US default: return altv1.Market_MARKET_UNSPECIFIED } } func timeframeFromProto(t altv1.Timeframe) (market.Timeframe, error) { switch t { case altv1.Timeframe_TIMEFRAME_DAILY: return market.TimeframeDaily, nil case altv1.Timeframe_TIMEFRAME_MINUTE_1: return market.TimeframeMin1, nil case altv1.Timeframe_TIMEFRAME_MINUTE_5: return market.TimeframeMin5, nil default: return "", fmt.Errorf("unsupported timeframe %q", t.String()) } } func timeframeToProto(t market.Timeframe) altv1.Timeframe { switch t { case market.TimeframeDaily: return altv1.Timeframe_TIMEFRAME_DAILY case market.TimeframeMin1: return altv1.Timeframe_TIMEFRAME_MINUTE_1 case market.TimeframeMin5: return altv1.Timeframe_TIMEFRAME_MINUTE_5 default: return altv1.Timeframe_TIMEFRAME_UNSPECIFIED } } // runStatusFromProto maps a request status filter to the domain status. The // unspecified enum maps to an empty status, which the store treats as "list all". func runStatusFromProto(s altv1.BacktestRunStatus) backtest.RunStatus { switch s { case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_PENDING: return backtest.RunStatusPending case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_RUNNING: return backtest.RunStatusRunning case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_SUCCEEDED: return backtest.RunStatusSucceeded case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_FAILED: return backtest.RunStatusFailed case altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_CANCELED: return backtest.RunStatusCanceled default: return "" } } func runStatusToProto(s backtest.RunStatus) altv1.BacktestRunStatus { switch s { case backtest.RunStatusPending: return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_PENDING case backtest.RunStatusRunning: return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_RUNNING case backtest.RunStatusSucceeded: return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_SUCCEEDED case backtest.RunStatusFailed: return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_FAILED case backtest.RunStatusCanceled: return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_CANCELED default: return altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_UNSPECIFIED } } func runSpecToProto(spec backtest.RunSpec) *altv1.BacktestRunSpec { return &altv1.BacktestRunSpec{ StrategyId: string(spec.StrategyID), Market: marketToProto(spec.Market), Timeframe: timeframeToProto(spec.Timeframe), FromUnixMs: timeToUnixMs(spec.From), ToUnixMs: timeToUnixMs(spec.To), } } func runToProto(run backtest.Run) *altv1.BacktestRun { return &altv1.BacktestRun{ Id: string(run.ID), Spec: runSpecToProto(run.Spec), Status: runStatusToProto(run.Status), CreatedAtUnixMs: timeToUnixMs(run.CreatedAt), UpdatedAtUnixMs: timeToUnixMs(run.UpdatedAt), } } func runsToProto(runs []backtest.Run) []*altv1.BacktestRun { out := make([]*altv1.BacktestRun, len(runs)) for i, run := range runs { out[i] = runToProto(run) } return out } func resultToProto(result backtest.Result) *altv1.BacktestResult { return &altv1.BacktestResult{ RunId: string(result.RunID), StartingCash: priceToProto(result.StartingCash), EndingEquity: priceToProto(result.EndingEquity), Trades: tradesToProto(result.Trades), Positions: positionsToProto(result.Positions), Summary: summaryToProto(result.Summary), EquityCurve: equityCurveToProto(result.EquityCurve), } } func resultsToProto(results []backtest.Result) []*altv1.BacktestResult { out := make([]*altv1.BacktestResult, len(results)) for i, result := range results { out[i] = resultToProto(result) } return out } func tradesToProto(trades []backtest.TradeSummary) []*altv1.BacktestTrade { if len(trades) == 0 { return nil } out := make([]*altv1.BacktestTrade, len(trades)) for i, t := range trades { out[i] = &altv1.BacktestTrade{ InstrumentId: string(t.InstrumentID), Side: string(t.Side), Quantity: quantityToProto(t.Quantity), Price: priceToProto(t.Price), TimestampUnixMs: timeToUnixMs(t.Timestamp), } } return out } func positionsToProto(positions []backtest.PositionSummary) []*altv1.BacktestPosition { if len(positions) == 0 { return nil } out := make([]*altv1.BacktestPosition, len(positions)) for i, p := range positions { out[i] = &altv1.BacktestPosition{ InstrumentId: string(p.InstrumentID), Quantity: quantityToProto(p.Quantity), LastPrice: priceToProto(p.LastPrice), } } return out } func summaryToProto(summary backtest.SummaryMetrics) *altv1.BacktestSummaryMetrics { return &altv1.BacktestSummaryMetrics{ StartingCash: priceToProto(summary.StartingCash), EndingEquity: priceToProto(summary.EndingEquity), TotalReturn: decimalToProto(summary.TotalReturn), TradeCount: int32(summary.TradeCount), } } func equityCurveToProto(points []backtest.EquityPoint) []*altv1.BacktestEquityPoint { if len(points) == 0 { return nil } out := make([]*altv1.BacktestEquityPoint, len(points)) for i, p := range points { out[i] = &altv1.BacktestEquityPoint{ TimestampUnixMs: timeToUnixMs(p.Timestamp), Equity: priceToProto(p.Equity), } } return out } func priceToProto(price market.Price) *altv1.Price { return &altv1.Price{ Currency: currencyToProto(price.Currency), Amount: decimalToProto(price.Amount), } } func quantityToProto(quantity market.Quantity) *altv1.Quantity { return &altv1.Quantity{ Amount: decimalToProto(quantity.Amount), } } func decimalToProto(decimal market.Decimal) *altv1.Decimal { return &altv1.Decimal{Value: decimal.Value} } func currencyToProto(c market.Currency) altv1.Currency { switch c { case market.CurrencyKRW: return altv1.Currency_CURRENCY_KRW case market.CurrencyUSD: return altv1.Currency_CURRENCY_USD default: return altv1.Currency_CURRENCY_UNSPECIFIED } } // timeToUnixMs converts a domain timestamp to epoch milliseconds. A zero time // maps to 0 so empty/unset timestamps stay unambiguous on the wire. func timeToUnixMs(t time.Time) int64 { if t.IsZero() { return 0 } return t.UTC().UnixMilli() }