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,404 @@
1
+ import { ILooseObject } from '../collections/collection'
2
+ import {
3
+ ContainedComponentField,
4
+ ContainedFieldType,
5
+ ContainedGroupField,
6
+ ContainedSimpleField,
7
+ IContainedSet
8
+ } from '../dictionary/contained'
9
+ import { FixDefinitions, MessageDefinition } from '../dictionary/definition'
10
+ import { TagType } from '../buffer/tag/tag-type'
11
+ import { Rng } from './rng'
12
+ import { MarketContext } from './market-context'
13
+ import { IFieldContext, ValueConventions } from './value-conventions'
14
+ import { AssetClass, IInstrumentSpec, InstrumentUniverse } from './instrument-universe'
15
+ import { fieldApplies, setApplies } from './applicability'
16
+
17
+ /**
18
+ * Builds an object for a message type that a counterparty might plausibly have sent.
19
+ *
20
+ * The difference from `MessageGenerator` is what gets left out. The old generator asked
21
+ * "should this tag be present" of every tag independently at one probability, which at
22
+ * any useful density produces every field in the dictionary and a message no human can
23
+ * read. Here optional fields are tiered - a field the generator understands is likely,
24
+ * an ordinary one is occasional, an exotic one is rare - so the shape of the output
25
+ * resembles the shape of real traffic even before the values are looked at.
26
+ *
27
+ * Two structural guarantees the encoder depends on are maintained by construction:
28
+ *
29
+ * - every instance of a repeating group carries the group delimiter, propagated down
30
+ * through whatever nested component happens to own it;
31
+ * - a component that ends up with no fields at all is dropped rather than emitted
32
+ * empty.
33
+ */
34
+
35
+ export interface IGeneratorOptions {
36
+ /** same seed, same message, byte for byte */
37
+ readonly seed?: number
38
+ /**
39
+ * multiplier on the optional field probabilities. 0 gives required fields only;
40
+ * 1 is the natural rate; above 1 fattens the message toward the old behaviour.
41
+ */
42
+ readonly density?: number
43
+ readonly asOf?: Date
44
+ readonly instrument?: string | IInstrumentSpec
45
+ readonly assetClass?: AssetClass
46
+ /** optional components below this depth are not populated; required ones still are */
47
+ readonly maxDepth?: number
48
+ /** upper bound on instances of any one group */
49
+ readonly maxGroupInstances?: number
50
+ /**
51
+ * dotted paths or bare names that must appear, e.g. `Instrument` or
52
+ * `TrdCapRptSideGrp.Parties`. Used by the scatter planner to guarantee the shape it
53
+ * is about to take apart.
54
+ */
55
+ readonly include?: readonly string[]
56
+ readonly exclude?: readonly string[]
57
+ readonly senderCompId?: string
58
+ readonly targetCompId?: string
59
+ }
60
+
61
+ export interface IGeneratedMessage {
62
+ readonly msgType: string
63
+ readonly name: string
64
+ readonly object: ILooseObject
65
+ readonly market: MarketContext
66
+ readonly definition: MessageDefinition
67
+ }
68
+
69
+ interface IWalkState {
70
+ readonly path: readonly string[]
71
+ readonly depth: number
72
+ readonly instance: number
73
+ /** tags that must be emitted somewhere below here, whatever the dice say */
74
+ readonly mustInclude: readonly number[]
75
+ }
76
+
77
+ /**
78
+ * Components a message of any kind is very likely to carry. Without this the tiering
79
+ * treats `Instrument` as no more probable than any other optional component, and a
80
+ * market data refresh comes out describing the price of nothing at all.
81
+ */
82
+ const coreSets = /^(Instrument|InstrumentLeg|UnderlyingInstrument|OrderQtyData|Parties|NestedParties[0-9]?|InstrmtLegGrp|TrdInstrmtLegGrp|InstrmtMDReqGrp|MDIncGrp|MDFullGrp)$/
83
+
84
+ /** a set that names legs, as opposed to a field that merely starts with Leg */
85
+ const legSet = /Leg(s|Grp|$|[A-Z])|InstrmtLeg/
86
+ /** UndInstrmtGrp and UnderlyingInstrument both, since the abbreviation is not uniform */
87
+ const underlyingSet = /Und(erlying|Instrmt)/
88
+
89
+ export class RealisticGenerator {
90
+ private readonly rng: Rng
91
+ private readonly density: number
92
+ private readonly maxDepth: number
93
+ private readonly maxGroupInstances: number
94
+ private readonly forced: Set<string>
95
+ private readonly forcedTags: number[]
96
+ private readonly suppressed: Set<string>
97
+ private assetClass: AssetClass = AssetClass.Equity
98
+
99
+ constructor (
100
+ public readonly definitions: FixDefinitions,
101
+ public readonly options: IGeneratorOptions = {}) {
102
+ this.rng = new Rng(options.seed ?? 0x5eed)
103
+ this.density = options.density ?? 1.0
104
+ this.maxDepth = options.maxDepth ?? 6
105
+ this.maxGroupInstances = options.maxGroupInstances ?? 2
106
+ // an entry that is all digits is a tag, which is the portable way to name a field:
107
+ // repository dictionaries call a group TrdInstrmtLegGrp and QuickFIX calls it NoLegs,
108
+ // but both agree that it is 555
109
+ const include = options.include ?? []
110
+ this.forcedTags = include.filter(s => /^[0-9]+$/.test(s)).map(s => parseInt(s, 10))
111
+ this.forced = new Set<string>(include.filter(s => !/^[0-9]+$/.test(s)))
112
+ this.suppressed = new Set<string>(options.exclude ?? [])
113
+ }
114
+
115
+ public generate (msgType: string): IGeneratedMessage {
116
+ const definition = this.definitions.message.get(msgType)
117
+ if (!definition) {
118
+ throw new Error(`definitions do not contain type ${msgType}`)
119
+ }
120
+ const instrument = this.resolveInstrument()
121
+ const market = new MarketContext({
122
+ rng: this.rng,
123
+ instrument,
124
+ assetClass: this.options.assetClass,
125
+ asOf: this.options.asOf,
126
+ senderCompId: this.options.senderCompId,
127
+ targetCompId: this.options.targetCompId
128
+ })
129
+ const conventions = new ValueConventions(market)
130
+ this.assetClass = market.instrument.assetClass
131
+ const state: IWalkState = { path: [], depth: 0, instance: 0, mustInclude: this.forcedTags }
132
+ const object = this.build(definition, market, conventions, state) ?? {}
133
+ return {
134
+ msgType: definition.msgType ?? msgType,
135
+ name: definition.name,
136
+ object,
137
+ market,
138
+ definition
139
+ }
140
+ }
141
+
142
+ private resolveInstrument (): IInstrumentSpec | undefined {
143
+ const wanted = this.options.instrument
144
+ if (!wanted) return undefined
145
+ if (typeof wanted !== 'string') return wanted
146
+ const found = InstrumentUniverse.byName(wanted)
147
+ if (!found) {
148
+ throw new Error(`no instrument named ${wanted} - try one of ${InstrumentUniverse.all.map(i => i.symbol).join(', ')}`)
149
+ }
150
+ return found
151
+ }
152
+
153
+ private build (
154
+ set: IContainedSet,
155
+ market: MarketContext,
156
+ conventions: ValueConventions,
157
+ state: IWalkState): ILooseObject | null {
158
+ const o: ILooseObject = {}
159
+ const fields = set.fields
160
+ for (let i = 0; i < fields.length; ++i) {
161
+ const field = fields[i]
162
+ switch (field.type) {
163
+ case ContainedFieldType.Simple:
164
+ this.simple(o, set, field as ContainedSimpleField, i, conventions, state)
165
+ break
166
+ case ContainedFieldType.Component:
167
+ this.component(o, field as ContainedComponentField, market, conventions, state)
168
+ break
169
+ case ContainedFieldType.Group:
170
+ this.group(o, field as ContainedGroupField, market, conventions, state)
171
+ break
172
+ default:
173
+ break
174
+ }
175
+ }
176
+ return Object.keys(o).length > 0 ? o : null
177
+ }
178
+
179
+ private simple (
180
+ o: ILooseObject,
181
+ set: IContainedSet,
182
+ sf: ContainedSimpleField,
183
+ index: number,
184
+ conventions: ValueConventions,
185
+ state: IWalkState): void {
186
+ const definition = sf.definition
187
+ // the encoder writes a Length from the RawData that follows it, so supplying one
188
+ // here would only give it the chance to disagree
189
+ if (definition.tagType === TagType.Length && RealisticGenerator.rawFollows(set, index)) {
190
+ return
191
+ }
192
+ const dotted = RealisticGenerator.dot(state.path, sf.name)
193
+ if (this.suppressed.has(dotted) || this.suppressed.has(sf.name)) return
194
+ const must = state.mustInclude.includes(definition.tag)
195
+ if (!must && !sf.required && !this.isForced(dotted, sf.name)) {
196
+ if (!fieldApplies(sf.name, this.assetClass)) return
197
+ if (!this.rng.bool(this.probability(sf))) return
198
+ }
199
+ const fieldContext: IFieldContext = { path: state.path, instance: state.instance }
200
+ const value = conventions.value(definition, fieldContext)
201
+ if (value != null && value !== '') {
202
+ o[sf.name] = value
203
+ }
204
+ }
205
+
206
+ private component (
207
+ o: ILooseObject,
208
+ cf: ContainedComponentField,
209
+ market: MarketContext,
210
+ conventions: ValueConventions,
211
+ state: IWalkState): void {
212
+ const name = cf.name
213
+ if (name === 'StandardHeader' || name === 'StandardTrailer') return
214
+ const dotted = RealisticGenerator.dot(state.path, name)
215
+ if (this.suppressed.has(dotted) || this.suppressed.has(name)) return
216
+
217
+ const definition = cf.definition
218
+ const carries = RealisticGenerator.carriesAny(definition, state.mustInclude)
219
+ if (!cf.required && !carries && !this.isForced(dotted, name)) {
220
+ if (state.depth >= this.maxDepth) return
221
+ if (!setApplies(name, this.assetClass)) return
222
+ if (!this.rng.bool(this.componentProbability(name, state.depth))) return
223
+ }
224
+ const restore = this.enter(name, market, state.instance)
225
+ const child = this.build(definition, market, conventions, {
226
+ path: [...state.path, name],
227
+ depth: state.depth + 1,
228
+ instance: state.instance,
229
+ // only the tags this component can actually reach travel further down
230
+ mustInclude: state.mustInclude.filter(t => definition.containedTag[t])
231
+ })
232
+ restore()
233
+ if (child) {
234
+ o[name] = child
235
+ }
236
+ }
237
+
238
+ private group (
239
+ o: ILooseObject,
240
+ gf: ContainedGroupField,
241
+ market: MarketContext,
242
+ conventions: ValueConventions,
243
+ state: IWalkState): void {
244
+ const name = gf.name
245
+ const dotted = RealisticGenerator.dot(state.path, name)
246
+ if (this.suppressed.has(dotted) || this.suppressed.has(name)) return
247
+
248
+ const definition = gf.definition
249
+ const carries = RealisticGenerator.carriesAny(definition, state.mustInclude)
250
+ if (!gf.required && !carries && !this.isForced(dotted, name)) {
251
+ if (state.depth >= this.maxDepth) return
252
+ if (!setApplies(name, this.assetClass)) return
253
+ if (!this.rng.bool(this.groupProbability(name, state.depth))) return
254
+ }
255
+
256
+ const isLegs = legSet.test(name) || legSet.test(definition.name)
257
+ const legs = market.instrument.legs
258
+ // a strategy has exactly the legs it has, so the group size is not a dice roll
259
+ const count = isLegs && legs && legs.length > 0
260
+ ? legs.length
261
+ : this.instances(state.depth)
262
+
263
+ // every instance has to open with the group delimiter or the encoder rejects it,
264
+ // and the delimiter may be owned by a component nested inside the instance body
265
+ const delimiter = definition.firstSimple?.definition.tag
266
+ const inherited = state.mustInclude.filter(t => definition.containedTag[t])
267
+ const must = delimiter ? [delimiter, ...inherited] : inherited
268
+
269
+ const instances: ILooseObject[] = []
270
+ for (let i = 0; i < count; ++i) {
271
+ const restore = this.enter(name, market, i)
272
+ const instance = this.build(definition, market, conventions, {
273
+ path: [...state.path, name],
274
+ depth: state.depth + 1,
275
+ instance: i,
276
+ mustInclude: must
277
+ })
278
+ restore()
279
+ // an instance whose only content is a nested group carries no delimiter, and the
280
+ // encoder rejects the whole group for it. This happens where `firstSimple` cannot
281
+ // name a delimiter to force - a group whose body opens with another group - so the
282
+ // guarantee above is not always enough and the shape has to be checked for.
283
+ if (instance && RealisticGenerator.hasOwnSimple(instance, definition)) {
284
+ instances.push(instance)
285
+ }
286
+ }
287
+
288
+ if (instances.length > 0) {
289
+ o[name] = instances
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Descending into a set that names legs or underlyings moves the market context onto
295
+ * that leg or underlying, so `LegPrice` inside the second leg resolves against the
296
+ * second contract without the value layer knowing where it is. Returns the undo, so
297
+ * the context is restored however the walk below it turns out.
298
+ *
299
+ * Both a group and the component inside it are checked, because dictionaries disagree
300
+ * about which one carries the name - `UndInstrmtGrp` wrapping `UnderlyingInstrument`
301
+ * in the repository rendering, a bare `NoUnderlyings` in QuickFIX.
302
+ */
303
+ private enter (name: string, market: MarketContext, instance: number): () => void {
304
+ const wasLeg = market.leg
305
+ const wasUnderlying = market.underlying
306
+ const legs = market.instrument.legs
307
+ if (legSet.test(name) && legs && legs.length > 0) {
308
+ market.leg = legs[instance % legs.length]
309
+ } else if (underlyingSet.test(name)) {
310
+ market.underlying = market.underlying ?? InstrumentUniverse.choose(
311
+ this.rng,
312
+ market.instrument.assetClass === AssetClass.Spread ? AssetClass.Future : AssetClass.Equity)
313
+ } else {
314
+ return () => {}
315
+ }
316
+ return () => {
317
+ market.leg = wasLeg
318
+ market.underlying = wasUnderlying
319
+ }
320
+ }
321
+
322
+ /**
323
+ * How likely an optional simple field is to appear. A field the value layer knows by
324
+ * tag is one a real message of this type probably carries; a low numbered tag is
325
+ * everyday FIX; everything else is somebody else's extension and should be rare.
326
+ */
327
+ private probability (sf: ContainedSimpleField): number {
328
+ const tag = sf.definition.tag
329
+ let base: number
330
+ if (ValueConventions.knows(tag)) {
331
+ base = 0.75
332
+ } else if (tag < 500) {
333
+ base = 0.3
334
+ } else {
335
+ base = 0.08
336
+ }
337
+ return Math.min(1, base * this.density)
338
+ }
339
+
340
+ private componentProbability (name: string, depth: number): number {
341
+ if (coreSets.test(name)) return Math.min(1, 0.9 * Math.max(0.5, this.density))
342
+ return Math.min(1, (0.45 / (depth + 1)) * this.density)
343
+ }
344
+
345
+ private groupProbability (name: string, depth: number): number {
346
+ if (coreSets.test(name)) return Math.min(1, 0.8 * Math.max(0.5, this.density))
347
+ return Math.min(1, (0.35 / (depth + 1)) * this.density)
348
+ }
349
+
350
+ private instances (depth: number): number {
351
+ const cap = Math.max(1, this.maxGroupInstances - depth + 1)
352
+ return this.rng.int(1, Math.min(this.maxGroupInstances, cap))
353
+ }
354
+
355
+ private isForced (dotted: string, name: string): boolean {
356
+ if (this.forced.has(dotted) || this.forced.has(name)) return true
357
+ // forcing a path forces everything above it
358
+ for (const f of this.forced) {
359
+ if (f.startsWith(`${dotted}.`)) return true
360
+ }
361
+ return false
362
+ }
363
+
364
+ /**
365
+ * whether this object emits at least one simple field at its own level - through
366
+ * nested components, which the encoder counts as the same level, but not through a
367
+ * group, which is framed separately and cannot serve as a delimiter
368
+ */
369
+ private static hasOwnSimple (o: ILooseObject, set: IContainedSet): boolean {
370
+ for (const field of set.fields) {
371
+ switch (field.type) {
372
+ case ContainedFieldType.Simple:
373
+ if (o[field.name] != null && o[field.name] !== '') return true
374
+ break
375
+ case ContainedFieldType.Component: {
376
+ const cf = field as ContainedComponentField
377
+ const child = o[cf.name]
378
+ if (child && RealisticGenerator.hasOwnSimple(child as ILooseObject, cf.definition)) return true
379
+ break
380
+ }
381
+ default:
382
+ break
383
+ }
384
+ }
385
+ return false
386
+ }
387
+
388
+ private static carriesAny (set: IContainedSet, tags: readonly number[]): boolean {
389
+ for (const t of tags) {
390
+ if (set.containedTag[t]) return true
391
+ }
392
+ return false
393
+ }
394
+
395
+ private static rawFollows (set: IContainedSet, index: number): boolean {
396
+ const next = set.fields[index + 1]
397
+ if (!next || next.type !== ContainedFieldType.Simple) return false
398
+ return (next as ContainedSimpleField).definition.tagType === TagType.RawData
399
+ }
400
+
401
+ private static dot (path: readonly string[], name: string): string {
402
+ return path.length > 0 ? `${path.join('.')}.${name}` : name
403
+ }
404
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A small deterministic generator, so a corpus written today reproduces byte for byte
3
+ * tomorrow. `Math.random` is fine for a benchmark that only wants volume; it is no use
4
+ * at all for a fixture someone has to debug, where "run it again and get the same
5
+ * message" is the whole point.
6
+ *
7
+ * mulberry32 - 32 bits of state, uniform enough for choosing fields and prices, and
8
+ * short enough to port to cspurefix unchanged.
9
+ */
10
+ export class Rng {
11
+ private state: number
12
+
13
+ constructor (seed: number = 0x5eed) {
14
+ // 0 is a fixed point of the mixer, so fold it away rather than silently degenerate
15
+ this.state = (seed >>> 0) || 0x9e3779b9
16
+ }
17
+
18
+ /**
19
+ * uniform in [0, 1)
20
+ */
21
+ public next (): number {
22
+ this.state = (this.state + 0x6d2b79f5) >>> 0
23
+ let t = this.state
24
+ t = Math.imul(t ^ (t >>> 15), t | 1)
25
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
26
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
27
+ }
28
+
29
+ /**
30
+ * uniform integer in [min, max] inclusive
31
+ */
32
+ public int (min: number, max: number): number {
33
+ if (max < min) return min
34
+ return min + Math.floor(this.next() * (max - min + 1))
35
+ }
36
+
37
+ public bool (probability: number = 0.5): boolean {
38
+ return this.next() < probability
39
+ }
40
+
41
+ public pick<T> (from: readonly T[]): T {
42
+ return from[this.int(0, from.length - 1)]
43
+ }
44
+
45
+ /**
46
+ * an index into `count` items, biased toward the front. `bias` of 1 is uniform;
47
+ * higher values pull harder toward 0.
48
+ *
49
+ * used for enumerated fields, where the dictionary lists values roughly in order of
50
+ * how ordinary they are - Side 1 and 2 before the twenty exotic ones - so a uniform
51
+ * pick produces a message no counterparty would ever send.
52
+ */
53
+ public frontBiased (count: number, bias: number = 3): number {
54
+ if (count <= 1) return 0
55
+ return Math.min(count - 1, Math.floor(Math.pow(this.next(), bias) * count))
56
+ }
57
+
58
+ /**
59
+ * a value near `centre`, within +/- `spreadPct`, snapped to `tick`
60
+ */
61
+ public around (centre: number, spreadPct: number, tick: number): number {
62
+ const move = centre * spreadPct * (this.next() * 2 - 1)
63
+ const raw = centre + move
64
+ const snapped = Math.round(raw / tick) * tick
65
+ // ticks are decimal fractions, so re-round to kill the binary residue
66
+ const dp = Rng.decimals(tick)
67
+ return Number(snapped.toFixed(dp))
68
+ }
69
+
70
+ private static decimals (tick: number): number {
71
+ const s = tick.toString()
72
+ const dot = s.indexOf('.')
73
+ if (dot < 0) return 0
74
+ const exp = s.indexOf('e')
75
+ if (exp > 0) return Math.min(10, parseInt(s.substring(exp + 2), 10))
76
+ return Math.min(10, s.length - dot - 1)
77
+ }
78
+
79
+ /**
80
+ * shuffle a copy, Fisher-Yates from the seeded stream
81
+ */
82
+ public shuffled<T> (from: readonly T[]): T[] {
83
+ const a = from.slice()
84
+ for (let i = a.length - 1; i > 0; --i) {
85
+ const j = this.int(0, i)
86
+ const t = a[i]
87
+ a[i] = a[j]
88
+ a[j] = t
89
+ }
90
+ return a
91
+ }
92
+ }