adding endpoints
This commit is contained in:
@ -38,6 +38,7 @@ type listTrade struct {
|
||||
Price decimal.Decimal
|
||||
OwnerTraderID string
|
||||
SessionID quickfix.SessionID
|
||||
Quoted bool
|
||||
}
|
||||
|
||||
// Manager wraps the QuickFIX initiator and implements domain.FIXSender.
|
||||
@ -237,7 +238,7 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
||||
return
|
||||
}
|
||||
|
||||
// Step 1: Send QuoteStatusReport (35=AI) to acknowledge the inquiry.
|
||||
// Send QuoteStatusReport (35=AI) to acknowledge the inquiry.
|
||||
if ackErr := m.sendQuoteStatusReport(quoteReqID, ownerTraderID, sessionID); ackErr != nil {
|
||||
ackErr = tracerr.Errorf("handleQuoteRequest: failed to send QuoteStatusReport (quoteReqID=%s): %w", quoteReqID, ackErr)
|
||||
slog.Error(ackErr.Error())
|
||||
@ -245,63 +246,12 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
||||
}
|
||||
slog.Info("QuoteStatusReport sent", "quoteReqID", quoteReqID)
|
||||
|
||||
// Step 2: Build and send Quote (35=S) with price.
|
||||
price := decimal.NewFromFloat(99.6)
|
||||
|
||||
sIDSource := enum.SecurityIDSource_ISIN_NUMBER
|
||||
if secIDSource == enum.SecurityIDSource_CUSIP {
|
||||
sIDSource = enum.SecurityIDSource_CUSIP
|
||||
}
|
||||
|
||||
quoteID := quoteReqID
|
||||
q := quote.New(
|
||||
field.NewQuoteID(quoteID),
|
||||
field.NewQuoteType(enum.QuoteType_SEND_QUOTE),
|
||||
field.NewTransactTime(time.Now()),
|
||||
)
|
||||
|
||||
q.SetSymbol("[N/A]")
|
||||
q.SetSecurityID(symbol)
|
||||
q.SetSecurityIDSource(sIDSource)
|
||||
q.SetQuoteReqID(quoteReqID)
|
||||
|
||||
if currency != "" {
|
||||
q.SetCurrency(currency)
|
||||
}
|
||||
|
||||
if !orderQty.IsZero() {
|
||||
q.SetOrderQty(orderQty, 0)
|
||||
}
|
||||
|
||||
if settlDate != "" {
|
||||
q.SetSettlDate(settlDate)
|
||||
}
|
||||
|
||||
q.SetPrice(price, 8)
|
||||
|
||||
if side == enum.Side_BUY {
|
||||
q.SetOfferPx(price, 8)
|
||||
q.SetSide(enum.Side_BUY)
|
||||
} else {
|
||||
q.SetBidPx(price, 8)
|
||||
q.SetSide(enum.Side_SELL)
|
||||
}
|
||||
|
||||
q.SetPriceType(enum.PriceType_PERCENTAGE)
|
||||
|
||||
if ownerTraderID != "" {
|
||||
q.SetOwnerTraderID(ownerTraderID)
|
||||
}
|
||||
|
||||
if sendErr := quickfix.SendToTarget(q, sessionID); sendErr != nil {
|
||||
sendErr = tracerr.Errorf("handleQuoteRequest: failed to send quote (quoteReqID=%s): %w", quoteReqID, sendErr)
|
||||
slog.Error(sendErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Quote sent", "quoteReqID", quoteReqID, "quoteID", quoteID, "symbol", symbol)
|
||||
|
||||
// Store trade state for subsequent steps.
|
||||
// Store trade state as pending; Quote (35=S) is sent later via REST endpoint.
|
||||
m.tradesMu.Lock()
|
||||
m.trades[quoteReqID] = &listTrade{
|
||||
QuoteReqID: quoteReqID,
|
||||
@ -312,9 +262,9 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
||||
Side: side,
|
||||
OrderQty: orderQty,
|
||||
SettlDate: settlDate,
|
||||
Price: price,
|
||||
OwnerTraderID: ownerTraderID,
|
||||
SessionID: sessionID,
|
||||
Quoted: false,
|
||||
}
|
||||
m.tradesMu.Unlock()
|
||||
|
||||
@ -327,18 +277,6 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
||||
"QuoteStatus": string(enum.QuoteStatus_ACCEPTED),
|
||||
"OwnerTraderID": ownerTraderID,
|
||||
}))
|
||||
|
||||
// Persist outgoing Quote.
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("S", quoteReqID, map[string]interface{}{
|
||||
"QuoteReqID": quoteReqID,
|
||||
"QuoteID": quoteID,
|
||||
"Symbol": symbol,
|
||||
"Side": string(side),
|
||||
"Price": price.String(),
|
||||
"OrderQty": orderQty.String(),
|
||||
"Currency": currency,
|
||||
"SettlDate": settlDate,
|
||||
}))
|
||||
}
|
||||
|
||||
// handleQuoteAck handles an incoming QuoteAck (35=CW).
|
||||
@ -479,23 +417,157 @@ func (m *Manager) GetTrades() []domain.ListTrade {
|
||||
|
||||
trades := make([]domain.ListTrade, 0, len(m.trades))
|
||||
for _, t := range m.trades {
|
||||
trades = append(trades, domain.ListTrade{
|
||||
QuoteReqID: t.QuoteReqID,
|
||||
ListID: t.ListID,
|
||||
Symbol: t.Symbol,
|
||||
SecurityIDSrc: string(t.SecurityIDSrc),
|
||||
Currency: t.Currency,
|
||||
Side: string(t.Side),
|
||||
OrderQty: t.OrderQty.String(),
|
||||
SettlDate: t.SettlDate,
|
||||
Price: t.Price.String(),
|
||||
OwnerTraderID: t.OwnerTraderID,
|
||||
})
|
||||
trades = append(trades, toDomainListTrade(t))
|
||||
}
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// GetPendingQuoteRequests returns trades that have received a QuoteRequest but not yet been quoted by the dealer.
|
||||
func (m *Manager) GetPendingQuoteRequests() []domain.ListTrade {
|
||||
m.tradesMu.RLock()
|
||||
defer m.tradesMu.RUnlock()
|
||||
|
||||
pending := make([]domain.ListTrade, 0)
|
||||
for _, t := range m.trades {
|
||||
if !t.Quoted {
|
||||
pending = append(pending, toDomainListTrade(t))
|
||||
}
|
||||
}
|
||||
|
||||
return pending
|
||||
}
|
||||
|
||||
func toDomainListTrade(t *listTrade) domain.ListTrade {
|
||||
return domain.ListTrade{
|
||||
QuoteReqID: t.QuoteReqID,
|
||||
ListID: t.ListID,
|
||||
Symbol: t.Symbol,
|
||||
SecurityIDSrc: string(t.SecurityIDSrc),
|
||||
Currency: t.Currency,
|
||||
Side: string(t.Side),
|
||||
OrderQty: t.OrderQty.String(),
|
||||
SettlDate: t.SettlDate,
|
||||
Price: t.Price.String(),
|
||||
OwnerTraderID: t.OwnerTraderID,
|
||||
}
|
||||
}
|
||||
|
||||
// SendQuote builds and sends a Quote (35=S) for an existing pending QuoteRequest at the given price.
|
||||
func (m *Manager) SendQuote(quoteReqID string, price decimal.Decimal) error {
|
||||
m.tradesMu.Lock()
|
||||
t, ok := m.trades[quoteReqID]
|
||||
if !ok {
|
||||
m.tradesMu.Unlock()
|
||||
err := tracerr.Errorf("SendQuote: quoteReqID %s not found", quoteReqID)
|
||||
slog.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if t.Quoted {
|
||||
m.tradesMu.Unlock()
|
||||
err := tracerr.Errorf("SendQuote: quote already sent for quoteReqID %s", quoteReqID)
|
||||
slog.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
sessionID := t.SessionID
|
||||
if sessionID == (quickfix.SessionID{}) {
|
||||
sessionID = m.anyActiveSessionID()
|
||||
if sessionID == (quickfix.SessionID{}) {
|
||||
m.tradesMu.Unlock()
|
||||
err := tracerr.Errorf("SendQuote: no active FIX session for quoteReqID %s", quoteReqID)
|
||||
slog.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
symbol := t.Symbol
|
||||
sIDSource := t.SecurityIDSrc
|
||||
currency := t.Currency
|
||||
side := t.Side
|
||||
orderQty := t.OrderQty
|
||||
settlDate := t.SettlDate
|
||||
ownerTraderID := t.OwnerTraderID
|
||||
m.tradesMu.Unlock()
|
||||
|
||||
quoteID := quoteReqID
|
||||
q := quote.New(
|
||||
field.NewQuoteID(quoteID),
|
||||
field.NewQuoteType(enum.QuoteType_SEND_QUOTE),
|
||||
field.NewTransactTime(time.Now()),
|
||||
)
|
||||
|
||||
q.SetSymbol("[N/A]")
|
||||
q.SetSecurityID(symbol)
|
||||
q.SetSecurityIDSource(sIDSource)
|
||||
q.SetQuoteReqID(quoteReqID)
|
||||
|
||||
if currency != "" {
|
||||
q.SetCurrency(currency)
|
||||
}
|
||||
|
||||
if !orderQty.IsZero() {
|
||||
q.SetOrderQty(orderQty, 0)
|
||||
}
|
||||
|
||||
if settlDate != "" {
|
||||
q.SetSettlDate(settlDate)
|
||||
}
|
||||
|
||||
q.SetPrice(price, 8)
|
||||
|
||||
if side == enum.Side_BUY {
|
||||
q.SetOfferPx(price, 8)
|
||||
q.SetSide(enum.Side_BUY)
|
||||
} else {
|
||||
q.SetBidPx(price, 8)
|
||||
q.SetSide(enum.Side_SELL)
|
||||
}
|
||||
|
||||
q.SetPriceType(enum.PriceType_PERCENTAGE)
|
||||
|
||||
if ownerTraderID != "" {
|
||||
q.SetOwnerTraderID(ownerTraderID)
|
||||
}
|
||||
|
||||
if sendErr := quickfix.SendToTarget(q, sessionID); sendErr != nil {
|
||||
sendErr = tracerr.Errorf("SendQuote: failed to send quote (quoteReqID=%s): %w", quoteReqID, sendErr)
|
||||
slog.Error(sendErr.Error())
|
||||
return sendErr
|
||||
}
|
||||
|
||||
m.tradesMu.Lock()
|
||||
if t, ok := m.trades[quoteReqID]; ok {
|
||||
t.Price = price
|
||||
t.Quoted = true
|
||||
}
|
||||
m.tradesMu.Unlock()
|
||||
|
||||
slog.Info("Quote sent", "quoteReqID", quoteReqID, "quoteID", quoteID, "symbol", symbol, "price", price.String())
|
||||
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("S", quoteReqID, map[string]interface{}{
|
||||
"QuoteReqID": quoteReqID,
|
||||
"QuoteID": quoteID,
|
||||
"Symbol": symbol,
|
||||
"Side": string(side),
|
||||
"Price": price.String(),
|
||||
"OrderQty": orderQty.String(),
|
||||
"Currency": currency,
|
||||
"SettlDate": settlDate,
|
||||
}))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) anyActiveSessionID() quickfix.SessionID {
|
||||
m.sessionsMu.RLock()
|
||||
defer m.sessionsMu.RUnlock()
|
||||
for _, s := range m.sessions {
|
||||
return s
|
||||
}
|
||||
return quickfix.SessionID{}
|
||||
}
|
||||
|
||||
// handleRawMessage persists raw FIX message strings to the logs table.
|
||||
func (m *Manager) handleRawMessage(direction string, msg *quickfix.Message) {
|
||||
quoteReqID := extractIdentifier(msg)
|
||||
@ -557,6 +629,10 @@ func (m *Manager) loadActiveTrades() error {
|
||||
trade.Symbol = v
|
||||
}
|
||||
|
||||
if v, ok := body["SecurityIDSource"].(string); ok {
|
||||
trade.SecurityIDSrc = enum.SecurityIDSource(v)
|
||||
}
|
||||
|
||||
if v, ok := body["Currency"].(string); ok {
|
||||
trade.Currency = v
|
||||
}
|
||||
@ -579,6 +655,14 @@ func (m *Manager) loadActiveTrades() error {
|
||||
|
||||
activeTrades[msg.QuoteReqID] = trade
|
||||
|
||||
case "S": // Outgoing Quote — dealer has already quoted this trade
|
||||
if t, ok := activeTrades[msg.QuoteReqID]; ok {
|
||||
t.Quoted = true
|
||||
if v, ok := msg.JMessage.Body["Price"].(string); ok {
|
||||
t.Price, _ = decimal.NewFromString(v)
|
||||
}
|
||||
}
|
||||
|
||||
case "CW": // QuoteAck — if rejected, trade is dead
|
||||
body := msg.JMessage.Body
|
||||
quoteAckStatus, _ := body["QuoteAckStatus"].(string)
|
||||
|
||||
Reference in New Issue
Block a user