alt/services/worker/internal/socket/live_mapping.go
toki c23fea2342 feat(live-trading): 실거래 risk guard를 추가한다
실거래 주문이 브로커에 도달하기 전에 kill switch와 계정별 주문 한도를 검증해야 한다. API, CLI, client parser surface와 완료된 review archive를 함께 정리한다.
2026-06-08 05:36:59 +09:00

146 lines
4.6 KiB
Go

package socket
import (
"errors"
"fmt"
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/packages/domain/trading"
"git.toki-labs.com/toki/alt/services/worker/internal/livetrading"
)
func liveCapabilityToProto(c trading.BrokerCapability) *altv1.LiveBrokerCapability {
cap := &altv1.LiveBrokerCapability{
Broker: string(c.Broker),
SupportsAmend: c.SupportsAmend,
SupportsCancel: c.SupportsCancel,
SupportsAccount: c.SupportsAccount,
SupportsAuditTags: c.SupportsAuditTags,
CheckedAtUnixMs: c.CheckedAt.UnixMilli(),
}
for m := range c.Markets {
cap.Markets = append(cap.Markets, marketToProto(m))
}
for v := range c.Venues {
cap.Venues = append(cap.Venues, venueToProto(v))
}
for _, t := range c.OrderTypes {
cap.OrderTypes = append(cap.OrderTypes, string(t))
}
return cap
}
// protoToSubmitRequest maps a SubmitLiveOrderRequest proto onto the domain
// SubmitRequest type. The intent.type field is preserved as-is so custom
// broker order type strings are not narrowed to market/limit.
// Returns an error for unsupported limit_price currency values.
func protoToSubmitRequest(req *altv1.SubmitLiveOrderRequest) (livetrading.SubmitRequest, error) {
intent := req.GetIntent()
domainIntent := trading.OrderIntent{}
if intent != nil {
domainIntent = trading.OrderIntent{
InstrumentID: market.InstrumentID(intent.GetInstrumentId()),
Side: trading.OrderSide(intent.GetSide()),
Type: trading.OrderType(intent.GetType()),
TimeInForce: trading.OrderTimeInForce(intent.GetTimeInForce()),
}
if q := intent.GetQuantity(); q != nil {
if amt := q.GetAmount(); amt != nil {
domainIntent.Quantity = market.Quantity{Amount: market.Decimal{Value: amt.GetValue()}}
}
}
if lp := intent.GetLimitPrice(); lp != nil {
if amt := lp.GetAmount(); amt != nil {
currency, err := currencyFromProto(lp.GetCurrency())
if err != nil {
return livetrading.SubmitRequest{}, fmt.Errorf("limit_price: %w", err)
}
domainIntent.LimitPrice = market.Price{
Currency: currency,
Amount: market.Decimal{Value: amt.GetValue()},
}
}
}
if tags := intent.GetCustomTags(); len(tags) > 0 {
domainIntent.CustomTags = tags
}
}
conf := req.GetOperatorConfirmation()
domainConf := livetrading.OperatorConfirmation{}
if conf != nil {
domainConf = livetrading.OperatorConfirmation{
Confirmed: conf.GetConfirmed(),
OperatorID: conf.GetOperatorId(),
Reason: conf.GetReason(),
ConfirmedAtMs: conf.GetConfirmedAtUnixMs(),
}
}
return livetrading.SubmitRequest{
AccountID: req.GetAccountId(),
Intent: domainIntent,
Confirmation: domainConf,
IdempotencyKey: req.GetIdempotencyKey(),
}, nil
}
// liveOrderToProto converts a domain LiveOrder to the proto wire type.
func liveOrderToProto(o livetrading.LiveOrder) *altv1.LiveOrder {
proto := &altv1.LiveOrder{
Id: o.ID,
BrokerId: o.BrokerOrderID,
AccountId: o.AccountID,
InstrumentId: string(o.Intent.InstrumentID),
Side: string(o.Intent.Side),
Type: string(o.Intent.Type),
TimeInForce: string(o.Intent.TimeInForce),
Status: string(o.Status),
BrokerStatus: string(o.BrokerStatus),
RejectionReason: o.RejectionReason,
CreatedAtUnixMs: o.CreatedAt.UnixMilli(),
UpdatedAtUnixMs: o.UpdatedAt.UnixMilli(),
}
if o.Intent.Quantity.Amount.Value != "" {
proto.Quantity = &altv1.Quantity{Amount: &altv1.Decimal{Value: o.Intent.Quantity.Amount.Value}}
}
if o.Intent.LimitPrice.Amount.Value != "" {
proto.LimitPrice = priceToProto(o.Intent.LimitPrice)
}
return proto
}
// isLiveErr unwraps err and checks whether it matches target using errors.Is.
func isLiveErr(err, target error) bool {
return errors.Is(err, target)
}
func riskPolicyToProto(p trading.RiskPolicy) *altv1.LiveRiskPolicy {
proto := &altv1.LiveRiskPolicy{
MaxDailyOrders: int32(p.MaxDailyOrders),
MaxOpenOrders: int32(p.MaxOpenOrders),
AllowShortSelling: p.AllowShortSelling,
}
if len(p.MaxOrderNotionalByCurrency) > 0 {
proto.MaxOrderNotionalByCurrency = make(map[string]string, len(p.MaxOrderNotionalByCurrency))
for cur, dec := range p.MaxOrderNotionalByCurrency {
proto.MaxOrderNotionalByCurrency[string(cur)] = dec.Value
}
}
return proto
}
func killSwitchToProto(ks trading.KillSwitchState) *altv1.LiveKillSwitchState {
return &altv1.LiveKillSwitchState{
Halted: ks.Halted,
Reason: ks.Reason,
}
}
func riskDecisionToProto(d trading.RiskDecision) *altv1.LiveRiskDecision {
return &altv1.LiveRiskDecision{
Allowed: d.Allowed,
Reason: d.Reason,
}
}