jspurefix 5.11.3 → 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.
- package/.gitattributes +4 -0
- package/dist/buffer/index.d.ts +6 -0
- package/dist/buffer/index.js +6 -0
- package/dist/buffer/index.js.map +1 -1
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.js +16 -0
- package/dist/collections/index.js.map +1 -1
- package/dist/dictionary/contained/index.d.ts +3 -0
- package/dist/dictionary/contained/index.js +3 -0
- package/dist/dictionary/contained/index.js.map +1 -1
- package/dist/dictionary/index.d.ts +5 -0
- package/dist/dictionary/index.js +5 -0
- package/dist/dictionary/index.js.map +1 -1
- package/dist/generator/applicability.d.ts +3 -0
- package/dist/generator/applicability.js +48 -0
- package/dist/generator/applicability.js.map +1 -0
- package/dist/generator/case-builder.d.ts +34 -0
- package/dist/generator/case-builder.js +111 -0
- package/dist/generator/case-builder.js.map +1 -0
- package/dist/generator/corpus-store.d.ts +43 -0
- package/dist/generator/corpus-store.js +115 -0
- package/dist/generator/corpus-store.js.map +1 -0
- package/dist/generator/index.d.ts +12 -0
- package/dist/generator/index.js +29 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/instrument-universe.d.ts +54 -0
- package/dist/generator/instrument-universe.js +286 -0
- package/dist/generator/instrument-universe.js.map +1 -0
- package/dist/generator/layout.d.ts +46 -0
- package/dist/generator/layout.js +208 -0
- package/dist/generator/layout.js.map +1 -0
- package/dist/generator/market-context.d.ts +64 -0
- package/dist/generator/market-context.js +119 -0
- package/dist/generator/market-context.js.map +1 -0
- package/dist/generator/outcome.d.ts +16 -0
- package/dist/generator/outcome.js +71 -0
- package/dist/generator/outcome.js.map +1 -0
- package/dist/generator/realistic-generator.d.ts +53 -0
- package/dist/generator/realistic-generator.js +275 -0
- package/dist/generator/realistic-generator.js.map +1 -0
- package/dist/generator/rng.d.ts +12 -0
- package/dist/generator/rng.js +60 -0
- package/dist/generator/rng.js.map +1 -0
- package/dist/generator/scatter.d.ts +50 -0
- package/dist/generator/scatter.js +285 -0
- package/dist/generator/scatter.js.map +1 -0
- package/dist/generator/scenarios.d.ts +13 -0
- package/dist/generator/scenarios.js +139 -0
- package/dist/generator/scenarios.js.map +1 -0
- package/dist/generator/value-conventions.d.ts +15 -0
- package/dist/generator/value-conventions.js +217 -0
- package/dist/generator/value-conventions.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/jsfix-cmd.d.ts +5 -0
- package/dist/jsfix-cmd.js +161 -0
- package/dist/jsfix-cmd.js.map +1 -1
- package/dist/store/fix-msg-ascii-store-resend.js +2 -2
- package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
- package/dist/store/fix-msg-store-record.d.ts +1 -1
- package/dist/store/fix-msg-store-record.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/transport/index.d.ts +7 -0
- package/dist/transport/index.js +7 -0
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/session/index.d.ts +1 -0
- package/dist/transport/session/index.js +1 -0
- package/dist/transport/session/index.js.map +1 -1
- package/docs/generated-cases.md +237 -0
- package/docs/scattered-components.md +60 -5
- package/jsfix.test_client.txt +62 -62
- package/jsfix.test_server.txt +72 -72
- package/package.json +5 -2
- package/src/buffer/index.ts +6 -0
- package/src/collections/index.ts +1 -1
- package/src/dictionary/contained/index.ts +3 -0
- package/src/dictionary/index.ts +5 -0
- package/src/generator/applicability.ts +87 -0
- package/src/generator/case-builder.ts +167 -0
- package/src/generator/corpus-store.ts +166 -0
- package/src/generator/index.ts +12 -0
- package/src/generator/instrument-universe.ts +347 -0
- package/src/generator/layout.ts +279 -0
- package/src/generator/market-context.ts +207 -0
- package/src/generator/outcome.ts +102 -0
- package/src/generator/realistic-generator.ts +404 -0
- package/src/generator/rng.ts +92 -0
- package/src/generator/scatter.ts +432 -0
- package/src/generator/scenarios.ts +167 -0
- package/src/generator/value-conventions.ts +295 -0
- package/src/index.ts +12 -0
- package/src/jsfix-cmd.ts +204 -0
- package/src/store/fix-msg-ascii-store-resend.ts +2 -1
- package/src/store/fix-msg-store-record.ts +4 -1
- package/src/store/index.ts +1 -0
- package/src/transport/index.ts +10 -0
- 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
|
+
}
|