- Archive completed subtask documents (02+01_api_lifecycle_capabilities, 03_client_bootstrap) - Update roadmap and PHASE.md for operator-surface progress - Refactor apps/client/lib/src/app/bootstrap.dart for client initialization - Update API socket layer: backtest, handlers, market, server, workerclient - Add bootstrap_test.dart for client app
147 lines
4.8 KiB
Go
147 lines
4.8 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(worker workerclient.WorkerClient) []sessionHandler {
|
|
return []sessionHandler{
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.ListInstrumentsRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.ListInstrumentsRequest, *altv1.ListInstrumentsResponse](&client.Communicator, func(req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
|
|
return handleListInstruments(worker, req)
|
|
})
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.ListBarsRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.ListBarsRequest, *altv1.ListBarsResponse](&client.Communicator, func(req *altv1.ListBarsRequest) (*altv1.ListBarsResponse, error) {
|
|
return handleListBars(worker, req)
|
|
})
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleListInstruments(worker workerclient.WorkerClient, 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()
|
|
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.ListInstrumentsResponse{Error: marketWorkerErrorInfo(err)}, nil
|
|
}
|
|
|
|
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(worker workerclient.WorkerClient, 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()
|
|
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.ListBarsResponse{Error: marketWorkerErrorInfo(err)}, nil
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|