Compare commits
3 Commits
develop
...
QFIXDPL-3/
| Author | SHA1 | Date | |
|---|---|---|---|
| 36b841fc66 | |||
| 68238d309a | |||
| 15a60bac92 |
2
Makefile
2
Makefile
@ -62,7 +62,7 @@ linux-build: check-env swag # Build a linux version for prod environment. Set e=
|
|||||||
env OUT_PATH=$(DEFAULT_OUT_PATH) GOARCH=amd64 GOOS=linux tools/build.sh $(e)
|
env OUT_PATH=$(DEFAULT_OUT_PATH) GOARCH=amd64 GOOS=linux tools/build.sh $(e)
|
||||||
|
|
||||||
deploy: # Deploy to remote server. Set e=environment: prod, dev, demo, open-demo; s=serverName; i=instance; e.g. make deploy e=dev s=nonprodFix i=dpl
|
deploy: # Deploy to remote server. Set e=environment: prod, dev, demo, open-demo; s=serverName; i=instance; e.g. make deploy e=dev s=nonprodFix i=dpl
|
||||||
make build e=$(e) && qscp build/out/distribution/qfixdpl.gz $(s):/home/quantex/qfixtb/$(i)/
|
make build e=$(e) && qscp build/out/distribution/qfixdpl.gz $(s):/home/quantex/qfixtb/dpl/
|
||||||
|
|
||||||
fmt: download-versions # Apply the Go formatter to the code
|
fmt: download-versions # Apply the Go formatter to the code
|
||||||
cd tools/check; unset GOPATH; GOBIN=$$PWD/../bin go install mvdan.cc/gofumpt@$(call get_version,gofumpt);
|
cd tools/check; unset GOPATH; GOBIN=$$PWD/../bin go install mvdan.cc/gofumpt@$(call get_version,gofumpt);
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gomodule/redigo/redis"
|
"github.com/gomodule/redigo/redis"
|
||||||
"github.com/sasha-s/go-deadlock"
|
"github.com/sasha-s/go-deadlock"
|
||||||
uuid "github.com/satori/go.uuid"
|
uuid "github.com/satori/go.uuid"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
"quantex.com/qfixdpl/src/app"
|
"quantex.com/qfixdpl/src/app"
|
||||||
"quantex.com/qfixdpl/src/app/version"
|
"quantex.com/qfixdpl/src/app/version"
|
||||||
@ -316,7 +318,8 @@ func (cont *Controller) GetLogs(ctx *gin.Context) {
|
|||||||
|
|
||||||
logs, err := cont.store.GetLogsByQuoteReqID(quoteReqID)
|
logs, err := cont.store.GetLogsByQuoteReqID(quoteReqID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("GetLogs: error fetching logs", "quoteReqID", quoteReqID, "error", err)
|
err = tracerr.Errorf("GetLogs: error fetching logs (quoteReqID=%s): %w", quoteReqID, err)
|
||||||
|
slog.Error(err.Error())
|
||||||
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "error fetching logs"})
|
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "error fetching logs"})
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -325,3 +328,57 @@ func (cont *Controller) GetLogs(ctx *gin.Context) {
|
|||||||
ctx.JSON(http.StatusOK, logs)
|
ctx.JSON(http.StatusOK, logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPendingQuoteRequests godoc
|
||||||
|
// @Summary List pending QuoteRequests
|
||||||
|
// @Description Returns all QuoteRequests received from TW that have not been quoted yet by the dealer
|
||||||
|
// @Tags fix
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} domain.ListTrade
|
||||||
|
// @Router /qfixdpl/v1/quote-requests [get]
|
||||||
|
func (cont *Controller) GetPendingQuoteRequests(ctx *gin.Context) {
|
||||||
|
pending := cont.tradeProvider.GetPendingQuoteRequests()
|
||||||
|
ctx.JSON(http.StatusOK, pending)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendQuote godoc
|
||||||
|
// @Summary Send a Quote for a pending QuoteRequest
|
||||||
|
// @Description Builds and sends a Quote (35=S) to TW for an existing QuoteRequest at the given price
|
||||||
|
// @Tags fix
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body SendQuoteRequest true "Quote to send"
|
||||||
|
// @Success 200 {object} Msg
|
||||||
|
// @Failure 400 {object} HTTPError
|
||||||
|
// @Failure 404 {object} HTTPError
|
||||||
|
// @Failure 409 {object} HTTPError
|
||||||
|
// @Failure 500 {object} HTTPError
|
||||||
|
// @Router /qfixdpl/v1/quotes [post]
|
||||||
|
func (cont *Controller) SendQuote(ctx *gin.Context) {
|
||||||
|
var req SendQuoteRequest
|
||||||
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
price, err := decimal.NewFromString(req.Price)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, HTTPError{Error: "invalid price: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cont.tradeProvider.SendQuote(req.QuoteReqID, price); err != nil {
|
||||||
|
msg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(msg, "not found"):
|
||||||
|
ctx.JSON(http.StatusNotFound, HTTPError{Error: "quoteReqID not found"})
|
||||||
|
case strings.Contains(msg, "already sent"):
|
||||||
|
ctx.JSON(http.StatusConflict, HTTPError{Error: "quote already sent for this quoteReqID"})
|
||||||
|
default:
|
||||||
|
ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "failed to send quote"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, Msg{Text: "Quote sent"})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package rest
|
package rest
|
||||||
|
|
||||||
|
|
||||||
type HTTPError struct {
|
type HTTPError struct {
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
@ -18,3 +17,7 @@ type Session struct {
|
|||||||
Email string
|
Email string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendQuoteRequest struct {
|
||||||
|
QuoteReqID string `json:"QuoteReqID" binding:"required"`
|
||||||
|
Price string `json:"Price" binding:"required" example:"99.6"`
|
||||||
|
}
|
||||||
|
|||||||
@ -23,6 +23,8 @@ func SetRoutes(api *API) {
|
|||||||
qfixdpl.GET("/health", cont.HealthCheck)
|
qfixdpl.GET("/health", cont.HealthCheck)
|
||||||
qfixdpl.GET("/trades", cont.GetTrades)
|
qfixdpl.GET("/trades", cont.GetTrades)
|
||||||
qfixdpl.GET("/trades/:quoteReqID/logs", cont.GetLogs)
|
qfixdpl.GET("/trades/:quoteReqID/logs", cont.GetLogs)
|
||||||
|
qfixdpl.GET("/quote-requests", cont.GetPendingQuoteRequests)
|
||||||
|
qfixdpl.POST("/quotes", cont.SendQuote)
|
||||||
|
|
||||||
backoffice := qfixdpl.Group("/backoffice")
|
backoffice := qfixdpl.Group("/backoffice")
|
||||||
backoffice.Use(cont.BackOfficeUser)
|
backoffice.Use(cont.BackOfficeUser)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gomodule/redigo/redis"
|
"github.com/gomodule/redigo/redis"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
"quantex.com/qfixdpl/src/app"
|
"quantex.com/qfixdpl/src/app"
|
||||||
"quantex.com/qfixdpl/src/app/version"
|
"quantex.com/qfixdpl/src/app/version"
|
||||||
@ -19,6 +20,8 @@ import (
|
|||||||
// TradeProvider exposes trade data from the FIX manager.
|
// TradeProvider exposes trade data from the FIX manager.
|
||||||
type TradeProvider interface {
|
type TradeProvider interface {
|
||||||
GetTrades() []domain.ListTrade
|
GetTrades() []domain.ListTrade
|
||||||
|
GetPendingQuoteRequests() []domain.ListTrade
|
||||||
|
SendQuote(quoteReqID string, price decimal.Decimal) error
|
||||||
}
|
}
|
||||||
|
|
||||||
const RedisMaxIdle = 3000 // In ms
|
const RedisMaxIdle = 3000 // In ms
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
"quantex.com/qfixdpl/quickfix"
|
"quantex.com/qfixdpl/quickfix"
|
||||||
@ -39,6 +38,7 @@ type listTrade struct {
|
|||||||
Price decimal.Decimal
|
Price decimal.Decimal
|
||||||
OwnerTraderID string
|
OwnerTraderID string
|
||||||
SessionID quickfix.SessionID
|
SessionID quickfix.SessionID
|
||||||
|
Quoted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager wraps the QuickFIX initiator and implements domain.FIXSender.
|
// Manager wraps the QuickFIX initiator and implements domain.FIXSender.
|
||||||
@ -77,13 +77,14 @@ func (m *Manager) Start() error {
|
|||||||
m.app = fixApp
|
m.app = fixApp
|
||||||
|
|
||||||
if err := m.loadActiveTrades(); err != nil {
|
if err := m.loadActiveTrades(); err != nil {
|
||||||
slog.Error("failed to load active trades from DB, starting with empty state", "error", err)
|
err = tracerr.Errorf("failed to load active trades from DB, starting with empty state: %w", err)
|
||||||
|
slog.Error(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(m.cfg.SettingsFile)
|
f, err := os.Open(m.cfg.SettingsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = tracerr.Errorf("error opening FIX settings file %q: %s", m.cfg.SettingsFile, err)
|
err = tracerr.Errorf("error opening FIX settings file %q: %w", m.cfg.SettingsFile, err)
|
||||||
log.Error().Msg(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -91,8 +92,8 @@ func (m *Manager) Start() error {
|
|||||||
|
|
||||||
settings, err := quickfix.ParseSettings(f)
|
settings, err := quickfix.ParseSettings(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = tracerr.Errorf("error parsing FIX settings: %s", err)
|
err = tracerr.Errorf("error parsing FIX settings: %w", err)
|
||||||
log.Error().Msg(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -100,16 +101,16 @@ func (m *Manager) Start() error {
|
|||||||
storeFactory := file.NewStoreFactory(settings)
|
storeFactory := file.NewStoreFactory(settings)
|
||||||
logFactory, err := filelog.NewLogFactory(settings)
|
logFactory, err := filelog.NewLogFactory(settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = tracerr.Errorf("error creating file log factory: %s", err)
|
err = tracerr.Errorf("error creating file log factory: %w", err)
|
||||||
log.Error().Msg(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
initiator, err := quickfix.NewInitiator(fixApp, storeFactory, settings, logFactory)
|
initiator, err := quickfix.NewInitiator(fixApp, storeFactory, settings, logFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = tracerr.Errorf("error creating FIX initiator: %s", err)
|
err = tracerr.Errorf("error creating FIX initiator: %w", err)
|
||||||
log.Error().Msg(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -117,8 +118,8 @@ func (m *Manager) Start() error {
|
|||||||
m.initiator = initiator
|
m.initiator = initiator
|
||||||
|
|
||||||
if err = m.initiator.Start(); err != nil {
|
if err = m.initiator.Start(); err != nil {
|
||||||
err = tracerr.Errorf("error starting FIX initiator: %s", err)
|
err = tracerr.Errorf("error starting FIX initiator: %w", err)
|
||||||
log.Error().Msg(err.Error())
|
slog.Error(err.Error())
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -194,7 +195,9 @@ func (m *Manager) sendExecutionAck(orderID, clOrdID, execID string, sessionID qu
|
|||||||
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 {
|
||||||
slog.Error("handleQuoteRequest: missing QuoteReqID", "error", err.Error())
|
err := tracerr.Errorf("handleQuoteRequest: missing QuoteReqID: %w", err)
|
||||||
|
slog.Error(err.Error())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,69 +238,20 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
|||||||
return
|
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 {
|
if ackErr := m.sendQuoteStatusReport(quoteReqID, ownerTraderID, sessionID); ackErr != nil {
|
||||||
slog.Error("handleQuoteRequest: failed to send QuoteStatusReport", "quoteReqID", quoteReqID, "error", ackErr.Error())
|
ackErr = tracerr.Errorf("handleQuoteRequest: failed to send QuoteStatusReport (quoteReqID=%s): %w", quoteReqID, ackErr)
|
||||||
|
slog.Error(ackErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Info("QuoteStatusReport sent", "quoteReqID", quoteReqID)
|
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
|
sIDSource := enum.SecurityIDSource_ISIN_NUMBER
|
||||||
if secIDSource == enum.SecurityIDSource_CUSIP {
|
if secIDSource == enum.SecurityIDSource_CUSIP {
|
||||||
sIDSource = enum.SecurityIDSource_CUSIP
|
sIDSource = enum.SecurityIDSource_CUSIP
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteID := quoteReqID
|
// Store trade state as pending; Quote (35=S) is sent later via REST endpoint.
|
||||||
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 {
|
|
||||||
slog.Error("handleQuoteRequest: failed to send quote", "quoteReqID", quoteReqID, "error", sendErr.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Quote sent", "quoteReqID", quoteReqID, "quoteID", quoteID, "symbol", symbol)
|
|
||||||
|
|
||||||
// Store trade state for subsequent steps.
|
|
||||||
m.tradesMu.Lock()
|
m.tradesMu.Lock()
|
||||||
m.trades[quoteReqID] = &listTrade{
|
m.trades[quoteReqID] = &listTrade{
|
||||||
QuoteReqID: quoteReqID,
|
QuoteReqID: quoteReqID,
|
||||||
@ -308,9 +262,9 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
|||||||
Side: side,
|
Side: side,
|
||||||
OrderQty: orderQty,
|
OrderQty: orderQty,
|
||||||
SettlDate: settlDate,
|
SettlDate: settlDate,
|
||||||
Price: price,
|
|
||||||
OwnerTraderID: ownerTraderID,
|
OwnerTraderID: ownerTraderID,
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
|
Quoted: false,
|
||||||
}
|
}
|
||||||
m.tradesMu.Unlock()
|
m.tradesMu.Unlock()
|
||||||
|
|
||||||
@ -323,18 +277,6 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
|||||||
"QuoteStatus": string(enum.QuoteStatus_ACCEPTED),
|
"QuoteStatus": string(enum.QuoteStatus_ACCEPTED),
|
||||||
"OwnerTraderID": ownerTraderID,
|
"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).
|
// handleQuoteAck handles an incoming QuoteAck (35=CW).
|
||||||
@ -346,7 +288,8 @@ func (m *Manager) handleQuoteAck(msg quoteack.QuoteAck, sessionID quickfix.Sessi
|
|||||||
m.persistMessage(quoteReqID, parseQuoteAck(msg))
|
m.persistMessage(quoteReqID, parseQuoteAck(msg))
|
||||||
|
|
||||||
if status != enum.QuoteAckStatus_ACCEPTED {
|
if status != enum.QuoteAckStatus_ACCEPTED {
|
||||||
slog.Error("handleQuoteAck: quote rejected by TW", "quoteReqID", quoteReqID, "quoteAckStatus", string(status), "text", text)
|
err := tracerr.Errorf("handleQuoteAck: quote rejected by TW (quoteReqID=%s, quoteAckStatus=%s, text=%s)", quoteReqID, string(status), text)
|
||||||
|
slog.Error(err.Error())
|
||||||
|
|
||||||
m.tradesMu.Lock()
|
m.tradesMu.Lock()
|
||||||
delete(m.trades, quoteReqID)
|
delete(m.trades, quoteReqID)
|
||||||
@ -381,7 +324,8 @@ func (m *Manager) handleQuoteResponse(msg quoteresponse.QuoteResponse, sessionID
|
|||||||
// Always send ACK regardless of whether the trade is in our map.
|
// Always send ACK regardless of whether the trade is in our map.
|
||||||
// TW will keep retrying until it receives an ACK.
|
// TW will keep retrying until it receives an ACK.
|
||||||
if ackErr := m.sendTradeRequestAck(quoteReqID, quoteRespID, sessionID); ackErr != nil {
|
if ackErr := m.sendTradeRequestAck(quoteReqID, quoteRespID, sessionID); ackErr != nil {
|
||||||
slog.Error("handleQuoteResponse: failed to send ACK", "quoteReqID", quoteReqID, "quoteRespID", quoteRespID, "error", ackErr.Error())
|
ackErr = tracerr.Errorf("handleQuoteResponse: failed to send ACK (quoteReqID=%s, quoteRespID=%s): %w", quoteReqID, quoteRespID, ackErr)
|
||||||
|
slog.Error(ackErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Info("QuoteResponse ACK sent", "quoteReqID", quoteReqID, "quoteRespID", quoteRespID)
|
slog.Info("QuoteResponse ACK sent", "quoteReqID", quoteReqID, "quoteRespID", quoteRespID)
|
||||||
@ -422,7 +366,8 @@ func (m *Manager) handleExecutionReport(msg executionreport.ExecutionReport, ses
|
|||||||
|
|
||||||
// Send ExecutionAck (35=BN) for every incoming ExecutionReport from TW.
|
// Send ExecutionAck (35=BN) for every incoming ExecutionReport from TW.
|
||||||
if ackErr := m.sendExecutionAck(orderID, clOrdID, execID, sessionID); ackErr != nil {
|
if ackErr := m.sendExecutionAck(orderID, clOrdID, execID, sessionID); ackErr != nil {
|
||||||
slog.Error("handleExecutionReport: failed to send ExecutionAck", "execID", execID, "error", ackErr.Error())
|
ackErr = tracerr.Errorf("handleExecutionReport: failed to send ExecutionAck (execID=%s): %w", execID, ackErr)
|
||||||
|
slog.Error(ackErr.Error())
|
||||||
} else {
|
} else {
|
||||||
slog.Info("ExecutionAck sent", "execID", execID)
|
slog.Info("ExecutionAck sent", "execID", execID)
|
||||||
}
|
}
|
||||||
@ -472,23 +417,157 @@ func (m *Manager) GetTrades() []domain.ListTrade {
|
|||||||
|
|
||||||
trades := make([]domain.ListTrade, 0, len(m.trades))
|
trades := make([]domain.ListTrade, 0, len(m.trades))
|
||||||
for _, t := range m.trades {
|
for _, t := range m.trades {
|
||||||
trades = append(trades, domain.ListTrade{
|
trades = append(trades, toDomainListTrade(t))
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return trades
|
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.
|
// handleRawMessage persists raw FIX message strings to the logs table.
|
||||||
func (m *Manager) handleRawMessage(direction string, msg *quickfix.Message) {
|
func (m *Manager) handleRawMessage(direction string, msg *quickfix.Message) {
|
||||||
quoteReqID := extractIdentifier(msg)
|
quoteReqID := extractIdentifier(msg)
|
||||||
@ -497,7 +576,8 @@ func (m *Manager) handleRawMessage(direction string, msg *quickfix.Message) {
|
|||||||
QuoteReqID: quoteReqID,
|
QuoteReqID: quoteReqID,
|
||||||
RawMsg: "[" + direction + "] " + msg.String(),
|
RawMsg: "[" + direction + "] " + msg.String(),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Error("failed to persist raw log", "error", err)
|
err = tracerr.Errorf("failed to persist raw log: %w", err)
|
||||||
|
slog.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,7 +587,8 @@ func (m *Manager) persistMessage(quoteReqID string, fixJSON domain.FixMessageJSO
|
|||||||
QuoteReqID: quoteReqID,
|
QuoteReqID: quoteReqID,
|
||||||
JMessage: fixJSON,
|
JMessage: fixJSON,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Error("failed to persist message", "msgType", fixJSON.MsgType, "quoteReqID", quoteReqID, "error", err)
|
err = tracerr.Errorf("failed to persist message (msgType=%s, quoteReqID=%s): %w", fixJSON.MsgType, quoteReqID, err)
|
||||||
|
slog.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,6 +629,10 @@ func (m *Manager) loadActiveTrades() error {
|
|||||||
trade.Symbol = v
|
trade.Symbol = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, ok := body["SecurityIDSource"].(string); ok {
|
||||||
|
trade.SecurityIDSrc = enum.SecurityIDSource(v)
|
||||||
|
}
|
||||||
|
|
||||||
if v, ok := body["Currency"].(string); ok {
|
if v, ok := body["Currency"].(string); ok {
|
||||||
trade.Currency = v
|
trade.Currency = v
|
||||||
}
|
}
|
||||||
@ -570,6 +655,14 @@ func (m *Manager) loadActiveTrades() error {
|
|||||||
|
|
||||||
activeTrades[msg.QuoteReqID] = trade
|
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
|
case "CW": // QuoteAck — if rejected, trade is dead
|
||||||
body := msg.JMessage.Body
|
body := msg.JMessage.Body
|
||||||
quoteAckStatus, _ := body["QuoteAckStatus"].(string)
|
quoteAckStatus, _ := body["QuoteAckStatus"].(string)
|
||||||
|
|||||||
104
src/client/fix/protocol.txt
Normal file
104
src/client/fix/protocol.txt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
Step 1
|
||||||
|
Direction: ← QuoteRequest (R)
|
||||||
|
Comentary: Tradeweb sends trade information to dealer.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=869 35=R 34=2 49=TRADEWEB 52=20160401-16:53:19.992 56=TW1_CORI_TEST_12345_DLRDPL 131=LST_20160401_TW1_CORI_NY302485.18_1 146=1 55=AMXLMM 2.375 09/08/16 48=02364WBC8 22=1 460=12 167=CORP 762=REGCORIPRC 541=20160908 225=20110908 470=MX 223=2.375 106=AMERICA MOVIL SAB DE CV 54=2 38=1000000 64=20160406 15=USD 6110=Comms 60=20160401-16:53:19 662=99.98046875 22570=0.76 663=1 699=912828UR9 761=1 423=6 44=-999999 5023=0.001000 66=NY302485.18 6847=1 75=20160401 464=Y 20086=1 20074=Y 20075=Y 20077=LatAm Comms 20078=pddealer 20079=60 20081=60 20090=60 20072=60 20098=60 5745=1 20073=RFQ 20076=Y 20156=N 20130=2000000000 20138=JPM,MER 20175=20120308 2115=0 22630=0 20265=LatAm 453=3 448=emack 447=C 452=3 802=3 523=Dev Test 803=2 523=NY 803=25 523=USA 803=4000 448=Tradeweb 447=C 452=1 802=1 523=Tradeweb0001 803=4002 448=DTCC 447=C 452=4 5114=2 5113=1 20169=A2 5113=0 20169=A- 10=121
|
||||||
|
|
||||||
|
Step 2
|
||||||
|
Direction: QuoteStatusReport (AI) →
|
||||||
|
Comentary: Dealer acknowledges trade.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=198 35=AI 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=2 52=20160401-16:53:19.988 131=LST_20160401_TW1_CORI_NY302485.18_1 117=LST_20160401_TW1_CORI_NY302485.18_1 55=[N/A] 60=20160401-16:53:19.988 297=0 10=106
|
||||||
|
|
||||||
|
Step 3
|
||||||
|
Direction: Quote (S) →
|
||||||
|
Comentary: Dealer sends quote.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=231 35=S 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=3 52=20160401-16:53:19.990 117=LST_20160401_TW1_CORI_NY302485.18_1 55=[N/A] 54=2 131=LST_20160401_TW1_CORI_NY302485.18_1 537=211 6153=emackdlr 44=.99 423=6 60=20160401-16:53:19.990 10=247
|
||||||
|
|
||||||
|
Step 4
|
||||||
|
Direction: ← QuoteAck (CW)
|
||||||
|
Comentary: Tradeweb acknowledges quote.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=186 35=CW 34=3 49=TRADEWEB 52=20160401-16:53:25.102 56=TW1_CORI_TEST_12345_DLRDPL 60=20160401-16:53:25 117=LST_20160401_TW1_CORI_NY302485.18_1 131=LST_20160401_TW1_CORI_NY302485.18_1 1865=1 10=154
|
||||||
|
|
||||||
|
Step 5
|
||||||
|
Direction: Quote (S) →
|
||||||
|
Comentary: Dealer sends quote.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=239 35=S 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=4 52=20160401-16:53:19.990 117=LST_20160401_TW1_CORI_NY302485.18_1 55=[N/A] 54=2 131=LST_20160401_TW1_CORI_NY302485.18_1 537=211 20087=5 6153=emackdlr 44=.97 423=6 60=20160401-16:53:19.990 10=114
|
||||||
|
|
||||||
|
Step 6
|
||||||
|
Direction: ← QuoteAck (CW)
|
||||||
|
Comentary: Tradeweb acknowledges quote.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=186 35=CW 34=4 49=TRADEWEB 52=20160401-16:53:30.055 56=TW1_CORI_TEST_12345_DLRDPL 60=20160401-16:53:30 117=LST_20160401_TW1_CORI_NY302485.18_1 131=LST_20160401_TW1_CORI_NY302485.18_1 1865=1 10=154
|
||||||
|
|
||||||
|
Step 7
|
||||||
|
Direction: Quote (S) →
|
||||||
|
Comentary: Dealer sends quote.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=239 35=S 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=6 52=20160401-16:53:19.990 117=LST_20160401_TW1_CORI_NY302485.18_1 55=[N/A] 54=2 131=LST_20160401_TW1_CORI_NY302485.18_1 537=211 20087=5 6153=emackdlr 44=.98 423=6 60=20160401-16:53:19.990 10=117
|
||||||
|
|
||||||
|
Step 8
|
||||||
|
Direction: ← QuoteAck (CW)
|
||||||
|
Commentary: Tradeweb acknowledges quote.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=186 35=CW 34=5 49=TRADEWEB 52=20160401-16:53:35.071 56=TW1_CORI_TEST_12345_DLRDPL 60=20160401-16:53:35 117=LST_20160401_TW1_CORI_NY302485.18_1 131=LST_20160401_TW1_CORI_NY302485.18_1 1865=1 10=163
|
||||||
|
|
||||||
|
Step 9
|
||||||
|
Direction: ← ExecutionReport (8)
|
||||||
|
Commentary: Tradeweb notifies dealer that the list trading (Due In time) has ended. ExecType=A
|
||||||
|
FIX Message: 8=FIXT.1.1 9=289 35=8 34=7 49=TRADEWEB 52=20160401-16:54:19.463 56=TW1_CORI_TEST_12345_DLRDPL 6=0 11=LST_20160401_TW1_CORI_NY302485.18_1 14=0 17=LST_20160401_TW1_CORI_NY302485.18_1_LISTEND-125419.463 37=LST_20160401_TW1_CORI_NY302485.18_1 39=D 55=[N/A] 60=20160401-16:54:19 75=20160401 150=I 151=0 20086=1 10=073
|
||||||
|
|
||||||
|
Step 10
|
||||||
|
Direction: ExecutionAck (BN) →
|
||||||
|
Commentary: Dealer acknowledges ExecutionReport message.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=255 35=BN 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=8 52=20160401-16:54:19.448 1036=1 37=LST_20160401_TW1_CORI_NY302485.18_1 11=LST_20160401_TW1_CORI_NY302485.18_1 17=LST_20160401_TW1_CORI_NY302485.18_1_LISTEND-125419.448 55=[N/A] 60=20160401-16:54:19.448 10=119
|
||||||
|
|
||||||
|
Step 11
|
||||||
|
Direction: ← QuoteResponse (AJ)
|
||||||
|
Commentary: Customer lifts after good-for time expires. Tradeweb informs dealer customer lift.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=431 35=AJ 34=10 49=TRADEWEB 52=20160401-16:55:32.855 56=TW1_CORI_TEST_12345_DLRDPL 11=LST_20160401_TW1_CORI_NY302485.18_1 22=1 38=1000000 44=0.98 48=02364WBC8 54=2 55=[N/A] 60=20160401-16:55:32 117=LST_20160401_TW1_CORI_NY302485.18_1 131=LST_20160401_TW1_CORI_NY302485.18_1 423=6 662=99.98046875 693=LST_20160401_TW1_CORI_NY302485.18_1_TRDREQ 694=1 2115=1 20074=Y 20075=N 20076=N 20079=60 20082=60 20156=N 22570=0.760289080299 22630=0 10=183
|
||||||
|
|
||||||
|
Step 12
|
||||||
|
Direction: QuoteStatusReport (AI) →
|
||||||
|
Commentary: Dealer acknowledges customer’s trade acceptance.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=206 35=AI 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=12 52=20160401-16:55:32.849 693=LST_20160401_TW1_CORI_NY302485.18_1_TRDREQ 131=LST_20160401_TW1_CORI_NY302485.18_1 55=[N/A] 60=20160401-16:55:32.849 297=0 10=189
|
||||||
|
|
||||||
|
Step 13
|
||||||
|
Direction: ← ExecutionReport (8)
|
||||||
|
Commentary: Tradeweb notifies the dealer that list has ended
|
||||||
|
FIX Message: incoming8=FIXT.1.1 9=290 35=8 34=11 49=TRADEWEB 52=20160401-16:55:32.855 56=TW1_CORI_TEST_12345_DLRDPL 6=0 11=LST_20160401_TW1_CORI_NY302485.18_1 14=0 17=LST_20160401_TW1_CORI_NY302485.18_1_LISTEND-125532.855 37=LST_20160401_TW1_CORI_NY302485.18_1 39=A 55=[N/A] 60=20160401-16:55:32 75=20160401 150=A 151=0 20086=1 10=095
|
||||||
|
|
||||||
|
Step 14
|
||||||
|
Direction: ExecutionAck (BN) →
|
||||||
|
Commentary: Dealer acknowledges ExecutionReport message.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=256 35=BN 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=13 52=20160401-16:55:32.852 1036=1 37=LST_20160401_TW1_CORI_NY302485.18_1 11=LST_20160401_TW1_CORI_NY302485.18_1 17=LST_20160401_TW1_CORI_NY302485.18_1_LISTEND-125532.852 55=[N/A] 60=20160401-16:55:32.852 10=155
|
||||||
|
|
||||||
|
Step 15
|
||||||
|
Direction: ExecutionReport (8) →
|
||||||
|
Commentary: Dealer accepts – re-quote is not allowed. ExecType=F, OrdStatus=Filled
|
||||||
|
FIX Message: 8=FIXT.1.1 9=283 35=8 34=14 49=TW1_CORI_TEST_12345_DLRDPL 52=20160401-16:55:32.850 56=TRADEWEB 6=0 14=0 17=LST_20160401_TW1_CORI_NY302485.18_1 37=LST_20160401_TW1_CORI_NY302485.18_1 39=2 54=2 55=[N/A] 150=F 151=0 6153=LST_20160401_TW1_CORI_NY302485.18_1 22631=POSTTRADE_STRING 22632=POSTTRADE_STRING 10=155
|
||||||
|
|
||||||
|
Step 16
|
||||||
|
Direction: ← ExecutionAck (BN)
|
||||||
|
Commentary: Tradeweb acknowledges ExecutionReport
|
||||||
|
FIX Message: 8=FIXT.1.1 9=233 35=BN 34=12 49=TRADEWEB 52=20160401-16:55:37.917 56=TW1_CORI_TEST_12345_DLRDPL 11=LST_20160401_TW1_CORI_NY302485.18_1 17=LST_20160401_TW1_CORI_NY302485.18_1 37=LST_20160401_TW1_CORI_NY302485.18_1 55=[N/A] 60=20160401-16:55:37 1036=1 10=051
|
||||||
|
|
||||||
|
Step 17
|
||||||
|
Direction: ← ExecutionReport (8)
|
||||||
|
Commentary: Tradeweb informs Dealer of outcome of a list trade item. OrdStatus=Filled, ExecType=Trade
|
||||||
|
FIX Message: 8=FIXT.1.1 9=337 35=8 34=13 49=TRADEWEB 52=20160401-16:55:37.917 56=TW1_CORI_TEST_12345_DLRDPL 6=0 11=LST_20160401_TW1_CORI_NY302485.18_1 14=0 17=LST_20160401_TW1_CORI_NY302485.18_1_TRDEND-125537.917 37=LST_20160401_TW1_CORI_NY302485.18_1 39=2 44=0.98 54=2 55=[N/A] 60=20160401-16:55:37 75=20160401 150=F 151=0 423=6 662=99.98046875 22570=0.760289080299 10=067
|
||||||
|
|
||||||
|
Step 18
|
||||||
|
Direction: ExecutionAck (BN) →
|
||||||
|
Comentary: Dealer acknowledges ExecutionReport message.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=255 35=BN 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=15 52=20160401-16:55:37.910 1036=1 37=LST_20160401_TW1_CORI_NY302485.18_1 11=LST_20160401_TW1_CORI_NY302485.18_1 17=LST_20160401_TW1_CORI_NY302485.18_1_TRDEND-125537.917 55=[N/A] 60=20160401-16:55:37.910 10=078
|
||||||
|
|
||||||
|
Step 19
|
||||||
|
Direction:
|
||||||
|
Comentary: Autospot at Tradeweb Treasury composite.
|
||||||
|
FIX Message:
|
||||||
|
|
||||||
|
Step 20
|
||||||
|
Direction: ← ExecutionReport (8)
|
||||||
|
Comentary: Tradeweb sends ExecutionReport message to confirm final outcome of list trade item including applicable cover quote, spot, settlement money information, etc. OrdStatus=Filled, ExecType=Order Status TradeSummary = Y
|
||||||
|
FIX Message: 8=FIXT.1.1 9=733 35=8 34=14 49=TRADEWEB 52=20160401-16:55:40.292 56=TW1_CORI_TEST_12345_DLRDPL 6=0 11=LST_20160401_TW1_CORI_NY302485.18_1 14=0 17=LST_20160401_TW1_CORI_NY302485.18_1_TRDSUMM-125540.277 22=1 37=LST_20160401_TW1_CORI_NY302485.18_1 38=1000000 39=2 44=0.98 48=02364WBC8 54=2 55=[N/A] 60=20160401-16:55:40 64=20160406 75=20160401 150=F 151=0 167=CORP 236=1.74 423=6 453=2 448=emack 447=C 452=3 802=3 523=Dev Test 803=2 523=NY 803=25 523=USA 803=4000 448=Tradeweb 447=C 452=1 802=2 523=Tradeweb0001 803=4002 523=YES 803=4003 526=TRD_20160401_TW1_CORI_23 662=99.98046875 1003=20160401.TW1.CORI.23 6153=emackdlr 6731=20160401.TW1.CORI.23 20115=100.265 20250=225000 22570=0.76 22630=0 22631=POSTTRADE_STRING 22634=160401.DLRX.TRSY.120 22636=Y 10=239
|
||||||
|
|
||||||
|
Step 21
|
||||||
|
Direction: ExecutionAck (BN) →
|
||||||
|
Comentary: Dealer acknowledges ExecutionReport message.
|
||||||
|
FIX Message: 8=FIXT.1.1 9=256 35=BN 49=TW1_CORI_TEST_12345_DLRDPL 56=TRADEWEB 34=16 52=20160401-16:55:40.287 1036=1 37=LST_20160401_TW1_CORI_NY302485.18_1 11=LST_20160401_TW1_CORI_NY302485.18_1 17=LST_20160401_TW1_CORI_NY302485.18_1_TRDSUMM-125540.277 55=[N/A] 60=20160401-16:55:40.287 10=182
|
||||||
@ -5,16 +5,16 @@ import "time"
|
|||||||
|
|
||||||
// ListTrade es la representacion exportada de un trade de List Trading.
|
// ListTrade es la representacion exportada de un trade de List Trading.
|
||||||
type ListTrade struct {
|
type ListTrade struct {
|
||||||
QuoteReqID string `json:"quote_req_id"`
|
QuoteReqID string
|
||||||
ListID string `json:"list_id"`
|
ListID string
|
||||||
Symbol string `json:"symbol"`
|
Symbol string
|
||||||
SecurityIDSrc string `json:"security_id_src"`
|
SecurityIDSrc string
|
||||||
Currency string `json:"currency"`
|
Currency string
|
||||||
Side string `json:"side"`
|
Side string
|
||||||
OrderQty string `json:"order_qty"`
|
OrderQty string
|
||||||
SettlDate string `json:"settl_date"`
|
SettlDate string
|
||||||
Price string `json:"price"`
|
Price string
|
||||||
OwnerTraderID string `json:"owner_trader_id"`
|
OwnerTraderID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// FixMessageJSON es la representacion estructurada de un mensaje FIX para almacenamiento.
|
// FixMessageJSON es la representacion estructurada de un mensaje FIX para almacenamiento.
|
||||||
|
|||||||
Reference in New Issue
Block a user