Live Trading Boundary의 남은 account-sync/audit-trail 작업을 완료해 운영자 headless workflow와 roadmap 완료 후보 상태를 함께 반영한다.
209 lines
6.6 KiB
Go
209 lines
6.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 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,
|
|
}
|
|
}
|