package postgres import ( "encoding/json" "fmt" "strconv" "github.com/jackc/pgx/v5/pgtype" "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/postgres/sqlc" ) func validateDecimal(val string) error { if val == "" { return fmt.Errorf("empty decimal value") } if _, err := strconv.ParseFloat(val, 64); err != nil { return fmt.Errorf("invalid decimal format: %s", val) } return nil } func stringToNumeric(val string) (pgtype.Numeric, error) { var num pgtype.Numeric if err := validateDecimal(val); err != nil { return num, err } if err := num.Scan(val); err != nil { return num, fmt.Errorf("failed to scan numeric: %w", err) } return num, nil } func numericToString(num pgtype.Numeric) (string, error) { if !num.Valid { return "0", nil } val, err := num.Value() if err != nil { return "", err } switch v := val.(type) { case string: return v, nil case []byte: return string(v), nil default: return fmt.Sprintf("%v", v), nil } } func mapInstrumentToParams(inst market.Instrument) (sqlc.UpsertInstrumentParams, error) { provBytes, err := json.Marshal(inst.ProviderSymbols) if err != nil { return sqlc.UpsertInstrumentParams{}, fmt.Errorf("failed to marshal provider symbols: %w", err) } if provBytes == nil { provBytes = []byte("{}") } return sqlc.UpsertInstrumentParams{ ID: string(inst.ID), Market: string(inst.Market), Venue: string(inst.Venue), Symbol: inst.Symbol, Name: inst.Name, Currency: string(inst.Currency), ProviderSymbols: provBytes, }, nil } func mapRowToInstrument(row sqlc.Instrument) (market.Instrument, error) { var provSymbols map[string]string if len(row.ProviderSymbols) > 0 { if err := json.Unmarshal(row.ProviderSymbols, &provSymbols); err != nil { return market.Instrument{}, fmt.Errorf("failed to unmarshal provider symbols: %w", err) } } if provSymbols == nil { provSymbols = make(map[string]string) } return market.Instrument{ ID: market.InstrumentID(row.ID), Market: market.Market(row.Market), Venue: market.Venue(row.Venue), Symbol: row.Symbol, Name: row.Name, Currency: market.Currency(row.Currency), ProviderSymbols: provSymbols, }, nil } func mapBarToParams(bar market.Bar) (sqlc.UpsertBarParams, error) { open, err := stringToNumeric(bar.Open.Amount.Value) if err != nil { return sqlc.UpsertBarParams{}, fmt.Errorf("invalid open amount: %w", err) } high, err := stringToNumeric(bar.High.Amount.Value) if err != nil { return sqlc.UpsertBarParams{}, fmt.Errorf("invalid high amount: %w", err) } low, err := stringToNumeric(bar.Low.Amount.Value) if err != nil { return sqlc.UpsertBarParams{}, fmt.Errorf("invalid low amount: %w", err) } closePrice, err := stringToNumeric(bar.Close.Amount.Value) if err != nil { return sqlc.UpsertBarParams{}, fmt.Errorf("invalid close amount: %w", err) } volume, err := stringToNumeric(bar.Volume.Amount.Value) if err != nil { return sqlc.UpsertBarParams{}, fmt.Errorf("invalid volume amount: %w", err) } return sqlc.UpsertBarParams{ InstrumentID: string(bar.InstrumentID), Timeframe: string(bar.Timeframe), Timestamp: pgtype.Timestamptz{Time: bar.Timestamp, Valid: !bar.Timestamp.IsZero()}, Open: open, High: high, Low: low, Close: closePrice, Volume: volume, }, nil } func mapRowToBar(row sqlc.Bar, currency market.Currency) (market.Bar, error) { openStr, err := numericToString(row.Open) if err != nil { return market.Bar{}, err } highStr, err := numericToString(row.High) if err != nil { return market.Bar{}, err } lowStr, err := numericToString(row.Low) if err != nil { return market.Bar{}, err } closeStr, err := numericToString(row.Close) if err != nil { return market.Bar{}, err } volumeStr, err := numericToString(row.Volume) if err != nil { return market.Bar{}, err } return market.Bar{ InstrumentID: market.InstrumentID(row.InstrumentID), Timeframe: market.Timeframe(row.Timeframe), Timestamp: row.Timestamp.Time, Open: market.Price{ Currency: currency, Amount: market.Decimal{Value: openStr}, }, High: market.Price{ Currency: currency, Amount: market.Decimal{Value: highStr}, }, Low: market.Price{ Currency: currency, Amount: market.Decimal{Value: lowStr}, }, Close: market.Price{ Currency: currency, Amount: market.Decimal{Value: closeStr}, }, Volume: market.Quantity{ Amount: market.Decimal{Value: volumeStr}, }, }, nil } func mapRunToParams(run backtest.Run) (sqlc.UpsertRunParams, error) { return sqlc.UpsertRunParams{ ID: string(run.ID), StrategyID: string(run.Spec.StrategyID), Market: string(run.Spec.Market), Timeframe: string(run.Spec.Timeframe), FromTime: pgtype.Timestamptz{Time: run.Spec.From, Valid: !run.Spec.From.IsZero()}, ToTime: pgtype.Timestamptz{Time: run.Spec.To, Valid: !run.Spec.To.IsZero()}, Status: string(run.Status), CreatedAt: pgtype.Timestamptz{Time: run.CreatedAt, Valid: !run.CreatedAt.IsZero()}, UpdatedAt: pgtype.Timestamptz{Time: run.UpdatedAt, Valid: !run.UpdatedAt.IsZero()}, }, nil } func mapRowToRun(row sqlc.BacktestRun) (backtest.Run, error) { return backtest.Run{ ID: backtest.RunID(row.ID), Spec: backtest.RunSpec{ StrategyID: backtest.StrategyID(row.StrategyID), Market: market.Market(row.Market), Timeframe: market.Timeframe(row.Timeframe), From: row.FromTime.Time, To: row.ToTime.Time, }, Status: backtest.RunStatus(row.Status), CreatedAt: row.CreatedAt.Time, UpdatedAt: row.UpdatedAt.Time, }, nil }