generic json for fix messages
This commit is contained in:
4
fix.cfg
4
fix.cfg
@ -5,6 +5,10 @@ SenderCompID=QUANTEX
|
||||
ResetOnLogon=Y
|
||||
FileStorePath=fix_store
|
||||
FileLogPath=fix_logs
|
||||
TransportDataDictionary=spec/FIXT11.xml
|
||||
AppDataDictionary=spec/FIX50SP2.xml
|
||||
AllowUnknownMessageFields=Y
|
||||
RejectInvalidMessage=N
|
||||
|
||||
[SESSION]
|
||||
BeginString=FIXT.1.1
|
||||
|
||||
@ -89,6 +89,24 @@ func (m FieldMap) Get(parser Field) MessageRejectError {
|
||||
return m.GetField(parser.Tag(), parser)
|
||||
}
|
||||
|
||||
// RawValues returns a copy of the underlying TagValue slice for a tag.
|
||||
// For repeating groups, the first entry is the count and the remaining
|
||||
// entries are the flattened inner-field values across all repetitions.
|
||||
func (m FieldMap) RawValues(tag Tag) []TagValue {
|
||||
m.rwLock.RLock()
|
||||
defer m.rwLock.RUnlock()
|
||||
|
||||
f, ok := m.tagLookup[tag]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]TagValue, len(f))
|
||||
copy(out, f)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Has returns true if the Tag is present in this FieldMap.
|
||||
func (m FieldMap) Has(tag Tag) bool {
|
||||
m.rwLock.RLock()
|
||||
|
||||
@ -86,6 +86,16 @@ func (tv TagValue) String() string {
|
||||
return string(tv.bytes)
|
||||
}
|
||||
|
||||
// Tag returns the FIX tag for this TagValue.
|
||||
func (tv TagValue) Tag() Tag {
|
||||
return tv.tag
|
||||
}
|
||||
|
||||
// Value returns the raw FIX value bytes for this TagValue.
|
||||
func (tv TagValue) Value() []byte {
|
||||
return tv.value
|
||||
}
|
||||
|
||||
func bytesTotal(bytes []byte) (total int) {
|
||||
for _, b := range bytes {
|
||||
total += int(b)
|
||||
|
||||
313
spec/FIXT11.xml
Normal file
313
spec/FIXT11.xml
Normal file
@ -0,0 +1,313 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<fix type='FIXT' major='1' minor='1' servicepack='0'>
|
||||
<header>
|
||||
<field name='BeginString' required='Y'/>
|
||||
<field name='BodyLength' required='Y'/>
|
||||
<field name='MsgType' required='Y'/>
|
||||
<field name='SenderCompID' required='Y'/>
|
||||
<field name='TargetCompID' required='Y'/>
|
||||
<field name='OnBehalfOfCompID' required='N'/>
|
||||
<field name='DeliverToCompID' required='N'/>
|
||||
<field name='SecureDataLen' required='N'/>
|
||||
<field name='SecureData' required='N'/>
|
||||
<field name='MsgSeqNum' required='Y'/>
|
||||
<field name='SenderSubID' required='N'/>
|
||||
<field name='SenderLocationID' required='N'/>
|
||||
<field name='TargetSubID' required='N'/>
|
||||
<field name='TargetLocationID' required='N'/>
|
||||
<field name='OnBehalfOfSubID' required='N'/>
|
||||
<field name='OnBehalfOfLocationID' required='N'/>
|
||||
<field name='DeliverToSubID' required='N'/>
|
||||
<field name='DeliverToLocationID' required='N'/>
|
||||
<field name='PossDupFlag' required='N'/>
|
||||
<field name='PossResend' required='N'/>
|
||||
<field name='SendingTime' required='Y'/>
|
||||
<field name='OrigSendingTime' required='N'/>
|
||||
<field name='XmlDataLen' required='N'/>
|
||||
<field name='XmlData' required='N'/>
|
||||
<field name='MessageEncoding' required='N'/>
|
||||
<field name='LastMsgSeqNumProcessed' required='N'/>
|
||||
<component name='HopGrp' required='N'/>
|
||||
<field name='ApplVerID' required='N'/>
|
||||
<field name='CstmApplVerID' required='N'/>
|
||||
</header>
|
||||
<trailer>
|
||||
<field name='SignatureLength' required='N'/>
|
||||
<field name='Signature' required='N'/>
|
||||
<field name='CheckSum' required='Y'/>
|
||||
</trailer>
|
||||
<messages>
|
||||
<message msgcat='admin' msgtype='0' name='Heartbeat'>
|
||||
<field name='TestReqID' required='N'/>
|
||||
</message>
|
||||
<message msgcat='admin' msgtype='1' name='TestRequest'>
|
||||
<field name='TestReqID' required='Y'/>
|
||||
</message>
|
||||
<message msgcat='admin' msgtype='2' name='ResendRequest'>
|
||||
<field name='BeginSeqNo' required='Y'/>
|
||||
<field name='EndSeqNo' required='Y'/>
|
||||
</message>
|
||||
<message msgcat='admin' msgtype='3' name='Reject'>
|
||||
<field name='RefSeqNum' required='Y'/>
|
||||
<field name='RefTagID' required='N'/>
|
||||
<field name='RefMsgType' required='N'/>
|
||||
<field name='SessionRejectReason' required='N'/>
|
||||
<field name='Text' required='N'/>
|
||||
<field name='EncodedTextLen' required='N'/>
|
||||
<field name='EncodedText' required='N'/>
|
||||
</message>
|
||||
<message msgcat='admin' msgtype='4' name='SequenceReset'>
|
||||
<field name='GapFillFlag' required='N'/>
|
||||
<field name='NewSeqNo' required='Y'/>
|
||||
</message>
|
||||
<message msgcat='admin' msgtype='5' name='Logout'>
|
||||
<field name='Text' required='N'/>
|
||||
<field name='EncodedTextLen' required='N'/>
|
||||
<field name='EncodedText' required='N'/>
|
||||
</message>
|
||||
<message msgcat='admin' msgtype='A' name='Logon'>
|
||||
<field name='EncryptMethod' required='Y'/>
|
||||
<field name='HeartBtInt' required='Y'/>
|
||||
<field name='RawDataLength' required='N'/>
|
||||
<field name='RawData' required='N'/>
|
||||
<field name='ResetSeqNumFlag' required='N'/>
|
||||
<field name='NextExpectedMsgSeqNum' required='N'/>
|
||||
<field name='MaxMessageSize' required='N'/>
|
||||
<field name='TestMessageIndicator' required='N'/>
|
||||
<field name='Username' required='N'/>
|
||||
<field name='Password' required='N'/>
|
||||
<field name='DefaultApplVerID' required='Y'/>
|
||||
<component name='MsgTypeGrp' required='N'/>
|
||||
</message>
|
||||
</messages>
|
||||
<components>
|
||||
<component name='HopGrp'>
|
||||
<group name='NoHops' required='N'>
|
||||
<field name='HopCompID' required='N'/>
|
||||
<field name='HopSendingTime' required='N'/>
|
||||
<field name='HopRefID' required='N'/>
|
||||
</group>
|
||||
</component>
|
||||
<component name='MsgTypeGrp'>
|
||||
<group name='NoMsgTypes' required='N'>
|
||||
<field name='RefMsgType' required='N'/>
|
||||
<field name='MsgDirection' required='N'/>
|
||||
<field name='RefApplVerID' required='N'/>
|
||||
<field name='RefCstmApplVerID' required='N'/>
|
||||
</group>
|
||||
</component>
|
||||
</components>
|
||||
<fields>
|
||||
<field name='BeginSeqNo' number='7' type='SEQNUM'/>
|
||||
<field name='BeginString' number='8' type='STRING'/>
|
||||
<field name='BodyLength' number='9' type='LENGTH'/>
|
||||
<field name='CheckSum' number='10' type='STRING'/>
|
||||
<field name='EndSeqNo' number='16' type='SEQNUM'/>
|
||||
<field name='MsgSeqNum' number='34' type='SEQNUM'/>
|
||||
<field number='35' name='MsgType' type='STRING'>
|
||||
<value enum='0' description='HEARTBEAT'/>
|
||||
<value enum='1' description='TEST_REQUEST'/>
|
||||
<value enum='2' description='RESEND_REQUEST'/>
|
||||
<value enum='3' description='REJECT'/>
|
||||
<value enum='4' description='SEQUENCE_RESET'/>
|
||||
<value enum='5' description='LOGOUT'/>
|
||||
<value enum='6' description='INDICATION_OF_INTEREST'/>
|
||||
<value enum='7' description='ADVERTISEMENT'/>
|
||||
<value enum='8' description='EXECUTION_REPORT'/>
|
||||
<value enum='9' description='ORDER_CANCEL_REJECT'/>
|
||||
<value enum='A' description='LOGON'/>
|
||||
<value enum='B' description='NEWS'/>
|
||||
<value enum='C' description='EMAIL'/>
|
||||
<value enum='D' description='ORDER_SINGLE'/>
|
||||
<value enum='E' description='ORDER_LIST'/>
|
||||
<value enum='F' description='ORDER_CANCEL_REQUEST'/>
|
||||
<value enum='G' description='ORDER_CANCEL_REPLACE_REQUEST'/>
|
||||
<value enum='H' description='ORDER_STATUS_REQUEST'/>
|
||||
<value enum='J' description='ALLOCATION_INSTRUCTION'/>
|
||||
<value enum='K' description='LIST_CANCEL_REQUEST'/>
|
||||
<value enum='L' description='LIST_EXECUTE'/>
|
||||
<value enum='M' description='LIST_STATUS_REQUEST'/>
|
||||
<value enum='N' description='LIST_STATUS'/>
|
||||
<value enum='P' description='ALLOCATION_INSTRUCTION_ACK'/>
|
||||
<value enum='Q' description='DONT_KNOW_TRADE'/>
|
||||
<value enum='R' description='QUOTE_REQUEST'/>
|
||||
<value enum='S' description='QUOTE'/>
|
||||
<value enum='T' description='SETTLEMENT_INSTRUCTIONS'/>
|
||||
<value enum='V' description='MARKET_DATA_REQUEST'/>
|
||||
<value enum='W' description='MARKET_DATA_SNAPSHOT_FULL_REFRESH'/>
|
||||
<value enum='X' description='MARKET_DATA_INCREMENTAL_REFRESH'/>
|
||||
<value enum='Y' description='MARKET_DATA_REQUEST_REJECT'/>
|
||||
<value enum='Z' description='QUOTE_CANCEL'/>
|
||||
<value enum='a' description='QUOTE_STATUS_REQUEST'/>
|
||||
<value enum='b' description='MASS_QUOTE_ACKNOWLEDGEMENT'/>
|
||||
<value enum='c' description='SECURITY_DEFINITION_REQUEST'/>
|
||||
<value enum='d' description='SECURITY_DEFINITION'/>
|
||||
<value enum='e' description='SECURITY_STATUS_REQUEST'/>
|
||||
<value enum='f' description='SECURITY_STATUS'/>
|
||||
<value enum='g' description='TRADING_SESSION_STATUS_REQUEST'/>
|
||||
<value enum='h' description='TRADING_SESSION_STATUS'/>
|
||||
<value enum='i' description='MASS_QUOTE'/>
|
||||
<value enum='j' description='BUSINESS_MESSAGE_REJECT'/>
|
||||
<value enum='k' description='BID_REQUEST'/>
|
||||
<value enum='l' description='BID_RESPONSE'/>
|
||||
<value enum='m' description='LIST_STRIKE_PRICE'/>
|
||||
<value enum='n' description='XML_MESSAGE'/>
|
||||
<value enum='o' description='REGISTRATION_INSTRUCTIONS'/>
|
||||
<value enum='p' description='REGISTRATION_INSTRUCTIONS_RESPONSE'/>
|
||||
<value enum='q' description='ORDER_MASS_CANCEL_REQUEST'/>
|
||||
<value enum='r' description='ORDER_MASS_CANCEL_REPORT'/>
|
||||
<value enum='s' description='NEW_ORDER_CROSS'/>
|
||||
<value enum='t' description='CROSS_ORDER_CANCEL_REPLACE_REQUEST'/>
|
||||
<value enum='u' description='CROSS_ORDER_CANCEL_REQUEST'/>
|
||||
<value enum='v' description='SECURITY_TYPE_REQUEST'/>
|
||||
<value enum='w' description='SECURITY_TYPES'/>
|
||||
<value enum='x' description='SECURITY_LIST_REQUEST'/>
|
||||
<value enum='y' description='SECURITY_LIST'/>
|
||||
<value enum='z' description='DERIVATIVE_SECURITY_LIST_REQUEST'/>
|
||||
<value enum='AA' description='DERIVATIVE_SECURITY_LIST'/>
|
||||
<value enum='AB' description='NEW_ORDER_MULTILEG'/>
|
||||
<value enum='AC' description='MULTILEG_ORDER_CANCEL_REPLACE'/>
|
||||
<value enum='AD' description='TRADE_CAPTURE_REPORT_REQUEST'/>
|
||||
<value enum='AE' description='TRADE_CAPTURE_REPORT'/>
|
||||
<value enum='AF' description='ORDER_MASS_STATUS_REQUEST'/>
|
||||
<value enum='AG' description='QUOTE_REQUEST_REJECT'/>
|
||||
<value enum='AH' description='RFQ_REQUEST'/>
|
||||
<value enum='AI' description='QUOTE_STATUS_REPORT'/>
|
||||
<value enum='AJ' description='QUOTE_RESPONSE'/>
|
||||
<value enum='AK' description='CONFIRMATION'/>
|
||||
<value enum='AL' description='POSITION_MAINTENANCE_REQUEST'/>
|
||||
<value enum='AM' description='POSITION_MAINTENANCE_REPORT'/>
|
||||
<value enum='AN' description='REQUEST_FOR_POSITIONS'/>
|
||||
<value enum='AO' description='REQUEST_FOR_POSITIONS_ACK'/>
|
||||
<value enum='AP' description='POSITION_REPORT'/>
|
||||
<value enum='AQ' description='TRADE_CAPTURE_REPORT_REQUEST_ACK'/>
|
||||
<value enum='AR' description='TRADE_CAPTURE_REPORT_ACK'/>
|
||||
<value enum='AS' description='ALLOCATION_REPORT'/>
|
||||
<value enum='AT' description='ALLOCATION_REPORT_ACK'/>
|
||||
<value enum='AU' description='CONFIRMATION_ACK'/>
|
||||
<value enum='AV' description='SETTLEMENT_INSTRUCTION_REQUEST'/>
|
||||
<value enum='AW' description='ASSIGNMENT_REPORT'/>
|
||||
<value enum='AX' description='COLLATERAL_REQUEST'/>
|
||||
<value enum='AY' description='COLLATERAL_ASSIGNMENT'/>
|
||||
<value enum='AZ' description='COLLATERAL_RESPONSE'/>
|
||||
<value enum='BA' description='COLLATERAL_REPORT'/>
|
||||
<value enum='BB' description='COLLATERAL_INQUIRY'/>
|
||||
<value enum='BC' description='NETWORK_STATUS_REQUEST'/>
|
||||
<value enum='BD' description='NETWORK_STATUS_RESPONSE'/>
|
||||
<value enum='BE' description='USER_REQUEST'/>
|
||||
<value enum='BF' description='USER_RESPONSE'/>
|
||||
<value enum='BG' description='COLLATERAL_INQUIRY_ACK'/>
|
||||
<value enum='BH' description='CONFIRMATION_REQUEST'/>
|
||||
<value enum='BI' description='TRADING_SESSION_LIST_REQUEST'/>
|
||||
<value enum='BJ' description='TRADING_SESSION_LIST'/>
|
||||
<value enum='BK' description='SECURITY_LIST_UPDATE_REPORT'/>
|
||||
<value enum='BL' description='ADJUSTED_POSITION_REPORT'/>
|
||||
<value enum='BM' description='ALLOCATION_INSTRUCTION_ALERT'/>
|
||||
<value enum='BN' description='EXECUTION_ACKNOWLEDGEMENT'/>
|
||||
<value enum='BO' description='CONTRARY_INTENTION_REPORT'/>
|
||||
<value enum='BP' description='SECURITY_DEFINITION_UPDATE_REPORT'/>
|
||||
</field>
|
||||
<field name='NewSeqNo' number='36' type='SEQNUM'/>
|
||||
<field name='PossDupFlag' number='43' type='BOOLEAN'/>
|
||||
<field name='RefSeqNum' number='45' type='SEQNUM'/>
|
||||
<field name='SenderCompID' number='49' type='STRING'/>
|
||||
<field name='SenderSubID' number='50' type='STRING'/>
|
||||
<field name='SendingTime' number='52' type='UTCTIMESTAMP'/>
|
||||
<field name='TargetCompID' number='56' type='STRING'/>
|
||||
<field name='TargetSubID' number='57' type='STRING'/>
|
||||
<field name='Text' number='58' type='STRING'/>
|
||||
<field name='Signature' number='89' type='DATA'/>
|
||||
<field name='SecureDataLen' number='90' type='LENGTH'/>
|
||||
<field name='SecureData' number='91' type='DATA'/>
|
||||
<field name='SignatureLength' number='93' type='LENGTH'/>
|
||||
<field name='RawDataLength' number='95' type='LENGTH'/>
|
||||
<field name='RawData' number='96' type='DATA'/>
|
||||
<field name='PossResend' number='97' type='BOOLEAN'/>
|
||||
<field name='EncryptMethod' number='98' type='INT'>
|
||||
<value description='NONE_OTHER' enum='0'/>
|
||||
<value description='PKCS' enum='1'/>
|
||||
<value description='DES' enum='2'/>
|
||||
<value description='PKCS_DES' enum='3'/>
|
||||
<value description='PGP_DES' enum='4'/>
|
||||
<value description='PGP_DES_MD5' enum='5'/>
|
||||
<value description='PEM_DES_MD5' enum='6'/>
|
||||
</field>
|
||||
<field name='HeartBtInt' number='108' type='INT'/>
|
||||
<field name='TestReqID' number='112' type='STRING'/>
|
||||
<field name='OnBehalfOfCompID' number='115' type='STRING'/>
|
||||
<field name='OnBehalfOfSubID' number='116' type='STRING'/>
|
||||
<field name='OrigSendingTime' number='122' type='UTCTIMESTAMP'/>
|
||||
<field name='GapFillFlag' number='123' type='BOOLEAN'/>
|
||||
<field name='DeliverToCompID' number='128' type='STRING'/>
|
||||
<field name='DeliverToSubID' number='129' type='STRING'/>
|
||||
<field name='ResetSeqNumFlag' number='141' type='BOOLEAN'/>
|
||||
<field name='SenderLocationID' number='142' type='STRING'/>
|
||||
<field name='TargetLocationID' number='143' type='STRING'/>
|
||||
<field name='OnBehalfOfLocationID' number='144' type='STRING'/>
|
||||
<field name='DeliverToLocationID' number='145' type='STRING'/>
|
||||
<field name='XmlDataLen' number='212' type='LENGTH'/>
|
||||
<field name='XmlData' number='213' type='DATA'/>
|
||||
<field number='347' name='MessageEncoding' type='STRING'>
|
||||
<value enum='ISO-2022-JP' description='ISO_2022_JP'/>
|
||||
<value enum='EUC-JP' description='EUC_JP'/>
|
||||
<value enum='SHIFT_JIS' description='SHIFT_JIS'/>
|
||||
<value enum='UTF-8' description='UTF_8'/>
|
||||
</field>
|
||||
<field name='EncodedTextLen' number='354' type='LENGTH'/>
|
||||
<field name='EncodedText' number='355' type='DATA'/>
|
||||
<field name='LastMsgSeqNumProcessed' number='369' type='SEQNUM'/>
|
||||
<field name='RefTagID' number='371' type='INT'/>
|
||||
<field name='RefMsgType' number='372' type='STRING'/>
|
||||
<field name='SessionRejectReason' number='373' type='INT'>
|
||||
<value description='INVALID_TAG_NUMBER' enum='0'/>
|
||||
<value description='REQUIRED_TAG_MISSING' enum='1'/>
|
||||
<value description='SENDINGTIME_ACCURACY_PROBLEM' enum='10'/>
|
||||
<value description='INVALID_MSGTYPE' enum='11'/>
|
||||
<value description='XML_VALIDATION_ERROR' enum='12'/>
|
||||
<value description='TAG_APPEARS_MORE_THAN_ONCE' enum='13'/>
|
||||
<value description='TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER' enum='14'/>
|
||||
<value description='REPEATING_GROUP_FIELDS_OUT_OF_ORDER' enum='15'/>
|
||||
<value description='INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP' enum='16'/>
|
||||
<value description='NON_DATA_VALUE_INCLUDES_FIELD_DELIMITER' enum='17'/>
|
||||
<value description='TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE' enum='2'/>
|
||||
<value description='UNDEFINED_TAG' enum='3'/>
|
||||
<value description='TAG_SPECIFIED_WITHOUT_A_VALUE' enum='4'/>
|
||||
<value description='VALUE_IS_INCORRECT' enum='5'/>
|
||||
<value description='INCORRECT_DATA_FORMAT_FOR_VALUE' enum='6'/>
|
||||
<value description='DECRYPTION_PROBLEM' enum='7'/>
|
||||
<value description='SIGNATURE_PROBLEM' enum='8'/>
|
||||
<value description='COMPID_PROBLEM' enum='9'/>
|
||||
<value description='OTHER' enum='99'/>
|
||||
</field>
|
||||
<field name='MaxMessageSize' number='383' type='LENGTH'/>
|
||||
<field name='NoMsgTypes' number='384' type='NUMINGROUP'/>
|
||||
<field name='MsgDirection' number='385' type='CHAR'>
|
||||
<value description='RECEIVE' enum='R'/>
|
||||
<value description='SEND' enum='S'/>
|
||||
</field>
|
||||
<field name='TestMessageIndicator' number='464' type='BOOLEAN'/>
|
||||
<field name='Username' number='553' type='STRING'/>
|
||||
<field name='Password' number='554' type='STRING'/>
|
||||
<field name='NoHops' number='627' type='NUMINGROUP'/>
|
||||
<field name='HopCompID' number='628' type='STRING'/>
|
||||
<field name='HopSendingTime' number='629' type='UTCTIMESTAMP'/>
|
||||
<field name='HopRefID' number='630' type='SEQNUM'/>
|
||||
<field name='NextExpectedMsgSeqNum' number='789' type='SEQNUM'/>
|
||||
<field name='ApplVerID' number='1128' type='STRING'>
|
||||
<value description='FIX27' enum='0'/>
|
||||
<value description='FIX30' enum='1'/>
|
||||
<value description='FIX40' enum='2'/>
|
||||
<value description='FIX41' enum='3'/>
|
||||
<value description='FIX42' enum='4'/>
|
||||
<value description='FIX43' enum='5'/>
|
||||
<value description='FIX44' enum='6'/>
|
||||
<value description='FIX50' enum='7'/>
|
||||
<value description='FIX50SP1' enum='8'/>
|
||||
<value description='FIX50SP2' enum='9'/>
|
||||
</field>
|
||||
<field name='CstmApplVerID' number='1129' type='STRING'/>
|
||||
<field name='RefApplVerID' number='1130' type='STRING'/>
|
||||
<field name='RefCstmApplVerID' number='1131' type='STRING'/>
|
||||
<field name='DefaultApplVerID' number='1137' type='STRING'/>
|
||||
</fields>
|
||||
</fix>
|
||||
@ -41,6 +41,7 @@ type Service struct {
|
||||
|
||||
type FIXConfig struct {
|
||||
SettingsFile string // path to fix.cfg file
|
||||
DataDictionaryFile string // path to FIX data dictionary XML (e.g. spec/FIX50SP2.xml)
|
||||
}
|
||||
|
||||
type ExtAuth struct {
|
||||
|
||||
172
src/client/fix/builder.go
Normal file
172
src/client/fix/builder.go
Normal file
@ -0,0 +1,172 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/datadictionary"
|
||||
)
|
||||
|
||||
// BuildFieldMap walks a quickfix.FieldMap and produces an enriched FieldMap
|
||||
// keyed by FixField (Name + Tag + Type) with values typed per the FIX
|
||||
// data dictionary. Repeating groups become []FieldMap; nested groups recurse.
|
||||
func BuildFieldMap(qfMap quickfix.FieldMap, dd *datadictionary.DataDictionary, fieldsByTag map[int]*datadictionary.FieldDef) FieldMap {
|
||||
out := FieldMap{}
|
||||
|
||||
for _, t := range qfMap.Tags() {
|
||||
tagInt := int(t)
|
||||
rawValues := qfMap.RawValues(t)
|
||||
if len(rawValues) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ff, fd := resolveField(dd, fieldsByTag, tagInt)
|
||||
|
||||
if ff.Type == "NUMINGROUP" {
|
||||
if fd != nil && fd.IsGroup() {
|
||||
out[ff] = buildGroup(rawValues[1:], dd, fd.Fields)
|
||||
} else {
|
||||
// Count tag with no group structure available (dictionary lookup
|
||||
// failed, or quickfix didn't nest the inner fields). Emit an empty
|
||||
// group rather than an int so the consumer's type expectation holds.
|
||||
out[ff] = []FieldMap{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
out[ff] = parseScalar(rawValues[0].Value(), ff.Type)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// buildGroup splits a flat slice of TagValues (the inner values of a NUMINGROUP,
|
||||
// excluding the count) into per-repetition FieldMaps. The first element of
|
||||
// innerFields is the delimiter that marks the start of each repetition.
|
||||
func buildGroup(tvs []quickfix.TagValue, dd *datadictionary.DataDictionary, innerFields []*datadictionary.FieldDef) []FieldMap {
|
||||
if len(innerFields) == 0 || len(tvs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
delimiter := innerFields[0].Tag()
|
||||
fdByTag := make(map[int]*datadictionary.FieldDef, len(innerFields))
|
||||
for _, f := range innerFields {
|
||||
fdByTag[f.Tag()] = f
|
||||
}
|
||||
|
||||
var (
|
||||
repetitions []FieldMap
|
||||
current FieldMap
|
||||
)
|
||||
|
||||
i := 0
|
||||
for i < len(tvs) {
|
||||
tv := tvs[i]
|
||||
tagInt := int(tv.Tag())
|
||||
|
||||
if tagInt == delimiter {
|
||||
current = FieldMap{}
|
||||
repetitions = append(repetitions, current)
|
||||
}
|
||||
|
||||
if current == nil {
|
||||
// Field appeared before the first delimiter; skip.
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
fd, known := fdByTag[tagInt]
|
||||
if !known {
|
||||
current[FixField{Name: fmt.Sprintf("tag_%d", tagInt), Tag: tagInt, Type: "STRING"}] = string(tv.Value())
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if fd.IsGroup() {
|
||||
nestedAllowed := allowedTags(fd.Fields)
|
||||
j := i + 1
|
||||
for j < len(tvs) && nestedAllowed[int(tvs[j].Tag())] {
|
||||
j++
|
||||
}
|
||||
nested := buildGroup(tvs[i+1:j], dd, fd.Fields)
|
||||
current[FixField{Name: fd.Name(), Tag: tagInt, Type: "NUMINGROUP"}] = nested
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
|
||||
current[FixField{Name: fd.Name(), Tag: tagInt, Type: fd.Type}] = parseScalar(tv.Value(), fd.Type)
|
||||
i++
|
||||
}
|
||||
|
||||
return repetitions
|
||||
}
|
||||
|
||||
// resolveField looks up the FixField metadata (name + type) for a tag.
|
||||
// Prefers the message-level FieldDef so group structure is preserved;
|
||||
// falls back to the global FieldTypeByTag, then to a synthetic "tag_<N>" STRING.
|
||||
func resolveField(dd *datadictionary.DataDictionary, fieldsByTag map[int]*datadictionary.FieldDef, tagInt int) (FixField, *datadictionary.FieldDef) {
|
||||
if fieldsByTag != nil {
|
||||
if fd, ok := fieldsByTag[tagInt]; ok {
|
||||
return FixField{Name: fd.Name(), Tag: tagInt, Type: fd.Type}, fd
|
||||
}
|
||||
}
|
||||
|
||||
if dd != nil {
|
||||
if ft, ok := dd.FieldTypeByTag[tagInt]; ok {
|
||||
return FixField{Name: ft.Name(), Tag: tagInt, Type: ft.Type}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return FixField{Name: fmt.Sprintf("tag_%d", tagInt), Tag: tagInt, Type: "STRING"}, nil
|
||||
}
|
||||
|
||||
// parseScalar converts raw FIX bytes into the Go type expected by the consumer
|
||||
// (GetKeyValue): bool for BOOLEAN, int for INT/SEQNUM, time.Time for UTCTIMESTAMP,
|
||||
// string for everything else.
|
||||
func parseScalar(raw []byte, fixType string) interface{} {
|
||||
s := string(raw)
|
||||
|
||||
switch fixType {
|
||||
case "INT", "SEQNUM", "LENGTH", "DAYOFMONTH":
|
||||
var v quickfix.FIXInt
|
||||
if err := v.Read(raw); err == nil {
|
||||
return int(v)
|
||||
}
|
||||
return s
|
||||
case "BOOLEAN":
|
||||
var v quickfix.FIXBoolean
|
||||
if err := v.Read(raw); err == nil {
|
||||
return bool(v)
|
||||
}
|
||||
return s
|
||||
case "UTCTIMESTAMP":
|
||||
var v quickfix.FIXUTCTimestamp
|
||||
if err := v.Read(raw); err == nil {
|
||||
return v.Time
|
||||
}
|
||||
return s
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// allowedTags returns the set of all tags valid inside a group (including
|
||||
// nested-group counts and their descendants), used to detect where a flat
|
||||
// nested-group slice ends within its parent.
|
||||
func allowedTags(fields []*datadictionary.FieldDef) map[int]bool {
|
||||
out := make(map[int]bool, len(fields))
|
||||
|
||||
var visit func(fs []*datadictionary.FieldDef)
|
||||
visit = func(fs []*datadictionary.FieldDef) {
|
||||
for _, f := range fs {
|
||||
out[f.Tag()] = true
|
||||
if f.IsGroup() {
|
||||
visit(f.Fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(fields)
|
||||
|
||||
return out
|
||||
}
|
||||
113
src/client/fix/dict.go
Normal file
113
src/client/fix/dict.go
Normal file
@ -0,0 +1,113 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"quantex.com/qfixdpl/src/common/tracerr"
|
||||
)
|
||||
|
||||
// FixField identifies a single FIX field by its dictionary metadata.
|
||||
type FixField struct {
|
||||
Name string
|
||||
Tag int
|
||||
Type string
|
||||
}
|
||||
|
||||
// FieldValue is the typed value for a FIX field.
|
||||
type FieldValue interface{}
|
||||
|
||||
// FieldMap is the enriched representation of a FIX FieldMap (header, body, or trailer).
|
||||
type FieldMap map[FixField]FieldValue
|
||||
|
||||
// GetMap converts a FieldMap into a JSON-friendly map keyed by field name.
|
||||
func GetMap(fieldMap FieldMap) map[string]interface{} {
|
||||
result := map[string]interface{}{}
|
||||
|
||||
for f, v := range fieldMap {
|
||||
k, val := GetKeyValue(f, v)
|
||||
result[k] = val
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//nolint:funlen,gocyclo,cyclop //it's long but easy to read
|
||||
func GetKeyValue(f FixField, value FieldValue) (key string, val interface{}) {
|
||||
key = f.Name
|
||||
|
||||
switch f.Type {
|
||||
case "NUMINGROUP":
|
||||
var groups []map[string]interface{}
|
||||
|
||||
fMapLst, ok := value.([]FieldMap)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as []FieldMap, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
for _, fieldMap := range fMapLst {
|
||||
groups = append(groups, GetMap(fieldMap))
|
||||
}
|
||||
|
||||
val = groups
|
||||
case "BOOLEAN":
|
||||
b, ok := value.(bool)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as bool, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = b
|
||||
case "INT":
|
||||
i, ok := value.(int)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as int, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = i
|
||||
case "SEQNUM":
|
||||
i, ok := value.(int)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as int, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = i
|
||||
case "UTCTIMESTAMP":
|
||||
t, ok := value.(time.Time)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as Time, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = t
|
||||
case "STRING":
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as string, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = s
|
||||
case "CHAR":
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as string, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = s
|
||||
default:
|
||||
s, ok := value.(string)
|
||||
if !ok {
|
||||
err := tracerr.Errorf("could not parse as string, value: %+v", value)
|
||||
slog.Error(err.Error())
|
||||
}
|
||||
|
||||
val = s
|
||||
}
|
||||
|
||||
return key, val
|
||||
}
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/datadictionary"
|
||||
"quantex.com/qfixdpl/quickfix/gen/enum"
|
||||
"quantex.com/qfixdpl/quickfix/gen/field"
|
||||
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/executionack"
|
||||
@ -27,18 +28,10 @@ import (
|
||||
)
|
||||
|
||||
type listTrade struct {
|
||||
QuoteReqID string
|
||||
ListID string
|
||||
Symbol string
|
||||
SecurityIDSrc enum.SecurityIDSource
|
||||
Currency string
|
||||
Side enum.Side
|
||||
OrderQty decimal.Decimal
|
||||
SettlDate string
|
||||
Price decimal.Decimal
|
||||
OwnerTraderID string
|
||||
QuoteRequest domain.FixMessageJSON
|
||||
SessionID quickfix.SessionID
|
||||
Quoted bool
|
||||
Price decimal.Decimal
|
||||
}
|
||||
|
||||
// Manager wraps the QuickFIX initiator and implements domain.FIXSender.
|
||||
@ -52,6 +45,7 @@ type Manager struct {
|
||||
store domain.PersistenceStore
|
||||
notify domain.Notifier
|
||||
cfg app.FIXConfig
|
||||
dict *datadictionary.DataDictionary
|
||||
}
|
||||
|
||||
func NewManager(cfg app.FIXConfig, store domain.PersistenceStore, notify domain.Notifier) *Manager {
|
||||
@ -76,6 +70,15 @@ func (m *Manager) Start() error {
|
||||
fixApp.onRawMessage = m.handleRawMessage
|
||||
m.app = fixApp
|
||||
|
||||
dict, err := datadictionary.Parse(m.cfg.DataDictionaryFile)
|
||||
if err != nil {
|
||||
err = tracerr.Errorf("error parsing FIX data dictionary %q: %w", m.cfg.DataDictionaryFile, err)
|
||||
slog.Error(err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
m.dict = dict
|
||||
|
||||
if err := m.loadActiveTrades(); err != nil {
|
||||
err = tracerr.Errorf("failed to load active trades from DB, starting with empty state: %w", err)
|
||||
slog.Error(err.Error())
|
||||
@ -191,13 +194,15 @@ func (m *Manager) sendExecutionAck(orderID, clOrdID, execID string, sessionID qu
|
||||
return quickfix.SendToTarget(bn, sessionID)
|
||||
}
|
||||
|
||||
// handleQuoteRequest auto-responds to an incoming QuoteRequest with a QuoteStatusReport (acknowledge) followed by a Quote at price 99.6.
|
||||
// handleQuoteRequest auto-responds to an incoming QuoteRequest with a QuoteStatusReport (acknowledge).
|
||||
// The Quote (35=S) is sent later via the REST endpoint when the dealer prices it.
|
||||
func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID quickfix.SessionID) {
|
||||
quoteReqID, err := msg.GetQuoteReqID()
|
||||
if err != nil {
|
||||
err := tracerr.Errorf("handleQuoteRequest: missing QuoteReqID: %w", err)
|
||||
slog.Error(err.Error())
|
||||
parsed := parseQuoteRequest(msg, m.dict)
|
||||
quoteReqID := parsed.QuoteReqID
|
||||
|
||||
if quoteReqID == "" {
|
||||
err := tracerr.Errorf("handleQuoteRequest: missing QuoteReqID")
|
||||
slog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@ -207,26 +212,22 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
symbol, currency, ownerTraderID, settlDate, listID, negotiationType string
|
||||
side enum.Side
|
||||
secIDSource enum.SecurityIDSource
|
||||
orderQty decimal.Decimal
|
||||
)
|
||||
|
||||
relatedSyms, relErr := msg.GetNoRelatedSym()
|
||||
if relErr == nil && relatedSyms.Len() > 0 {
|
||||
sym := relatedSyms.Get(0)
|
||||
symbol, _ = sym.GetSecurityID()
|
||||
secIDSource, _ = sym.GetSecurityIDSource()
|
||||
currency, _ = sym.GetCurrency()
|
||||
side, _ = sym.GetSide()
|
||||
ownerTraderID, _ = sym.GetOwnerTraderID()
|
||||
orderQty, _ = sym.GetOrderQty()
|
||||
settlDate, _ = sym.GetSettlDate()
|
||||
listID, _ = sym.GetListID()
|
||||
negotiationType, _ = sym.GetNegotiationType()
|
||||
bodyKeys := make([]string, 0, len(parsed.Body))
|
||||
for k := range parsed.Body {
|
||||
bodyKeys = append(bodyKeys, k)
|
||||
}
|
||||
slog.Info("handleQuoteRequest: parsed body keys", "quoteReqID", quoteReqID, "keys", bodyKeys)
|
||||
|
||||
relSym := firstGroup(parsed.Body, "NoRelatedSym")
|
||||
relSymKeys := make([]string, 0, len(relSym))
|
||||
for k := range relSym {
|
||||
relSymKeys = append(relSymKeys, k)
|
||||
}
|
||||
slog.Info("handleQuoteRequest: NoRelatedSym keys", "quoteReqID", quoteReqID, "keys", relSymKeys)
|
||||
|
||||
listID := getString(relSym, "ListID")
|
||||
negotiationType := getString(relSym, "NegotiationType")
|
||||
ownerTraderID := getString(relSym, "OwnerTraderID")
|
||||
|
||||
if listID == "" {
|
||||
slog.Warn("handleQuoteRequest: missing ListID", "quoteReqID", quoteReqID)
|
||||
@ -246,33 +247,20 @@ func (m *Manager) handleQuoteRequest(msg quoterequest.QuoteRequest, sessionID qu
|
||||
}
|
||||
slog.Info("QuoteStatusReport sent", "quoteReqID", quoteReqID)
|
||||
|
||||
sIDSource := enum.SecurityIDSource_ISIN_NUMBER
|
||||
if secIDSource == enum.SecurityIDSource_CUSIP {
|
||||
sIDSource = enum.SecurityIDSource_CUSIP
|
||||
}
|
||||
|
||||
// Store trade state as pending; Quote (35=S) is sent later via REST endpoint.
|
||||
m.tradesMu.Lock()
|
||||
m.trades[quoteReqID] = &listTrade{
|
||||
QuoteReqID: quoteReqID,
|
||||
ListID: listID,
|
||||
Symbol: symbol,
|
||||
SecurityIDSrc: sIDSource,
|
||||
Currency: currency,
|
||||
Side: side,
|
||||
OrderQty: orderQty,
|
||||
SettlDate: settlDate,
|
||||
OwnerTraderID: ownerTraderID,
|
||||
QuoteRequest: parsed,
|
||||
SessionID: sessionID,
|
||||
Quoted: false,
|
||||
}
|
||||
m.tradesMu.Unlock()
|
||||
|
||||
// Persist structured message (outside mutex).
|
||||
m.persistMessage(quoteReqID, parseQuoteRequest(msg))
|
||||
// Persist incoming QuoteRequest.
|
||||
m.persistMessage(quoteReqID, parsed)
|
||||
|
||||
// Persist outgoing QuoteStatusReport.
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("AI", quoteReqID, map[string]interface{}{
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("AI", quoteReqID, map[string]any{
|
||||
"QuoteReqID": quoteReqID,
|
||||
"QuoteStatus": string(enum.QuoteStatus_ACCEPTED),
|
||||
"OwnerTraderID": ownerTraderID,
|
||||
@ -285,7 +273,7 @@ func (m *Manager) handleQuoteAck(msg quoteack.QuoteAck, sessionID quickfix.Sessi
|
||||
status, _ := msg.GetQuoteAckStatus()
|
||||
text, _ := msg.GetText()
|
||||
|
||||
m.persistMessage(quoteReqID, parseQuoteAck(msg))
|
||||
m.persistMessage(quoteReqID, parseQuoteAck(msg, m.dict))
|
||||
|
||||
if status != enum.QuoteAckStatus_ACCEPTED {
|
||||
err := tracerr.Errorf("handleQuoteAck: quote rejected by TW (quoteReqID=%s, quoteAckStatus=%s, text=%s)", quoteReqID, string(status), text)
|
||||
@ -331,10 +319,10 @@ func (m *Manager) handleQuoteResponse(msg quoteresponse.QuoteResponse, sessionID
|
||||
slog.Info("QuoteResponse ACK sent", "quoteReqID", quoteReqID, "quoteRespID", quoteRespID)
|
||||
|
||||
// Persist incoming QuoteResponse.
|
||||
m.persistMessage(quoteReqID, parseQuoteResponse(msg))
|
||||
m.persistMessage(quoteReqID, parseQuoteResponse(msg, m.dict))
|
||||
|
||||
// Persist outgoing ACK.
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("AI", quoteReqID, map[string]interface{}{
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("AI", quoteReqID, map[string]any{
|
||||
"QuoteReqID": quoteReqID,
|
||||
"QuoteRespID": quoteRespID,
|
||||
"QuoteStatus": string(enum.QuoteStatus_ACCEPTED),
|
||||
@ -394,10 +382,10 @@ func (m *Manager) handleExecutionReport(msg executionreport.ExecutionReport, ses
|
||||
}
|
||||
|
||||
// Persist incoming ExecutionReport.
|
||||
m.persistMessage(clOrdID, parseExecutionReport(msg))
|
||||
m.persistMessage(clOrdID, parseExecutionReport(msg, m.dict))
|
||||
|
||||
// Persist outgoing ExecutionAck.
|
||||
m.persistMessage(clOrdID, buildOutgoingMessageJSON("BN", clOrdID, map[string]interface{}{
|
||||
m.persistMessage(clOrdID, buildOutgoingMessageJSON("BN", clOrdID, map[string]any{
|
||||
"OrderID": orderID,
|
||||
"ExecID": execID,
|
||||
"ClOrdID": clOrdID,
|
||||
@ -439,18 +427,14 @@ func (m *Manager) GetPendingQuoteRequests() []domain.ListTrade {
|
||||
}
|
||||
|
||||
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,
|
||||
out := domain.ListTrade{
|
||||
QuoteRequest: t.QuoteRequest,
|
||||
Quoted: t.Quoted,
|
||||
}
|
||||
if !t.Price.IsZero() {
|
||||
out.Price = t.Price.String()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SendQuote builds and sends a Quote (35=S) for an existing pending QuoteRequest at the given price.
|
||||
@ -481,15 +465,20 @@ func (m *Manager) SendQuote(quoteReqID string, price decimal.Decimal) error {
|
||||
}
|
||||
}
|
||||
|
||||
symbol := t.Symbol
|
||||
sIDSource := t.SecurityIDSrc
|
||||
currency := t.Currency
|
||||
side := t.Side
|
||||
orderQty := t.OrderQty
|
||||
settlDate := t.SettlDate
|
||||
ownerTraderID := t.OwnerTraderID
|
||||
relSym := firstGroup(t.QuoteRequest.Body, "NoRelatedSym")
|
||||
symbol := getString(relSym, "SecurityID")
|
||||
sIDSource := enum.SecurityIDSource(getString(relSym, "SecurityIDSource"))
|
||||
currency := getString(relSym, "Currency")
|
||||
side := enum.Side(getString(relSym, "Side"))
|
||||
orderQty := getDecimal(relSym, "OrderQty")
|
||||
settlDate := getString(relSym, "SettlDate")
|
||||
ownerTraderID := getString(relSym, "OwnerTraderID")
|
||||
m.tradesMu.Unlock()
|
||||
|
||||
if sIDSource != enum.SecurityIDSource_CUSIP {
|
||||
sIDSource = enum.SecurityIDSource_ISIN_NUMBER
|
||||
}
|
||||
|
||||
quoteID := quoteReqID
|
||||
q := quote.New(
|
||||
field.NewQuoteID(quoteID),
|
||||
@ -545,7 +534,7 @@ func (m *Manager) SendQuote(quoteReqID string, price decimal.Decimal) error {
|
||||
|
||||
slog.Info("Quote sent", "quoteReqID", quoteReqID, "quoteID", quoteID, "symbol", symbol, "price", price.String())
|
||||
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("S", quoteReqID, map[string]interface{}{
|
||||
m.persistMessage(quoteReqID, buildOutgoingMessageJSON("S", quoteReqID, map[string]any{
|
||||
"QuoteReqID": quoteReqID,
|
||||
"QuoteID": quoteID,
|
||||
"Symbol": symbol,
|
||||
@ -559,6 +548,45 @@ func (m *Manager) SendQuote(quoteReqID string, price decimal.Decimal) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// firstGroup returns the first repetition of a NUMINGROUP field, or nil.
|
||||
func firstGroup(body map[string]any, name string) map[string]any {
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
groups, ok := body[name].([]map[string]any)
|
||||
if !ok || len(groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
return groups[0]
|
||||
}
|
||||
|
||||
// getString reads a string value from a body map, tolerating nil maps and missing keys.
|
||||
func getString(body map[string]any, name string) string {
|
||||
if body == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := body[name].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDecimal reads a decimal value from a body map. Numeric FIX types come through
|
||||
// as strings (e.g. "10000"); INT-typed counts may be int. Both are accepted.
|
||||
func getDecimal(body map[string]any, name string) decimal.Decimal {
|
||||
if body == nil {
|
||||
return decimal.Decimal{}
|
||||
}
|
||||
switch v := body[name].(type) {
|
||||
case string:
|
||||
d, _ := decimal.NewFromString(v)
|
||||
return d
|
||||
case int:
|
||||
return decimal.NewFromInt(int64(v))
|
||||
}
|
||||
return decimal.Decimal{}
|
||||
}
|
||||
|
||||
func (m *Manager) anyActiveSessionID() quickfix.SessionID {
|
||||
m.sessionsMu.RLock()
|
||||
defer m.sessionsMu.RUnlock()
|
||||
@ -608,84 +636,38 @@ func (m *Manager) loadActiveTrades() error {
|
||||
continue
|
||||
}
|
||||
|
||||
body := msg.JMessage.Body
|
||||
|
||||
nt, _ := body["NegotiationType"].(string)
|
||||
if nt != "RFQ" {
|
||||
relSym := firstGroup(msg.JMessage.Body, "NoRelatedSym")
|
||||
if getString(relSym, "NegotiationType") != "RFQ" {
|
||||
continue
|
||||
}
|
||||
if getString(relSym, "ListID") == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
listID, _ := body["ListID"].(string)
|
||||
if listID == "" {
|
||||
continue
|
||||
activeTrades[msg.QuoteReqID] = &listTrade{
|
||||
QuoteRequest: msg.JMessage,
|
||||
}
|
||||
|
||||
trade := &listTrade{
|
||||
QuoteReqID: msg.QuoteReqID,
|
||||
ListID: listID,
|
||||
}
|
||||
|
||||
if v, ok := body["SecurityID"].(string); ok {
|
||||
trade.Symbol = v
|
||||
}
|
||||
|
||||
if v, ok := body["SecurityIDSource"].(string); ok {
|
||||
trade.SecurityIDSrc = enum.SecurityIDSource(v)
|
||||
}
|
||||
|
||||
if v, ok := body["Currency"].(string); ok {
|
||||
trade.Currency = v
|
||||
}
|
||||
|
||||
if v, ok := body["Side"].(string); ok {
|
||||
trade.Side = enum.Side(v)
|
||||
}
|
||||
|
||||
if v, ok := body["OrderQty"].(string); ok {
|
||||
trade.OrderQty, _ = decimal.NewFromString(v)
|
||||
}
|
||||
|
||||
if v, ok := body["SettlDate"].(string); ok {
|
||||
trade.SettlDate = v
|
||||
}
|
||||
|
||||
if v, ok := body["OwnerTraderID"].(string); ok {
|
||||
trade.OwnerTraderID = v
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
t.Price = getDecimal(msg.JMessage.Body, "Price")
|
||||
}
|
||||
|
||||
case "CW": // QuoteAck — if rejected, trade is dead
|
||||
body := msg.JMessage.Body
|
||||
quoteAckStatus, _ := body["QuoteAckStatus"].(string)
|
||||
|
||||
if quoteAckStatus != string(enum.QuoteAckStatus_ACCEPTED) {
|
||||
if getString(msg.JMessage.Body, "QuoteAckStatus") != string(enum.QuoteAckStatus_ACCEPTED) {
|
||||
delete(activeTrades, msg.QuoteReqID)
|
||||
}
|
||||
|
||||
case "AJ": // QuoteResponse — _TRDSUMM means trade is done (flow 8.6)
|
||||
body := msg.JMessage.Body
|
||||
quoteRespID, _ := body["QuoteRespID"].(string)
|
||||
|
||||
if strings.HasSuffix(quoteRespID, "_TRDSUMM") {
|
||||
if strings.HasSuffix(getString(msg.JMessage.Body, "QuoteRespID"), "_TRDSUMM") {
|
||||
delete(activeTrades, msg.QuoteReqID)
|
||||
}
|
||||
|
||||
case "8": // ExecutionReport — _TRDSUMM means trade is done (flow 8.4)
|
||||
body := msg.JMessage.Body
|
||||
execID, _ := body["ExecID"].(string)
|
||||
clOrdID, _ := body["ClOrdID"].(string)
|
||||
|
||||
if strings.Contains(execID, "_TRDSUMM") {
|
||||
delete(activeTrades, clOrdID)
|
||||
if strings.Contains(getString(body, "ExecID"), "_TRDSUMM") {
|
||||
delete(activeTrades, getString(body, "ClOrdID"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/datadictionary"
|
||||
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/executionreport"
|
||||
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoteack"
|
||||
"quantex.com/qfixdpl/quickfix/gen/fix50sp2/quoterequest"
|
||||
@ -12,188 +13,56 @@ import (
|
||||
"quantex.com/qfixdpl/src/domain"
|
||||
)
|
||||
|
||||
func extractHeader(msg *quickfix.Message) map[string]interface{} {
|
||||
header := make(map[string]interface{})
|
||||
// buildFixMessageJSON walks the full FIX message (header + body + trailer)
|
||||
// using the data dictionary and returns a fully populated FixMessageJSON.
|
||||
func buildFixMessageJSON(direction, msgType, quoteReqID string, msg *quickfix.Message, dd *datadictionary.DataDictionary) domain.FixMessageJSON {
|
||||
var (
|
||||
headerFields map[int]*datadictionary.FieldDef
|
||||
trailerFields map[int]*datadictionary.FieldDef
|
||||
bodyFields map[int]*datadictionary.FieldDef
|
||||
)
|
||||
|
||||
if v, err := msg.Header.GetBytes(tag.BeginString); err == nil {
|
||||
header["BeginString"] = string(v)
|
||||
if dd != nil {
|
||||
if dd.Header != nil {
|
||||
headerFields = dd.Header.Fields
|
||||
}
|
||||
if v, err := msg.Header.GetBytes(tag.MsgType); err == nil {
|
||||
header["MsgType"] = string(v)
|
||||
if dd.Trailer != nil {
|
||||
trailerFields = dd.Trailer.Fields
|
||||
}
|
||||
if v, err := msg.Header.GetBytes(tag.SenderCompID); err == nil {
|
||||
header["SenderCompID"] = string(v)
|
||||
}
|
||||
if v, err := msg.Header.GetBytes(tag.TargetCompID); err == nil {
|
||||
header["TargetCompID"] = string(v)
|
||||
}
|
||||
if v, err := msg.Header.GetBytes(tag.MsgSeqNum); err == nil {
|
||||
header["MsgSeqNum"] = string(v)
|
||||
}
|
||||
if v, err := msg.Header.GetBytes(tag.SendingTime); err == nil {
|
||||
header["SendingTime"] = string(v)
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
func parseQuoteRequest(msg quoterequest.QuoteRequest) domain.FixMessageJSON {
|
||||
quoteReqID, _ := msg.GetQuoteReqID()
|
||||
body := map[string]interface{}{"QuoteReqID": quoteReqID}
|
||||
|
||||
if relSyms, err := msg.GetNoRelatedSym(); err == nil && relSyms.Len() > 0 {
|
||||
sym := relSyms.Get(0)
|
||||
if v, e := sym.GetSecurityID(); e == nil {
|
||||
body["SecurityID"] = v
|
||||
}
|
||||
if v, e := sym.GetSecurityIDSource(); e == nil {
|
||||
body["SecurityIDSource"] = string(v)
|
||||
}
|
||||
if v, e := sym.GetCurrency(); e == nil {
|
||||
body["Currency"] = v
|
||||
}
|
||||
if v, e := sym.GetSide(); e == nil {
|
||||
body["Side"] = string(v)
|
||||
}
|
||||
if v, e := sym.GetOrderQty(); e == nil {
|
||||
body["OrderQty"] = v.String()
|
||||
}
|
||||
if v, e := sym.GetSettlDate(); e == nil {
|
||||
body["SettlDate"] = v
|
||||
}
|
||||
if v, e := sym.GetListID(); e == nil {
|
||||
body["ListID"] = v
|
||||
}
|
||||
if v, e := sym.GetOwnerTraderID(); e == nil {
|
||||
body["OwnerTraderID"] = v
|
||||
}
|
||||
if v, e := sym.GetNegotiationType(); e == nil {
|
||||
body["NegotiationType"] = v
|
||||
if md, ok := dd.Messages[msgType]; ok {
|
||||
bodyFields = md.Fields
|
||||
}
|
||||
}
|
||||
|
||||
return domain.FixMessageJSON{
|
||||
Direction: "IN",
|
||||
MsgType: "R",
|
||||
Direction: direction,
|
||||
MsgType: msgType,
|
||||
QuoteReqID: quoteReqID,
|
||||
Header: extractHeader(msg.Message),
|
||||
Body: body,
|
||||
Header: GetMap(BuildFieldMap(msg.Header.FieldMap, dd, headerFields)),
|
||||
Body: GetMap(BuildFieldMap(msg.Body.FieldMap, dd, bodyFields)),
|
||||
Trailer: GetMap(BuildFieldMap(msg.Trailer.FieldMap, dd, trailerFields)),
|
||||
ReceiveTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func parseQuoteAck(msg quoteack.QuoteAck) domain.FixMessageJSON {
|
||||
func parseQuoteRequest(msg quoterequest.QuoteRequest, dd *datadictionary.DataDictionary) domain.FixMessageJSON {
|
||||
quoteReqID, _ := msg.GetQuoteReqID()
|
||||
body := map[string]interface{}{"QuoteReqID": quoteReqID}
|
||||
|
||||
if v, e := msg.GetQuoteID(); e == nil {
|
||||
body["QuoteID"] = v
|
||||
}
|
||||
if v, e := msg.GetQuoteAckStatus(); e == nil {
|
||||
body["QuoteAckStatus"] = string(v)
|
||||
}
|
||||
if v, e := msg.GetText(); e == nil {
|
||||
body["Text"] = v
|
||||
return buildFixMessageJSON("IN", "R", quoteReqID, msg.Message, dd)
|
||||
}
|
||||
|
||||
return domain.FixMessageJSON{
|
||||
Direction: "IN",
|
||||
MsgType: "CW",
|
||||
QuoteReqID: quoteReqID,
|
||||
Header: extractHeader(msg.Message),
|
||||
Body: body,
|
||||
ReceiveTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func parseQuoteResponse(msg quoteresponse.QuoteResponse) domain.FixMessageJSON {
|
||||
func parseQuoteAck(msg quoteack.QuoteAck, dd *datadictionary.DataDictionary) domain.FixMessageJSON {
|
||||
quoteReqID, _ := msg.GetQuoteReqID()
|
||||
body := map[string]interface{}{"QuoteReqID": quoteReqID}
|
||||
|
||||
if v, e := msg.GetQuoteRespID(); e == nil {
|
||||
body["QuoteRespID"] = v
|
||||
}
|
||||
if v, e := msg.GetQuoteRespType(); e == nil {
|
||||
body["QuoteRespType"] = string(v)
|
||||
}
|
||||
if v, e := msg.GetSide(); e == nil {
|
||||
body["Side"] = string(v)
|
||||
}
|
||||
if v, e := msg.GetPrice(); e == nil {
|
||||
body["Price"] = v.String()
|
||||
}
|
||||
if v, e := msg.GetOrderQty(); e == nil {
|
||||
body["OrderQty"] = v.String()
|
||||
}
|
||||
if v, e := msg.GetClOrdID(); e == nil {
|
||||
body["ClOrdID"] = v
|
||||
return buildFixMessageJSON("IN", "CW", quoteReqID, msg.Message, dd)
|
||||
}
|
||||
|
||||
return domain.FixMessageJSON{
|
||||
Direction: "IN",
|
||||
MsgType: "AJ",
|
||||
QuoteReqID: quoteReqID,
|
||||
Header: extractHeader(msg.Message),
|
||||
Body: body,
|
||||
ReceiveTime: time.Now(),
|
||||
}
|
||||
func parseQuoteResponse(msg quoteresponse.QuoteResponse, dd *datadictionary.DataDictionary) domain.FixMessageJSON {
|
||||
quoteReqID, _ := msg.GetQuoteReqID()
|
||||
return buildFixMessageJSON("IN", "AJ", quoteReqID, msg.Message, dd)
|
||||
}
|
||||
|
||||
func parseExecutionReport(msg executionreport.ExecutionReport) domain.FixMessageJSON {
|
||||
func parseExecutionReport(msg executionreport.ExecutionReport, dd *datadictionary.DataDictionary) domain.FixMessageJSON {
|
||||
clOrdID, _ := msg.GetClOrdID()
|
||||
body := map[string]interface{}{"ClOrdID": clOrdID}
|
||||
|
||||
if v, e := msg.GetExecID(); e == nil {
|
||||
body["ExecID"] = v
|
||||
}
|
||||
if v, e := msg.GetOrderID(); e == nil {
|
||||
body["OrderID"] = v
|
||||
}
|
||||
if v, e := msg.GetExecType(); e == nil {
|
||||
body["ExecType"] = string(v)
|
||||
}
|
||||
if v, e := msg.GetOrdStatus(); e == nil {
|
||||
body["OrdStatus"] = string(v)
|
||||
}
|
||||
if v, e := msg.GetListID(); e == nil {
|
||||
body["ListID"] = v
|
||||
}
|
||||
if v, e := msg.GetSide(); e == nil {
|
||||
body["Side"] = string(v)
|
||||
}
|
||||
if v, e := msg.GetSymbol(); e == nil {
|
||||
body["Symbol"] = v
|
||||
}
|
||||
if v, e := msg.GetSecurityID(); e == nil {
|
||||
body["SecurityID"] = v
|
||||
}
|
||||
if v, e := msg.GetCurrency(); e == nil {
|
||||
body["Currency"] = v
|
||||
}
|
||||
if v, e := msg.GetPrice(); e == nil {
|
||||
body["Price"] = v.String()
|
||||
}
|
||||
if v, e := msg.GetLastPx(); e == nil {
|
||||
body["LastPx"] = v.String()
|
||||
}
|
||||
if v, e := msg.GetLastQty(); e == nil {
|
||||
body["LastQty"] = v.String()
|
||||
}
|
||||
if v, e := msg.GetOrderQty(); e == nil {
|
||||
body["OrderQty"] = v.String()
|
||||
}
|
||||
if v, e := msg.GetSettlDate(); e == nil {
|
||||
body["SettlDate"] = v
|
||||
}
|
||||
|
||||
return domain.FixMessageJSON{
|
||||
Direction: "IN",
|
||||
MsgType: "8",
|
||||
QuoteReqID: clOrdID,
|
||||
Header: extractHeader(msg.Message),
|
||||
Body: body,
|
||||
ReceiveTime: time.Now(),
|
||||
}
|
||||
return buildFixMessageJSON("IN", "8", clOrdID, msg.Message, dd)
|
||||
}
|
||||
|
||||
// extractIdentifier extracts the trade identifier from a parsed FIX message.
|
||||
@ -218,7 +87,7 @@ func extractIdentifier(msg *quickfix.Message) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildOutgoingMessageJSON(msgType, quoteReqID string, body map[string]interface{}) domain.FixMessageJSON {
|
||||
func buildOutgoingMessageJSON(msgType, quoteReqID string, body map[string]any) domain.FixMessageJSON {
|
||||
return domain.FixMessageJSON{
|
||||
Direction: "OUT",
|
||||
MsgType: msgType,
|
||||
|
||||
@ -5,16 +5,9 @@ import "time"
|
||||
|
||||
// ListTrade es la representacion exportada de un trade de List Trading.
|
||||
type ListTrade struct {
|
||||
QuoteReqID string
|
||||
ListID string
|
||||
Symbol string
|
||||
SecurityIDSrc string
|
||||
Currency string
|
||||
Side string
|
||||
OrderQty string
|
||||
SettlDate string
|
||||
Price string
|
||||
OwnerTraderID string
|
||||
QuoteRequest FixMessageJSON `json:"quote_request"`
|
||||
Quoted bool `json:"quoted"`
|
||||
Price string `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
// FixMessageJSON es la representacion estructurada de un mensaje FIX para almacenamiento.
|
||||
@ -22,8 +15,9 @@ type FixMessageJSON struct {
|
||||
Direction string `json:"direction"`
|
||||
MsgType string `json:"msg_type"`
|
||||
QuoteReqID string `json:"quote_req_id"`
|
||||
Header map[string]interface{} `json:"header"`
|
||||
Body map[string]interface{} `json:"body"`
|
||||
Header map[string]any `json:"header"`
|
||||
Body map[string]any `json:"body"`
|
||||
Trailer map[string]any `json:"trailer"`
|
||||
ReceiveTime time.Time `json:"receive_time"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user