alt/services/api/internal/socket/market.go
toki ec938475e0 chore: socket rail implementation and task archival
- Add backtest and market socket handlers for API and Worker
- Add proto definitions for backtest and market services
- Update client socket integration and tests
- Generate protobuf code for Go and Dart
- Archive completed agent tasks under agent-task/archive/2026/05/
2026-05-30 22:57:18 +09:00

135 lines
4.3 KiB
Go

package socket
import (
"context"
"errors"
protoSocket "git.toki-labs.com/toki/proto-socket/go"
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
"git.toki-labs.com/toki/alt/services/api/internal/workerclient"
)
const (
marketErrorInvalidRequest = "invalid_request"
marketErrorUnavailable = "unavailable"
marketErrorTimeout = "timeout"
marketErrorInternal = "internal"
)
// marketHandlers returns the API session handlers for market data reads. The
// API validates query shape and forwards the request unchanged to the worker.
func marketHandlers() []sessionHandler {
return []sessionHandler{
{
requestType: protoSocket.TypeNameOf(&altv1.ListInstrumentsRequest{}),
register: func(client *protoSocket.WsClient) {
protoSocket.AddRequestListenerTyped[*altv1.ListInstrumentsRequest, *altv1.ListInstrumentsResponse](&client.Communicator, handleListInstruments)
},
},
{
requestType: protoSocket.TypeNameOf(&altv1.ListBarsRequest{}),
register: func(client *protoSocket.WsClient) {
protoSocket.AddRequestListenerTyped[*altv1.ListBarsRequest, *altv1.ListBarsResponse](&client.Communicator, handleListBars)
},
},
}
}
func handleListInstruments(req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
if !isValidMarketFilter(req.GetMarket()) {
return &altv1.ListInstrumentsResponse{Error: marketInvalidRequest("market filter is unsupported")}, nil
}
if Worker == nil {
return &altv1.ListInstrumentsResponse{Error: marketWorkerUnavailable()}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
defer cancel()
res, err := Worker.ListInstruments(ctx, req)
if err != nil {
return &altv1.ListInstrumentsResponse{Error: marketWorkerErrorInfo(err)}, nil
}
if res == nil {
return &altv1.ListInstrumentsResponse{Error: marketInternalError("worker returned no list instruments response")}, nil
}
return res, nil
}
func handleListBars(req *altv1.ListBarsRequest) (*altv1.ListBarsResponse, error) {
if req.GetInstrumentId() == "" {
return &altv1.ListBarsResponse{Error: marketInvalidRequest("instrument_id is required")}, nil
}
if !isValidTimeframe(req.GetTimeframe()) {
return &altv1.ListBarsResponse{Error: marketInvalidRequest("timeframe is unsupported")}, nil
}
if req.GetFromUnixMs() == 0 {
return &altv1.ListBarsResponse{Error: marketInvalidRequest("from_unix_ms is required")}, nil
}
if req.GetToUnixMs() == 0 {
return &altv1.ListBarsResponse{Error: marketInvalidRequest("to_unix_ms is required")}, nil
}
if req.GetFromUnixMs() > req.GetToUnixMs() {
return &altv1.ListBarsResponse{Error: marketInvalidRequest("from_unix_ms cannot be after to_unix_ms")}, nil
}
if Worker == nil {
return &altv1.ListBarsResponse{Error: marketWorkerUnavailable()}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
defer cancel()
res, err := Worker.ListBars(ctx, req)
if err != nil {
return &altv1.ListBarsResponse{Error: marketWorkerErrorInfo(err)}, nil
}
if res == nil {
return &altv1.ListBarsResponse{Error: marketInternalError("worker returned no list bars response")}, nil
}
return res, nil
}
func isValidMarketFilter(m altv1.Market) bool {
switch m {
case altv1.Market_MARKET_UNSPECIFIED, altv1.Market_MARKET_KR, altv1.Market_MARKET_US:
return true
default:
return false
}
}
func isValidTimeframe(t altv1.Timeframe) bool {
switch t {
case altv1.Timeframe_TIMEFRAME_DAILY, altv1.Timeframe_TIMEFRAME_MINUTE_1, altv1.Timeframe_TIMEFRAME_MINUTE_5:
return true
default:
return false
}
}
func marketInvalidRequest(reason string) *altv1.ErrorInfo {
return errorInfo(marketErrorInvalidRequest, "invalid market request: "+reason)
}
func marketWorkerUnavailable() *altv1.ErrorInfo {
return errorInfo(marketErrorUnavailable, "market worker unavailable")
}
func marketInternalError(message string) *altv1.ErrorInfo {
return errorInfo(marketErrorInternal, message)
}
func marketWorkerErrorInfo(err error) *altv1.ErrorInfo {
if err == nil {
return nil
}
switch {
case errors.Is(err, workerclient.ErrUnavailable):
return errorInfo(marketErrorUnavailable, "market worker unavailable")
case errors.Is(err, workerclient.ErrTimeout):
return errorInfo(marketErrorTimeout, "market worker timeout")
default:
return errorInfo(marketErrorInternal, err.Error())
}
}