TimelordUK / jspurefix

native typescript FIX engine
MIT License
58 stars 27 forks source link

Adding optional fields into the StandardHeader #22

Closed DominikStefancik closed 1 year ago

DominikStefancik commented 2 years ago

Hi,

I'm using jspurefix to communicate with our client's FIX OMS. When sending application messages, the client requires optional tags DeliverToCompID and SenderSubID to be provided in the standard header, otherwise the messages are not processed and I don't get any response from them. I have tried to "inject" the fields into the header manually and then send the message, like this

const header: ILooseObject = this.config.factory.header(
        msgType,
        (this.transport.transmitter as AsciiMsgTransmitter).msgSeqNum,
        new Date(),
        { 
            DeliverToCompID: 'some_string',
            SenderSubID: 'some_string'
        }
);

fixMessage.StandardHeader = header;

this.send(msgType, fixMessage)

After I do that, the fixMessage object contains the header with the additional fields, but when I send it, the fields are not present. Neither they are seen in the logs when the onEncoded method is called, nor the client doesn't see them when the message is received.

How can I add the additional fields in the header and be sure they are included in the message when sent? Is my approach a correct one?

Thank you for your advice!

TimelordUK commented 2 years ago

hello,

if you use the quckfix style XML data dictionary you can look at

https://github.com/TimelordUK/jspf-md-demo

where you would add your new fields into the definitions within the XML and then run the generator

 "generate": "cd node_modules/jspurefix/dist && node jsfix-cmd \"--dict=../../data/FIX44-MD.xml\" \"--compile\" \"--output=../../src/types/\"",

this will regenerate the messages so. intellisense works with new fields. The data dictionary is needed to correctly send and parse messages.

let us know how it goes - perhaps create a sinple repo with just the additional fields required which we can help get working

DominikStefancik commented 2 years ago

Hi,

As far as I understand, the generator is for creating custom types or extending existing ones with fields which are not there yet. That is not my problem. The IStandardHeader interface already has the fields DeliverToCompID and SenderSubID defined in it. My problem is the following: I create an order message, e.g. NewOrderSingle with fields required by the client’s OMS. When the message is sent, it has the StandardHeader with the required fields (BeginString, BodyLength, MsgType, SenderCompID, …). What I need, is that the header contains additional (optional) fields on top of that. So if I printed such a message out, its StandardHeader would look similar to this:

"StandardHeader": {
        "SenderCompID": “sender-id",
        "TargetCompID": “target-id",
         ... (other required fields),
         ...,
    “DeliverToCompID”: "some_string",
        “SenderSubID”: "some_string",
        ...
 }

I looked at the source code of the repo and debugged the samples provided in it. This is what I found:

  1. An order is created with required fields. It however doesn’t contain the StandardHeader yet.
  2. Then it is sent to the counter party by the FixSession, which uses transport.transmitter (of type MsgTransmitter) to do that.
  3. The transmitter then uses the 
public encodeMessage (msgType: string, obj: ILooseObject) 
method which adds the StandardHeader to the object before it encodes it.

There are two concrete classes of the MsgTransmitter. I’m using the AsciiMsgTransmitter in my code. In its encodeMessage implementation, it creates the header with headerProps, like this

const { StandardHeader, ...bodyProps } = obj

const headerProps: Partial<IStandardHeader> = {
  ...(StandardHeader?.PossDupFlag ? { PossDupFlag: StandardHeader?.PossDupFlag } : {}),
  ...(StandardHeader?.MsgSeqNum ? { MsgSeqNum: StandardHeader?.MsgSeqNum } : {})
}

const hdr: ILooseObject = factory.header(msgType, this.msgSeqNum,this.time || new Date(), headerProps)

This means that even if I manually set the header and add it to the object before sending it like this

const header: ILooseObject = this.config.factory.header(
        msgType,
        (this.transport.transmitter as AsciiMsgTransmitter).msgSeqNum,
        new Date(),
        { 
            DeliverToCompID: 'some_string',
            SenderSubID: 'some_string'
        }
);

fixMessage.StandardHeader = header;

this.send(msgType, fixMessage)

The encodeMessage will not use the added fields and create a header without them.

Why is it implemented like this? Why are only PossDupFlag and MsgSeqNum used in the overrideData parameter of the ISessionMsgFactory.header method? Is there any other way to add the additional fields into the header and make sure they are encoded?

TimelordUK commented 2 years ago

hello,

it is not ideal but for now can you use the mutator within the factory - if you look at unit test session.test.js

somewhere in your app assign the mutator and then the object is gven to you after it is created.

hopefully this will work

clientFactory.mutator = mutateSeqNo
function mutateSeqNo (description: ISessionDescription, type: string, o: ILooseObject): ILooseObject {
  switch (type) {
    case 'StandardHeader': {
      const hdr = o as IStandardHeader
// set your fields here
      if (hdr.MsgSeqNum === 2) {
        hdr.MsgSeqNum = 0
      }
      break
    }
  }
  return o
}

It would be better for us to change the methods non private and then an app could extend factory

and create extended factory - but this will not work as code stands.

clientFactory = new SessionMsgFactoryExt (clientDescription)
  serverFactory = new SessionMsgFactory(serverDescription)

  const clientConfig = new JsFixConfig(clientFactory, definitions, clientDescription, AsciiChars.Pipe)
export class SessionMsgFactoryExt extends SessionMsgFactory {
   asciiHeader (msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>): ILooseObject {
   }
}
DominikStefancik commented 2 years ago

Hi,

Thank you for the hint! Using custom mutator helped. The optional tags are in the header now.

The situation got a bit more complicated, though :) The value for the DeliverToCompID tag has to be different for each of the client’s department.
 The use case is this:
 If I send an order for a certain asset type (e.g. Stock), it has to route to a department A. If I send an order for another type, it has to route to a different department. In order to say to which department the message should be sent, I have to set a different value for the tag DeliverToCompID.



That means, that every time I call the mutator, I have to know the asset type. Something like this

function myMutator (description: ISessionDescription, type: string, o: ILooseObject): ILooseObject {
  switch (type) {
    case 'StandardHeader': {
      const hdr = o as IStandardHeader

      const assetType = await asynCallToGetAssetType()
      if (assetType === ‘Stock’) {
        hdr.DeliverToCompID = ‘DepA’
      } else if(…) {
        hdr.DeliverToCompID = ‘DepB’
      } else {
        hdr.DeliverToCompID = ‘DepC’
      }
      break
    }
  }
  return o
}

The problem is, I know the asset type when an order message (e.g. IOrderSingle) is being constructed. Otherwise, I have to us an async call to get the asset type. If I wanted to use this async call in the mutator, I would have to change the function definition to be async and return a promise.

function async myMutator (description: ISessionDescription, type: string, o: ILooseObject): Promise<ILooseObject>{ … } 

When I tested it, the messages are not sent.

Is there any other way that mutator could use async calls in its body that would work? If that doesn’t work, is it possible to set the tag DeliverToCompID in the StandardHeader of a message in the time when it is being constructed, but also make sure that it is not overwritten in the encodeMessage as I described in my previous comment?

TimelordUK commented 2 years ago

ok having a look will report back shortly.

TimelordUK commented 2 years ago

are you able to test the current master git banch

if you look at ascii encode tests

"8=FIX4.4|9=0000159|35=D|49=init-comp|56=accept-comp|128=DepC|34=10|57=fix|52=20210926-18:43:48.413|11=Cli1|1=MyAcc|55=MSCb|48=459200101|22=4|167=CB|54=1|38=100|40=2|44=1000|59=0|10=107|"

function createOrder (id: number, symbol: string, securityType: SecurityType, side: Side, qty: number, price: number): INewOrderSingle {
  return {
    ClOrdID: `Cli${id}`,
    Account: 'MyAcc',
    Side: side,
    Price: price,
    OrdType: OrdType.Limit,
    OrderQtyData: {
      OrderQty: qty
    } as IOrderQtyData,
    Instrument: {
      SecurityType: securityType,
      Symbol: symbol,
      SecurityID: '459200101',
      SecurityIDSource: SecurityIDSource.IsinNumber
    } as IInstrument,
    TimeInForce: TimeInForce.Day
  } as INewOrderSingle
}

function getCompID(securityType: SecurityType): string {
  switch (securityType) {
    case SecurityType.CommonStock: {
      return 'DepA'
    }

    case SecurityType.CorporateBond: {
      return 'DepB'
    }

    case SecurityType.ConvertibleBond: {
      return 'DepC'
    }

    default:
      return 'DepD'
  }
}

test('encode custom header 1 - expect DeliverToCompID DepA', async () => {
  const type = SecurityType.CommonStock
  const o1 = createOrder(1, 'MS', type, Side.Buy, 100, 1000.0)
  const h: ILooseObject = {
    DeliverToCompID: getCompID(type)
  }
  o1.StandardHeader = h as IStandardHeader
  const nosd = definitions.message.get('NewOrderSingle')
  const fix = toFixMessage(o1, nosd)
  expect(fix).toBeTruthy()
  const res: ParsingResult = await toParse(fix)
  const tag = res.view.getTyped('DeliverToCompID')
  expect(tag).toEqual('DepA')
  expect(res.event).toEqual('msg')
  expect(res.msgType).toEqual(MsgType.NewOrderSingle)
  const parsed: INewOrderSingle = res.view.toObject()
  expect(parsed.StandardHeader.DeliverToCompID).toEqual('DepA')
})

test('encode custom header 2 - expect DeliverToCompID DepC', async () => {
  const type = SecurityType.ConvertibleBond
  const o1 = createOrder(1, 'MSCb', type, Side.Buy, 100, 1000.0)
  const h: ILooseObject = {
    DeliverToCompID: getCompID(type)
  }
  o1.StandardHeader = h as IStandardHeader
  const nosd = definitions.message.get('NewOrderSingle')
  const fix = toFixMessage(o1, nosd)
  expect(fix).toBeTruthy()
  const res: ParsingResult = await toParse(fix)
  const tag = res.view.getTyped('DeliverToCompID')
  expect(tag).toEqual('DepC')
  expect(res.event).toEqual('msg')
  expect(res.msgType).toEqual(MsgType.NewOrderSingle)
  const parsed: INewOrderSingle = res.view.toObject()
  expect(parsed.StandardHeader.DeliverToCompID).toEqual('DepC')
})

image

pcicman commented 2 years ago

Thanks for the changes! Looking at the code this looks very promising, I've run into a similar problem with OrigSendingTime yesterday.

Related question - is there a reason why SendingTime is not considered and does not take precedence before this.time || new Date()? We have a scenario where we need to track the SendingTime and pack it back to OrigSendingTime <122> on replay. Is this something you would consider?

TimelordUK commented 2 years ago

I had a go at adding this but as yet have not added tests for it - in latest release

hmvs commented 2 years ago

I can see that the issue is not closed yet, so the question is, is the mutator still the preferred way of adding field to the header?

TimelordUK commented 2 years ago

https://github.com/TimelordUK/jspurefix/issues/39

take a look here - the better solution is to install your own message factory for total control

On Mon, 13 Jun 2022 at 15:25, Vadym Kurachevskyi @.***> wrote:

I can see that the issue is not closed yet, so the question is, is the mutator still the preferred way of adding field to the header?

— Reply to this email directly, view it on GitHub https://github.com/TimelordUK/jspurefix/issues/22#issuecomment-1153990543, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABXWJG64UBEKQDKXQJ6E443VO5AEDANCNFSM5DJLGLVA . You are receiving this because you commented.Message ID: @.***>

TimelordUK commented 2 years ago

specifically here https://github.com/TimelordUK/jspf-md-demo/blob/master/src/app/app.ts

this whole sample is a good start point note locally installed factory which you can the change

hmvs commented 2 years ago

Yay. It worked. Thanks for the super-fast response!

TimelordUK commented 1 year ago

closing think this is resolved