diff --git a/src/client/fix/application.go b/src/client/fix/application.go index 759c174..e615843 100644 --- a/src/client/fix/application.go +++ b/src/client/fix/application.go @@ -6,6 +6,7 @@ import ( "quantex.com/qfixdpl/quickfix" "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/tag" "quantex.com/qfixdpl/src/domain" @@ -18,6 +19,7 @@ type application struct { onLogout func(quickfix.SessionID) onQuote func(quote.Quote, quickfix.SessionID) onQuoteRequest func(quoterequest.QuoteRequest, quickfix.SessionID) + onQuoteAck func(quoteack.QuoteAck, quickfix.SessionID) } 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(quoteack.Route(app.handleQuoteAck)) app.router.AddRoute(quoterequest.Route(app.handleQuoteRequest)) return app @@ -107,6 +110,27 @@ func (a *application) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionI 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 { quoteID, err := msg.GetQuoteID() if err != nil { diff --git a/src/client/fix/manager.go b/src/client/fix/manager.go index 9278d95..71714ad 100644 --- a/src/client/fix/manager.go +++ b/src/client/fix/manager.go @@ -1,6 +1,7 @@ package fix import ( + "fmt" "log/slog" "os" "sync" @@ -14,6 +15,7 @@ import ( "quantex.com/qfixdpl/quickfix/gen/field" "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quote" "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoterequest" + "quantex.com/qfixdpl/quickfix/gen/fix50sp2/quotestatusreport" "quantex.com/qfixdpl/quickfix/store/file" "quantex.com/qfixdpl/src/app" "quantex.com/qfixdpl/src/common/tracerr" @@ -139,7 +141,7 @@ func (m *Manager) SendQuote( } q := quote.New( - field.NewQuoteID("NONREF"), + field.NewQuoteID(quoteID), field.NewQuoteType(enum.QuoteType_INDICATIVE), field.NewTransactTime(time.Now()), ) @@ -152,7 +154,7 @@ func (m *Manager) SendQuote( q.SetSymbol("[N/A]") q.SetSecurityID(symbol) q.SetSecurityIDSource(sIDSource) - q.SetQuoteID(quoteID) + q.SetQuoteReqID(clOrdID) if currency != "" { q.SetCurrency(currency) @@ -180,7 +182,23 @@ func (m *Manager) SendQuote( 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) { quoteReqID, err := msg.GetQuoteReqID() if err != nil { @@ -189,9 +207,10 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu } var ( - symbol, currency string - side enum.Side - secIDSource enum.SecurityIDSource + symbol, currency, ownerTraderID, settlDate string + side enum.Side + secIDSource enum.SecurityIDSource + orderQty decimal.Decimal ) relatedSyms, relErr := msg.GetNoRelatedSym() @@ -201,33 +220,68 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu secIDSource, _ = sym.GetSecurityIDSource() currency, _ = sym.GetCurrency() 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) - 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 { - offerPx = price + q.SetOfferPx(price, 8) + q.SetSide(enum.Side_BUY) } else { - bidPx = price + q.SetBidPx(price, 8) + q.SetSide(enum.Side_SELL) } - var sIDSource string - if secIDSource == enum.SecurityIDSource_ISIN_NUMBER { - sIDSource = "4" - } else { - sIDSource = "1" + q.SetPriceType(enum.PriceType_PERCENTAGE) + + if ownerTraderID != "" { + q.SetOwnerTraderID(ownerTraderID) } - if sendErr := m.SendQuote( - quoteReqID, - quoteReqID, - symbol, - sIDSource, - currency, - bidPx, - offerPx, - ); sendErr != nil { + if sendErr := quickfix.SendToTarget(q, sessionID); sendErr != nil { slog.Error("handleQuoteRequest: failed to send quote", "quoteReqID", quoteReqID, "error", sendErr.Error()) + return } + + slog.Info("Quote sent", "quoteReqID", quoteReqID, "quoteID", quoteID, "symbol", symbol) }