jspurefix 5.11.2 → 5.11.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.
Files changed (104) hide show
  1. package/.gitattributes +4 -0
  2. package/dist/buffer/index.d.ts +6 -0
  3. package/dist/buffer/index.js +6 -0
  4. package/dist/buffer/index.js.map +1 -1
  5. package/dist/collections/index.d.ts +1 -0
  6. package/dist/collections/index.js +16 -0
  7. package/dist/collections/index.js.map +1 -1
  8. package/dist/config/winston-logger.js +2 -0
  9. package/dist/config/winston-logger.js.map +1 -1
  10. package/dist/dictionary/contained/index.d.ts +3 -0
  11. package/dist/dictionary/contained/index.js +3 -0
  12. package/dist/dictionary/contained/index.js.map +1 -1
  13. package/dist/dictionary/index.d.ts +5 -0
  14. package/dist/dictionary/index.js +5 -0
  15. package/dist/dictionary/index.js.map +1 -1
  16. package/dist/generator/applicability.d.ts +3 -0
  17. package/dist/generator/applicability.js +48 -0
  18. package/dist/generator/applicability.js.map +1 -0
  19. package/dist/generator/case-builder.d.ts +34 -0
  20. package/dist/generator/case-builder.js +111 -0
  21. package/dist/generator/case-builder.js.map +1 -0
  22. package/dist/generator/corpus-store.d.ts +43 -0
  23. package/dist/generator/corpus-store.js +115 -0
  24. package/dist/generator/corpus-store.js.map +1 -0
  25. package/dist/generator/index.d.ts +12 -0
  26. package/dist/generator/index.js +29 -0
  27. package/dist/generator/index.js.map +1 -0
  28. package/dist/generator/instrument-universe.d.ts +54 -0
  29. package/dist/generator/instrument-universe.js +286 -0
  30. package/dist/generator/instrument-universe.js.map +1 -0
  31. package/dist/generator/layout.d.ts +46 -0
  32. package/dist/generator/layout.js +208 -0
  33. package/dist/generator/layout.js.map +1 -0
  34. package/dist/generator/market-context.d.ts +64 -0
  35. package/dist/generator/market-context.js +119 -0
  36. package/dist/generator/market-context.js.map +1 -0
  37. package/dist/generator/outcome.d.ts +16 -0
  38. package/dist/generator/outcome.js +71 -0
  39. package/dist/generator/outcome.js.map +1 -0
  40. package/dist/generator/realistic-generator.d.ts +53 -0
  41. package/dist/generator/realistic-generator.js +275 -0
  42. package/dist/generator/realistic-generator.js.map +1 -0
  43. package/dist/generator/rng.d.ts +12 -0
  44. package/dist/generator/rng.js +60 -0
  45. package/dist/generator/rng.js.map +1 -0
  46. package/dist/generator/scatter.d.ts +50 -0
  47. package/dist/generator/scatter.js +285 -0
  48. package/dist/generator/scatter.js.map +1 -0
  49. package/dist/generator/scenarios.d.ts +13 -0
  50. package/dist/generator/scenarios.js +139 -0
  51. package/dist/generator/scenarios.js.map +1 -0
  52. package/dist/generator/value-conventions.d.ts +15 -0
  53. package/dist/generator/value-conventions.js +217 -0
  54. package/dist/generator/value-conventions.js.map +1 -0
  55. package/dist/index.d.ts +6 -0
  56. package/dist/index.js +4 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/jsfix-cmd.d.ts +5 -0
  59. package/dist/jsfix-cmd.js +161 -0
  60. package/dist/jsfix-cmd.js.map +1 -1
  61. package/dist/store/fix-msg-ascii-store-resend.js +2 -2
  62. package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
  63. package/dist/store/fix-msg-store-record.d.ts +1 -1
  64. package/dist/store/fix-msg-store-record.js.map +1 -1
  65. package/dist/store/index.d.ts +1 -0
  66. package/dist/store/index.js +1 -0
  67. package/dist/store/index.js.map +1 -1
  68. package/dist/transport/index.d.ts +7 -0
  69. package/dist/transport/index.js +7 -0
  70. package/dist/transport/index.js.map +1 -1
  71. package/dist/transport/session/index.d.ts +1 -0
  72. package/dist/transport/session/index.js +1 -0
  73. package/dist/transport/session/index.js.map +1 -1
  74. package/docs/generated-cases.md +237 -0
  75. package/docs/instrumentation.md +14 -0
  76. package/docs/scattered-components.md +60 -5
  77. package/jsfix.test_client.txt +62 -62
  78. package/jsfix.test_server.txt +72 -72
  79. package/package.json +5 -2
  80. package/src/buffer/index.ts +6 -0
  81. package/src/collections/index.ts +1 -1
  82. package/src/config/winston-logger.ts +8 -0
  83. package/src/dictionary/contained/index.ts +3 -0
  84. package/src/dictionary/index.ts +5 -0
  85. package/src/generator/applicability.ts +87 -0
  86. package/src/generator/case-builder.ts +167 -0
  87. package/src/generator/corpus-store.ts +166 -0
  88. package/src/generator/index.ts +12 -0
  89. package/src/generator/instrument-universe.ts +347 -0
  90. package/src/generator/layout.ts +279 -0
  91. package/src/generator/market-context.ts +207 -0
  92. package/src/generator/outcome.ts +102 -0
  93. package/src/generator/realistic-generator.ts +404 -0
  94. package/src/generator/rng.ts +92 -0
  95. package/src/generator/scatter.ts +432 -0
  96. package/src/generator/scenarios.ts +167 -0
  97. package/src/generator/value-conventions.ts +295 -0
  98. package/src/index.ts +12 -0
  99. package/src/jsfix-cmd.ts +204 -0
  100. package/src/store/fix-msg-ascii-store-resend.ts +2 -1
  101. package/src/store/fix-msg-store-record.ts +4 -1
  102. package/src/store/index.ts +1 -0
  103. package/src/transport/index.ts +10 -0
  104. package/src/transport/session/index.ts +1 -0
@@ -0,0 +1,279 @@
1
+ import { ILooseObject } from '../collections/collection'
2
+ import {
3
+ ContainedComponentField,
4
+ ContainedField,
5
+ ContainedFieldType,
6
+ ContainedGroupField,
7
+ ContainedSimpleField,
8
+ IContainedSet
9
+ } from '../dictionary/contained'
10
+ import { TagType } from '../buffer/tag/tag-type'
11
+
12
+ /**
13
+ * Where every tag on the wire came from in the dictionary.
14
+ *
15
+ * The parser answers this question from position, which is exactly the assumption the
16
+ * scattered component work is about, so it cannot be the source of truth here. Instead
17
+ * the layout is built by walking the object and the dictionary in lock step with
18
+ * `AsciiEncoder.encodeObject` - same field ordering, same treatment of components,
19
+ * groups and raw data - and consuming one token per emission. The walk is not a parse:
20
+ * it knows what the encoder was about to write before it wrote it.
21
+ *
22
+ * If the two ever disagree the token stream will not line up and `Layout.build` throws,
23
+ * which makes this a standing check that the mirror is still faithful.
24
+ */
25
+
26
+ export interface IToken {
27
+ readonly tag: number
28
+ /** the `tag=value` text exactly as encoded, without the delimiter */
29
+ readonly text: string
30
+ }
31
+
32
+ export enum LayoutKind {
33
+ Message = 'message',
34
+ Component = 'component',
35
+ Group = 'group',
36
+ Instance = 'instance',
37
+ Field = 'field'
38
+ }
39
+
40
+ export class LayoutNode {
41
+ public readonly children: LayoutNode[] = []
42
+
43
+ constructor (
44
+ public readonly kind: LayoutKind,
45
+ public readonly name: string,
46
+ public readonly path: string,
47
+ /** for a field, its token; for a group, the token carrying NoXXX */
48
+ public readonly tokenIndex: number = -1,
49
+ public readonly set: IContainedSet | null = null,
50
+ /** a raw data field is written as a Length token and then the data token */
51
+ public readonly extraTokenIndex: number = -1) {
52
+ }
53
+
54
+ /** token indices below this node, in the order the encoder wrote them */
55
+ public canonical (into: number[] = []): number[] {
56
+ if (this.tokenIndex >= 0) into.push(this.tokenIndex)
57
+ if (this.extraTokenIndex >= 0) into.push(this.extraTokenIndex)
58
+ for (const c of this.children) {
59
+ c.canonical(into)
60
+ }
61
+ return into
62
+ }
63
+
64
+ public get isLevel (): boolean {
65
+ return this.kind === LayoutKind.Message || this.kind === LayoutKind.Instance
66
+ }
67
+ }
68
+
69
+ /**
70
+ * split a wire message into tokens. Values containing the delimiter would defeat this,
71
+ * which is why the generator never produces one.
72
+ */
73
+ export function tokenise (wire: string, delimiter: string): IToken[] {
74
+ const parts = wire.split(delimiter)
75
+ const tokens: IToken[] = []
76
+ for (const p of parts) {
77
+ if (p.length === 0) continue
78
+ const eq = p.indexOf('=')
79
+ if (eq <= 0) continue
80
+ tokens.push({ tag: parseInt(p.substring(0, eq), 10), text: p })
81
+ }
82
+ return tokens
83
+ }
84
+
85
+ export function assemble (tokens: readonly IToken[], order: readonly number[], delimiter: string): string {
86
+ let out = ''
87
+ for (const i of order) {
88
+ out += tokens[i].text
89
+ out += delimiter
90
+ }
91
+ return out
92
+ }
93
+
94
+ export interface ILayout {
95
+ readonly root: LayoutNode
96
+ readonly tokens: readonly IToken[]
97
+ /** index of the first body token; everything before it is the standard header */
98
+ readonly bodyStart: number
99
+ /** index one past the last body token; everything from here is the trailer */
100
+ readonly bodyEnd: number
101
+ }
102
+
103
+ export class Layout {
104
+ private index: number = 0
105
+
106
+ private constructor (start: number) {
107
+ this.index = start
108
+ }
109
+
110
+ /**
111
+ * Build the layout of `object` encoded as `wire`.
112
+ *
113
+ * The header and trailer are written by the transmitter rather than from the object,
114
+ * so their extent is discovered rather than walked: the body is located by matching
115
+ * the tag sequence the walk predicts against the token stream.
116
+ */
117
+ public static build (
118
+ wire: string,
119
+ delimiter: string,
120
+ object: ILooseObject,
121
+ set: IContainedSet): ILayout {
122
+ const tokens = tokenise(wire, delimiter)
123
+ const expected = Layout.expectedTags(object, set)
124
+ const bodyStart = Layout.locate(tokens, expected)
125
+ const builder = new Layout(bodyStart)
126
+ const root = new LayoutNode(LayoutKind.Message, set.name, set.name, -1, set)
127
+ builder.walk(object, set, root, set.name)
128
+ return {
129
+ root,
130
+ tokens,
131
+ bodyStart,
132
+ bodyEnd: builder.index
133
+ }
134
+ }
135
+
136
+ /**
137
+ * the tag sequence the encoder will write for this object, used only to find where
138
+ * the body begins
139
+ */
140
+ private static expectedTags (object: ILooseObject, set: IContainedSet): number[] {
141
+ const out: number[] = []
142
+ const collect = (o: ILooseObject, s: IContainedSet): void => {
143
+ for (const field of Layout.presentFields(o, s)) {
144
+ switch (field.type) {
145
+ case ContainedFieldType.Simple: {
146
+ const sf = field as ContainedSimpleField
147
+ if (sf.definition.tagType === TagType.RawData && o[Layout.lengthName(s, sf)] == null) {
148
+ out.push(Layout.lengthTag(s, sf))
149
+ }
150
+ out.push(sf.definition.tag)
151
+ break
152
+ }
153
+ case ContainedFieldType.Component: {
154
+ const cf = field as ContainedComponentField
155
+ collect(o[cf.name] as ILooseObject, cf.definition)
156
+ break
157
+ }
158
+ case ContainedFieldType.Group: {
159
+ const gf = field as ContainedGroupField
160
+ const nof = gf.definition.noOfField
161
+ if (!nof) break
162
+ out.push(nof.tag)
163
+ const instances = Layout.instancesOf(o, gf)
164
+ for (const instance of instances) {
165
+ collect(instance, gf.definition)
166
+ }
167
+ break
168
+ }
169
+ default:
170
+ break
171
+ }
172
+ }
173
+ }
174
+ collect(object, set)
175
+ return out
176
+ }
177
+
178
+ private static locate (tokens: readonly IToken[], expected: readonly number[]): number {
179
+ if (expected.length === 0) return 0
180
+ for (let start = 0; start + expected.length <= tokens.length; ++start) {
181
+ let ok = true
182
+ for (let i = 0; i < expected.length; ++i) {
183
+ if (tokens[start + i].tag !== expected[i]) {
184
+ ok = false
185
+ break
186
+ }
187
+ }
188
+ if (ok) return start
189
+ }
190
+ throw new Error('layout walk does not match the encoded message - the mirror of AsciiEncoder has drifted')
191
+ }
192
+
193
+ /**
194
+ * the fields of `set` that `o` actually carries, in the order the encoder writes them.
195
+ * This is `AsciiEncoder.getFields`, deliberately duplicated rather than shared, because
196
+ * the encoder is a hot path and should not grow a seam for a test tool.
197
+ */
198
+ private static presentFields (o: ILooseObject, set: IContainedSet): ContainedField[] {
199
+ const fields: ContainedField[] = []
200
+ for (const key of Object.keys(o)) {
201
+ const field = set.localNameToField.get(key)
202
+ if (field) fields.push(field)
203
+ }
204
+ fields.sort((a, b) => a.position - b.position)
205
+ return fields
206
+ }
207
+
208
+ private static instancesOf (o: ILooseObject, gf: ContainedGroupField): ILooseObject[] {
209
+ const nof = gf.definition.noOfField
210
+ const instances = o[gf.name] ?? (nof ? o[nof.name] : null)
211
+ return Array.isArray(instances) ? instances : []
212
+ }
213
+
214
+ private static lengthField (set: IContainedSet, sf: ContainedSimpleField): ContainedSimpleField | null {
215
+ const prior = set.fields[sf.position - 1]
216
+ if (!prior || prior.type !== ContainedFieldType.Simple) return null
217
+ return prior as ContainedSimpleField
218
+ }
219
+
220
+ private static lengthName (set: IContainedSet, sf: ContainedSimpleField): string {
221
+ return Layout.lengthField(set, sf)?.name ?? ''
222
+ }
223
+
224
+ private static lengthTag (set: IContainedSet, sf: ContainedSimpleField): number {
225
+ return Layout.lengthField(set, sf)?.definition.tag ?? 0
226
+ }
227
+
228
+ private walk (o: ILooseObject, set: IContainedSet, parent: LayoutNode, path: string): void {
229
+ for (const field of Layout.presentFields(o, set)) {
230
+ switch (field.type) {
231
+ case ContainedFieldType.Simple: {
232
+ const sf = field as ContainedSimpleField
233
+ let lengthIndex = -1
234
+ if (sf.definition.tagType === TagType.RawData && o[Layout.lengthName(set, sf)] == null) {
235
+ lengthIndex = this.index++
236
+ }
237
+ const dataIndex = this.index++
238
+ // the length and its data are one indivisible unit - separating them would
239
+ // produce a message that is not merely scattered but unparseable
240
+ parent.children.push(new LayoutNode(
241
+ LayoutKind.Field,
242
+ sf.name,
243
+ `${path}.${sf.name}`,
244
+ lengthIndex >= 0 ? lengthIndex : dataIndex,
245
+ null,
246
+ lengthIndex >= 0 ? dataIndex : -1))
247
+ break
248
+ }
249
+ case ContainedFieldType.Component: {
250
+ const cf = field as ContainedComponentField
251
+ const node = new LayoutNode(
252
+ LayoutKind.Component, cf.name, `${path}.${cf.name}`, -1, cf.definition)
253
+ parent.children.push(node)
254
+ this.walk(o[cf.name] as ILooseObject, cf.definition, node, node.path)
255
+ break
256
+ }
257
+ case ContainedFieldType.Group: {
258
+ const gf = field as ContainedGroupField
259
+ const nof = gf.definition.noOfField
260
+ if (!nof) break
261
+ const countIndex = this.index++
262
+ const node = new LayoutNode(
263
+ LayoutKind.Group, gf.name, `${path}.${gf.name}`, countIndex, gf.definition)
264
+ parent.children.push(node)
265
+ const instances = Layout.instancesOf(o, gf)
266
+ for (let i = 0; i < instances.length; ++i) {
267
+ const instance = new LayoutNode(
268
+ LayoutKind.Instance, `${gf.name}[${i}]`, `${node.path}[${i}]`, -1, gf.definition)
269
+ node.children.push(instance)
270
+ this.walk(instances[i], gf.definition, instance, instance.path)
271
+ }
272
+ break
273
+ }
274
+ default:
275
+ break
276
+ }
277
+ }
278
+ }
279
+ }
@@ -0,0 +1,207 @@
1
+ import { Rng } from './rng'
2
+ import { AssetClass, IInstrumentSpec, ILegSpec, InstrumentUniverse } from './instrument-universe'
3
+
4
+ /**
5
+ * Everything one generated message agrees about.
6
+ *
7
+ * The old generator drew each field independently, which is why its output never added
8
+ * up - an OrderQty of -483.04 filled at a price of 97319 on a security called `Nunc`.
9
+ * A message is a statement about a single trade, so the trade is decided once, up front,
10
+ * and every field is then read off it.
11
+ *
12
+ * The walker moves this context as it descends: entering a leg group instance sets
13
+ * `leg`, so `LegPrice` is drawn from that leg rather than from the parent.
14
+ */
15
+
16
+ export interface IParty {
17
+ readonly id: string
18
+ readonly idSource: string
19
+ readonly role: number
20
+ readonly desc: string
21
+ }
22
+
23
+ export interface IMarketContextOptions {
24
+ readonly rng: Rng
25
+ readonly instrument?: IInstrumentSpec
26
+ readonly assetClass?: AssetClass
27
+ /** anchor for every date in the message; defaults to today */
28
+ readonly asOf?: Date
29
+ readonly senderCompId?: string
30
+ readonly targetCompId?: string
31
+ }
32
+
33
+ const firms: readonly IParty[] = [
34
+ { id: 'MGSTGB2L', idSource: 'C', role: 1, desc: 'executing firm' },
35
+ { id: 'CHASGB2L', idSource: 'C', role: 4, desc: 'clearing firm' },
36
+ { id: 'BARCGB22', idSource: 'C', role: 17, desc: 'contra firm' },
37
+ { id: 'ABNANL2A', idSource: 'C', role: 13, desc: 'order origination firm' }
38
+ ]
39
+
40
+ const traders: readonly IParty[] = [
41
+ { id: 'jhalliday', idSource: 'D', role: 12, desc: 'executing trader' },
42
+ { id: 'rmcdowall', idSource: 'D', role: 11, desc: 'order origination trader' },
43
+ { id: 'kbannerjee', idSource: 'D', role: 12, desc: 'executing trader' }
44
+ ]
45
+
46
+ const accounts: readonly string[] = [
47
+ 'ACCT-00417', 'ACCT-01925', 'OMNI-4471', 'SEGR-2210'
48
+ ]
49
+
50
+ const desks: readonly string[] = ['ENERGY', 'MACRO', 'CASH-EQ', 'FICC']
51
+
52
+ const freeText: readonly string[] = [
53
+ 'give up to clearer',
54
+ 'client instruction on file',
55
+ 'worked over the close',
56
+ 'cross with house book',
57
+ 'blocked - settle T+2'
58
+ ]
59
+
60
+ export class MarketContext {
61
+ public readonly rng: Rng
62
+ public readonly instrument: IInstrumentSpec
63
+ public readonly asOf: Date
64
+
65
+ /** the trade this message describes */
66
+ public readonly side: string
67
+ public readonly orderQty: number
68
+ public readonly price: number
69
+ public readonly lastPx: number
70
+ public readonly lastQty: number
71
+ public readonly cumQty: number
72
+ public readonly leavesQty: number
73
+ public readonly avgPx: number
74
+
75
+ public readonly tradeDate: Date
76
+ public readonly transactTime: Date
77
+ public readonly settlDate: Date
78
+
79
+ public readonly clOrdId: string
80
+ public readonly origClOrdId: string
81
+ public readonly orderId: string
82
+ public readonly execId: string
83
+ public readonly tradeId: string
84
+ public readonly account: string
85
+ public readonly desk: string
86
+ public readonly senderCompId: string
87
+ public readonly targetCompId: string
88
+
89
+ public readonly firms: readonly IParty[]
90
+ public readonly traders: readonly IParty[]
91
+
92
+ /** set while the walker is inside a leg, cleared on the way out */
93
+ public leg: ILegSpec | null = null
94
+ /** set while the walker is inside an underlying */
95
+ public underlying: IInstrumentSpec | null = null
96
+
97
+ private sequence: number = 0
98
+
99
+ constructor (options: IMarketContextOptions) {
100
+ const rng = this.rng = options.rng
101
+ this.instrument = options.instrument ?? InstrumentUniverse.choose(rng, options.assetClass)
102
+ const inst = this.instrument
103
+ this.asOf = options.asOf ?? new Date()
104
+
105
+ this.side = rng.bool(0.5) ? '1' : '2'
106
+ const lots = rng.int(1, 40)
107
+ this.orderQty = lots * inst.lotSize
108
+ this.price = rng.around(inst.refPx, 0.004, inst.tick)
109
+ this.lastPx = rng.around(this.price, 0.002, inst.tick)
110
+ // a partial fill more often than not, which is what a real execution stream looks like
111
+ const filledLots = rng.bool(0.35) ? lots : rng.int(1, Math.max(1, lots - 1))
112
+ this.cumQty = filledLots * inst.lotSize
113
+ this.leavesQty = this.orderQty - this.cumQty
114
+ this.lastQty = this.cumQty
115
+ this.avgPx = this.lastPx
116
+
117
+ this.tradeDate = MarketContext.midnightUtc(this.asOf)
118
+ this.transactTime = new Date(this.asOf.getTime() - rng.int(0, 90) * 1000)
119
+ this.settlDate = MarketContext.addBusinessDays(this.tradeDate, inst.assetClass === AssetClass.Fx ? 2 : 2)
120
+
121
+ const stamp = MarketContext.yyyymmdd(this.tradeDate)
122
+ this.clOrdId = `CLORD-${stamp}-${rng.int(10000, 99999)}`
123
+ this.origClOrdId = `CLORD-${stamp}-${rng.int(10000, 99999)}`
124
+ this.orderId = `ORD-${rng.int(1000000, 9999999)}`
125
+ this.execId = `EXE-${rng.int(1000000, 9999999)}`
126
+ this.tradeId = `TRD-${stamp}-${rng.int(1000, 9999)}`
127
+ this.account = rng.pick(accounts)
128
+ this.desk = rng.pick(desks)
129
+ this.senderCompId = options.senderCompId ?? 'init-comp'
130
+ this.targetCompId = options.targetCompId ?? 'accept-comp'
131
+
132
+ this.firms = rng.shuffled(firms)
133
+ this.traders = rng.shuffled(traders)
134
+ }
135
+
136
+ /**
137
+ * the instrument whose fields should be read right now - a leg is described by its own
138
+ * contract, an underlying by the underlying, and anything else by the parent
139
+ */
140
+ public current (): { symbol: string, securityId: string, securityIdSource: string, securityType: string, refPx: number, maturityMonthYear?: string } {
141
+ const leg = this.leg
142
+ if (leg) return leg
143
+ const under = this.underlying
144
+ if (under) return under
145
+ return this.instrument
146
+ }
147
+
148
+ public nextSeq (): number {
149
+ return ++this.sequence
150
+ }
151
+
152
+ public text (): string {
153
+ return this.rng.pick(freeText)
154
+ }
155
+
156
+ public party (index: number): IParty {
157
+ const all = [...this.firms, ...this.traders]
158
+ return all[index % all.length]
159
+ }
160
+
161
+ /**
162
+ * a quantity that fits the contract - always a whole number of lots, so a message
163
+ * never claims a fraction of a future
164
+ */
165
+ public quantity (max?: number): number {
166
+ const lot = this.instrument.lotSize
167
+ const cap = max != null ? Math.max(1, Math.floor(max / lot)) : 40
168
+ return this.rng.int(1, cap) * lot
169
+ }
170
+
171
+ /**
172
+ * what the trade is worth - quantity by price, and by the contract multiplier where
173
+ * there is one, so a Brent fill does not report a notional of six dollars
174
+ */
175
+ public grossAmount (): number {
176
+ const multiplier = this.instrument.contractMultiplier ?? 1
177
+ return Number((this.lastQty * this.lastPx * multiplier).toFixed(2))
178
+ }
179
+
180
+ public priceNear (centre?: number): number {
181
+ const inst = this.instrument
182
+ const leg = this.leg
183
+ const base = centre ?? (leg ? leg.refPx : inst.refPx)
184
+ return this.rng.around(base, 0.006, leg ? 0.01 : inst.tick)
185
+ }
186
+
187
+ public static midnightUtc (d: Date): Date {
188
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0))
189
+ }
190
+
191
+ public static addBusinessDays (from: Date, days: number): Date {
192
+ const d = new Date(from.getTime())
193
+ let left = days
194
+ while (left > 0) {
195
+ d.setUTCDate(d.getUTCDate() + 1)
196
+ const day = d.getUTCDay()
197
+ if (day !== 0 && day !== 6) left--
198
+ }
199
+ return d
200
+ }
201
+
202
+ public static yyyymmdd (d: Date): string {
203
+ const m = d.getUTCMonth() + 1
204
+ const day = d.getUTCDate()
205
+ return `${d.getUTCFullYear()}${m < 10 ? '0' : ''}${m}${day < 10 ? '0' : ''}${day}`
206
+ }
207
+ }
@@ -0,0 +1,102 @@
1
+ import { ILooseObject } from '../collections/collection'
2
+
3
+ /**
4
+ * What the engine did with a scattered message, stated more precisely than "the objects
5
+ * differ".
6
+ *
7
+ * The distinction earns its place because the two failures found so far are not equally
8
+ * serious, and an application can defend against only one of them:
9
+ *
10
+ * - **loses fields** — the scattered parse is a strict subset of the canonical one.
11
+ * Data is missing, but nothing present is false. An application checking for the
12
+ * fields it needs will notice.
13
+ * - **mis-attributes** — the scattered parse contains something the canonical one does
14
+ * not: a value moved into a component that never carried it, or a value changed.
15
+ * Nothing looks wrong, and no amount of care at the application catches it.
16
+ *
17
+ * Keeping them apart is also what lets a corpus entry record an expectation sharp enough
18
+ * to be worth freezing. "Differs" would pass for the wrong reason.
19
+ */
20
+
21
+ export enum CaseOutcome {
22
+ /** the scattered ordering parses to the same object - the engine handled it */
23
+ RoundTrips = 'round-trips',
24
+ /** parses, but with fields dropped; every value present is still correct */
25
+ LosesFields = 'loses-fields',
26
+ /** parses, and carries a value the message did not say - the dangerous one */
27
+ MisAttributes = 'mis-attributes',
28
+ /** refused outright, which for a legal ordering is a defect, but a loud one */
29
+ Rejected = 'rejected'
30
+ }
31
+
32
+ export interface IOutcome {
33
+ readonly outcome: CaseOutcome
34
+ /** leaf paths the canonical parse has and the scattered one does not */
35
+ readonly missing: string[]
36
+ /** leaf paths the scattered parse has and the canonical one does not */
37
+ readonly added: string[]
38
+ /** leaf paths in both, disagreeing */
39
+ readonly changed: string[]
40
+ }
41
+
42
+ /**
43
+ * every leaf of an object as `path -> json`, so two parses can be compared without
44
+ * caring how the tree is shaped
45
+ */
46
+ export function leaves (o: any, path: string = '', into: Map<string, string> = new Map<string, string>()): Map<string, string> {
47
+ if (o === null || o === undefined || typeof o !== 'object' || o instanceof Date || Buffer.isBuffer(o)) {
48
+ into.set(path, JSON.stringify(o))
49
+ return into
50
+ }
51
+ for (const key of Object.keys(o)) {
52
+ leaves(o[key], `${path}.${key}`, into)
53
+ }
54
+ return into
55
+ }
56
+
57
+ export function classify (canonical: ILooseObject | null, scattered: ILooseObject | null): IOutcome {
58
+ if (!scattered) {
59
+ return { outcome: CaseOutcome.Rejected, missing: [], added: [], changed: [] }
60
+ }
61
+ const a = leaves(canonical)
62
+ const b = leaves(scattered)
63
+ const missing: string[] = []
64
+ const added: string[] = []
65
+ const changed: string[] = []
66
+ for (const [k, v] of a) {
67
+ const other = b.get(k)
68
+ if (other === undefined) {
69
+ missing.push(k)
70
+ } else if (other !== v) {
71
+ changed.push(k)
72
+ }
73
+ }
74
+ for (const k of b.keys()) {
75
+ if (!a.has(k)) added.push(k)
76
+ }
77
+ missing.sort()
78
+ added.sort()
79
+ changed.sort()
80
+ if (missing.length === 0 && added.length === 0 && changed.length === 0) {
81
+ return { outcome: CaseOutcome.RoundTrips, missing, added, changed }
82
+ }
83
+ if (added.length > 0 || changed.length > 0) {
84
+ return { outcome: CaseOutcome.MisAttributes, missing, added, changed }
85
+ }
86
+ return { outcome: CaseOutcome.LosesFields, missing, added, changed }
87
+ }
88
+
89
+ export function describeOutcome (o: IOutcome): string {
90
+ switch (o.outcome) {
91
+ case CaseOutcome.RoundTrips:
92
+ return 'round trips - the scattered ordering parses to the same object'
93
+ case CaseOutcome.LosesFields:
94
+ return `loses ${o.missing.length} field(s): ${o.missing.join(', ')}`
95
+ case CaseOutcome.MisAttributes:
96
+ return `mis-attributes ${o.added.length + o.changed.length} value(s): ${[...o.added, ...o.changed].join(', ')}`
97
+ case CaseOutcome.Rejected:
98
+ return 'rejected - the parser refused a legal ordering'
99
+ default:
100
+ return 'unknown'
101
+ }
102
+ }