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 accountSnapshotToProto(snap trading.AccountSnapshot) *altv1.LiveAccountSnapshot { proto := &altv1.LiveAccountSnapshot{ AccountId: snap.AccountID, Broker: string(snap.Broker), SyncedAtUnixMs: snap.SyncedAt.UnixMilli(), Stale: snap.Stale, } for _, cb := range snap.Cash { proto.Cash = append(proto.Cash, &altv1.LiveCashBalance{ Currency: currencyToProto(cb.Currency), Amount: &altv1.Decimal{Value: cb.Amount.Value}, }) } for _, pos := range snap.Positions { proto.Positions = append(proto.Positions, positionSnapshotToProto(pos)) } return proto } func positionSnapshotToProto(pos trading.PositionSnapshot) *altv1.LivePositionSnapshot { p := &altv1.LivePositionSnapshot{ InstrumentId: string(pos.InstrumentID), Side: string(pos.Side), MarketValue: &altv1.Decimal{Value: pos.MarketValue.Value}, Pnl: &altv1.Decimal{Value: pos.PnL.Value}, } if pos.Quantity.Amount.Value != "" { p.Quantity = &altv1.Quantity{Amount: &altv1.Decimal{Value: pos.Quantity.Amount.Value}} } if pos.AvgPrice.Amount.Value != "" { p.AvgPrice = priceToProto(pos.AvgPrice) } if pos.CurrentPrice.Amount.Value != "" { p.CurrentPrice = priceToProto(pos.CurrentPrice) } return p } 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, } } func protoToAuditFilter(req *altv1.ListLiveAuditEventsRequest) trading.AuditFilter { return trading.AuditFilter{ AccountID: req.GetAccountId(), OrderID: req.GetOrderId(), EventType: trading.AuditEventType(req.GetEventType()), Limit: int(req.GetLimit()), } } func auditEventToProto(ev trading.AuditEvent) *altv1.LiveAuditEvent { return &altv1.LiveAuditEvent{ EventId: ev.EventID, Type: string(ev.Type), TimestampUnixMs: ev.Timestamp.UnixMilli(), Broker: string(ev.Broker), AccountId: ev.AccountID, Payload: ev.Payload, OrderId: ev.OrderID, Status: ev.Status, Reason: ev.Reason, Actor: ev.Actor, CorrelationId: ev.CorrelationID, } }