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,432 @@
1
+ import { ILayout, LayoutKind, LayoutNode, assemble } from './layout'
2
+ import { FragmentSafety, FragmentSafetyCode } from '../dictionary/fragment-safety'
3
+ import { Rng } from './rng'
4
+
5
+ /**
6
+ * Taking a well formed message apart in a way the specification still permits.
7
+ *
8
+ * Two facts from `docs/scattered-components.md` are the whole basis of this:
9
+ *
10
+ * Contiguity is only forced where framing exists, and framing only exists where it is
11
+ * needed.
12
+ *
13
+ * A repeating group is the sole self describing construct in tag value FIX, so a group
14
+ * run - its `NoXXX` and every instance - is atomic and moves as one block. Everywhere
15
+ * else a component's extent is a convention of the dictionary with no wire
16
+ * representation, so its tags may legally be interleaved with its siblings.
17
+ *
18
+ * And, within one group instance, the delimiter must come first. That is the only other
19
+ * ordering the wire actually constrains, and it is honoured by pinning.
20
+ *
21
+ * Because the result is a permutation of the body's tokens, the encoded length and the
22
+ * sum of the bytes are both unchanged - so `BodyLength` and `CheckSum` stay correct
23
+ * without being recomputed. A scattered message differs from its canonical twin only in
24
+ * the order of the fields, which is precisely the claim under test.
25
+ *
26
+ * ## Depth
27
+ *
28
+ * A *site* is one component made non-adjacent within its level. `--russian-doll n` asks
29
+ * for n sites, each one level further into the message than the last:
30
+ *
31
+ * 1 Instrument scattered in the message body - the shape the shipped repair handles
32
+ * 2 and again inside a group instance, one level down the structure stack
33
+ * 3 and again inside a group instance of that instance
34
+ *
35
+ * A site one level down is preferred where it genuinely sits *within* the site above -
36
+ * a component of the scattered component, or a group carried by it - because that is the
37
+ * russian doll the design note describes. Where the dictionary offers no such shape, and
38
+ * standard FIX 4.4 very often does not, the planner settles for a site at the same
39
+ * structure depth reached through a sibling group instead, and says which it found. Both
40
+ * are past what the engine handles: detection is populated only at structure depth 1, so
41
+ * a component scattered inside any group instance goes unnoticed however it was reached.
42
+ *
43
+ * The planner degrades rather than failing. A message that cannot express three levels
44
+ * yields two and a note, because which shapes a dictionary can express is a finding in
45
+ * itself.
46
+ */
47
+
48
+ export enum Containment {
49
+ /** the message body */
50
+ Root = 'root',
51
+ /** genuinely inside the site above it - a nested component, or a group it carries */
52
+ Within = 'within',
53
+ /** the same structure depth, but reached through a sibling group */
54
+ Deeper = 'deeper'
55
+ }
56
+
57
+ export interface IScatterSite {
58
+ /** dotted path of the level the site sits in - the message body or a group instance */
59
+ readonly level: string
60
+ /** dotted path of the component made non-adjacent */
61
+ readonly component: string
62
+ /** how this site relates to the one above it */
63
+ readonly containment: Containment
64
+ /**
65
+ * how many group instance bodies enclose the site. 0 is the message body, which is
66
+ * the only depth today's detection is populated at, so anything above 0 is past what
67
+ * the engine notices.
68
+ */
69
+ readonly structureDepth: number
70
+ /** how many runs the component was broken into */
71
+ readonly fragments: number
72
+ /** tags belonging to the component, in the order they now appear */
73
+ readonly tags: number[]
74
+ /**
75
+ * tags of this component that a *sibling* at the same level also claims. Phase 0 of
76
+ * the design note found 57 of these in the QuickFIX FIX50SP2 dictionary, all of them
77
+ * `Currency(15)` declared both inside `Instrument` and beside it. No repair
78
+ * attributing by tag identity can tell which one a scattered tag belonged to, so a
79
+ * case with a non-empty list here is testing something unanswerable rather than
80
+ * something broken.
81
+ */
82
+ readonly contendedTags: number[]
83
+ }
84
+
85
+ interface ISiteRef {
86
+ readonly level: LayoutNode
87
+ readonly component: LayoutNode
88
+ readonly containment: Containment
89
+ readonly structureDepth: number
90
+ }
91
+
92
+ export interface IScatterPlan {
93
+ readonly depth: number
94
+ readonly sites: readonly IScatterSite[]
95
+ }
96
+
97
+ export interface IScatterResult {
98
+ readonly wire: string
99
+ readonly plan: IScatterPlan
100
+ /** body token order, for a caller that wants to show the move */
101
+ readonly order: readonly number[]
102
+ }
103
+
104
+ interface IUnit {
105
+ /** tokens in the order they should now appear; a group run carries its whole subtree */
106
+ readonly tokens: number[]
107
+ /** component nodes of this level enclosing the unit, outermost first */
108
+ readonly owners: readonly LayoutNode[]
109
+ readonly node: LayoutNode
110
+ }
111
+
112
+ /** a level, and the chain of components to scatter within it, outermost first */
113
+ type LevelChains = Map<LayoutNode, LayoutNode[]>
114
+
115
+ export class Scatterer {
116
+ constructor (
117
+ private readonly layout: ILayout,
118
+ private readonly rng: Rng,
119
+ private readonly fragments: number = 2) {
120
+ }
121
+
122
+ /**
123
+ * the deepest chain of scatter sites this message can support, up to `depth`.
124
+ *
125
+ * Returns fewer sites than asked for rather than failing: which shapes a dictionary
126
+ * can actually express is a finding in its own right, and a caller that wanted three
127
+ * levels is better served by two and a note than by an exception.
128
+ */
129
+ public plan (depth: number): ISiteRef[] {
130
+ for (let want = depth; want >= 1; --want) {
131
+ const chain = this.findChain(this.layout.root, want, Containment.Root)
132
+ if (chain) return chain
133
+ }
134
+ return []
135
+ }
136
+
137
+ public scatter (depth: number, delimiter: string): IScatterResult {
138
+ const sites = this.plan(depth)
139
+ const chains: LevelChains = new Map<LayoutNode, LayoutNode[]>()
140
+ for (const site of sites) {
141
+ const existing = chains.get(site.level)
142
+ if (existing) {
143
+ existing.push(site.component)
144
+ } else {
145
+ chains.set(site.level, [site.component])
146
+ }
147
+ }
148
+ const order = this.orderLevel(this.layout.root, chains)
149
+ const { tokens, bodyStart, bodyEnd } = this.layout
150
+ const head: number[] = []
151
+ for (let i = 0; i < bodyStart; ++i) head.push(i)
152
+ const tail: number[] = []
153
+ for (let i = bodyEnd; i < tokens.length; ++i) tail.push(i)
154
+ const full = [...head, ...order, ...tail]
155
+ const position = new Map<number, number>()
156
+ full.forEach((tokenIndex, at) => position.set(tokenIndex, at))
157
+ return {
158
+ wire: assemble(tokens, full, delimiter),
159
+ order,
160
+ plan: {
161
+ depth: sites.length,
162
+ sites: sites.map(site => ({
163
+ level: site.level.path,
164
+ component: site.component.path,
165
+ containment: site.containment,
166
+ structureDepth: site.structureDepth,
167
+ contendedTags: Scatterer.contended(site),
168
+ fragments: Scatterer.runs(site.component.canonical().map(t => position.get(t) ?? -1)),
169
+ tags: site.component.canonical()
170
+ .map(t => ({ t, at: position.get(t) ?? -1 }))
171
+ .sort((a, b) => a.at - b.at)
172
+ .map(x => tokens[x.t].tag)
173
+ }))
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * tags of the scattered component that a sibling of it, at the same level, also claims
180
+ */
181
+ private static contended (site: ISiteRef): number[] {
182
+ const set = site.level.set
183
+ if (!set) return []
184
+ const name = site.component.name
185
+ const path = site.level.path
186
+ const out: number[] = []
187
+ for (const f of FragmentSafety.analyseSet(set, path)) {
188
+ if (f.code !== FragmentSafetyCode.SiblingTagCollision) continue
189
+ if (f.path !== path) continue
190
+ if (!f.owners.includes(name)) continue
191
+ out.push(f.tag)
192
+ }
193
+ return out
194
+ }
195
+
196
+ /**
197
+ * how many separate runs these positions form once sorted - one if the component is
198
+ * still contiguous, which is how a plan reports that it failed to achieve anything
199
+ */
200
+ private static runs (positions: number[]): number {
201
+ const sorted = positions.slice().sort((a, b) => a - b)
202
+ let count = sorted.length > 0 ? 1 : 0
203
+ for (let i = 1; i < sorted.length; ++i) {
204
+ if (sorted[i] !== sorted[i - 1] + 1) count++
205
+ }
206
+ return count
207
+ }
208
+
209
+ // ---------------------------------------------------------------- planning
210
+
211
+ /**
212
+ * a chain of sites, each one level further into the message than the last
213
+ */
214
+ private findChain (
215
+ level: LayoutNode,
216
+ depth: number,
217
+ containment: Containment,
218
+ structureDepth: number = 0): ISiteRef[] | null {
219
+ if (depth <= 0) return []
220
+ const units = this.units(level)
221
+ for (const candidate of Scatterer.candidates(level, units)) {
222
+ const site: ISiteRef = { level, component: candidate, containment, structureDepth }
223
+ if (depth === 1) return [site]
224
+ const inner = this.findInside(level, candidate, units, depth - 1, structureDepth)
225
+ if (inner) return [site, ...inner]
226
+ }
227
+ // Nothing scatterable at this level. A market data refresh is the ordinary case:
228
+ // its body is a handful of loose fields and one group, so every component it has
229
+ // lives inside an entry. Descend rather than report the message unscatterable.
230
+ for (const u of units) {
231
+ if (u.node.kind !== LayoutKind.Group) continue
232
+ for (const instance of u.node.children) {
233
+ const chain = this.findChain(instance, depth, Containment.Deeper, structureDepth + 1)
234
+ if (chain) return chain
235
+ }
236
+ }
237
+ return null
238
+ }
239
+
240
+ /**
241
+ * components of this level worth scattering: at least two units of their own, and at
242
+ * least one unit of the level left over to put between them
243
+ */
244
+ private static candidates (level: LayoutNode, units: readonly IUnit[]): LayoutNode[] {
245
+ const owned = new Map<LayoutNode, number>()
246
+ for (const u of units) {
247
+ for (const owner of u.owners) {
248
+ owned.set(owner, (owned.get(owner) ?? 0) + 1)
249
+ }
250
+ }
251
+ const out: LayoutNode[] = []
252
+ for (const [node, count] of owned) {
253
+ if (count < 2) continue
254
+ if (count >= units.length) continue
255
+ out.push(node)
256
+ }
257
+ // widest first, so the plan uses the component with the most room to be broken up
258
+ out.sort((a, b) => (owned.get(b) ?? 0) - (owned.get(a) ?? 0))
259
+ if (level.kind === LayoutKind.Instance && units.length <= 2) {
260
+ // the delimiter is pinned, so a level of two units has nothing spare to interleave
261
+ return []
262
+ }
263
+ return out
264
+ }
265
+
266
+ /**
267
+ * the next site down. Preference order is the containment order of the design note:
268
+ * a component nested inside this one, then a group this component carries, then - only
269
+ * because standard FIX rarely offers either - any group at this level.
270
+ */
271
+ private findInside (
272
+ level: LayoutNode,
273
+ outer: LayoutNode,
274
+ units: readonly IUnit[],
275
+ depth: number,
276
+ structureDepth: number): ISiteRef[] | null {
277
+ const inside = units.filter(u => u.owners.includes(outer))
278
+
279
+ // a component of the scattered component, scattered among its parent's own tags
280
+ const nested = new Map<LayoutNode, number>()
281
+ for (const u of inside) {
282
+ const at = u.owners.indexOf(outer)
283
+ for (let i = at + 1; i < u.owners.length; ++i) {
284
+ const n = u.owners[i]
285
+ nested.set(n, (nested.get(n) ?? 0) + 1)
286
+ }
287
+ }
288
+ const ordered = Array.from(nested.entries())
289
+ .filter(([, count]) => count >= 2 && count < inside.length)
290
+ .sort((a, b) => b[1] - a[1])
291
+ for (const [node] of ordered) {
292
+ const site: ISiteRef = { level, component: node, containment: Containment.Within, structureDepth }
293
+ if (depth === 1) return [site]
294
+ const deeper = this.findInside(level, node, units, depth - 1, structureDepth)
295
+ if (deeper) return [site, ...deeper]
296
+ }
297
+
298
+ // a group instance is a level of its own; one carried by the scattered component is
299
+ // genuinely within it, one carried by a sibling only reaches the same structure depth
300
+ const carried = units.filter(u => u.node.kind === LayoutKind.Group && u.owners.includes(outer))
301
+ const siblings = units.filter(u => u.node.kind === LayoutKind.Group && !u.owners.includes(outer))
302
+ for (const [group, containment] of [
303
+ ...carried.map(u => [u.node, Containment.Within] as [LayoutNode, Containment]),
304
+ ...siblings.map(u => [u.node, Containment.Deeper] as [LayoutNode, Containment])
305
+ ]) {
306
+ for (const instance of group.children) {
307
+ const chain = this.findChain(instance, depth, containment, structureDepth + 1)
308
+ if (chain && chain.length === depth) return chain
309
+ }
310
+ }
311
+ return null
312
+ }
313
+
314
+ // ---------------------------------------------------------------- ordering
315
+
316
+ /**
317
+ * the units of one level: every field is a unit, a component contributes its fields to
318
+ * the enclosing level rather than a unit of its own, and a whole group run is a single
319
+ * atomic unit because the wire frames it
320
+ */
321
+ private units (level: LayoutNode, chains?: LevelChains): IUnit[] {
322
+ const out: IUnit[] = []
323
+ const visit = (node: LayoutNode, owners: LayoutNode[]): void => {
324
+ for (const child of node.children) {
325
+ switch (child.kind) {
326
+ case LayoutKind.Field: {
327
+ const tokens = [child.tokenIndex]
328
+ if (child.extraTokenIndex >= 0) tokens.push(child.extraTokenIndex)
329
+ out.push({ tokens, owners: owners.slice(), node: child })
330
+ break
331
+ }
332
+ case LayoutKind.Component:
333
+ visit(child, [...owners, child])
334
+ break
335
+ case LayoutKind.Group: {
336
+ const tokens = [child.tokenIndex]
337
+ for (const instance of child.children) {
338
+ // an instance is a level, so it may carry a scatter site of its own
339
+ for (const t of chains ? this.orderLevel(instance, chains) : instance.canonical()) {
340
+ tokens.push(t)
341
+ }
342
+ }
343
+ out.push({ tokens, owners: owners.slice(), node: child })
344
+ break
345
+ }
346
+ default:
347
+ break
348
+ }
349
+ }
350
+ }
351
+ visit(level, [])
352
+ return out
353
+ }
354
+
355
+ private orderLevel (level: LayoutNode, chains: LevelChains): number[] {
356
+ const units = this.units(level, chains)
357
+ const chain = chains.get(level) ?? []
358
+ let arranged: IUnit[]
359
+ if (level.kind === LayoutKind.Instance && units.length > 0) {
360
+ // the group delimiter has to stay at the front of the instance
361
+ const pinned = units[0]
362
+ const rest = units.slice(1)
363
+ const target = chain.length > 0 ? chain[0] : null
364
+ if (target && pinned.owners.includes(target)) {
365
+ // the pinned unit already belongs to the component being scattered, so putting
366
+ // the other units of the level next is enough to break its run
367
+ arranged = [pinned, ...this.arrange(rest, chain, true)]
368
+ } else {
369
+ arranged = [pinned, ...this.arrange(rest, chain, false)]
370
+ }
371
+ } else {
372
+ arranged = this.arrange(units, chain, false)
373
+ }
374
+ const out: number[] = []
375
+ for (const u of arranged) {
376
+ for (const t of u.tokens) out.push(t)
377
+ }
378
+ return out
379
+ }
380
+
381
+ /**
382
+ * @param restFirst put the untargeted units before the targeted ones, used when the
383
+ * pinned delimiter already belongs to the target
384
+ */
385
+ private arrange (units: IUnit[], chain: readonly LayoutNode[], restFirst: boolean): IUnit[] {
386
+ if (chain.length === 0 || units.length === 0) return units
387
+ const target = chain[0]
388
+ const inTarget = units.filter(u => u.owners.includes(target))
389
+ const rest = units.filter(u => !u.owners.includes(target))
390
+ if (inTarget.length === 0 || rest.length === 0) return units
391
+ // deeper sites in the same level are scattered within the target's own units first
392
+ const ordered = chain.length > 1 ? this.arrange(inTarget, chain.slice(1), false) : inTarget
393
+ return restFirst
394
+ ? Scatterer.weave(rest, ordered, this.fragments, this.rng)
395
+ : Scatterer.weave(ordered, rest, this.fragments, this.rng)
396
+ }
397
+
398
+ /**
399
+ * break `target` into runs and put pieces of `filler` between them
400
+ */
401
+ private static weave (target: IUnit[], filler: IUnit[], fragments: number, rng: Rng): IUnit[] {
402
+ const f = Math.min(fragments, target.length, filler.length + 1)
403
+ if (f < 2) return [...target, ...filler]
404
+ const targetRuns = Scatterer.split(target, f, rng)
405
+ const fillerRuns = Scatterer.split(filler, f - 1, rng)
406
+ const out: IUnit[] = []
407
+ for (let i = 0; i < f; ++i) {
408
+ out.push(...targetRuns[i])
409
+ if (i < f - 1) out.push(...fillerRuns[i])
410
+ }
411
+ return out
412
+ }
413
+
414
+ private static split (units: IUnit[], parts: number, rng: Rng): IUnit[][] {
415
+ if (parts <= 1) return [units]
416
+ const cuts = new Set<number>()
417
+ let guard = 0
418
+ while (cuts.size < parts - 1 && guard++ < 100) {
419
+ cuts.add(rng.int(1, units.length - 1))
420
+ }
421
+ const sorted = Array.from(cuts).sort((a, b) => a - b)
422
+ const out: IUnit[][] = []
423
+ let from = 0
424
+ for (const cut of sorted) {
425
+ out.push(units.slice(from, cut))
426
+ from = cut
427
+ }
428
+ out.push(units.slice(from))
429
+ while (out.length < parts) out.push([])
430
+ return out
431
+ }
432
+ }
@@ -0,0 +1,167 @@
1
+ import { IGeneratorOptions } from './realistic-generator'
2
+ import { AssetClass } from './instrument-universe'
3
+
4
+ /**
5
+ * Named shapes of traffic, so that asking for something realistic does not mean
6
+ * knowing which tags to force on.
7
+ *
8
+ * A scenario is only a bundle of generator options - it has no logic of its own, and
9
+ * anything it does can be done on the command line instead. Its value is that the
10
+ * combination which produces a *convincing* message is not obvious: a multileg trade
11
+ * capture needs legs, sides and parties present together, and a bond one needs the
12
+ * fixed income half of `Instrument` that the applicability gate would otherwise
13
+ * suppress.
14
+ *
15
+ * Fields are forced by **tag**, not by name, because the name is not portable. The
16
+ * repository rendering calls the leg group `TrdInstrmtLegGrp` and QuickFIX calls it
17
+ * `NoLegs`; both agree it is 555. A scenario therefore runs against any dictionary
18
+ * that carries the message at all.
19
+ */
20
+
21
+ export interface IScenario {
22
+ readonly name: string
23
+ readonly description: string
24
+ /** as it appears on the wire in tag 35 */
25
+ readonly msgType: string
26
+ readonly options: IGeneratorOptions
27
+ }
28
+
29
+ const NoLegs = '555'
30
+ const NoSides = '552'
31
+ const NoPartyIDs = '453'
32
+ const NoUnderlyings = '711'
33
+ const NoMDEntries = '268'
34
+ const NoAllocs = '78'
35
+ const NoRelatedSym = '146'
36
+
37
+ const scenarios: readonly IScenario[] = [
38
+ {
39
+ name: 'spread-trade-capture',
40
+ description: 'ICE Brent calendar spread, parent plus two legs, both sides with parties',
41
+ msgType: 'AE',
42
+ options: {
43
+ instrument: 'BRN JAN26-FEB26',
44
+ include: [NoLegs, NoSides, NoPartyIDs],
45
+ density: 0.5,
46
+ maxGroupInstances: 2
47
+ }
48
+ },
49
+ {
50
+ name: 'crack-spread-capture',
51
+ description: 'ICE Brent/Gasoil crack spread trade capture with legs and underlyings',
52
+ msgType: 'AE',
53
+ options: {
54
+ instrument: 'BRN-GAS CRACK',
55
+ include: [NoLegs, NoSides, NoPartyIDs, NoUnderlyings],
56
+ density: 0.5,
57
+ maxGroupInstances: 2
58
+ }
59
+ },
60
+ {
61
+ name: 'equity-fill',
62
+ description: 'partial fill on a listed equity order, with the executing parties',
63
+ msgType: '8',
64
+ options: {
65
+ instrument: 'AAPL',
66
+ assetClass: AssetClass.Equity,
67
+ include: [NoPartyIDs],
68
+ density: 0.5
69
+ }
70
+ },
71
+ {
72
+ name: 'equity-order',
73
+ description: 'plain new order single into a lit venue',
74
+ msgType: 'D',
75
+ options: {
76
+ instrument: 'VOD.L',
77
+ assetClass: AssetClass.Equity,
78
+ density: 0.4
79
+ }
80
+ },
81
+ {
82
+ name: 'future-order',
83
+ description: 'new order single on a listed future, give-up parties attached',
84
+ msgType: 'D',
85
+ options: {
86
+ instrument: 'BRN',
87
+ assetClass: AssetClass.Future,
88
+ include: [NoPartyIDs],
89
+ density: 0.45
90
+ }
91
+ },
92
+ {
93
+ name: 'bond-trade-capture',
94
+ description: 'treasury trade capture - the one case where the fixed income half of Instrument belongs',
95
+ msgType: 'AE',
96
+ options: {
97
+ instrument: 'US91282CLW12',
98
+ assetClass: AssetClass.Bond,
99
+ include: [NoSides, NoPartyIDs],
100
+ density: 0.6
101
+ }
102
+ },
103
+ {
104
+ name: 'option-order',
105
+ description: 'listed option order carrying strike and expiry',
106
+ msgType: 'D',
107
+ options: {
108
+ instrument: 'ESZ5 C6000',
109
+ assetClass: AssetClass.Option,
110
+ density: 0.45
111
+ }
112
+ },
113
+ {
114
+ name: 'md-incremental',
115
+ description: 'incremental market data refresh, several entries against one book',
116
+ msgType: 'X',
117
+ options: {
118
+ instrument: 'ESZ5',
119
+ assetClass: AssetClass.Future,
120
+ include: [NoMDEntries],
121
+ density: 0.35,
122
+ maxGroupInstances: 4
123
+ }
124
+ },
125
+ {
126
+ name: 'md-request',
127
+ description: 'market data request over a small watch list',
128
+ msgType: 'V',
129
+ options: {
130
+ assetClass: AssetClass.Equity,
131
+ include: [NoRelatedSym],
132
+ density: 0.6,
133
+ maxGroupInstances: 3
134
+ }
135
+ },
136
+ {
137
+ name: 'block-allocation',
138
+ description: 'allocation instruction breaking a block across accounts',
139
+ msgType: 'J',
140
+ options: {
141
+ instrument: 'AAPL',
142
+ assetClass: AssetClass.Equity,
143
+ include: [NoAllocs, NoPartyIDs],
144
+ density: 0.45,
145
+ maxGroupInstances: 3
146
+ }
147
+ }
148
+ ]
149
+
150
+ export const Scenarios = {
151
+ all: scenarios,
152
+
153
+ get (name: string): IScenario | undefined {
154
+ return scenarios.find(s => s.name === name)
155
+ },
156
+
157
+ names (): string[] {
158
+ return scenarios.map(s => s.name)
159
+ },
160
+
161
+ describe (): string {
162
+ const width = Math.max(...scenarios.map(s => s.name.length))
163
+ return scenarios
164
+ .map(s => ` ${s.name.padEnd(width)} ${s.msgType.padEnd(2)} ${s.description}`)
165
+ .join('\n')
166
+ }
167
+ }