Add QuoteStatusReport and QuoteAck handlers

This commit is contained in:
Facu Marion
2026-03-13 14:20:38 -03:00
parent fbcaac95f5
commit 710772b052
2 changed files with 101 additions and 23 deletions

View File

@ -6,6 +6,7 @@ import (
"quantex.com/qfixdpl/quickfix" "quantex.com/qfixdpl/quickfix"
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quote" "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quote"
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoteack"
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoterequest" "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoterequest"
"quantex.com/qfixdpl/quickfix/gen/tag" "quantex.com/qfixdpl/quickfix/gen/tag"
"quantex.com/qfixdpl/src/domain" "quantex.com/qfixdpl/src/domain"
@ -18,6 +19,7 @@ type application struct {
onLogout func(quickfix.SessionID) onLogout func(quickfix.SessionID)
onQuote func(quote.Quote, quickfix.SessionID) onQuote func(quote.Quote, quickfix.SessionID)
onQuoteRequest func(quoterequest.QuoteRequest, quickfix.SessionID) onQuoteRequest func(quoterequest.QuoteRequest, quickfix.SessionID)
onQuoteAck func(quoteack.QuoteAck, quickfix.SessionID)
} }
func newApplication(n domain.Notifier) *application { func newApplication(n domain.Notifier) *application {
@ -27,6 +29,7 @@ func newApplication(n domain.Notifier) *application {
} }
app.router.AddRoute(quote.Route(app.handleQuote)) app.router.AddRoute(quote.Route(app.handleQuote))
app.router.AddRoute(quoteack.Route(app.handleQuoteAck))
app.router.AddRoute(quoterequest.Route(app.handleQuoteRequest)) app.router.AddRoute(quoterequest.Route(app.handleQuoteRequest))
return app return app
@ -107,6 +110,27 @@ func (a *application) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionI
return nil return nil
} }
func (a *application) handleQuoteAck(msg quoteack.QuoteAck, sessionID quickfix.SessionID) quickfix.MessageRejectError {
quoteReqID, _ := msg.GetQuoteReqID()
quoteID, _ := msg.GetQuoteID()
status, _ := msg.GetQuoteAckStatus()
text, _ := msg.GetText()
slog.Info("QuoteAck received",
"quoteReqID", quoteReqID,
"quoteID", quoteID,
"quoteAckStatus", status,
"text", text,
"session", sessionID.String(),
)
if a.onQuoteAck != nil {
a.onQuoteAck(msg, sessionID)
}
return nil
}
func (a *application) handleQuote(msg quote.Quote, sessionID quickfix.SessionID) quickfix.MessageRejectError { func (a *application) handleQuote(msg quote.Quote, sessionID quickfix.SessionID) quickfix.MessageRejectError {
quoteID, err := msg.GetQuoteID() quoteID, err := msg.GetQuoteID()
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package fix package fix
import ( import (
"fmt"
"log/slog" "log/slog"
"os" "os"
"sync" "sync"
@ -14,6 +15,7 @@ import (
"quantex.com/qfixdpl/quickfix/gen/field" "quantex.com/qfixdpl/quickfix/gen/field"
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quote" "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quote"
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoterequest" "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoterequest"
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quotestatusreport"
"quantex.com/qfixdpl/quickfix/store/file" "quantex.com/qfixdpl/quickfix/store/file"
"quantex.com/qfixdpl/src/app" "quantex.com/qfixdpl/src/app"
"quantex.com/qfixdpl/src/common/tracerr" "quantex.com/qfixdpl/src/common/tracerr"
@ -139,7 +141,7 @@ func (m *Manager) SendQuote(
} }
q := quote.New( q := quote.New(
field.NewQuoteID("NONREF"), field.NewQuoteID(quoteID),
field.NewQuoteType(enum.QuoteType_INDICATIVE), field.NewQuoteType(enum.QuoteType_INDICATIVE),
field.NewTransactTime(time.Now()), field.NewTransactTime(time.Now()),
) )
@ -152,7 +154,7 @@ func (m *Manager) SendQuote(
q.SetSymbol("[N/A]") q.SetSymbol("[N/A]")
q.SetSecurityID(symbol) q.SetSecurityID(symbol)
q.SetSecurityIDSource(sIDSource) q.SetSecurityIDSource(sIDSource)
q.SetQuoteID(quoteID) q.SetQuoteReqID(clOrdID)
if currency != "" { if currency != "" {
q.SetCurrency(currency) q.SetCurrency(currency)
@ -180,7 +182,23 @@ func (m *Manager) SendQuote(
return nil return nil
} }
// handleQuoteRequest auto-responds to an incoming QuoteRequest with a quote at price 99.6. // sendQuoteStatusReport sends a QuoteStatusReport (35=AI) to acknowledge the incoming QuoteRequest.
func (m *Manager) sendQuoteStatusReport(quoteReqID, ownerTraderID string, sessionID quickfix.SessionID) error {
qsr := quotestatusreport.New(
field.NewTransactTime(time.Now()),
field.NewQuoteStatus(enum.QuoteStatus_ACCEPTED),
)
qsr.SetQuoteReqID(quoteReqID)
qsr.SetQuoteID(quoteReqID)
qsr.SetSymbol("[N/A]")
if ownerTraderID != "" {
qsr.SetOwnerTraderID(ownerTraderID)
}
return quickfix.SendToTarget(qsr, sessionID)
}
// handleQuoteRequest auto-responds to an incoming QuoteRequest with a QuoteStatusReport (acknowledge) followed by a Quote at price 99.6.
func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID quickfix.SessionID) { func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID quickfix.SessionID) {
quoteReqID, err := msg.GetQuoteReqID() quoteReqID, err := msg.GetQuoteReqID()
if err != nil { if err != nil {
@ -189,9 +207,10 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
} }
var ( var (
symbol, currency string symbol, currency, ownerTraderID, settlDate string
side enum.Side side enum.Side
secIDSource enum.SecurityIDSource secIDSource enum.SecurityIDSource
orderQty decimal.Decimal
) )
relatedSyms, relErr := msg.GetNoRelatedSym() relatedSyms, relErr := msg.GetNoRelatedSym()
@ -201,33 +220,68 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
secIDSource, _ = sym.GetSecurityIDSource() secIDSource, _ = sym.GetSecurityIDSource()
currency, _ = sym.GetCurrency() currency, _ = sym.GetCurrency()
side, _ = sym.GetSide() side, _ = sym.GetSide()
ownerTraderID, _ = sym.GetOwnerTraderID()
orderQty, _ = sym.GetOrderQty()
settlDate, _ = sym.GetSettlDate()
} }
// Step 1: Send QuoteStatusReport (35=AI) to acknowledge the inquiry.
if ackErr := m.sendQuoteStatusReport(quoteReqID, ownerTraderID, sessionID); ackErr != nil {
slog.Error("handleQuoteRequest: failed to send QuoteStatusReport", "quoteReqID", quoteReqID, "error", ackErr.Error())
return
}
slog.Info("QuoteStatusReport sent", "quoteReqID", quoteReqID)
// Step 2: Build and send Quote (35=S) with price.
price := decimal.NewFromFloat(99.6) price := decimal.NewFromFloat(99.6)
bidPx, offerPx := decimal.Zero, decimal.Zero
sIDSource := enum.SecurityIDSource_ISIN_NUMBER
if secIDSource == enum.SecurityIDSource_CUSIP {
sIDSource = enum.SecurityIDSource_CUSIP
}
quoteID := fmt.Sprintf("Q-%d", time.Now().UnixMilli())
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)
}
if side == enum.Side_BUY { if side == enum.Side_BUY {
offerPx = price q.SetOfferPx(price, 8)
q.SetSide(enum.Side_BUY)
} else { } else {
bidPx = price q.SetBidPx(price, 8)
q.SetSide(enum.Side_SELL)
} }
var sIDSource string q.SetPriceType(enum.PriceType_PERCENTAGE)
if secIDSource == enum.SecurityIDSource_ISIN_NUMBER {
sIDSource = "4" if ownerTraderID != "" {
} else { q.SetOwnerTraderID(ownerTraderID)
sIDSource = "1"
} }
if sendErr := m.SendQuote( if sendErr := quickfix.SendToTarget(q, sessionID); sendErr != nil {
quoteReqID,
quoteReqID,
symbol,
sIDSource,
currency,
bidPx,
offerPx,
); sendErr != nil {
slog.Error("handleQuoteRequest: failed to send quote", "quoteReqID", quoteReqID, "error", sendErr.Error()) slog.Error("handleQuoteRequest: failed to send quote", "quoteReqID", quoteReqID, "error", sendErr.Error())
return
} }
slog.Info("Quote sent", "quoteReqID", quoteReqID, "quoteID", quoteID, "symbol", symbol)
} }