jspurefix 5.6.2 → 5.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,66 +3,67 @@
3
3
  [![CI](https://github.com/TimelordUK/jspurefix/actions/workflows/ci.yml/badge.svg)](https://github.com/TimelordUK/jspurefix/actions/workflows/ci.yml)
4
4
  [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
5
5
 
6
- ## C# Port Available
7
-
8
- This TypeScript FIX engine has been ported to C#:
9
-
10
- - **Codebase**: [cspurefix](https://github.com/TimelordUK/cspurefix)
11
- - **Demo**: [purefix-standalone-demo](https://github.com/TimelordUK/purefix-standalone-demo)
12
- - **NuGet**: [PureFix.Types.Core](https://www.nuget.org/packages/PureFix.Types.Core/)
13
-
14
- ---
15
-
16
- 1. fast 100% native clean fix engine for Node JS
17
- 1. supports tls encrypted sessions over tcp
18
- 1. represent data dictionary as quickfix or repo notation
19
- 1. compile interface types against definitions
20
- 1. ascii / fixml supported
21
- 1. parses repeat groups, components and raw data fields
22
- 1. views make convinient ways of accessing data
23
- 1. complete trade capture sample to get started
24
- 1. a skeleton example shows connection, login and session only
25
- 1. parse fix logs into human readable format - JSON, tokens
26
- 1. socket session management for login, heartbeat etc
27
- 1. implement httpInitiator or acceptor
28
-
29
- ## Typescript FIX Engine for Node JS
30
-
31
- This fix engine provides a fast easy API to parse or send finacial protocol FIX messages. It is implemented entirely in typescript and runs in Node JS. Messages of any complexity can be handled providing they are backed by a suitable data dictionary. All structures within a message will be resolved for easy access - groups of components containing groups etc.
32
-
33
- ## Installing
34
-
35
- compiled typescript is now included. Install the package from npm:
6
+ A fast, fully native TypeScript [FIX protocol](https://www.fixtrading.org/) engine for Node.js. Built around a data-dictionary driven parser, with first-class support for sessions over TCP/TLS, persistent message stores, sequence recovery, FIXML over HTTP, and generated typed interfaces for any FIX dialect.
7
+
8
+ ## Table of Contents
9
+
10
+ - [Features](#features)
11
+ - [Installation](#installation)
12
+ - [Examples](#examples)
13
+ - [Quickstart](#quickstart)
14
+ - [Session Configuration](#session-configuration)
15
+ - [TLS](#tls)
16
+ - [Body length padding](#body-length-padding)
17
+ - [Persistence & Recovery](#persistence--recovery)
18
+ - [Choosing a store](#choosing-a-store)
19
+ - [`ResetSeqNumFlag` semantics](#resetseqnumflag-semantics)
20
+ - [Resending messages](#resending-messages)
21
+ - [Working with Messages](#working-with-messages)
22
+ - [FIXML over HTTP](#fixml-over-http)
23
+ - [Data Dictionaries](#data-dictionaries)
24
+ - [`jsfix` CLI log parsing & stats](#jsfix-cli--log-parsing--stats)
25
+ - [Performance](#performance)
26
+ - [Developing on jspurefix](#developing-on-jspurefix)
27
+ - [C# Port](#c-port)
28
+ - [License](#license)
29
+
30
+ ## Features
31
+
32
+ - 100% native TypeScript — no native bindings, runs anywhere Node.js does
33
+ - ASCII (tag=value) and FIXML message encodings
34
+ - Repeating groups, components, nested structures and raw data fields
35
+ - Dictionary-driven: load QuickFIX XML or FIX repository definitions, compile typed interfaces
36
+ - Full session lifecycle: logon, heartbeats, test requests, resend requests, logout
37
+ - TLS-encrypted sessions over TCP
38
+ - Pluggable persistent message store (in-memory or file) with sequence recovery
39
+ - HTTP initiator/acceptor for FIXML
40
+ - Command-line tool for parsing FIX logs into tokens, JSON, or structure dumps
41
+ - Sample applications: trade capture, market data, FIXML OMS, recovering skeleton
42
+
43
+ ## Installation
36
44
 
37
45
  ```shell
38
- npm install jspurefix
39
- cd node_modules/jspurefix && npm run unzip-repo
46
+ npm install jspurefix
47
+ cd node_modules/jspurefix && npm run unzip-repo
40
48
  ```
41
49
 
42
- see our [demo application](https://github.com/TimelordUK/jspf-demo/)
50
+ `unzip-repo` extracts the bundled FIX dictionaries. The `postinstall` hook will normally do this for you, but the command is exposed in case you need to re-run it.
43
51
 
44
- ```shell
45
- https://github.com/TimelordUK/jspf-demo
46
- ```
52
+ A standalone demo project lives at [TimelordUK/jspf-demo](https://github.com/TimelordUK/jspf-demo) — the fastest way to see a working initiator/acceptor.
47
53
 
48
- if you wish to generate custom messages this is easily doing using quick fix XML format - see [quickfix demo](https://github.com/TimelordUK/jspf-md-demo)
54
+ ## Examples
49
55
 
50
- edit the dictionary file data/generate and run the generator.
56
+ | description | link |
57
+ |---|---|
58
+ | a good start point showing FIX 4 and 5 market data request/snapshot and custom generated types. | [jspf-md-demo](https://github.com/TimelordUK/jspf-md-demo) |
59
+ | simple example with a custom dictionary that targets cserver | [jspf-cserver](https://github.com/TimelordUK/jspf-cserver) |
60
+ | example trade capture applicaton | [jspf-demo](https://github.com/TimelordUK/jspf-demo) |
51
61
 
52
- ```shell
53
- npm run generate
54
- ```
62
+ ## Quickstart
55
63
 
56
- check the messages generated in src\types
57
-
58
- ```shell
59
- https://github.com/TimelordUK/jspf-md-demo
60
- ```
61
-
62
- import types for usage with a client
64
+ Import the session types you need from `jspurefix` and the typed FIX messages for your dialect:
63
65
 
64
66
  ```typescript
65
-
66
67
  import {
67
68
  AsciiSession,
68
69
  MsgView,
@@ -70,102 +71,94 @@ import {
70
71
  IJsFixLogger,
71
72
  Dictionary,
72
73
  MsgType,
73
- IJsFixConfig,
74
74
  initiator,
75
75
  acceptor,
76
76
  makeConfig
77
77
  } from 'jspurefix'
78
- // types for the specific FIX dialect
78
+
79
79
  import {
80
80
  ITradeCaptureReport,
81
81
  ITradeCaptureReportRequest,
82
82
  ITradeCaptureReportRequestAck
83
83
  } from 'jspurefix/dist/types/FIX4.4/repo'
84
-
85
84
  ```
86
85
 
87
- see example trade-capture-client - use
88
-
89
- ### getting started
86
+ A minimal session subclasses `AsciiSession` and implements two callbacks: `onReady` (connection up, logon confirmed) and `onApplicationMsg` (a non-session message arrived).
90
87
 
91
- clone from git
92
-
93
- unix
94
-
95
- ```shell
96
- git clone https://github.com/TimelordUK/jspurefix.git
97
- cd jspurefix
98
- script/build.sh
99
- ```
100
-
101
- ```shell
102
- npm install
103
- npm run unzip-repo
104
- ./node_modules/.bin/tsc --version
105
- ./node_modules/.bin/tsc
106
- npm run tcp-tc
107
- ```
88
+ ```typescript
89
+ class TradeCaptureClient extends AsciiSession {
90
+ constructor (public readonly config: IJsFixConfig) {
91
+ super(config)
92
+ this.logReceivedMsgs = true
93
+ this.fixLog = config.logFactory.plain(`jsfix.${config.description.application.name}.txt`)
94
+ this.logger = config.logFactory.logger(`${this.me}:TradeCaptureClient`)
95
+ }
108
96
 
109
- windows
97
+ protected onReady (view: MsgView): void {
98
+ const tcr: ITradeCaptureReportRequest =
99
+ TradeFactory.tradeCaptureReportRequest('all-trades', new Date())
100
+ this.send(MsgType.TradeCaptureReportRequest, tcr)
101
+ }
110
102
 
111
- ```shell
112
- git clone https://github.com/TimelordUK/jspurefix.git
113
- cd jspurefix
114
- script\build.cmd
103
+ protected onApplicationMsg (msgType: string, view: MsgView): void {
104
+ switch (msgType) {
105
+ case MsgType.TradeCaptureReport: {
106
+ const tc: ITradeCaptureReport = view.toObject()
107
+ this.logger.info(`tc ${tc.TradeReportID} ${tc.Instrument.Symbol} ${tc.LastQty} @ ${tc.LastPx}`)
108
+ break
109
+ }
110
+ }
111
+ }
112
+ }
115
113
  ```
116
114
 
117
- or
115
+ The full sample lives at `src/sample/tcp/trade-capture/` and runs both sides over a local socket:
118
116
 
119
117
  ```shell
120
- git clone https://github.com/TimelordUK/jspurefix.git
121
- cd jspurefix
122
- npm install
123
- npm run unzip-repo
124
- node_modules/.bin/tsc
125
- npm run tcp-tc
118
+ npm run tcp-tc # full trade-capture client/server demo
119
+ npm run tcp-sk # bare skeleton: connect, log on, idle
120
+ npm run http-oms # FIXML order/exec-report over HTTP
126
121
  ```
127
122
 
128
- ## Run Sample
129
-
130
- The code provided in src/sample/tcp/trade_capture is a good place to start in building an application with this library. In this case both client and server are run together communicating over a socket. In reality a client is more likely connecting to an external acceptor such as CME, ICE.
123
+ Each demo terminates after about a minute, or with Ctrl-C.
131
124
 
132
- There is also a skeleton application which shows all application code stripped away to just enough to create a connection, login and see the session.
125
+ ## Session Configuration
133
126
 
134
- Both examples will automatically close down after ~1 minute or can be terminated CTRL-C
127
+ A session is described by a JSON file (or any object matching `ISessionDescription`). Example: `data/session/test-initiator.json`.
135
128
 
136
- ensure the repo dictionary has been unpacked above. This provides the data dictionary
137
- used to parse messages.
138
-
139
- run examples such as the trade capture, simple skeleton or http.
140
-
141
- ```shell
142
- npm run tcp-tc
143
- ```
144
-
145
- ```shell
146
- npm run tcp-sk
147
- ```
148
-
149
- included is an example fixml over http application where an order is submitted and execution report returned.
150
-
151
- ```shell
152
- npm run http-oms
129
+ ```json
130
+ {
131
+ "application": {
132
+ "type": "initiator",
133
+ "name": "test_client",
134
+ "reconnectSeconds": 10,
135
+ "tcp": { "host": "localhost", "port": 2344 },
136
+ "protocol": "ascii",
137
+ "dictionary": "repo44"
138
+ },
139
+ "Username": "js-client",
140
+ "Password": "pwd-client",
141
+ "EncryptMethod": 0,
142
+ "ResetSeqNumFlag": true,
143
+ "HeartBtInt": 30,
144
+ "SenderCompId": "init-comp",
145
+ "TargetCompID": "accept-comp",
146
+ "TargetSubID": "fix",
147
+ "BeginString": "FIX.4.4"
148
+ }
153
149
  ```
154
150
 
155
- ## tls SSL encryption
156
-
157
- see example data\session\test-initiator-tls.json
151
+ ### TLS
158
152
 
159
- to run the provided example tls trade capture a script such as script\getKey.ps1 can be run to generate self certified certificates. This is a powershell script that requires openssl in the tunnel e.g. /mingw64/bin/openssl. the ca field below is only required when using self certified and will not be needed for a third party vendor where certificates are provided. Set enableTrace flag whilst diagnosing the session.
153
+ Add a `tls` block under `application.tcp`. The `ca` field is only needed for self-signed certificates; commercial vendors will supply this for you. `script/getKey.ps1` will generate a self-signed CA + client/server pair (requires `openssl` on the path).
160
154
 
161
155
  ```json
162
156
  {
163
157
  "application": {
164
- "reconnectSeconds": 10,
165
158
  "type": "initiator",
166
159
  "name": "test_client",
167
160
  "tcp": {
168
- "host" : "localhost",
161
+ "host": "localhost",
169
162
  "port": 2344,
170
163
  "tls": {
171
164
  "timeout": 10000,
@@ -173,45 +166,66 @@ to run the provided example tls trade capture a script such as script\getKey.ps1
173
166
  "enableTrace": true,
174
167
  "key": "data/session/certs/client/client.key",
175
168
  "cert": "data/session/certs/client/client.crt",
176
- "ca": [
177
- "data/session/certs/ca/ca.crt"
178
- ]
169
+ "ca": ["data/session/certs/ca/ca.crt"]
179
170
  }
180
171
  },
181
172
  "protocol": "ascii",
182
173
  "dictionary": "repo44"
183
174
  },
184
- "Username": "js-tls-client",
185
- "Password": "pwd-tls-client",
186
- "EncryptMethod": 0,
187
- "ResetSeqNumFlag": true,
188
- "LastSentSeqNum": 10,
189
- "LastReceivedSeqNum": 11,
190
- "HeartBtInt": 30,
191
- "SenderCompId": "init-tls-comp",
192
- "TargetCompID": "accept-tls-comp",
193
- "TargetSubID": "fix",
194
- "BeginString": "FIX4.4",
195
- "BodyLengthChars": 6
175
+ "BeginString": "FIX4.4"
196
176
  }
197
177
  ```
198
178
 
199
- ## configure the body length field padding width
179
+ See `data/session/test-initiator-tls.json` for the complete file.
200
180
 
201
- see example initiator file data\session\test-initiator-tls.json.
181
+ ### Body length padding
202
182
 
203
- i.e. include field BodyLengthChars which defaults to 7 characters if omitted.
183
+ `BodyLengthChars` controls how the tag-9 body-length field is zero-padded. Defaults to `7`; set to a smaller value (e.g. `6`) when interoperating with a counterparty that requires it.
184
+
185
+ ```json
186
+ { "BodyLengthChars": 6 }
187
+ ```
188
+
189
+ ## Persistence & Recovery
190
+
191
+ By default, every session uses an in-memory message store — sequence numbers and any stored messages are lost when the process exits. For production use you'll typically want either persisted sequences or a full file-backed store with replay.
192
+
193
+ ### Choosing a store
194
+
195
+ Add a `store` block to the session description. Omit it to keep the in-memory default.
196
+
197
+ ```json
198
+ {
199
+ "store": { "type": "memory" }
200
+ }
201
+ ```
204
202
 
205
203
  ```json
206
204
  {
207
- "BodsyLengthChars": 6
205
+ "store": {
206
+ "type": "file",
207
+ "directory": "/var/fix/sessions"
208
+ }
208
209
  }
209
210
  ```
210
211
 
211
- ## launching without resetting message sequences
212
+ The file store writes QuickFIX-compatible files into the directory:
213
+
214
+ | File | Contents |
215
+ | --- | --- |
216
+ | `<session>.seqnums` | Current sender / target sequence numbers |
217
+ | `<session>.session` | Session creation timestamp |
218
+ | `<session>.header` | Index lines `seqnum,offset,length` into `.body` |
219
+ | `<session>.body` | Concatenated raw FIX messages for resend |
220
+
221
+ Session names are derived from `BeginString-SenderCompID-TargetCompID`.
212
222
 
213
- If the message sequences are persisted over multiple sessions and are not reset on logon (ie. `"ResetSeqNumFlag": false,`),
214
- then the previously used sequence numbers can be set as follows:
223
+ You can also pass a custom factory programmatically via `IJsFixConfig.sessionStoreFactory` useful for testing or for plugging in an alternative backend (Redis, S3, etc.).
224
+
225
+ ### `ResetSeqNumFlag` semantics
226
+
227
+ - `"ResetSeqNumFlag": true` — every logon resets sender/target sequences back to 1. The engine clears the persisted store before sending the Logon, so the message correctly carries `MsgSeqNum=1` even after a reconnect with recovered state. (See [issue #140](https://github.com/TimelordUK/jspurefix/issues/140).)
228
+ - `"ResetSeqNumFlag": false` — sequences are preserved across logons. To seed initial sequences for a brand-new session (no persisted store), set `LastSentSeqNum` and `LastReceivedSeqNum`:
215
229
 
216
230
  ```json
217
231
  {
@@ -221,21 +235,20 @@ then the previously used sequence numbers can be set as follows:
221
235
  }
222
236
  ```
223
237
 
224
- ## resending messages
238
+ With a file store configured, `LastSentSeqNum` / `LastReceivedSeqNum` are only consulted the first time a session is started; subsequent runs read from the persisted `.seqnums` file.
225
239
 
226
- By default, the library will not resend past messages as this requires persisting messages which depending on the volume,
227
- may also require a database. If you want to support resending you must override `AsciiSession.onResendRequest()` with a resending logic.
228
- Additionally, make sure to include the original message sequence and the duplicate message flag in the FIX object:
240
+ ### Resending messages
241
+
242
+ When a counterparty asks for missed messages, the engine needs access to the originals. The file store keeps every encoded message and the bundled `AsciiSession.onResendRequest()` will replay from it automatically. If you're using the in-memory store and want resend support, override `onResendRequest()` with your own retrieval logic and set the duplicate flag on each replayed message:
229
243
 
230
244
  ```typescript
231
245
  {
232
- ...messageBodyData,
233
- StandardHeader: { PossDupFlag: true, MsgSeqNum: sequenceNumber},
246
+ ...messageBodyData,
247
+ StandardHeader: { PossDupFlag: true, MsgSeqNum: sequenceNumber }
234
248
  }
235
-
236
249
  ```
237
250
 
238
- ### Example
251
+ Example payload:
239
252
 
240
253
  ```json
241
254
  {
@@ -252,229 +265,75 @@ Additionally, make sure to include the original message sequence and the duplica
252
265
  }
253
266
  ```
254
267
 
255
- ## Unit Tests
256
-
257
- there is a comprehensive suite of tests which can be run
258
-
259
- ```shell
260
- npm t
261
- ```
262
-
263
- ```bash
264
- PASS src/test/elastic-buffer.test.ts
265
- RUNS src/test/session.test.ts
266
- PASS src/test/encode-proxy.test.tsst.ts
267
- PASS src/test/execution-report.test.ts
268
- PASS src/test/view-decode.test.ts
269
- PASS src/test/ascii-encoder.test.ts
270
- PASS src/test/ascii-parser.test.ts
271
- PASS src/test/includes.test.ts
272
- PASS src/test/fixml-alloc-parse.test.ts (9.433s)
273
- PASS src/test/repo-full-fixml-msg.test.ts (6.025s)
274
- PASS src/test/fixml-mkt-data-settle-parse.test.ts (6.021s)
275
- PASS src/test/qf-full-msg.test.ts
276
- PASS src/test/logon.test.ts
277
- PASS src/test/fixml-mkt-data-fut-parse.test.ts (7.761s)
278
- PASS src/test/time-formatter.test.ts
279
- PASS src/test/ascii-segment.test.ts
280
- PASS src/test/session-state.test.ts
281
- PASS src/test/fixml-tc-bi-lateral-parse.test.ts (7.534s)
282
- PASS src/test/ascii-tag-pos.test.ts
283
- PASS src/test/fix-log-replay.test.ts
284
- PASS src/test/repo-full-ascii-msg.test.ts
285
- PASS src/test/session.test.ts (52.637s)
286
-
287
- Test Suites: 21 passed, 21 total
288
- Tests: 204 passed, 204 total
289
- Snapshots: 0 total
290
- Time: 54.606s, estimated 65s
291
- Ran all test suites.
292
- ```
293
-
294
- ## Dictionary Definitions
295
-
296
- base definitions on existing template e.g. quickfix format FIX44.xml
297
- create an alias in data/dictionary.json
298
- compile interfaces
299
-
300
- ```shell
301
- npm run cmd -- --dict=repo42 --compile
302
- ```
303
-
304
- use the alias in a session file e.g. data/session/test-httpInitiator.json
305
-
306
- ## sample trade-capture-client.ts
307
-
308
- the method onApplicationMsg is called when a message is received. In this case the client has inherited from AsciiSession which carries out the session management.
309
-
310
- ```typescript
311
- constructor (public readonly config: IJsFixConfig) {
312
- super(config)
313
- this.logReceivedMsgs = true
314
- this.reports = new Dictionary<ITradeCaptureReport>()
315
- this.fixLog = config.logFactory.plain(`jsfix.${config.description.application.name}.txt`)
316
- this.logger = config.logFactory.logger(`${this.me}:TradeCaptureClient`)
317
- }
318
-
319
- protected onApplicationMsg (msgType: string, view: MsgView): void {
320
- this.logger.info(`${view.toJson()}`)
321
- switch (msgType) {
322
- case MsgType.TradeCaptureReport: {
323
- // create an object and cast to the interface
324
- const tc: ITradeCaptureReport = view.toObject()
325
- this.reports.addUpdate(tc.TradeReportID, tc)
326
- this.logger.info(`[reports: ${this.reports.count()}] received tc ExecID = ${tc.ExecID} TradeReportID = ${tc.TradeReportID} Symbol = ${tc.Instrument.Symbol} ${tc.LastQty} @ ${tc.LastPx}`)
327
- break
328
- }
329
-
330
- case MsgType.TradeCaptureReportRequestAck: {
331
- const tc: ITradeCaptureReportRequestAck = view.toObject()
332
- this.logger.info(`received tcr ack ${tc.TradeRequestID} ${tc.TradeRequestStatus}`)
333
- break
334
- }
335
- }
336
- }
337
- ```
338
-
339
- the client onReady method is called when a connection is made and logon established and confirmed. In this case, client sends a trade capture request to the server.
340
-
341
- ```typescript
342
- protected onReady (view: MsgView): void {
343
- this.logger.info('ready')
344
- const tcr: ITradeCaptureReportRequest = TradeFactory.tradeCaptureReportRequest('all-trades', new Date())
345
- // send request to server
346
- this.send(MsgType.TradeCaptureReportRequest, tcr)
347
- const logoutSeconds = 32
348
- this.logger.info(`will logout after ${logoutSeconds}`)
349
- setTimeout(() => {
350
- this.done()
351
- }, logoutSeconds * 1000)
352
- }
353
-
354
- ```
355
-
356
- ## working with Views
357
-
358
- see src/test/view-decode.test.ts
359
-
360
-
361
- note that a view can only be used within a callback context unless it is cloned. Once returned, the memory is re-used for next message. It is intended to convert to an object or parsed into an application specific message.
268
+ ## Working with Messages
362
269
 
363
- fetch a group view
270
+ A `MsgView` is a zero-copy view over the parse buffer. The view is only valid inside the callback that received it — clone it (`view.clone()`) if you need to hold onto it past the current tick. Most code converts the view to a typed object via `toObject()`.
364
271
 
365
272
  ```typescript
366
- const noMDEntriesView: MsgView = view.getView('NoMDEntries')
367
- const mmEntryView: MsgView = noMDEntriesView.getGroupInstance(1)
368
-
369
- const mmEntryExpireTimeAsString: string = mmEntryView.getString('ExpireTime')
370
- expect(mmEntryExpireTimeAsString).toEqual('20180608-20:53:14.000')
371
- expect(mmEntryView.getString(126)).toEqual('20180608-20:53:14.000')
273
+ import { ITradeCaptureReport } from 'jspurefix/dist/types/FIX4.4/repo'
274
+ const tc: ITradeCaptureReport = view.toObject()
372
275
  ```
373
276
 
374
- fetch single tags
277
+ Read a single tag by name or number:
375
278
 
376
279
  ```typescript
377
- const erView: MsgView = views[0]
378
280
  expect(erView.getString(35)).toEqual('8')
379
281
  expect(erView.getString('MsgType')).toEqual('8')
380
- expect(erView.getString(8)).toEqual('FIX4.4')
381
282
  expect(erView.getTyped(9)).toEqual(6542)
382
283
  expect(erView.getTyped('TotNumReports')).toEqual(19404)
383
- expect(erView.getTyped('StrikePrice')).toEqual(52639)
384
284
  ```
385
285
 
386
- fetch repeated tag
286
+ Read several tags in one call:
387
287
 
388
288
  ```typescript
389
- const erView: MsgView = views[0]
390
- expect(erView.getStrings('PartyID')).toEqual(['magna.', 'iaculis', 'vitae,'])
289
+ const [a, b, c, d] = view.getTypedTags([8, 9, 35, 49])
391
290
  ```
392
291
 
393
- fetch a set of tags
292
+ Read all instances of a repeated tag:
394
293
 
395
294
  ```typescript
396
- const [a, b, c, d] = view.getTypedTags([8, 9, 35, 49])
397
- expect(a).toEqual('FIX4.4')
398
- expect(b).toEqual(2955)
399
- expect(c).toEqual('W')
400
- expect(d).toEqual('sender-10')
295
+ expect(erView.getStrings('PartyID')).toEqual(['magna.', 'iaculis', 'vitae,'])
401
296
  ```
402
297
 
403
- convert view into an object which can be used alongside an interface for intellisense.
298
+ Drill into a repeating group or component:
404
299
 
405
300
  ```typescript
406
- import { ITradeCaptureReport } from '../../../types/FIX4.4/repo/trade_capture_report'
407
- import { ITradeCaptureReportRequest } from '../../../types/FIX4.4/repo/trade_capture_report_request'
301
+ const noMDEntriesView: MsgView = view.getView('NoMDEntries')
302
+ const firstEntry: MsgView = noMDEntriesView.getGroupInstance(1)
303
+ const expireTime: string = firstEntry.getString('ExpireTime')
408
304
 
409
- const tc: ITradeCaptureReport = view.toObject()
305
+ const instrument: IInstrument = view.getView('Instrument').toObject()
410
306
  ```
411
307
 
412
- from data/examples/FIX.4.4/quickfix/execution-report
413
-
414
- get first in group fetched from object where group is array
308
+ Convert nested structures in one call:
415
309
 
416
310
  ```typescript
417
- import { IUndInstrmtGrp } from '../types/FIX4.4/quickfix/set/und_instrmt_grp'
418
- import { IUnderlyingInstrument } from '../types/FIX4.4/quickfix/set/underlying_instrument'
419
- const erView: MsgView = views[0]
420
- const undInstrmtGrpView: MsgView = erView.getView('UndInstrmtGrp')
421
- const undInstrmtGrpViewAsObject: IUndInstrmtGrp = undInstrmtGrpView.toObject()
422
- expect(undInstrmtGrpViewAsObject.NoUnderlyings.length).toEqual(2)
423
- const underlying0: IUnderlyingInstrument = undInstrmtGrpViewAsObject.NoUnderlyings[0].UnderlyingInstrument
424
- expect(underlying0.UnderlyingSymbol).toEqual('massa.')
311
+ const legGrp: IInstrumentLeg[] = view.getView('InstrmtLegGrp.NoLegs').toObject()
425
312
  ```
426
313
 
427
- fetch nested structure in one call
428
-
429
- ```typescript
430
- const legGrpView = view.getView('InstrmtLegGrp.NoLegs')
431
- expect(legGrpView).toBeTruthy()
432
- const legGrp: IInstrumentLeg[] = legGrpView.toObject()
433
- expect(legGrp).toBeTruthy()
434
- expect(Array.isArray(legGrp))
435
- expect(legGrp.length).toEqual(2)
436
- ```
437
-
438
- get a tokenised view of tags in view
314
+ Dump a tokenised view of every tag in a message:
439
315
 
440
316
  ```typescript
441
317
  console.log(view.toString())
442
318
  ```
443
319
 
444
- get a component from parent - this is very low cost
320
+ See `src/test/ascii/view-decode.test.ts` for many more examples.
445
321
 
446
- ```typescript
447
- const instrumentView: MsgView = view.getView('Instrument')
448
- const instrumentObject: IInstrument = view.getView('Instrument').toObject()
449
- ```
322
+ ## FIXML over HTTP
450
323
 
451
- ## FIXML
324
+ ASCII and FIXML sessions share the same `AsciiSession`-style application API — the framing is the only thing that changes. A small HTTP OMS demo lives at `src/sample/http/oms/`.
452
325
 
453
- Please see sample code src/sample/http for an example of how fixml can be used. Note regardless of using AsciiChars or Fixml application code looks very similar for example the client in this case.
326
+ Build an order:
454
327
 
455
328
  ```typescript
456
- protected onReady (view: MsgView): void {
457
- this.logger.info('onReady')
458
- const logoutSeconds = this.logoutSeconds
459
- const req = this.factory.createOrder('IBM', Side.Buy, 10000, 100.12)
460
- this.send('NewOrderSingle', req)
461
- this.logger.info(`will logout after ${logoutSeconds}`)
462
- setTimeout(() => {
463
- this.done()
464
- }, 11 * 1000)
465
- }
466
-
467
329
  public createOrder (symbol: string, side: Side, qty: number, price: number): INewOrderSingle {
468
- const id: number = this.id++
469
330
  return {
470
- ClOrdID: `Cli${id}`,
331
+ ClOrdID: `Cli${this.id++}`,
471
332
  Account: this.account,
472
333
  Side: side,
473
334
  Price: price,
474
335
  OrdType: OrdType.Limit,
475
- OrderQtyData: {
476
- OrderQty: qty
477
- } as IOrderQtyData,
336
+ OrderQtyData: { OrderQty: qty } as IOrderQtyData,
478
337
  Instrument: {
479
338
  Symbol: symbol,
480
339
  SecurityID: '459200101',
@@ -485,335 +344,154 @@ public createOrder (symbol: string, side: Side, qty: number, price: number): INe
485
344
  }
486
345
  ```
487
346
 
488
- this renders to this message sent over http
347
+ That renders to:
489
348
 
490
349
  ```xml
491
350
  <FIXML>
492
- <Order ID="Cli1" Acct="TradersRUs" Side="1" Typ="2" Px="100.12" TmInForce="1">
493
- <Hdr SID="accept-comp" TID="init-comp" SSub="user123" TSub="INC"/>
494
- <Instrmt Sym="IBM" ID="459200101" Src="4"/>
495
- <OrdQty Qty="10000"/>
496
- </Order>
351
+ <Order ID="Cli1" Acct="TradersRUs" Side="1" Typ="2" Px="100.12" TmInForce="1">
352
+ <Hdr SID="accept-comp" TID="init-comp" SSub="user123" TSub="INC"/>
353
+ <Instrmt Sym="IBM" ID="459200101" Src="4"/>
354
+ <OrdQty Qty="10000"/>
355
+ </Order>
497
356
  </FIXML>
498
357
  ```
499
358
 
500
- the server receives this message and sends back an execution report :-
359
+ The server receives the order and produces an execution report:
501
360
 
502
361
  ```typescript
503
- protected onApplicationMsg (msgType: string, view: MsgView): void {
504
- // dispatch messages
505
- this.logger.info(view.toJson())
506
- switch (msgType) {
507
- case 'Order': {
508
- const order: INewOrderSingle = view.toObject()
509
- this.logger.info(`received order id ${order.ClOrdID}`)
510
- const fill: IExecutionReport = this.factory.fillOrder(order)
511
- this.send('ExecutionReport', fill)
512
- }
362
+ protected onApplicationMsg (msgType: string, view: MsgView): void {
363
+ if (msgType === 'Order') {
364
+ const order: INewOrderSingle = view.toObject()
365
+ const fill: IExecutionReport = this.factory.fillOrder(order)
366
+ this.send('ExecutionReport', fill)
513
367
  }
514
368
  }
515
-
516
- public fillOrder (order: INewOrderSingle): IExecutionReport {
517
- const id: number = this.execId++
518
- return {
519
- ClOrdID: order.ClOrdID,
520
- OrdType: order.OrdType,
521
- TransactTime: new Date(),
522
- AvgPx: order.Price,
523
- LeavesQty: 0,
524
- LastPx: order.Price,
525
- ExecType: ExecType.OrderStatus,
526
- OrdStatus: OrdStatus.Filled,
527
- ExecID: `exec${id}`,
528
- Side: order.Side,
529
- Price: order.Price,
530
- OrderQtyData: {
531
- OrderQty: order.OrderQtyData.OrderQty
532
- } as IOrderQtyData,
533
- Instrument: {
534
- Symbol: order.Instrument.Symbol,
535
- SecurityID: order.Instrument.SecurityID,
536
- SecurityIDSource: SecurityIDSource.IsinNumber
537
- } as IInstrument
538
- } as IExecutionReport
539
- }
540
369
  ```
541
370
 
542
- the fixml is sent back to the client :-
371
+ Reply on the wire:
543
372
 
544
373
  ```xml
545
374
  <FIXML>
546
- <ExecRpt ID="Cli1" ExecID="exec1" ExecTyp="I" Stat="2" Side="1" Typ="2" Px="100.12" LastPx="100.12" LeavesQty="0" AvgPx="100.12" TxnTm="2018-10-07T12:16:12.584">
547
- <Hdr SID="accept-comp" TID="init-comp" TSub="fix"/>
548
- <Instrmt Sym="IBM" ID="459200101" Src="4"/>
549
- <OrdQty Qty="10000"/>
550
- </ExecRpt>
375
+ <ExecRpt ID="Cli1" ExecID="exec1" ExecTyp="I" Stat="2" Side="1" Typ="2"
376
+ Px="100.12" LastPx="100.12" LeavesQty="0" AvgPx="100.12"
377
+ TxnTm="2018-10-07T12:16:12.584">
378
+ <Hdr SID="accept-comp" TID="init-comp" TSub="fix"/>
379
+ <Instrmt Sym="IBM" ID="459200101" Src="4"/>
380
+ <OrdQty Qty="10000"/>
381
+ </ExecRpt>
551
382
  </FIXML>
552
383
  ```
553
384
 
554
- ## Performance
385
+ ## Data Dictionaries
555
386
 
556
- These messages have been randomly generated with command line tool. They are syntactically valid.
387
+ jspurefix ships definitions for FIX 4.0–4.4 and FIX 5.0 SP0/SP1/SP2 in both QuickFIX XML and FIX-repository formats, under bundled aliases such as `repo44`, `qf44`, `qf50sp2`. The alias map lives at `data/dictionary.json`.
557
388
 
558
- ### data/examples/FIX.4.4/repo/execution-report/fix.txt
389
+ To add a custom dialect:
559
390
 
560
- ```shell
561
- npm run repo44-bench-er
562
- ```
563
- ### performance on Windows Intel Core I7-4770 @ 3.5 GHz
564
- ```shell
565
- [8]: repeats = 250000, fields = 58, length = 604 chars, elapsed ms 3658, 14.632 micros per msg
566
- [8]: iterations = 80000, fields = 646, length = 6572 chars, elapsed ms 16499, 206.23749999999998 micros per msg
567
- ```
568
- ### performance on Windows 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
569
- ```shell
570
- [8]: iterations = 80000, fields = 646, length = 6572 chars, elapsed ms 7476, 93.45 micros per msg
571
- ```
391
+ 1. Drop your QuickFIX-style XML into `data/`.
392
+ 2. Add an alias to `data/dictionary.json`.
393
+ 3. Generate typed interfaces under `src/types`:
572
394
 
573
- ### data/examples/FIX.4.4/repo/security-definition/fix.txt
395
+ ```shell
396
+ npm run cmd -- --dict=repo42 --compile
397
+ ```
574
398
 
575
- ```shell
576
- npm run repo44-bench-sd
577
- ```
399
+ 4. Reference the alias from your session description (`"dictionary": "repo42"`).
578
400
 
579
- ### performance on Windows Intel Core I7-4770 @ 3.5 GHz
580
- ```shell
581
- [d]: repeats = 150000, fields = 223, length = 2233 chars, elapsed ms 7962, 53.080000000000005 micros per msg
582
- d]: iterations = 150000, fields = 229, length = 2466 chars, elapsed ms 8672, 57.81333333333333 micros per msg
583
- ```
584
- ### performance on Windows 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
585
- ```
586
- [d]: iterations = 150000, fields = 229, length = 2466 chars, elapsed ms 4628, 30.85333333333333 micros per msg
587
- ```
401
+ See [jspf-md-demo](https://github.com/TimelordUK/jspf-md-demo) for a worked example.
588
402
 
589
- ### data/examples/FIX.4.4/repo/trade-capture/fix.txt
403
+ ## `jsfix` CLI — log parsing & stats
404
+
405
+ The `jsfix-cmd` tool parses any FIX log given an appropriate dictionary.
406
+
407
+ Token dump for a specific message type:
590
408
 
591
409
  ```shell
592
- npm run repo44-bench-tc
593
- ```
594
- ### performance on Windows Intel Core I7-4770 @ 3.5 GHz
595
- ```shell
596
- [AE]: repeats = 30000, fields = 613, length = 5818 chars, elapsed ms 5206, 173.53333333333333 micros per msg
597
- [AE]: iterations = 30000, fields = 578, length = 5741 chars, elapsed ms 5245, 174.83333333333334 micros per msg```
410
+ npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --type=AD --tokens
598
411
  ```
599
412
 
600
- ### performance on Windows 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
601
- ```shell
602
- [AE]: iterations = 30000, fields = 578, length = 5741 chars, elapsed ms 2725, 90.83333333333333 micros per msg```
413
+ ```
414
+ [0] 8 (BeginString) = FIX4.4, [1] 9 (BodyLength) = 0000135
415
+ [2] 35 (MsgType) = AD[TradeCaptureReportRequest], [3] 49 (SenderCompID) = init-comp
416
+ [4] 56 (TargetCompID) = accept-comp, [5] 34 (MsgSeqNum) = 2
417
+ ...
603
418
  ```
604
419
 
605
- ### data/examples/FIX.4.4/quickfix/heartbeat/fix.txt
420
+ Per-type message counts for the file:
606
421
 
607
422
  ```shell
608
- npm run qf-bench-hb
423
+ npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --stats
609
424
  ```
610
425
 
611
- ### performance on Windows Intel Core I7-4770 @ 3.5 GHz
612
- ```shell
613
- [0]: iterations = 250000, fields = 10, length = 131 chars, elapsed ms 950, 3.8 micros per msg
426
+ ```json
427
+ { "0": 1, "5": 2, "A": 2, "AD": 1, "AQ": 2, "AE": 5 }
614
428
  ```
615
- ### performance on Windows 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
616
- ```shell
617
- [0]: iterations = 250000, fields = 10, length = 131 chars, elapsed ms 468, 1.8719999999999999 micros per msg
618
- ````
619
429
 
620
- ### data/examples/FIX.4.4/quickfix/logon/fix.txt
430
+ Convert to typed objects:
621
431
 
622
432
  ```shell
623
- npm run qf-bench-lo
433
+ npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --type=AD --objects
624
434
  ```
625
435
 
626
- ### performance on Windows Intel Core I7-4770 @ 3.5 GHz
436
+ Show the parser's view of nested structures within a message:
437
+
627
438
  ```shell
628
- [A]: iterations = 250000, fields = 22, length = 214 chars, elapsed ms 1466, 5.864 micros per msg
439
+ npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --type=AD --structures
629
440
  ```
630
- ### performance on Windows 12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
441
+
442
+ Repeat-parse for benchmarking (`--repeats=N`):
443
+
631
444
  ```shell
632
- [A]: iterations = 250000, fields = 22, length = 214 chars, elapsed ms 693, 2.7720000000000002 micros per msg
445
+ npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --stats --repeats=20
633
446
  ```
634
447
 
635
- ## Log parsing
448
+ ## Performance
636
449
 
637
- the command line tool jsfix can be used to parse any fix log providing an appropriate dictionary is provided.
450
+ Numbers below are illustrative generated messages, single-threaded, parser-only (no I/O). Run them yourself with the corresponding `npm run` script.
638
451
 
639
- ## parsing fields
452
+ | Benchmark | Script | Fields/msg | Length (chars) | I7-4770 @ 3.5 GHz | i7-12700H @ 2.3 GHz | Ryzen 9 7950X @ 4.5 GHz |
453
+ | --- | --- | --- | --- | --- | --- | --- |
454
+ | Heartbeat | `npm run qf-bench-hb` | 10 | 131 | 3.8 µs/msg | 1.9 µs/msg | 1.6 µs/msg |
455
+ | Logon | `npm run qf-bench-lo` | 22 | 214 | 5.9 µs/msg | 2.8 µs/msg | 2.3 µs/msg |
456
+ | Execution Report (large) | `npm run repo44-bench-er` | 646 | 6 571 | 206.2 µs/msg | 93.5 µs/msg | 72.9 µs/msg |
457
+ | Security Definition † | `npm run repo44-bench-sd` | 52 | 557 | — | — | 5.6 µs/msg |
458
+ | Trade Capture † | `npm run repo44-bench-tc` | 112 | 1 137 | — | — | 9.3 µs/msg |
640
459
 
641
- ```shell
642
- npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --type=AD --tokens
643
- ```
460
+ † The SD and TC fixtures have shrunk since the older measurements were taken (SD was 229 fields / 2 466 chars, TC was 578 fields / 5 741 chars), so the prior numbers aren't comparable with the current fixture and have been dropped from the row. Re-running them on the older hardware would produce a clean third column. Ryzen measurements taken on Node 24 LTS, WSL2.
644
461
 
645
- ```shell
646
- [0] 8 (BeginString) = FIX4.4, [1] 9 (BodyLength) = 0000135
647
- [2] 35 (MsgType) = AD[TradeCaptureReportRequest], [3] 49 (SenderCompID) = init-comp
648
- [4] 56 (TargetCompID) = accept-comp, [5] 34 (MsgSeqNum) = 2
649
- [6] 57 (TargetSubID) = fix, [7] 52 (SendingTime) = 20180923-16:07:04.763
650
- [8] 568 (TradeRequestID) = all-trades, [9] 569 (TradeRequestType) = 0[AllTrades]
651
- [10] 263 (SubscriptionRequestType) = 1[SnapshotAndUpdates], [11] 580 (NoDates) = 1
652
- [12] 75 (TradeDate) = 20180923, [13] 10 (CheckSum) = 250
653
- ```
462
+ ## Developing on jspurefix
654
463
 
655
- ## stats on entire file
464
+ Clone and build:
656
465
 
657
466
  ```shell
658
- npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --stats
467
+ git clone https://github.com/TimelordUK/jspurefix.git
468
+ cd jspurefix
469
+ npm install # postinstall unpacks the FIX dictionaries
470
+ npm run build
659
471
  ```
660
472
 
661
- ```json
662
- messages 13 elapsed ms 8
663
- {
664
- "0": 1,
665
- "5": 2,
666
- "A": 2,
667
- "AD": 1,
668
- "AQ": 2,
669
- "AE": 5
670
- }
671
- ```
672
-
673
- ## benchmark parsing repeated reads of file
473
+ Run the test suite (Jest, single worker, with coverage):
674
474
 
675
- ```cmd
676
- npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --stats --repeats=20
475
+ ```shell
476
+ npm test
677
477
  ```
678
478
 
679
- ```json
680
- messages 13 elapsed ms 0
681
- {
682
- "0": 1,
683
- "5": 2,
684
- "A": 2,
685
- "AD": 1,
686
- "AQ": 2,
687
- "AE": 5
688
- }
689
- ```
479
+ The full suite currently runs 535 tests across 43 suites and takes ~70 s on a modern laptop. `script/build.sh` (unix) and `script\build.cmd` (windows) wrap install + build + test if you want a one-shot bootstrap.
690
480
 
691
- ## parse message type in a file
481
+ Try a sample end-to-end:
692
482
 
693
- ```cmd
694
- npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --type=AD --objects
695
- ```
696
-
697
- ```json
698
- {
699
- "StandardHeader": {
700
- "BeginString": "FIX4.4",
701
- "BodyLength": 135,
702
- "MsgType": "AD",
703
- "SenderCompID": "init-comp",
704
- "TargetCompID": "accept-comp",
705
- "MsgSeqNum": 2,
706
- "TargetSubID": "fix",
707
- "SendingTime": "2018-09-23T16:07:04.763Z"
708
- },
709
- "TradeRequestID": "all-trades",
710
- "TradeRequestType": 0,
711
- "SubscriptionRequestType": "1",
712
- "TrdCapDtGrp": [
713
- {
714
- "TradeDate": "2018-09-22T23:00:00.000Z"
715
- }
716
- ],
717
- "StandardTrailer": {
718
- "CheckSum": "250"
719
- }
720
- }
483
+ ```shell
484
+ npm run tcp-tc # trade-capture client + server
721
485
  ```
722
486
 
723
- as above where --type=AE --objects
487
+ ## C# Port
724
488
 
725
- ```json
726
- {
727
- "StandardHeader": {
728
- "BeginString": "FIX4.4",
729
- "BodyLength": 213,
730
- "MsgType": "AE",
731
- "SenderCompID": "accept-comp",
732
- "TargetCompID": "init-comp",
733
- "MsgSeqNum": 4,
734
- "TargetSubID": "fix",
735
- "SendingTime": "2018-09-23T16:07:04.986Z"
736
- },
737
- "TradeReportID": "100001",
738
- "TradeReportTransType": 0,
739
- "TradeReportType": 0,
740
- "TrdType": 0,
741
- "ExecID": "600001",
742
- "OrdStatus": "2",
743
- "PreviouslyReported": false,
744
- "Instrument": {
745
- "Symbol": "Gold",
746
- "SecurityID": "Gold.INC"
747
- },
748
- "LastQty": 107,
749
- "LastPx": 45.38,
750
- "TradeDate": "2018-09-22T23:00:00.000Z",
751
- "TransactTime": "2018-09-23T16:07:04.776Z",
752
- "StandardTrailer": {
753
- "CheckSum": "54"
754
- }
755
- }
756
- ```
489
+ This engine has been ported to C# as [cspurefix](https://github.com/TimelordUK/cspurefix), which is kept in lockstep with this codebase. If you're on .NET:
757
490
 
758
- ## show all structures within a message
491
+ - **Source**: [TimelordUK/cspurefix](https://github.com/TimelordUK/cspurefix)
492
+ - **Demo**: [TimelordUK/purefix-standalone-demo](https://github.com/TimelordUK/purefix-standalone-demo)
493
+ - **NuGet**: [PureFix.Types.Core](https://www.nuget.org/packages/PureFix.Types.Core/)
759
494
 
760
- ```shell
761
- npm run cmd -- --dict=repo44 --fix=data/examples/FIX.4.4/jsfix.test_client.txt --delimiter="|" --type=AD --structures
762
- ```
495
+ ## License
763
496
 
764
- ```json
765
- [
766
- {
767
- "name": "StandardHeader",
768
- "depth": 2,
769
- "startTag": 8,
770
- "startPosition": 0,
771
- "endTag": 52,
772
- "endPosition": 7,
773
- "delimiterTag": 0,
774
- "delimiterPositions": []
775
- },
776
- {
777
- "name": "TrdCapDtGrp",
778
- "depth": 1,
779
- "startTag": 580,
780
- "startPosition": 11,
781
- "endTag": 75,
782
- "endPosition": 12,
783
- "delimiterTag": 75,
784
- "delimiterPositions": [
785
- 12
786
- ]
787
- },
788
- {
789
- "name": "StandardTrailer",
790
- "depth": 1,
791
- "startTag": 10,
792
- "startPosition": 13,
793
- "endTag": 10,
794
- "endPosition": 13,
795
- "delimiterTag": 0,
796
- "delimiterPositions": []
797
- },
798
- {
799
- "name": "TradeCaptureReportRequest",
800
- "depth": 1,
801
- "startTag": 8,
802
- "startPosition": 0,
803
- "endTag": 10,
804
- "endPosition": 13,
805
- "delimiterTag": 0,
806
- "delimiterPositions": []
807
- },
808
- {
809
- "name": "StandardTrailer",
810
- "depth": 0,
811
- "startTag": 10,
812
- "startPosition": 14,
813
- "endTag": 10,
814
- "endPosition": 13,
815
- "delimiterTag": 0,
816
- "delimiterPositions": []
817
- }
818
- ]
819
- ```
497
+ MIT. See [LICENSE](./LICENSE).