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,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
|
+
}
|