실거래 주문이 브로커에 도달하기 전에 kill switch와 계정별 주문 한도를 검증해야 한다. API, CLI, client parser surface와 완료된 review archive를 함께 정리한다.
146 lines
4.6 KiB
Go
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,
|
|
}
|
|
}
|