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,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
|
|
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
|
-
|
|
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 {
|
package/src/store/index.ts
CHANGED
package/src/transport/index.ts
CHANGED
|
@@ -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'
|