Post trade working
This commit is contained in:
4
Makefile
4
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
|
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)
|
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
|
deploy: # Deploy to remote server. Set s=serverName, e=environment. e.g. make deploy e=dev s=nonprodFix
|
||||||
tools/deploy.sh $(e)
|
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
|
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);
|
||||||
|
|||||||
19
fix_pt.cfg
Normal file
19
fix_pt.cfg
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
ConnectionType=initiator
|
||||||
|
HeartBtInt=30
|
||||||
|
ReconnectInterval=30
|
||||||
|
SenderCompID=<CONFIGURAR>
|
||||||
|
ResetOnLogon=Y
|
||||||
|
FileStorePath=fix_store
|
||||||
|
FileLogPath=fix_logs
|
||||||
|
|
||||||
|
[SESSION]
|
||||||
|
BeginString=FIXT.1.1
|
||||||
|
DefaultApplVerID=FIX.5.0SP2
|
||||||
|
TargetCompID=<CONFIGURAR>
|
||||||
|
TransportDataDictionary=spec/FIXT11.xml
|
||||||
|
AppDataDictionary=spec/FIX50SP2.xml
|
||||||
|
StartTime=00:00:00
|
||||||
|
EndTime=00:00:00
|
||||||
|
SocketConnectHost=<CONFIGURAR>
|
||||||
|
SocketConnectPort=<CONFIGURAR>
|
||||||
@ -36,6 +36,11 @@ type Service struct {
|
|||||||
AuthorizedServices map[string]AuthorizedService `toml:"AuthorizedServices"`
|
AuthorizedServices map[string]AuthorizedService `toml:"AuthorizedServices"`
|
||||||
APIBasePort string
|
APIBasePort string
|
||||||
EnableJWTAuth bool // Enable JWT authentication for service-to-service communication
|
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 {
|
type ExtAuth struct {
|
||||||
|
|||||||
136
src/client/fix/application.go
Normal file
136
src/client/fix/application.go
Normal file
@ -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
|
||||||
|
}
|
||||||
360
src/client/fix/manager.go
Normal file
360
src/client/fix/manager.go
Normal file
@ -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
|
||||||
63
src/client/fix/parser.go
Normal file
63
src/client/fix/parser.go
Normal file
@ -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 ""
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"quantex.com/qfixpt/src/app"
|
"quantex.com/qfixpt/src/app"
|
||||||
"quantex.com/qfixpt/src/client/api/rest"
|
"quantex.com/qfixpt/src/client/api/rest"
|
||||||
"quantex.com/qfixpt/src/client/data"
|
"quantex.com/qfixpt/src/client/data"
|
||||||
|
"quantex.com/qfixpt/src/client/fix"
|
||||||
googlechat "quantex.com/qfixpt/src/client/notify/google"
|
googlechat "quantex.com/qfixpt/src/client/notify/google"
|
||||||
"quantex.com/qfixpt/src/client/store"
|
"quantex.com/qfixpt/src/client/store"
|
||||||
"quantex.com/qfixpt/src/client/store/external"
|
"quantex.com/qfixpt/src/client/store/external"
|
||||||
@ -38,6 +39,13 @@ func Runner(cfg app.Config) error {
|
|||||||
|
|
||||||
userData := data.New()
|
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{
|
apiConfig := rest.Config{
|
||||||
Port: cfg.APIBasePort,
|
Port: cfg.APIBasePort,
|
||||||
AllowedOrigins: cfg.AllowedOrigins,
|
AllowedOrigins: cfg.AllowedOrigins,
|
||||||
|
|||||||
61
src/domain/posttrade.go
Normal file
61
src/domain/posttrade.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user