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.
Files changed (100) 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/dictionary/contained/index.d.ts +3 -0
  9. package/dist/dictionary/contained/index.js +3 -0
  10. package/dist/dictionary/contained/index.js.map +1 -1
  11. package/dist/dictionary/index.d.ts +5 -0
  12. package/dist/dictionary/index.js +5 -0
  13. package/dist/dictionary/index.js.map +1 -1
  14. package/dist/generator/applicability.d.ts +3 -0
  15. package/dist/generator/applicability.js +48 -0
  16. package/dist/generator/applicability.js.map +1 -0
  17. package/dist/generator/case-builder.d.ts +34 -0
  18. package/dist/generator/case-builder.js +111 -0
  19. package/dist/generator/case-builder.js.map +1 -0
  20. package/dist/generator/corpus-store.d.ts +43 -0
  21. package/dist/generator/corpus-store.js +115 -0
  22. package/dist/generator/corpus-store.js.map +1 -0
  23. package/dist/generator/index.d.ts +12 -0
  24. package/dist/generator/index.js +29 -0
  25. package/dist/generator/index.js.map +1 -0
  26. package/dist/generator/instrument-universe.d.ts +54 -0
  27. package/dist/generator/instrument-universe.js +286 -0
  28. package/dist/generator/instrument-universe.js.map +1 -0
  29. package/dist/generator/layout.d.ts +46 -0
  30. package/dist/generator/layout.js +208 -0
  31. package/dist/generator/layout.js.map +1 -0
  32. package/dist/generator/market-context.d.ts +64 -0
  33. package/dist/generator/market-context.js +119 -0
  34. package/dist/generator/market-context.js.map +1 -0
  35. package/dist/generator/outcome.d.ts +16 -0
  36. package/dist/generator/outcome.js +71 -0
  37. package/dist/generator/outcome.js.map +1 -0
  38. package/dist/generator/realistic-generator.d.ts +53 -0
  39. package/dist/generator/realistic-generator.js +275 -0
  40. package/dist/generator/realistic-generator.js.map +1 -0
  41. package/dist/generator/rng.d.ts +12 -0
  42. package/dist/generator/rng.js +60 -0
  43. package/dist/generator/rng.js.map +1 -0
  44. package/dist/generator/scatter.d.ts +50 -0
  45. package/dist/generator/scatter.js +285 -0
  46. package/dist/generator/scatter.js.map +1 -0
  47. package/dist/generator/scenarios.d.ts +13 -0
  48. package/dist/generator/scenarios.js +139 -0
  49. package/dist/generator/scenarios.js.map +1 -0
  50. package/dist/generator/value-conventions.d.ts +15 -0
  51. package/dist/generator/value-conventions.js +217 -0
  52. package/dist/generator/value-conventions.js.map +1 -0
  53. package/dist/index.d.ts +6 -0
  54. package/dist/index.js +4 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/jsfix-cmd.d.ts +5 -0
  57. package/dist/jsfix-cmd.js +161 -0
  58. package/dist/jsfix-cmd.js.map +1 -1
  59. package/dist/store/fix-msg-ascii-store-resend.js +2 -2
  60. package/dist/store/fix-msg-ascii-store-resend.js.map +1 -1
  61. package/dist/store/fix-msg-store-record.d.ts +1 -1
  62. package/dist/store/fix-msg-store-record.js.map +1 -1
  63. package/dist/store/index.d.ts +1 -0
  64. package/dist/store/index.js +1 -0
  65. package/dist/store/index.js.map +1 -1
  66. package/dist/transport/index.d.ts +7 -0
  67. package/dist/transport/index.js +7 -0
  68. package/dist/transport/index.js.map +1 -1
  69. package/dist/transport/session/index.d.ts +1 -0
  70. package/dist/transport/session/index.js +1 -0
  71. package/dist/transport/session/index.js.map +1 -1
  72. package/docs/generated-cases.md +237 -0
  73. package/docs/scattered-components.md +60 -5
  74. package/jsfix.test_client.txt +62 -62
  75. package/jsfix.test_server.txt +72 -72
  76. package/package.json +5 -2
  77. package/src/buffer/index.ts +6 -0
  78. package/src/collections/index.ts +1 -1
  79. package/src/dictionary/contained/index.ts +3 -0
  80. package/src/dictionary/index.ts +5 -0
  81. package/src/generator/applicability.ts +87 -0
  82. package/src/generator/case-builder.ts +167 -0
  83. package/src/generator/corpus-store.ts +166 -0
  84. package/src/generator/index.ts +12 -0
  85. package/src/generator/instrument-universe.ts +347 -0
  86. package/src/generator/layout.ts +279 -0
  87. package/src/generator/market-context.ts +207 -0
  88. package/src/generator/outcome.ts +102 -0
  89. package/src/generator/realistic-generator.ts +404 -0
  90. package/src/generator/rng.ts +92 -0
  91. package/src/generator/scatter.ts +432 -0
  92. package/src/generator/scenarios.ts +167 -0
  93. package/src/generator/value-conventions.ts +295 -0
  94. package/src/index.ts +12 -0
  95. package/src/jsfix-cmd.ts +204 -0
  96. package/src/store/fix-msg-ascii-store-resend.ts +2 -1
  97. package/src/store/fix-msg-store-record.ts +4 -1
  98. package/src/store/index.ts +1 -0
  99. package/src/transport/index.ts +10 -0
  100. package/src/transport/session/index.ts +1 -0
@@ -0,0 +1,295 @@
1
+ import { SimpleFieldDefinition } from '../dictionary/definition'
2
+ import { TagType } from '../buffer/tag/tag-type'
3
+ import { MarketContext } from './market-context'
4
+
5
+ /**
6
+ * How a tag gets a value that looks like the thing it names.
7
+ *
8
+ * Three layers, tried in order, and the ordering is the design:
9
+ *
10
+ * 1. a table of tags whose meaning the generator actually knows, read off the
11
+ * `MarketContext` so they agree with each other;
12
+ * 2. conventions over the field *name* - anything ending `Px`, `Qty`, `Currency`,
13
+ * `Date` and so on - which is what lets an unfamiliar message type, or a broker
14
+ * dialect nobody here has seen, still come out readable;
15
+ * 3. the type, as a last resort, but with ranges a trader would not blink at rather
16
+ * than a signed float with seven decimal places.
17
+ *
18
+ * Layer 2 is doing most of the work. FIX field naming is remarkably disciplined -
19
+ * `LegLastPx`, `UnderlyingSettlPrice`, `AllocSettlCurrency` - so a suffix is very nearly
20
+ * a type declaration, and treating it as one covers thousands of fields for the cost of
21
+ * a dozen rules.
22
+ *
23
+ * Which *instrument* a value is read from is not decided here. The walker moves
24
+ * `MarketContext.leg` and `.underlying` as it descends, so `LegPrice` inside the second
25
+ * leg resolves against that leg without this layer knowing where it is.
26
+ */
27
+
28
+ export interface IFieldContext {
29
+ /** set names from the message down to the field, e.g. ['TrdCapRptSideGrp', 'Parties'] */
30
+ readonly path: readonly string[]
31
+ /** index within the nearest enclosing group instance, 0 when not in a group */
32
+ readonly instance: number
33
+ }
34
+
35
+ const mics: readonly string[] = ['XLON', 'XNAS', 'XNYS', 'XETR', 'IFEU', 'XCME', 'XOFF']
36
+ const currencies: readonly string[] = ['USD', 'EUR', 'GBP', 'JPY', 'CHF']
37
+ const sessions: readonly string[] = ['DAY', 'REG', 'ELECTRONIC', 'AFTERHOURS']
38
+ const ratings: readonly string[] = ['AAA', 'AA+', 'A', 'BBB+', 'BB-']
39
+ const countries: readonly string[] = ['US', 'GB', 'DE', 'NL', 'JP']
40
+ /** a field named for a classification but with no enumeration behind it in this
41
+ * dictionary - still a small ordinal, not a five digit number */
42
+ const ordinalName = /(Type|Status|Reason|Indicator|Action|Capacity|Method|Mode|Level)$/
43
+
44
+ type Handler = (m: MarketContext, ctx: IFieldContext, sf: SimpleFieldDefinition) => any
45
+
46
+ /**
47
+ * Tags the generator understands well enough to keep consistent with the rest of the
48
+ * message. Everything absent from here still gets a plausible value from the name
49
+ * conventions below - it just is not tied to the trade.
50
+ */
51
+ const byTag: Map<number, Handler> = new Map<number, Handler>([
52
+ [1, m => m.account],
53
+ [6, m => m.avgPx],
54
+ [11, (m, ctx) => (ctx.instance > 0 ? `${m.clOrdId}-${ctx.instance}` : m.clOrdId)],
55
+ [14, m => m.cumQty],
56
+ [15, m => m.instrument.currency],
57
+ [17, m => `${m.execId}-${m.nextSeq()}`],
58
+ [22, m => m.current().securityIdSource],
59
+ [30, m => m.instrument.securityExchange],
60
+ [31, m => m.lastPx],
61
+ [32, m => m.lastQty],
62
+ [37, (m, ctx) => (ctx.instance > 0 ? `${m.orderId}-${ctx.instance}` : m.orderId)],
63
+ [38, m => m.orderQty],
64
+ [41, m => m.origClOrdId],
65
+ [44, m => m.price],
66
+ [48, m => m.current().securityId],
67
+ [54, (m, ctx) => (ctx.instance % 2 === 0 ? m.side : (m.side === '1' ? '2' : '1'))],
68
+ [55, m => m.current().symbol],
69
+ [58, m => m.text()],
70
+ [60, m => m.transactTime],
71
+ [64, m => m.settlDate],
72
+ [75, m => m.tradeDate],
73
+ [76, m => m.firms[0].id],
74
+ [100, m => m.instrument.securityExchange],
75
+ [107, m => m.instrument.name],
76
+ [110, m => m.instrument.lotSize],
77
+ [151, m => m.leavesQty],
78
+ [167, m => m.current().securityType],
79
+ [200, m => m.current().maturityMonthYear ?? m.instrument.maturityMonthYear],
80
+ [201, m => m.instrument.putOrCall],
81
+ [202, m => m.instrument.strikePrice],
82
+ [207, m => m.instrument.securityExchange],
83
+ [231, m => m.instrument.contractMultiplier],
84
+ [336, m => m.rng.pick(sessions)],
85
+ [375, m => m.firms[2].id],
86
+ [381, m => m.grossAmount()],
87
+ [447, (m, ctx) => m.party(ctx.instance).idSource],
88
+ [448, (m, ctx) => m.party(ctx.instance).id],
89
+ [452, (m, ctx) => m.party(ctx.instance).role],
90
+ [460, m => m.instrument.product],
91
+ [461, m => m.instrument.cfiCode],
92
+ [541, m => m.instrument.maturityDate],
93
+ [571, m => m.tradeId],
94
+ [255, m => m.rng.pick(ratings)],
95
+ [470, m => m.rng.pick(countries)],
96
+ [472, m => m.rng.pick(countries)],
97
+ [65, () => undefined],
98
+ [572, m => `${m.tradeId}-REF`],
99
+
100
+ // legs - resolved through current(), which the walker has already pointed at the leg
101
+ [600, m => m.current().symbol],
102
+ [602, m => m.current().securityId],
103
+ [603, m => m.current().securityIdSource],
104
+ [609, m => m.current().securityType],
105
+ [610, m => m.current().maturityMonthYear],
106
+ [623, m => m.leg?.ratio ?? 1],
107
+ [624, m => m.leg?.side ?? m.side],
108
+ [566, m => m.priceNear()],
109
+ [637, m => m.priceNear()],
110
+ [687, m => m.lastQty * (m.leg?.ratio ?? 1)],
111
+
112
+ // underlyings
113
+ [311, m => m.underlying?.symbol ?? m.instrument.symbol],
114
+ [309, m => m.underlying?.securityId ?? m.instrument.securityId],
115
+ [305, m => m.underlying?.securityIdSource ?? m.instrument.securityIdSource],
116
+
117
+ // allocation
118
+ [79, m => m.account],
119
+ [80, m => m.lastQty],
120
+
121
+ // market data
122
+ [270, m => m.priceNear()],
123
+ [271, m => m.quantity()],
124
+ [272, m => m.tradeDate],
125
+ [273, m => m.transactTime],
126
+ [346, m => m.rng.int(1, 25)]
127
+ ])
128
+
129
+ interface INameRule {
130
+ readonly test: RegExp
131
+ readonly apply: Handler
132
+ }
133
+
134
+ /**
135
+ * Suffix first, then a few prefixes. Order matters - `PriceType` must not be caught by
136
+ * the `Price` rule, so the more specific patterns are listed above the general ones.
137
+ */
138
+ const nameRules: readonly INameRule[] = [
139
+ { test: /(Type|Source|Status|Reason|Indicator|Capacity|Restrictions)$/, apply: () => undefined },
140
+
141
+ { test: /(Px|Price)$/, apply: m => m.priceNear() },
142
+ { test: /(Qty|Quantity|Shares|Size|Volume)$/, apply: m => m.quantity() },
143
+ { test: /(Currency|Ccy)$/, apply: m => m.rng.pick(currencies) },
144
+ { test: /Symbol$/, apply: m => m.current().symbol },
145
+ { test: /(Exchange|Mkt|Market)$/, apply: m => m.rng.pick(mics) },
146
+ { test: /SecurityID$/, apply: m => m.current().securityId },
147
+ { test: /(MonthYear)$/, apply: m => m.current().maturityMonthYear ?? m.instrument.maturityMonthYear },
148
+ { test: /(Amt|Amount|Notional|Consideration)$/, apply: m => m.grossAmount() },
149
+ { test: /(Rate|Yield|Coupon)$/, apply: m => Number((m.rng.int(1, 800) / 10000).toFixed(5)) },
150
+ { test: /(Percent|Pct|Ratio)$/, apply: m => Number((m.rng.int(0, 10000) / 100).toFixed(2)) },
151
+ { test: /Account$/, apply: m => m.account },
152
+ { test: /(Desk|Division)$/, apply: m => m.desk },
153
+ { test: /(Text|Desc|Description|Comment|Instructions)$/, apply: m => m.text() },
154
+ { test: /(Timestamp|Time)$/, apply: m => m.transactTime },
155
+ { test: /Date$/, apply: m => m.tradeDate },
156
+ { test: /(Broker|Firm)$/, apply: (m, ctx) => m.party(ctx.instance).id },
157
+ { test: /(Trader|User|Operator)$/, apply: m => m.traders[0].id }
158
+ ]
159
+
160
+ function initials (name: string): string {
161
+ const caps = name.replace(/[^A-Z]/g, '')
162
+ if (caps.length >= 2) return caps.substring(0, 4)
163
+ return name.substring(0, 3).toUpperCase()
164
+ }
165
+
166
+ export class ValueConventions {
167
+ constructor (public readonly market: MarketContext) {
168
+ }
169
+
170
+ /**
171
+ * whether this tag is one the generator understands rather than merely guesses at.
172
+ * The field walker reads it as a proxy for "would a real message of this type carry
173
+ * this field", which is a crude signal but a better one than the tag number alone.
174
+ */
175
+ public static knows (tag: number): boolean {
176
+ return byTag.has(tag)
177
+ }
178
+
179
+ /**
180
+ * a value for this field, or undefined if the caller should leave the tag out
181
+ */
182
+ public value (sf: SimpleFieldDefinition, ctx: IFieldContext): any {
183
+ const exact = byTag.get(sf.tag)
184
+ if (exact) {
185
+ const v = exact(this.market, ctx, sf)
186
+ if (v != null) return this.conform(sf, v)
187
+ // a handler declining - e.g. StrikePrice on a future - is a reason to omit, not
188
+ // a reason to fall through and invent one
189
+ return undefined
190
+ }
191
+ if (sf.isEnum()) {
192
+ return this.enumValue(sf)
193
+ }
194
+ for (const rule of nameRules) {
195
+ if (!rule.test.test(sf.name)) continue
196
+ const v = rule.apply(this.market, ctx, sf)
197
+ if (v == null) break
198
+ return this.conform(sf, v)
199
+ }
200
+ return this.byType(sf, ctx)
201
+ }
202
+
203
+ /**
204
+ * Dictionaries list enumerated values roughly in order of how ordinary they are, so a
205
+ * front biased pick lands on Side 1 or 2 far more often than on the twenty exotics
206
+ * below them. Not true universally, but true often enough to be worth more than a
207
+ * uniform draw, and it stays deterministic.
208
+ */
209
+ private enumValue (sf: SimpleFieldDefinition): any {
210
+ const keys = Array.from(sf.enums.keys())
211
+ if (keys.length === 0) return undefined
212
+ const choice = keys[this.market.rng.frontBiased(keys.length)]
213
+ switch (sf.tagType) {
214
+ case TagType.Int: return parseInt(choice, 10)
215
+ case TagType.Float: return parseFloat(choice)
216
+ case TagType.Boolean: return choice === 'Y'
217
+ default: return choice
218
+ }
219
+ }
220
+
221
+ /**
222
+ * a value supplied by tag or by name may not be of the type the dictionary declares -
223
+ * `SecurityIDSource` is a String in one dictionary and an Int in another - so coerce
224
+ * rather than emit something the encoder will mangle
225
+ */
226
+ private conform (sf: SimpleFieldDefinition, v: any): any {
227
+ switch (sf.tagType) {
228
+ case TagType.RawData: {
229
+ // a name rule may well have produced a perfectly good string for EncodedText,
230
+ // but the encoder writes the preceding Length only when handed a Buffer, and a
231
+ // raw field arriving without its length is unparseable
232
+ if (Buffer.isBuffer(v)) return v
233
+ return Buffer.from(String(v))
234
+ }
235
+ case TagType.Int:
236
+ case TagType.Length: {
237
+ if (typeof v === 'number') return Math.round(v)
238
+ const n = parseInt(String(v), 10)
239
+ return isNaN(n) ? undefined : n
240
+ }
241
+ case TagType.Float: {
242
+ if (typeof v === 'number') return v
243
+ const f = parseFloat(String(v))
244
+ return isNaN(f) ? undefined : f
245
+ }
246
+ case TagType.Boolean: {
247
+ if (typeof v === 'boolean') return v
248
+ return String(v) === 'Y' || String(v) === 'true'
249
+ }
250
+ case TagType.String: {
251
+ if (v instanceof Date) return MarketContext.yyyymmdd(v)
252
+ return String(v)
253
+ }
254
+ default:
255
+ return v
256
+ }
257
+ }
258
+
259
+ private byType (sf: SimpleFieldDefinition, ctx: IFieldContext): any {
260
+ const m = this.market
261
+ const rng = m.rng
262
+ switch (sf.tagType) {
263
+ case TagType.String:
264
+ if (ordinalName.test(sf.name)) return String(rng.int(0, 4))
265
+ // identifier shaped, and carrying the field it came from, so a message can be
266
+ // read back to the dictionary by eye
267
+ return `${initials(sf.name)}-${rng.int(1000, 99999)}`
268
+ case TagType.Int:
269
+ return ordinalName.test(sf.name) ? rng.int(0, 4) : rng.int(1, 500)
270
+ case TagType.Float:
271
+ return Number((rng.int(1, 100000) / 100).toFixed(2))
272
+ case TagType.Length:
273
+ // written by the encoder from the RawData it precedes
274
+ return undefined
275
+ case TagType.Boolean:
276
+ return rng.bool(0.5)
277
+ case TagType.UtcTimestamp:
278
+ return m.transactTime
279
+ case TagType.UtcTimeOnly: {
280
+ const t = m.transactTime
281
+ return new Date(Date.UTC(0, 0, 0, t.getUTCHours(), t.getUTCMinutes(), t.getUTCSeconds(), 0))
282
+ }
283
+ case TagType.UtcDateOnly:
284
+ return m.tradeDate
285
+ case TagType.LocalDate: {
286
+ const d = m.tradeDate
287
+ return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0)
288
+ }
289
+ case TagType.RawData:
290
+ return Buffer.from(`${initials(sf.name)}:${m.tradeId}:${ctx.instance}`)
291
+ default:
292
+ return undefined
293
+ }
294
+ }
295
+ }
package/src/index.ts CHANGED
@@ -4,3 +4,15 @@ export * from './buffer'
4
4
  export * from './transport'
5
5
  export * from './config'
6
6
  export * from './types'
7
+ export * from './collections'
8
+ export * from './store'
9
+ export * from './util/definition-factory'
10
+ export * from './util/dictionary-path'
11
+
12
+ // These live in the generated dictionaries but appear in the public signature of
13
+ // the session message factory, so an application overriding logon() or header()
14
+ // has to be able to name them. Without this it can only reach them behind a
15
+ // dist/ path, which is what drove the module augmentation in issue #85. The
16
+ // FIXML one is aliased because both dictionaries call theirs IStandardHeader.
17
+ export type { IStandardHeader, IHop } from './types/FIX4.4/repo'
18
+ export type { IStandardHeader as IFixmlStandardHeader } from './types/FIXML50SP2'
package/src/jsfix-cmd.ts CHANGED
@@ -5,6 +5,7 @@ import { AsciiParser, AsciiView, AsciiChars } from './buffer/ascii'
5
5
  import { ILooseObject } from './collections/collection'
6
6
  import { SimpleFieldDefinition, FixDefinitions } from './dictionary/definition'
7
7
  import { MessageGenerator, JsonHelper, getWords, DefinitionFactory } from './util'
8
+ import { CaseBuilder, CorpusStore, IGeneratedCase, IGeneratorOptions, IOutcome, Scenarios, classify, describeCase, describeOutcome } from './generator'
8
9
  import { ISessionDescription, FileDuplex, StringDuplex } from './transport'
9
10
 
10
11
  import { MsgTag } from './types'
@@ -118,6 +119,46 @@ const options = buildOptions({
118
119
  default: 0.8
119
120
  },
120
121
 
122
+ scenarios: {
123
+ type: 'boolean',
124
+ default: false
125
+ },
126
+
127
+ realistic: {
128
+ type: 'boolean',
129
+ default: false
130
+ },
131
+
132
+ 'russian-doll': {
133
+ type: 'number',
134
+ default: 0
135
+ },
136
+
137
+ fragments: {
138
+ type: 'number',
139
+ default: 2
140
+ },
141
+
142
+ seed: {
143
+ type: 'number',
144
+ default: 0
145
+ },
146
+
147
+ 'as-of': {
148
+ type: 'string',
149
+ default: ''
150
+ },
151
+
152
+ 'corpus-add': {
153
+ type: 'string',
154
+ default: ''
155
+ },
156
+
157
+ note: {
158
+ type: 'string',
159
+ default: ''
160
+ },
161
+
121
162
  repeats: {
122
163
  type: 'number',
123
164
  alias: 'r',
@@ -342,6 +383,15 @@ export class JsfixCmd {
342
383
  }
343
384
 
344
385
  protected async generate (): Promise<void> {
386
+ if (argv.scenarios) {
387
+ console.log('scenarios - a named shape of traffic, generated to look like the real thing')
388
+ console.log(Scenarios.describe())
389
+ return
390
+ }
391
+ if (argv.scenario || argv.realistic || argv['russian-doll'] > 0) {
392
+ await this.generateCase()
393
+ return
394
+ }
345
395
  const lipPath: string = path.join(this.root, 'data/examples/lipsum.txt')
346
396
  const words: string[] = await getWords(lipPath)
347
397
  const generator = new MessageGenerator(words, this.definitions)
@@ -361,6 +411,134 @@ export class JsfixCmd {
361
411
  }
362
412
  }
363
413
 
414
+ /**
415
+ * The realistic generator: a message that reads like traffic, optionally taken apart
416
+ * into a legal but scattered ordering, and then round tripped through the parser to
417
+ * say whether this engine survives the shape.
418
+ */
419
+ private async generateCase (): Promise<void> {
420
+ if (!this.session) {
421
+ console.log('provide a session json file e.g. --session=data/session/test-initiator.json')
422
+ return
423
+ }
424
+ const delimiter = String.fromCharCode(this.delimiter)
425
+ const builder = new CaseBuilder(this.definitions, (msgType, obj) => this.encodeObject(msgType, obj))
426
+ const generated = builder.build({
427
+ scenario: argv.scenario,
428
+ msgType: argv.type,
429
+ seed: argv.seed || 0x5eed,
430
+ depth: argv['russian-doll'] || 0,
431
+ fragments: argv.fragments || 2,
432
+ delimiter,
433
+ generator: JsfixCmd.generatorOverrides()
434
+ })
435
+ console.log(describeCase(generated, delimiter))
436
+ console.log('')
437
+ console.log(JSON.stringify(generated.object, null, 4))
438
+ console.log('')
439
+ console.log(`canonical ${generated.canonical}`)
440
+ if (generated.plan.depth > 0) {
441
+ console.log(`scattered ${generated.scattered}`)
442
+ }
443
+ const outcome = await this.reportRoundTrip(generated)
444
+ if (argv.unit) {
445
+ await JsfixCmd.writeFile('./fix.txt', generated.scattered)
446
+ await JsfixCmd.writeFile('./canonical.txt', generated.canonical)
447
+ await JsfixCmd.writeFile('./object.json', JSON.stringify(generated.object, null, 4))
448
+ await JsfixCmd.writeFile('./plan.json', JSON.stringify(generated.plan, null, 4))
449
+ await JsfixCmd.writeFile('./case.txt', describeCase(generated, delimiter))
450
+ }
451
+ if (argv['corpus-add']) {
452
+ this.addToCorpus(generated, outcome, delimiter)
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Only the options actually given on the command line. A key present with an undefined
458
+ * value still wins a spread, so building this object unconditionally would silently
459
+ * erase whatever density the chosen scenario had asked for.
460
+ */
461
+ private static generatorOverrides (): IGeneratorOptions {
462
+ const overrides: ILooseObject = {}
463
+ // a seed fixes every choice the generator makes, but dates are read off the wall
464
+ // clock unless anchored, so a reproducible corpus needs --as-of as well
465
+ if (argv['as-of']) {
466
+ overrides.asOf = new Date(argv['as-of'])
467
+ }
468
+ if (argv.realistic && argv.density != null) {
469
+ overrides.density = parseFloat(argv.density)
470
+ }
471
+ return overrides
472
+ }
473
+
474
+ /**
475
+ * Freeze this case into the corpus. What is written is the bytes and the expectation,
476
+ * not the seed - a seed only reproduces a message while the generator is unchanged, and
477
+ * a corpus has to outlive the tool that proposed it.
478
+ */
479
+ private addToCorpus (generated: IGeneratedCase, outcome: IOutcome | null, delimiter: string): void {
480
+ if (generated.plan.depth === 0) {
481
+ console.log('cannot add to the corpus - nothing was scattered, so there is nothing to assert')
482
+ return
483
+ }
484
+ if (!outcome) {
485
+ console.log('cannot add to the corpus - the canonical encoding did not parse')
486
+ return
487
+ }
488
+ const name = `${argv['corpus-add']}`
489
+ const note = argv.note ? `${argv.note}` : describeOutcome(outcome)
490
+ // the corpus records the session relative to data/, and a path may arrive with
491
+ // either separator on windows
492
+ const session = `${argv.session}`.replace(/^.*data[\\/]/, '')
493
+ const store = new CorpusStore(path.join(this.root, 'data/corpus'))
494
+ const entry = CorpusStore.fromCase(
495
+ name,
496
+ note,
497
+ `${argv.dict ?? this.sessionDescription?.application?.dictionary ?? 'unknown'}`,
498
+ session,
499
+ delimiter,
500
+ generated,
501
+ outcome.outcome,
502
+ outcome,
503
+ argv['as-of'] ? new Date(argv['as-of']) : undefined)
504
+ const written = store.save(entry, generated.canonical, generated.scattered)
505
+ console.log(`corpus case ${name} written to ${written}`)
506
+ console.log(`expected outcome recorded as ${outcome.outcome} - read it before committing`)
507
+ }
508
+
509
+ /**
510
+ * Parse both encodings and compare. The canonical one is contiguous by construction
511
+ * so it is the oracle; the scattered one says the same thing in a different order, so
512
+ * any difference at all is the engine losing information.
513
+ */
514
+ private async reportRoundTrip (generated: IGeneratedCase): Promise<IOutcome | null> {
515
+ const canonical = await this.parseOnce(generated.canonical)
516
+ if (!canonical.view) {
517
+ console.log(`canonical does not parse: ${canonical.error ?? 'no message'}`)
518
+ return null
519
+ }
520
+ if (generated.plan.depth === 0) {
521
+ console.log('canonical parses')
522
+ return null
523
+ }
524
+ const scattered = await this.parseOnce(generated.scattered)
525
+ const outcome = classify(
526
+ canonical.view.toObject(),
527
+ scattered.view ? scattered.view.toObject() : null)
528
+ console.log(describeOutcome(outcome))
529
+ return outcome
530
+ }
531
+
532
+ private async parseOnce (fix: string): Promise<{ view: MsgView | null, error: string | null }> {
533
+ const transport: MsgTransport = new MsgTransport(1, this.session.config, new StringDuplex(fix))
534
+ try {
535
+ const view = await this.firstMessage(transport)
536
+ return { view, error: null }
537
+ } catch (e: any) {
538
+ return { view: null, error: e?.message ?? `${e}` }
539
+ }
540
+ }
541
+
364
542
  private async single (generator: MessageGenerator, density: number): Promise<void> {
365
543
  if (!argv.type) {
366
544
  console.log('specify type to generate e.g. --type = AE')
@@ -778,6 +956,32 @@ function showHelp (): void {
778
956
  console.log('npm run fixml -- --field=50')
779
957
  console.log()
780
958
 
959
+ console.log('list the named scenarios - lifelike messages built from a book of real instruments')
960
+ console.log('node dist/jsfix-cmd --generate --scenarios --session=data/session/test-initiator.json')
961
+ console.log('npm run scenarios')
962
+ console.log()
963
+
964
+ console.log('generate one lifelike message - prices on tick, quantities in lots, dates that agree')
965
+ console.log('node dist/jsfix-cmd --generate --scenario=spread-trade-capture --delimiter="|" --session=data/session/test-initiator.json')
966
+ console.log('npm run scenario -- --scenario=equity-fill')
967
+ console.log()
968
+
969
+ console.log('take the message apart - russian doll n scatters a component inside a component, n levels deep,')
970
+ console.log('then parses both orderings and says whether this engine survives the shape')
971
+ console.log('node dist/jsfix-cmd --generate --scenario=spread-trade-capture --russian-doll=2 --delimiter="|" --session=data/session/test-initiator.json')
972
+ console.log('npm run doll -- --scenario=spread-trade-capture --russian-doll=2')
973
+ console.log('add --seed=42 --as-of=2026-01-15T10:30:00Z to reproduce a case exactly, --unit to write the files')
974
+ console.log()
975
+
976
+ console.log('keep an interesting case - freezes the two encodings and the outcome into data/corpus,')
977
+ console.log('where src/test/ascii/corpus.test.ts picks it up. read what it wrote, then commit it')
978
+ console.log('npm run doll -- --scenario=spread-trade-capture --russian-doll=2 --seed=1 --as-of=2026-01-15T10:30:00Z --corpus-add=my-case --note="what this shows"')
979
+ console.log()
980
+
981
+ console.log('any message type, lifelike rather than dense - see docs/scattered-components.md for what doll depth means')
982
+ console.log('node dist/jsfix-cmd --generate --realistic --type=8 --density=0.5 --delimiter="|" --session=data/session/test-initiator.json')
983
+ console.log()
984
+
781
985
  console.log('generate unit test set of files - i.e. randomly generate an object, encode to fix. density 1 is all fields')
782
986
  console.log('node dist/jsfix-cmd --generate --type=AE --density=0.8 --unit --delimiter="|" --session=data/session/test-initiator.json')
783
987
  console.log('npm run repo44-unit -- --type=AE')
@@ -2,7 +2,8 @@ import { IFixMsgStore } from './fix-msg-store'
2
2
  import { FixMsgStoreRecord, IFixMsgStoreRecord } from './fix-msg-store-record'
3
3
  import { IJsFixConfig } from '../config'
4
4
  import { MsgType } from '../types'
5
- import { ElasticBuffer, MsgView } from '../buffer'
5
+ import { ElasticBuffer } from '../buffer/elastic-buffer'
6
+ import { MsgView } from '../buffer/msg-view'
6
7
  import { AsciiParser } from '../buffer/ascii'
7
8
  import { ISequenceReset, IStandardHeader } from '../types/FIX4.4/repo'
8
9
 
@@ -1,5 +1,8 @@
1
1
  import { ILooseObject } from '../collections/collection'
2
- import { MsgView } from '../buffer'
2
+ // straight at the module, not the ../buffer barrel: the barrel now names the
3
+ // ascii parser, which reaches config and back into this file - see the madge
4
+ // check in npm run circular
5
+ import { MsgView } from '../buffer/msg-view'
3
6
  import { MsgTag } from '../types'
4
7
 
5
8
  export interface IFixMsgStoreRecord {
@@ -11,3 +11,4 @@ export * from './memory-session-stream-provider'
11
11
  export * from './file-session-stream-provider'
12
12
  export * from './file-session-store'
13
13
  export * from './store-config'
14
+ export * from './fix-msg-store-state'
@@ -13,3 +13,13 @@ export * from './ascii/ascii-session'
13
13
  export * from './ascii/ascii-session-msg-factory'
14
14
  export * from './fixml/fixml-session-msg-factory'
15
15
  export * from './session'
16
+ export * from './factory'
17
+ // the transport description types an application writes in its session JSON -
18
+ // the concrete tcp/http entities stay out of the root barrel so that requiring
19
+ // jspurefix does not drag express in behind them
20
+ export * from './tcp/tcp-transport-description'
21
+ export * from './tcp/tls-options'
22
+ export * from './http/http-transport-description'
23
+ export * from './http/http-adapter'
24
+ export * from './http/html-options'
25
+ export * from './http/html-route'
@@ -12,3 +12,4 @@ export * from './resend-request-manager'
12
12
  export * from './fix-clock'
13
13
  export * from './session-registry'
14
14
  export * from './clone-session-description'
15
+ export * from './fix-session-state-args'