TimelordUK / jspurefix

native typescript FIX engine
MIT License
61 stars 28 forks source link

Parameter "SenderSubID" missing during login #69

Open dawadam opened 1 year ago

dawadam commented 1 year ago

I am trying to login to cTrader broker and "SenderSubID" parametter is needed. I'm using a ctrader custom dictionary and adding this parameter on the json file, but it's not being sent. And no response from the server.

json file :

{
  "application": {
    "reconnectSeconds": 10,
    "type": "initiator",
    "name": "test_ctrader",
    "tcp": {
      "host": "h51.p.ctrader.com",
      "port": 5201
    },
    "protocol": "ascii",
    "dictionary": "../../resources/FIX44-CSERVER.xml"
  },
  "BeginString": "FIX4.4",
  "Username": "123456",
  "Password": "azerty",
  "EncryptMethod": 0,
  "ResetSeqNumFlag": true,
  "HeartBtInt": 30,
  "SenderCompId": "demo.aaa.123456",
  "TargetCompID": "cServer",
  "SenderSubID": "QUOTE"
}

Application code :

import "reflect-metadata"

import {
    EngineFactory,
    SessionLauncher,
    AsciiSession,
    MsgView,
    IJsFixLogger,
    IJsFixConfig
} from 'jspurefix'

/**
 * 
 */
class FixTest extends AsciiSession {

    private readonly logger: IJsFixLogger
    private readonly fixLog: IJsFixLogger

    constructor(public readonly config: IJsFixConfig) {
        super(config)
        this.logReceivedMsgs = true

        this.fixLog = config.logFactory.plain(`jsfix.${config.description.application.name}.txt`)
        this.logger = config.logFactory.logger(`${this.me}:TradeCaptureClient`)
    }

    /**
     * 
     * @param msgType 
     * @param txt 
     */

    protected onDecoded(msgType: string, txt: string): void {
        console.log('onDecoded', msgType, txt)
        this.fixLog.info(txt)
    }

    /**
     * 
     */
    protected onEncoded(msgType: string, txt: string): void {
        console.log('onEncoded', msgType, txt)
        this.logger.info('test')
        this.fixLog.info(txt)
    }

    /**
     * 
     * @param msgType 
     * @param view 
     */
    protected onApplicationMsg(msgType: string, view: MsgView): void {
        console.log('onApplicationMsg', msgType)
        switch (msgType) {

        }
    }

    /**
     * 
     * @param view 
     */
    protected onReady(view: MsgView): void {
        console.log('READY')
    }

    /**
     * 
     * @param error 
     */
    protected onStopped(error?: Error): void {
        console.log('STOPPPED')
    }

    /**
     * 
     */
    protected onLogon(view: MsgView, user: string, password: string): boolean {
        console.log('LOGON')

        return true
    }

}

/**
 * 
 */
class AppLauncher extends SessionLauncher {
    public constructor(client: string = '../../jspurefix-test-initiator.json') {
        super(client)
        this.root = __dirname
    }

    protected override makeFactory(config: IJsFixConfig): EngineFactory {
        return {
            makeSession: () => new FixTest(config)
        } as EngineFactory
    }
}

const l = new AppLauncher()
l.exec()

Logon fix message :

8=FIX4.4|9=0000105|35=A|49=demo.aaa.123456|56=cServer|34=1|52=20230827-16:38:22.081|98=0|108=30|141=Y|553=123456|554=azerty|10=176|

No SenderSubID parameter, identifier is 50

How add it in login message ?

TimelordUK commented 1 year ago

please see jspf-md-demo project

you need to install a custom msg factory

class MySessionContainer extends SessionContainer {
  protected makeSessionFactory (description: ISessionDescription): ISessionMsgFactory {
    return new MsgFact(description)
  }
}

you will need to add new property here which constructs a header object based on json properties

 public header (msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>): ILooseObject {
    const description = this.description
    const bodyLength: number = Math.max(4, description.BodyLengthChars ?? 7)
    const placeHolder = Math.pow(10, bodyLength - 1) + 1
    const o: IStandardHeader = {
      BeginString: description.BeginString,
      BodyLength: placeHolder,
      MsgType: msgType,
      SenderCompID: description.SenderCompId,
     // e.g.  SenderSubID: 'QUOTE', 
      MsgSeqNum: seqNum,
      SendingTime: time,
      TargetCompID: description.TargetCompID,
      TargetSubID: description.TargetSubID,
      ...overrideData
    }
    return this.mutate(o, 'StandardHeader')
  }
TimelordUK commented 1 year ago

it is a bug in sense the base class should send this field but its missing and this is only way to override at the moment - i will patch at some point soon such that the default implementtion will take care of it

dawadam commented 1 year ago

Ok, but ILogon don't include SenderSubID property.

export interface ILogon {
  StandardHeader: IStandardHeader
  EncryptMethod: number// 98
  HeartBtInt: number// 108
  RawDataLength?: number// 95
  RawData?: Buffer// 96
  ResetSeqNumFlag?: boolean// 141
  NextExpectedMsgSeqNum?: number// 789
  MaxMessageSize?: number// 383
  MsgTypeGrp?: IMsgTypeGrp[]
  TestMessageIndicator?: boolean// 464
  Username?: string// 553
  Password?: string// 554
  NewPassword?: string// 925
  EncryptedPasswordMethod?: number// 1400
  EncryptedPasswordLen?: number// 1401
  EncryptedPassword?: Buffer// 1402
  EncryptedNewPasswordLen?: number// 1403
  EncryptedNewPassword?: Buffer// 1404
  SessionStatus?: number// 1409
  DefaultApplVerID: string// 1137
  DefaultApplExtID?: number// 1407
  DefaultCstmApplVerID?: string// 1408
  Text?: string// 58
  EncodedTextLen?: number// 354
  EncodedText?: Buffer// 355
  StandardTrailer: IStandardTrailer
}
dawadam commented 1 year ago

It doesn't seem to work, here is the complete code.

_jspurefixmsg-fact.ts

import { ISessionDescription, MsgType, ASessionMsgFactory } from 'jspurefix'
import { ILogout } from 'jspurefix/dist/types/FIX4.4/cserver/logout'
import { ILooseObject } from 'jspurefix/dist/collections/collection'
import { EncryptMethod, ILogon, IStandardHeader } from 'jspurefix/dist/types/FIX4.4/cserver'

/**
 * 
 */
export class MsgFact extends ASessionMsgFactory {
  constructor(readonly description: ISessionDescription) {
    super(description, (_description: ISessionDescription, _type: string, o: ILooseObject) => o)
    this.isAscii = description?.application?.protocol === 'ascii'
  }

  /**
   * 
   */
  public logon(): ILooseObject {
    const description = this.description

    const o: ILogon = {
      Username: description.Username,
      Password: description.Password,
      HeartBtInt: description.HeartBtInt,
      ResetSeqNumFlag: description.ResetSeqNumFlag,
      // @ts-ignore
      SenderSubID: description.SenderSubID,
      EncryptMethod: EncryptMethod.NoneOther,
      StandardHeader: undefined,
      StandardTrailer: undefined
    }
    return this.mutate(o, MsgType.Logon)
  }

  /**
   * 
   */
  public logout(text: string): ILooseObject {
    const o: ILogout = {
      Text: text,
      StandardHeader: undefined,
      StandardTrailer: undefined
    }
    return this.mutate(o, MsgType.Logout)
  }

  /**
   * 
   */
  public header(msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>): ILooseObject {
    const description = this.description
    const bodyLength: number = Math.max(4, description.BodyLengthChars ?? 7)
    const placeHolder = Math.pow(10, bodyLength - 1) + 1
    const o: IStandardHeader = {
      BeginString: description.BeginString,
      BodyLength: placeHolder,
      MsgType: msgType,
      SenderCompID: description.SenderCompId,
      MsgSeqNum: seqNum,
      SendingTime: time,
      TargetCompID: description.TargetCompID,
      TargetSubID: description.TargetSubID,
      ...overrideData
    }
    return this.mutate(o, 'StandardHeader')
  }
}

jspurefix.ts

import "reflect-metadata"

import {
    EngineFactory,
    SessionLauncher,
    AsciiSession,
    MsgView,
    IJsFixLogger,
    IJsFixConfig,
    SessionContainer,
    ISessionDescription,
    ISessionMsgFactory
} from 'jspurefix'

import { MsgFact } from './jspurefix_msg-fact'

/**
 * 
 */
class FixTest extends AsciiSession {

    private readonly logger: IJsFixLogger
    private readonly fixLog: IJsFixLogger

    constructor(public readonly config: IJsFixConfig) {
        super(config)

        this.logReceivedMsgs = true

        this.fixLog = config.logFactory.plain(`jsfix.${config.description.application.name}.txt`)
        this.logger = config.logFactory.logger(`${this.me}:TradeCaptureClient`)
    }

    /**
     * 
     * @param msgType 
     * @param txt 
     */

    protected onDecoded(msgType: string, txt: string): void {
        console.log('onDecoded', msgType, txt)
        this.fixLog.info(txt)
    }

    /**
     * 
     */
    protected onEncoded(msgType: string, txt: string): void {
        console.log('onEncoded', msgType, txt)
        this.logger.info('test')
        this.fixLog.info(txt)
    }

    /**
     * 
     * @param msgType 
     * @param view 
     */
    protected onApplicationMsg(msgType: string, view: MsgView): void {
        console.log('onApplicationMsg', msgType)
        switch (msgType) {

        }
    }

    /**
     * 
     * @param view 
     */
    protected onReady(view: MsgView): void {
        console.log('READY')
    }

    /**
     * 
     * @param error 
     */
    protected onStopped(error?: Error): void {
        console.log('STOPPPED')
    }

    /**
     * 
     */
    protected onLogon(view: MsgView, user: string, password: string): boolean {
        console.log('LOGON')

        return true
    }

}

/**
 * 
 */
class MySessionContainer extends SessionContainer {
    protected makeSessionFactory(description: ISessionDescription): ISessionMsgFactory {
        return new MsgFact(description)
    }
}

/**
 * 
 */
class AppLauncher extends SessionLauncher {
    public constructor(client: string = '../../jspurefix-test-initiator.json') {
        super(client)
        this.sessionContainer = new MySessionContainer()
        this.root = __dirname
    }

    protected override makeFactory(config: IJsFixConfig): EngineFactory {
        return {
            makeSession: () => new FixTest(config)
        } as EngineFactory
    }
}

const l = new AppLauncher()
l.exec()

jspurefix-test-initiator.json

{
  "application": {
    "reconnectSeconds": 10,
    "type": "initiator",
    "name": "test_ctrader",
    "tcp": {
      "host": "h51.p.ctrader.com",
      "port": 5201
    },
    "protocol": "ascii",
    "dictionary": "../../resources/FIX44-CSERVER.xml"
  },
  "BeginString": "FIX4.4",
  "Username": "123",
  "Password": "abc",
  "EncryptMethod": 0,
  "ResetSeqNumFlag": true,
  "HeartBtInt": 30,
  "SenderCompId": "demo.icmarkets.123",
  "TargetCompID": "cServer",
  "SenderSubID": "QUOTE"
}

I think i will be wait the patch. Code is big for just connection. Do you have free time for that right now? :)

TimelordUK commented 1 year ago

the field does not go in the logon message - it should be in the header

 public header(msgType: string, seqNum: number, time: Date, overrideData?: Partial<IStandardHeader>): ILooseObject {
    const description = this.description
    const bodyLength: number = Math.max(4, description.BodyLengthChars ?? 7)
    const placeHolder = Math.pow(10, bodyLength - 1) + 1
    const o: IStandardHeader = {
      BeginString: description.BeginString,
      BodyLength: placeHolder,
      MsgType: msgType,
      SenderCompID: description.SenderCompId,
      MsgSeqNum: seqNum,
      SendingTime: time,
      TargetCompID: description.TargetCompID,
      TargetSubID: description.TargetSubID,
      ...overrideData
    }
    return this.mutate(o, 'StandardHeader')
  }
dawadam commented 1 year ago

Thanks, it works, the FIX message is correct.

8=FIX4.4|9=0000122|35=A|49=demo.icmarkets.123|56=cServer|34=1|50=QUOTE|52=20230827-20:51:43.527|98=0|108=30|141=Y|553=8665823|554=abc|10=000|

No response from the server, but that must be another issue.