From 01e04df0f6738e760b0529826bfaeb6fe7f8f0d2 Mon Sep 17 00:00:00 2001 From: Facundo Marion Date: Thu, 19 Mar 2026 12:55:27 -0300 Subject: [PATCH] Post trade working --- Makefile | 4 +- fix_pt.cfg | 19 ++ src/app/model.go | 5 + src/client/fix/application.go | 136 +++++++++++++ src/client/fix/manager.go | 360 ++++++++++++++++++++++++++++++++++ src/client/fix/parser.go | 63 ++++++ src/cmd/service/service.go | 8 + src/domain/posttrade.go | 61 ++++++ 8 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 fix_pt.cfg create mode 100644 src/client/fix/application.go create mode 100644 src/client/fix/manager.go create mode 100644 src/client/fix/parser.go create mode 100644 src/domain/posttrade.go diff --git a/Makefile b/Makefile index b149483..46118dd 100644 --- a/Makefile +++ b/Makefile @@ -61,8 +61,8 @@ only-build: check-env linux-build: check-env swag # Build a linux version for prod environment. Set e=environment: prod, dev, demo, open-demo env OUT_PATH=$(DEFAULT_OUT_PATH) GOARCH=amd64 GOOS=linux tools/build.sh $(e) -deploy: check-env # Deploy to remote server. Set e=environment: prod, dev, demo, open-demo - tools/deploy.sh $(e) +deploy: # Deploy to remote server. Set s=serverName, e=environment. e.g. make deploy e=dev s=nonprodFix + make linux-build e=$(e) && qscp build/out/distribution/qfixpt.gz $(s):/home/quantex/qfixtb/qfixpt/ 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); diff --git a/fix_pt.cfg b/fix_pt.cfg new file mode 100644 index 0000000..853428e --- /dev/null +++ b/fix_pt.cfg @@ -0,0 +1,19 @@ +[DEFAULT] +ConnectionType=initiator +HeartBtInt=30 +ReconnectInterval=30 +SenderCompID= +ResetOnLogon=Y +FileStorePath=fix_store +FileLogPath=fix_logs + +[SESSION] +BeginString=FIXT.1.1 +DefaultApplVerID=FIX.5.0SP2 +TargetCompID= +TransportDataDictionary=spec/FIXT11.xml +AppDataDictionary=spec/FIX50SP2.xml +StartTime=00:00:00 +EndTime=00:00:00 +SocketConnectHost= +SocketConnectPort= diff --git a/src/app/model.go b/src/app/model.go index 1c437db..10340f4 100644 --- a/src/app/model.go +++ b/src/app/model.go @@ -36,6 +36,11 @@ type Service struct { AuthorizedServices map[string]AuthorizedService `toml:"AuthorizedServices"` APIBasePort string EnableJWTAuth bool // Enable JWT authentication for service-to-service communication + FIX FIXConfig +} + +type FIXConfig struct { + SettingsFile string // path to fix_pt.cfg file } type ExtAuth struct { diff --git a/src/client/fix/application.go b/src/client/fix/application.go new file mode 100644 index 0000000..ffc170c --- /dev/null +++ b/src/client/fix/application.go @@ -0,0 +1,136 @@ +package fix + +import ( + "log/slog" + + "quantex.com/qfixpt/quickfix" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/allocationreport" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/confirmation" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/tradecapturereport" + "quantex.com/qfixpt/quickfix/gen/tag" + "quantex.com/qfixpt/src/domain" +) + +type application struct { + router *quickfix.MessageRouter + notifier domain.Notifier + onLogon func(quickfix.SessionID) + onLogout func(quickfix.SessionID) + onTradeCaptureReport func(tradecapturereport.TradeCaptureReport, quickfix.SessionID) + onAllocationReport func(allocationreport.AllocationReport, quickfix.SessionID) + onConfirmation func(confirmation.Confirmation, quickfix.SessionID) +} + +func newApplication(n domain.Notifier) *application { + app := &application{ + router: quickfix.NewMessageRouter(), + notifier: n, + } + + app.router.AddRoute(tradecapturereport.Route(app.handleTradeCaptureReport)) + app.router.AddRoute(allocationreport.Route(app.handleAllocationReport)) + app.router.AddRoute(confirmation.Route(app.handleConfirmation)) + + return app +} + +func (a *application) OnCreate(sessionID quickfix.SessionID) { + slog.Info("FIX session created", "session", sessionID.String()) +} + +func (a *application) OnLogon(sessionID quickfix.SessionID) { + slog.Info("FIX session logged on", "session", sessionID.String()) + if a.onLogon != nil { + a.onLogon(sessionID) + } +} + +func (a *application) OnLogout(sessionID quickfix.SessionID) { + slog.Info("FIX session logged out", "session", sessionID.String()) + + go a.notifier.SendMsg(domain.MessageChannelError, "Logout", domain.MessageStatusWarning, nil) + + if a.onLogout != nil { + a.onLogout(sessionID) + } +} + +func (a *application) ToAdmin(_ *quickfix.Message, _ quickfix.SessionID) {} + +func (a *application) ToApp(_ *quickfix.Message, _ quickfix.SessionID) error { return nil } + +func (a *application) FromAdmin(_ *quickfix.Message, _ quickfix.SessionID) quickfix.MessageRejectError { + return nil +} + +func (a *application) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) quickfix.MessageRejectError { + beginString, _ := msg.Header.GetBytes(tag.BeginString) + msgType, _ := msg.Header.GetBytes(tag.MsgType) + + var applVerID quickfix.FIXString + msg.Header.GetField(tag.ApplVerID, &applVerID) + + slog.Info("FIX FromApp received", + "beginString", string(beginString), + "msgType", string(msgType), + "applVerID", string(applVerID), + "session", sessionID.String(), + "rawMsg", msg.String(), + ) + + rejErr := a.router.Route(msg, sessionID) + if rejErr != nil { + slog.Error("FIX FromApp routing failed", + "msgType", string(msgType), + "error", rejErr.Error(), + "isBusinessReject", rejErr.IsBusinessReject(), + ) + } + + return rejErr +} + +func (a *application) handleTradeCaptureReport(msg tradecapturereport.TradeCaptureReport, sessionID quickfix.SessionID) quickfix.MessageRejectError { + tradeReportID, _ := msg.GetTradeReportID() + + slog.Info("TradeCaptureReport received", + "tradeReportID", tradeReportID, + "session", sessionID.String(), + ) + + if a.onTradeCaptureReport != nil { + a.onTradeCaptureReport(msg, sessionID) + } + + return nil +} + +func (a *application) handleAllocationReport(msg allocationreport.AllocationReport, sessionID quickfix.SessionID) quickfix.MessageRejectError { + allocReportID, _ := msg.GetAllocReportID() + + slog.Info("AllocationReport received", + "allocReportID", allocReportID, + "session", sessionID.String(), + ) + + if a.onAllocationReport != nil { + a.onAllocationReport(msg, sessionID) + } + + return nil +} + +func (a *application) handleConfirmation(msg confirmation.Confirmation, sessionID quickfix.SessionID) quickfix.MessageRejectError { + confirmID, _ := msg.GetConfirmID() + + slog.Info("Confirmation received", + "confirmID", confirmID, + "session", sessionID.String(), + ) + + if a.onConfirmation != nil { + a.onConfirmation(msg, sessionID) + } + + return nil +} diff --git a/src/client/fix/manager.go b/src/client/fix/manager.go new file mode 100644 index 0000000..dd37f48 --- /dev/null +++ b/src/client/fix/manager.go @@ -0,0 +1,360 @@ +package fix + +import ( + "log/slog" + "os" + "time" + + "github.com/rs/zerolog/log" + "github.com/shopspring/decimal" + + "quantex.com/qfixpt/quickfix" + "quantex.com/qfixpt/quickfix/gen/enum" + "quantex.com/qfixpt/quickfix/gen/field" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/allocationreport" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/allocationreportack" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/confirmation" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/confirmationack" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/tradecapturereport" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/tradecapturereportack" + filelog "quantex.com/qfixpt/quickfix/log/file" + filestore "quantex.com/qfixpt/quickfix/store/file" + "quantex.com/qfixpt/src/app" + "quantex.com/qfixpt/src/common/tracerr" + "quantex.com/qfixpt/src/domain" +) + +// Custom Tradeweb tags not in generated code. +const ( + tagTestMessage = 23029 + tagTWTradeID = 23068 + tagTWOrigTradeID = 23096 + tagTWClrID = 23025 + tagDlrClrID = 23027 + tagClearingStatus = 5440 +) + +// Manager wraps the QuickFIX initiator for post-trade message handling. +type Manager struct { + initiator *quickfix.Initiator + app *application + notify domain.Notifier + cfg app.FIXConfig +} + +func NewManager(cfg app.FIXConfig, notify domain.Notifier) *Manager { + return &Manager{ + notify: notify, + cfg: cfg, + } +} + +func (m *Manager) Start() error { + fixApp := newApplication(m.notify) + fixApp.onLogon = m.onLogon + fixApp.onLogout = m.onLogout + fixApp.onTradeCaptureReport = m.handleTradeCaptureReport + fixApp.onAllocationReport = m.handleAllocationReport + fixApp.onConfirmation = m.handleConfirmation + m.app = fixApp + + f, err := os.Open(m.cfg.SettingsFile) + if err != nil { + err = tracerr.Errorf("error opening FIX settings file %q: %s", m.cfg.SettingsFile, err) + log.Error().Msg(err.Error()) + + return err + } + defer f.Close() + + settings, err := quickfix.ParseSettings(f) + if err != nil { + err = tracerr.Errorf("error parsing FIX settings: %s", err) + log.Error().Msg(err.Error()) + + return err + } + + storeFactory := filestore.NewStoreFactory(settings) + logFactory, err := filelog.NewLogFactory(settings) + if err != nil { + err = tracerr.Errorf("error creating file log factory: %s", err) + log.Error().Msg(err.Error()) + + return err + } + + initiator, err := quickfix.NewInitiator(fixApp, storeFactory, settings, logFactory) + if err != nil { + err = tracerr.Errorf("error creating FIX initiator: %s", err) + log.Error().Msg(err.Error()) + + return err + } + + m.initiator = initiator + + if err = m.initiator.Start(); err != nil { + err = tracerr.Errorf("error starting FIX initiator: %s", err) + log.Error().Msg(err.Error()) + + return err + } + + slog.Info("FIX initiator started", "settings", m.cfg.SettingsFile) + + return nil +} + +func (m *Manager) Stop() { + if m.initiator != nil { + m.initiator.Stop() + slog.Info("FIX initiator stopped") + } +} + +func (m *Manager) onLogon(sessionID quickfix.SessionID) { + slog.Info("FIX PT session logged on", "session", sessionID.String()) +} + +func (m *Manager) onLogout(sessionID quickfix.SessionID) { + slog.Info("FIX PT session logged out", "session", sessionID.String()) +} + +// handleTradeCaptureReport processes an incoming TradeCaptureReport (35=AE). +func (m *Manager) handleTradeCaptureReport(msg tradecapturereport.TradeCaptureReport, sessionID quickfix.SessionID) { + tradeReportID, _ := msg.GetTradeReportID() + + // ACK immediately before any validation. + ack := tradecapturereportack.New() + ack.SetTradeReportID(tradeReportID) + + if err := quickfix.SendToTarget(ack, sessionID); err != nil { + slog.Error("failed to send TradeCaptureReportAck", "tradeReportID", tradeReportID, "error", err.Error()) + } else { + slog.Info("TradeCaptureReportAck sent", "tradeReportID", tradeReportID) + } + + // Check if test message. + testMessage := getCustomBool(&msg.Message.Body.FieldMap, tagTestMessage) + if testMessage { + slog.Info("test TradeCaptureReport, skipping", "tradeReportID", tradeReportID) + return + } + + // Parse trade data. + tradeReportType, _ := msg.GetTradeReportType() + tradeID, _ := msg.GetTradeID() + execID, _ := msg.GetExecID() + lastQty, _ := msg.GetLastQty() + lastPx, _ := msg.GetLastPx() + spread, _ := msg.GetSpread() + settlDate, _ := msg.GetSettlDate() + tradeDate, _ := msg.GetTradeDate() + grossTradeAmt, _ := msg.GetGrossTradeAmt() + tradeReportRefID, _ := msg.GetTradeReportRefID() + + // Custom fields. + twTradeID := getCustomString(&msg.Message.Body.FieldMap, tagTWTradeID) + twOrigTradeID := getCustomString(&msg.Message.Body.FieldMap, tagTWOrigTradeID) + + // Extract side-level fields (parties, accrued interest, net money). + var side string + var accruedInterest, netMoney decimal.Decimal + var clearingFirm, twTraderID string + + sides, sideErr := msg.GetNoSides() + if sideErr == nil && sides.Len() > 0 { + s := sides.Get(0) + sideVal, _ := s.GetSide() + side = string(sideVal) + accruedInterest, _ = s.GetAccruedInterestAmt() + netMoney, _ = s.GetNetMoney() + + clearingFirm = extractPartyByRole(s, enum.PartyRole_CLEARING_FIRM) + // 1007 is a custom Tradeweb role, access via string comparison. + twTraderID = extractPartyByRole(s, "1007") + } + + trade := domain.Trade{ + TradeReportID: tradeReportID, + TradeID: tradeID, + ExecID: execID, + LastQty: lastQty, + LastPx: lastPx, + Spread: spread, + Side: side, + AccruedInterest: accruedInterest, + NetMoney: netMoney, + GrossTradeAmt: grossTradeAmt, + SettlDate: settlDate, + TradeDate: tradeDate, + TradeReportType: string(tradeReportType), + TradeReportRefID: tradeReportRefID, + ClearingFirm: clearingFirm, + TradewebTraderID: twTraderID, + TWTradeID: twTradeID, + TWOrigTradeID: twOrigTradeID, + } + + slog.Info("TradeCaptureReport parsed", + "tradeReportID", trade.TradeReportID, + "tradeReportType", trade.TradeReportType, + "tradeID", trade.TradeID, + "lastQty", trade.LastQty.String(), + "lastPx", trade.LastPx.String(), + "side", trade.Side, + "settlDate", trade.SettlDate, + ) + + switch string(tradeReportType) { + case "101": // TRDCONF — trade confirmation + // TODO: upsert trade en BD + slog.Info("TRDCONF: trade confirmation", "tradeReportID", tradeReportID, "tradeID", tradeID) + case "102": // TRDBLOCK — block trade, log only + slog.Info("TRDBLOCK: block trade", "tradeReportID", tradeReportID, "tradeID", tradeID) + case "103": // TRDCORR — trade correction + // TODO: update/correct trade en BD (usando TradeReportRefID) + slog.Info("TRDCORR: trade correction", "tradeReportID", tradeReportID, "refID", tradeReportRefID) + case "104": // TRDCXL — trade cancellation + // TODO: cancel trade en BD (usando TradeReportRefID) + slog.Info("TRDCXL: trade cancel", "tradeReportID", tradeReportID, "refID", tradeReportRefID) + case "105", "106", "107", "108": // log and ack only, no business logic + slog.Info("trade report (no business logic)", "tradeReportType", string(tradeReportType), "tradeReportID", tradeReportID) + default: + slog.Warn("unexpected TradeReportType", "tradeReportType", string(tradeReportType), "tradeReportID", tradeReportID) + } +} + +// handleAllocationReport processes an incoming AllocationReport (35=AS). +func (m *Manager) handleAllocationReport(msg allocationreport.AllocationReport, sessionID quickfix.SessionID) { + allocReportID, _ := msg.GetAllocReportID() + + // ACK immediately. + ack := allocationreportack.New(field.NewAllocReportID(allocReportID)) + + if err := quickfix.SendToTarget(ack, sessionID); err != nil { + slog.Error("failed to send AllocationReportAck", "allocReportID", allocReportID, "error", err.Error()) + } else { + slog.Info("AllocationReportAck sent", "allocReportID", allocReportID) + } + + // Check if test message. + testMessage := getCustomBool(&msg.Message.Body.FieldMap, tagTestMessage) + if testMessage { + slog.Info("test AllocationReport, skipping", "allocReportID", allocReportID) + return + } + + allocTransType, _ := msg.GetAllocTransType() + + // Extract TradeID from the first NoExecs entry if available. + tradeID := getCustomString(&msg.Message.Body.FieldMap, 1003) + + alloc := domain.Allocation{ + AllocReportID: allocReportID, + AllocTransType: string(allocTransType), + TradeID: tradeID, + } + + // Parse NoAllocs repeating group. + allocs, allocErr := msg.GetNoAllocs() + if allocErr == nil { + for i := 0; i < allocs.Len(); i++ { + entry := allocs.Get(i) + account, _ := entry.GetAllocAccount() + qty, _ := entry.GetAllocQty() + individualID, _ := entry.GetIndividualAllocID() + price, _ := entry.GetAllocPrice() + + alloc.Entries = append(alloc.Entries, domain.AllocEntry{ + AllocAccount: account, + AllocQty: qty, + IndividualAllocID: individualID, + AllocPrice: price, + }) + } + } + + slog.Info("AllocationReport parsed", + "allocReportID", alloc.AllocReportID, + "allocTransType", alloc.AllocTransType, + "tradeID", alloc.TradeID, + "numEntries", len(alloc.Entries), + ) + + switch allocTransType { + case enum.AllocTransType_NEW: + // TODO: insert allocations en BD + slog.Info("new allocation", "allocReportID", allocReportID) + case enum.AllocTransType_REPLACE: + // TODO: update allocations en BD + slog.Info("replace allocation", "allocReportID", allocReportID) + case enum.AllocTransType_CANCEL: + // TODO: cancel allocations en BD + slog.Info("cancel allocation", "allocReportID", allocReportID) + default: + slog.Warn("unhandled AllocTransType", "allocTransType", string(allocTransType), "allocReportID", allocReportID) + } +} + +// handleConfirmation processes an incoming Confirmation (35=AK). +func (m *Manager) handleConfirmation(msg confirmation.Confirmation, sessionID quickfix.SessionID) { + confirmID, _ := msg.GetConfirmID() + tradeDate, _ := msg.GetTradeDate() + transactTime, _ := msg.GetTransactTime() + + // ACK immediately. + ack := confirmationack.New( + field.NewConfirmID(confirmID), + field.NewTradeDate(tradeDate), + field.NewTransactTime(transactTime), + field.NewAffirmStatus(enum.AffirmStatus_RECEIVED), + ) + + if err := quickfix.SendToTarget(ack, sessionID); err != nil { + slog.Error("failed to send ConfirmationAck", "confirmID", confirmID, "error", err.Error()) + } else { + slog.Info("ConfirmationAck sent", "confirmID", confirmID) + } + + // Check if test message. + testMessage := getCustomBool(&msg.Message.Body.FieldMap, tagTestMessage) + if testMessage { + slog.Info("test Confirmation, skipping", "confirmID", confirmID) + return + } + + confirmStatus, _ := msg.GetConfirmStatus() + confirmTransType, _ := msg.GetConfirmTransType() + individualAllocID, _ := msg.GetIndividualAllocID() + + // Custom fields. + twClrID := getCustomString(&msg.Message.Body.FieldMap, tagTWClrID) + dlrClrID := getCustomString(&msg.Message.Body.FieldMap, tagDlrClrID) + clearingStatus := getCustomString(&msg.Message.Body.FieldMap, tagClearingStatus) + + conf := domain.ConfirmationMsg{ + ConfirmID: confirmID, + ConfirmStatus: string(confirmStatus), + ConfirmTransType: string(confirmTransType), + IndividualAllocID: individualAllocID, + TWClrID: twClrID, + DlrClrID: dlrClrID, + ClearingStatus: clearingStatus, + TradeDate: tradeDate, + } + + slog.Info("Confirmation parsed", + "confirmID", conf.ConfirmID, + "confirmStatus", conf.ConfirmStatus, + "confirmTransType", conf.ConfirmTransType, + "individualAllocID", conf.IndividualAllocID, + "clearingStatus", conf.ClearingStatus, + ) + + // TODO: persistir/actualizar confirmation status en BD +} + +// Ensure transactTime has a sensible default if not present in the message. +var _ = time.Now // used by confirmationack.New via field.NewTransactTime diff --git a/src/client/fix/parser.go b/src/client/fix/parser.go new file mode 100644 index 0000000..6952a6e --- /dev/null +++ b/src/client/fix/parser.go @@ -0,0 +1,63 @@ +package fix + +import ( + "quantex.com/qfixpt/quickfix" + "quantex.com/qfixpt/quickfix/gen/enum" + "quantex.com/qfixpt/quickfix/gen/fix50sp2/tradecapturereport" +) + +func getCustomString(body *quickfix.FieldMap, t int) string { + v, err := body.GetString(quickfix.Tag(t)) + if err != nil { + return "" + } + + return v +} + +func getCustomInt(body *quickfix.FieldMap, t int) int { + v, err := body.GetInt(quickfix.Tag(t)) + if err != nil { + return 0 + } + + return v +} + +func getCustomBool(body *quickfix.FieldMap, t int) bool { + v, err := body.GetBool(quickfix.Tag(t)) + if err != nil { + return false + } + + return v +} + +// extractPartyByRole iterates the Parties repeating group inside a NoSides entry +// and returns the PartyID for the given target role. +func extractPartyByRole(side tradecapturereport.NoSides, targetRole enum.PartyRole) string { + parties, err := side.GetNoPartyIDs() + if err != nil { + return "" + } + + for i := 0; i < parties.Len(); i++ { + party := parties.Get(i) + + role, roleErr := party.GetPartyRole() + if roleErr != nil { + continue + } + + if role == targetRole { + id, idErr := party.GetPartyID() + if idErr != nil { + return "" + } + + return id + } + } + + return "" +} diff --git a/src/cmd/service/service.go b/src/cmd/service/service.go index 6aef2cd..6e9a098 100644 --- a/src/cmd/service/service.go +++ b/src/cmd/service/service.go @@ -8,6 +8,7 @@ import ( "quantex.com/qfixpt/src/app" "quantex.com/qfixpt/src/client/api/rest" "quantex.com/qfixpt/src/client/data" + "quantex.com/qfixpt/src/client/fix" googlechat "quantex.com/qfixpt/src/client/notify/google" "quantex.com/qfixpt/src/client/store" "quantex.com/qfixpt/src/client/store/external" @@ -38,6 +39,13 @@ func Runner(cfg app.Config) error { userData := data.New() + // Initialize FIX Post-Trade Manager. + fixManager := fix.NewManager(cfg.FIX, notify) + if err = fixManager.Start(); err != nil { + return fmt.Errorf("error starting FIX initiator: %w", err) + } + defer fixManager.Stop() + apiConfig := rest.Config{ Port: cfg.APIBasePort, AllowedOrigins: cfg.AllowedOrigins, diff --git a/src/domain/posttrade.go b/src/domain/posttrade.go new file mode 100644 index 0000000..a465690 --- /dev/null +++ b/src/domain/posttrade.go @@ -0,0 +1,61 @@ +package domain + +import "github.com/shopspring/decimal" + +// Trade represents data extracted from a TradeCaptureReport (35=AE). +type Trade struct { + TradeReportID string // 571 + TradeID string // 1003 + ExecID string // 17 + OrigTradeID string // 1903 + OrigSecondaryID string // 1906 + LastQty decimal.Decimal // 32 + LastPx decimal.Decimal // 31 + Spread decimal.Decimal // 218 (236 = benchmark curve name) + Side string // 54 + AccruedInterest decimal.Decimal // 159 + NetMoney decimal.Decimal // 118 + GrossTradeAmt decimal.Decimal // 381 + SettlDate string // 64 + TradeDate string // 75 + ListID string // 66 + TradeReportType string // 856 + TradeHandlInst string // 487 + TradeReportRefID string // 572 + ClearingFirm string // Party role=4 + TradewebTraderID string // Party role=1007 (custom) + TestMessage bool // 23029 (custom) + TWTradeID string // 23068 (custom) + TWOrigTradeID string // 23096 (custom) +} + +// Allocation represents data extracted from an AllocationReport (35=AS). +type Allocation struct { + AllocReportID string // 755 + AllocTransType string // 71 + TradeID string // 1003 + TestMessage bool // 23029 (custom) + Entries []AllocEntry // NoAllocs group (78) +} + +// AllocEntry represents a single entry in the NoAllocs repeating group. +type AllocEntry struct { + AllocAccount string // 79 + AllocQty decimal.Decimal // 80 + IndividualAllocID string // 467 + AllocPrice decimal.Decimal // 154 (via custom tag access if needed) + CashSettlAmount decimal.Decimal // 742 (via custom tag access if needed) +} + +// ConfirmationMsg represents data extracted from a Confirmation (35=AK). +type ConfirmationMsg struct { + ConfirmID string // 664 + ConfirmStatus string // 665 + ConfirmTransType string // 666 + IndividualAllocID string // 467 + TWClrID string // 23025 (custom) + DlrClrID string // 23027 (custom) + ClearingStatus string // 5440 (custom) + TradeDate string // 75 + TestMessage bool // 23029 (custom) +}