- 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/
152 lines
5 KiB
Go
152 lines
5 KiB
Go
package socket
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
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/packages/domain/market"
|
|
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
|
)
|
|
|
|
const (
|
|
marketErrorInvalidRequest = "invalid_request"
|
|
marketErrorUnavailable = "unavailable"
|
|
marketErrorNotFound = "not_found"
|
|
marketErrorTimeout = "timeout"
|
|
marketErrorInternal = "internal"
|
|
)
|
|
|
|
func marketHandlers(deps Deps) []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(deps, 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(deps, req)
|
|
},
|
|
)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleListInstruments(deps Deps, req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
|
|
marketFilter, hasMarketFilter, err := marketFilterFromProto(req.GetMarket())
|
|
if err != nil {
|
|
return &altv1.ListInstrumentsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
|
}
|
|
if deps.Instruments == nil {
|
|
return &altv1.ListInstrumentsResponse{Error: marketUnavailableError("instrument store is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
insts, err := deps.Instruments.ListInstruments(ctx)
|
|
if err != nil {
|
|
return &altv1.ListInstrumentsResponse{Error: marketBackendErrorInfo(err)}, nil
|
|
}
|
|
filtered := filterInstruments(insts, marketFilter, hasMarketFilter, req.GetProvider())
|
|
return &altv1.ListInstrumentsResponse{Instruments: instrumentsToProto(filtered)}, nil
|
|
}
|
|
|
|
func handleListBars(deps Deps, req *altv1.ListBarsRequest) (*altv1.ListBarsResponse, error) {
|
|
if req.GetInstrumentId() == "" {
|
|
return &altv1.ListBarsResponse{Error: marketInvalidRequest("instrument_id is required")}, nil
|
|
}
|
|
timeframe, err := timeframeFromProto(req.GetTimeframe())
|
|
if err != nil {
|
|
return &altv1.ListBarsResponse{Error: marketInvalidRequest(err.Error())}, 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 deps.Bars == nil {
|
|
return &altv1.ListBarsResponse{Error: marketUnavailableError("bar store is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
from := time.UnixMilli(req.GetFromUnixMs()).UTC()
|
|
to := time.UnixMilli(req.GetToUnixMs()).UTC()
|
|
bars, err := deps.Bars.GetBars(ctx, market.InstrumentID(req.GetInstrumentId()), timeframe, from, to)
|
|
if err != nil {
|
|
return &altv1.ListBarsResponse{Error: marketBackendErrorInfo(err)}, nil
|
|
}
|
|
return &altv1.ListBarsResponse{Bars: barsToProto(bars)}, nil
|
|
}
|
|
|
|
func marketFilterFromProto(m altv1.Market) (market.Market, bool, error) {
|
|
switch m {
|
|
case altv1.Market_MARKET_UNSPECIFIED:
|
|
return "", false, nil
|
|
case altv1.Market_MARKET_KR, altv1.Market_MARKET_US:
|
|
converted, err := marketFromProto(m)
|
|
return converted, true, err
|
|
default:
|
|
return "", false, errors.New("market filter is unsupported")
|
|
}
|
|
}
|
|
|
|
func filterInstruments(insts []market.Instrument, marketFilter market.Market, hasMarketFilter bool, provider string) []market.Instrument {
|
|
if !hasMarketFilter && provider == "" {
|
|
return insts
|
|
}
|
|
out := make([]market.Instrument, 0, len(insts))
|
|
for _, inst := range insts {
|
|
if hasMarketFilter && inst.Market != marketFilter {
|
|
continue
|
|
}
|
|
if provider != "" && inst.ProviderSymbols[provider] == "" {
|
|
continue
|
|
}
|
|
out = append(out, inst)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func marketInvalidRequest(reason string) *altv1.ErrorInfo {
|
|
return errorInfo(marketErrorInvalidRequest, "invalid market request: "+reason)
|
|
}
|
|
|
|
func marketUnavailableError(message string) *altv1.ErrorInfo {
|
|
return errorInfo(marketErrorUnavailable, message)
|
|
}
|
|
|
|
func marketBackendErrorInfo(err error) *altv1.ErrorInfo {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch {
|
|
case errors.Is(err, storage.ErrInstrumentNotFound):
|
|
return errorInfo(marketErrorNotFound, err.Error())
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
return errorInfo(marketErrorTimeout, err.Error())
|
|
default:
|
|
return errorInfo(marketErrorInternal, err.Error())
|
|
}
|
|
}
|