jspurefix
Version:
pure node js fix engine
596 lines (533 loc) • 20.2 kB
text/typescript
import { ElasticBuffer, MsgView, MsgParser, AsciiParser, AsciiView, Ascii } from './buffer'
import { ILooseObject } from './collections/collection'
import { SimpleFieldDefinition, FixDefinitions } from './dictionary/definition'
import { MessageGenerator, JsonHelper, getDefinitions, getDictPath } from './util'
import { AsciiMsgTransmitter, ISessionDescription, SessionMsgFactory, MsgTransport, FileDuplex, StringDuplex } from './transport'
import { MsgCompiler, EnumCompiler, ICompilerSettings } from './dictionary'
import { MsgTag } from './types/enum'
import { JsFixConfig } from './config'
import * as util from 'util'
const fs = require('node-fs-extra')
import * as minimist from 'minimist'
import * as path from 'path'
const argv: any = minimist(process.argv.slice(2))
import { getWords } from './util/buffer-helper'
enum PrintMode {
Structure = 1,
Object = 2,
Verbose = 3,
Stats = 4,
Token = 5,
Encoded = 6
}
enum Command {
Generate = 1,
Replay = 2,
Lookup = 3,
Encode = 4,
Benchmark = 5,
Compile = 6,
Unknown = 7
}
export class JsfixCmd {
private readonly root: string = path.join(__dirname, '../')
private definitions: FixDefinitions
private jsonHelper: JsonHelper
private session: AsciiMsgTransmitter
private sessionDescription: ISessionDescription
private delimiter: number = Ascii.Soh
private stats: ILooseObject = {}
private filter: string = null
private messages: number = 0
private print: boolean = true
private static getCommand (): Command {
let command: Command = Command.Unknown
if (argv.compile) {
command = Command.Compile
} else if (argv.generate) {
command = Command.Generate
} else if (argv.fix) {
command = argv.benchmark ? Command.Benchmark : Command.Replay
} else if (argv.field) {
command = Command.Lookup
} else if (argv.json) {
command = Command.Encode
}
return command
}
private static getPrintMode (): PrintMode {
let mode: PrintMode = PrintMode.Stats
if (argv.tokens) {
mode = PrintMode.Token
} else if (argv.stats) {
mode = PrintMode.Stats
} else if (argv.objects) {
mode = PrintMode.Object
} else if (argv.verbose) {
mode = PrintMode.Verbose
} else if (argv.structures) {
mode = PrintMode.Structure
} else if (argv.encoded) {
mode = PrintMode.Encoded
}
return mode
}
private static async writeFile (name: string, api: string) {
const writer = util.promisify(fs.writeFile)
await writer(name, api, {
encoding: 'utf8'}
).catch((e: Error) => {
throw e
})
}
public exec (): Promise<any> {
return new Promise<any>((resolve, reject) => {
this.init().then(async () => {
let actioned: boolean = true
let command = JsfixCmd.getCommand()
switch (command) {
case Command.Generate: {
// produce a test message or a valid fix log of n messages
await this.generate()
break
}
case Command.Encode: {
// encode a json message back to fix
this.encode()
break
}
case Command.Replay: {
// parse a file into either objects, tokens, structures or stats
const repeats: number = !isNaN(argv.repeats) ? argv.repeats : 1
try {
for (let i: number = 0; i < repeats; ++i) {
await this.replay()
}
} catch (e) {
reject(e)
}
break
}
case Command.Benchmark: {
// time how long to parse 10000 repeats of contents of file
const repeats: number = !isNaN(argv.repeats) ? argv.repeats : 10000
try {
await this.benchmark(repeats)
} catch (e) {
reject(e)
}
break
}
case Command.Lookup: {
// lookup a field
this.field()
break
}
case Command.Compile: {
await this.compile()
break
}
case Command.Unknown:
default: {
actioned = false
}
}
resolve(actioned)
}).catch((e) => {
reject(e)
})
})
}
private firstMessage (t: MsgTransport): Promise<MsgView> {
return new Promise<MsgView>((resolve, reject) => {
t.receiver.on('msg', (msgType: string, msgView: MsgView) => {
resolve(msgView.clone())
})
t.receiver.on('error', (e) => {
reject(e)
})
})
}
private async generate () {
const lipPath: string = path.join(this.root, 'data/examples/lipsum.txt')
const words: string[] = await getWords(lipPath)
const generator = new MessageGenerator(words, this.definitions)
let density = 1
if (argv.density) {
density = parseFloat(argv.density)
}
if (isNaN(density)) {
console.log('density must be numeric in range > 0 density <= 1.0')
return
}
if (argv.script) {
await this.script(generator, density)
} else {
await this.single(generator, density)
}
}
private async single (generator: MessageGenerator, density: number) {
if (!argv.type) {
console.log('specify type to generate e.g. --type = AE')
return
}
const msgType: string = `${argv.type}`
let makeGroups: boolean = true
if (argv.groups) {
makeGroups = argv.groups === 'true'
}
const obj: ILooseObject = generator.generate(msgType, density, makeGroups)
console.log(JSON.stringify(obj, null, 4))
const fix: string = this.encodeObject(msgType, obj)
const ft: MsgTransport = new MsgTransport(1, this.session.config, new StringDuplex(fix))
if (argv.unit) {
await this.unitTest(fix, obj, ft)
} else {
this.subscribe(ft)
}
}
private async script (generator: MessageGenerator, density: number) {
let buffer: ElasticBuffer = new ElasticBuffer()
const repeats: number = argv.repeats || 50
const key: string = MsgTag.MsgType.toString()
const sf = this.definitions.simple.get(key)
const session: AsciiMsgTransmitter = this.session
for (let i = 0; i < repeats; ++i) {
const msgType: string = MessageGenerator.getRandomEnum(sf).toString()
console.log(`i = ${i} ${msgType}`)
const obj: ILooseObject = generator.generate(msgType, density)
session.encodeMessage(msgType, obj)
buffer.writeBuffer(session.buffer.slice())
buffer.writeString(require('os').EOL)
}
await JsfixCmd.writeFile('./fix.txt', buffer.slice().toString('utf8'))
}
private async unitTest (fix: string, obj: ILooseObject, ft: MsgTransport) {
const view: MsgView = await this.firstMessage(ft)
const summary = view.structure.summary()
await JsfixCmd.writeFile('./fix.txt', fix)
await JsfixCmd.writeFile('./object.json', JSON.stringify(obj, null, 4))
await JsfixCmd.writeFile('./token.txt', view.toString())
await JsfixCmd.writeFile('./structure.json', JSON.stringify(summary, null, 4))
}
private encodeObject (msgType: string, object: ILooseObject): string {
const session: AsciiMsgTransmitter = this.session
session.encodeMessage(msgType, object)
return session.buffer.toString()
}
private field (): void {
let sf: SimpleFieldDefinition
const tag: number = parseInt(argv.field, 10)
const definitions = this.definitions
if (!isNaN(tag)) {
sf = definitions.tagToSimple[tag]
} else {
sf = definitions.simple.get(argv.field)
}
if (sf) {
console.log(sf.toString())
}
}
ensureExists (path: string): Promise<any> {
return new Promise<any>((accept, reject) => {
fs.mkdirp(path, (err: Error) => {
if (err) {
reject(err)
} else {
accept()
}
})
})
}
private async compileDefinitions (outputPath: string) {
await this.ensureExists(path.join(outputPath, 'set'))
await this.ensureExists(path.join(outputPath, 'enum'))
const definitions = this.definitions
const compilerSettings: ICompilerSettings = require('../data/compiler.json')
compilerSettings.output = outputPath
const msgCompiler: MsgCompiler = new MsgCompiler(definitions, compilerSettings)
await msgCompiler.generate()
const enumCompiler: EnumCompiler = new EnumCompiler(definitions, compilerSettings)
const writeFile = path.join(compilerSettings.output, './enum/all-enum.ts')
await enumCompiler.generate(writeFile)
}
private async compile () {
let output = argv.output
const dp = getDictPath(argv.dict)
if (dp) {
output = dp.output
}
output = path.join(this.root, output)
await this.compileDefinitions(output)
}
private async init (): Promise<any> {
let session: string = argv.session || 'data/session/test-initiator.json'
session = this.norm(session)
this.sessionDescription = require(session)
let dict: string
if (argv.dict) {
dict = argv.dict
} else {
dict = this.sessionDescription.application.dictionary
}
this.definitions = await getDefinitions(dict)
const definitions = this.definitions
if (argv.delimiter) {
this.delimiter = Ascii.firstChar(argv.delimiter)
}
this.jsonHelper = new JsonHelper(definitions)
if (argv.session) {
const description = this.sessionDescription
const config = new JsFixConfig(new SessionMsgFactory(description), definitions, description, this.delimiter)
this.session = new AsciiMsgTransmitter(config)
}
}
private async dispatch (ft: MsgTransport): Promise<any> {
if (argv.type != null) {
this.filter = argv.type.toString()
}
let time: boolean = false
if (argv.time || argv.stats) {
this.print = false
time = true
}
this.subscribe(ft)
const startsAt: Date = new Date()
await ft.wait()
const elapsed: number = new Date().getTime() - startsAt.getTime()
if (time) {
console.log(`messages ${this.messages} elapsed ms ${elapsed}`)
}
if (argv.stats) {
console.log(JSON.stringify(this.stats, null, 4))
}
}
private subscribe (ft: MsgTransport) {
this.messages = 0
this.stats = {}
const filter = this.filter
// the receiver is message parser which is piped from an input stream - file, socket
ft.receiver.on('msg', (msgType: string, m: AsciiView) => {
if (filter) {
if (msgType !== filter) {
return
}
}
++this.messages
this.onMsg(msgType, m)
})
}
private onMsg (msgType: string, m: MsgView) {
const mode: PrintMode = JsfixCmd.getPrintMode()
const print = this.print
const stats = this.stats
switch (mode) {
case PrintMode.Stats: {
if (!stats[msgType]) {
stats[msgType] = 1
} else {
stats[msgType] = stats[msgType] + 1
}
break
}
case PrintMode.Verbose: {
const verbose = m.toVerbose()
if (verbose) {
console.log(verbose)
}
break
}
case PrintMode.Object: {
const asObject: ILooseObject = m.toObject()
if (print) {
const def = this.definitions.message.get(msgType)
console.log(`${msgType} [${def.name}] = ${JSON.stringify(asObject, null, 4)}`)
console.log()
}
break
}
case PrintMode.Structure: {
const summary = m.structure.summary()
if (print) {
console.log(JSON.stringify(summary, null, 4))
}
break
}
case PrintMode.Token: {
const tokens = m.toString()
if (print) {
console.log(tokens)
}
break
}
case PrintMode.Encoded: {
const fix: string = this.encodeObject(msgType, m.toObject())
console.log(fix)
break
}
default:
throw new Error(`unknown mode ${mode}`)
}
}
private async replay (): Promise<any> {
if (!argv.fix) {
console.log('provide a path to fix file i.e. --fix=data/examples/execution-report/fix.txt')
return
}
const fix: string = this.norm(argv.fix)
const config = new JsFixConfig(null, this.definitions, this.sessionDescription, this.delimiter)
const ft: MsgTransport = new MsgTransport(1, config, new FileDuplex(fix))
await this.dispatch(ft)
}
private async benchmark (repeats: number): Promise<any> {
if (!argv.fix) {
console.log('provide a path to fix file i.e. --fix=data/examples/execution-report/fix.txt')
return
}
return new Promise<any>((accept, reject) => {
const fix: string = this.norm(argv.fix)
const fs = require('fs')
const definitions = this.definitions
const delimiter = this.delimiter
fs.readFile(fix, 'utf8', async (err: Error, contents: string) => {
if (err) {
reject(err)
}
const startsAt: Date = new Date()
let i = 0
const asciiParser: MsgParser = new AsciiParser(definitions, new StringDuplex(contents.repeat(repeats)).readable, delimiter)
asciiParser.on('msg', (msgType: string, v: MsgView) => {
++i
if (i === repeats) {
const elapsed: number = new Date().getTime() - startsAt.getTime()
console.log(contents)
console.log(v.toString())
console.log(`[${msgType}]: repeats = ${repeats}, fields = ${v.structure.tags.nextTagPos}, length = ${contents.length} chars, elapsed ms ${elapsed}, ${(elapsed / repeats) * 1000} micros per msg`)
accept()
}
})
})
})
}
private encode (): void {
const session: AsciiMsgTransmitter = this.session
if (!session) {
console.log('provide a session json file e.g. --session=data/session/test-initiator.json')
return
}
if (!argv.type) {
console.log('provide a message type e.g. --type=8')
return
}
if (!argv.json) {
console.log('provide a json representation e.g. data/examples/execution-report/object.json')
return
}
const ts: string = argv.type.toString()
const msg: ILooseObject = this.jsonHelper.fromJson(path.join(this.root, argv.json), ts)
session.encodeMessage(ts, msg)
const fix: string = session.buffer.toString()
console.log(fix)
}
private norm (p: string): string {
let f: string = p
if (!path.isAbsolute(p)) {
f = path.join(this.root, f)
}
return f
}
}
function showHelp (): void {
console.log('this help page')
console.log('npm run cmd')
console.log('npm run cmd -- --help')
console.log()
console.log('token format i.e. [602] 687 (LegQty) = 33589')
console.log('jsfix-cmd --dict=data/FIX44.xml --fix=data/examples/quickfix/FIX.4.4/execution-report/fix.txt --delimiter="|" --tokens')
console.log()
console.log('token format use fix repo dictionary')
console.log('jsfix-cmd --dict=data/fix_repo/FIX.4.4/Base --fix=data/examples/quickfix/FIX.4.4/execution-report/fix.txt'
+ ' --delimiter="|" --tokens')
console.log()
console.log('structure format i.e. show locations of components etc.')
console.log('jsfix-cmd --dict=data/FIX44.xml --fix=data/examples/FIX.4.4/quickfix/execution-report/fix.txt' +
' --delimiter="|" --tokens --structures')
console.log()
console.log('full JS object in JSON format.')
console.log('jsfix-cmd --dict=data/FIX44.xml --fix=data/examples/FIX.4.4/quickfix/execution-report/fix.txt' +
' --delimiter="|" --tokens --objects')
console.log()
console.log('full JS object in JSON format - filter only type messages.')
console.log('jsfix-cmd --dict=data/FIX44.xml --fix=data/examples/FIX.4.4/quickfix/execution-report/fix.txt' +
' --delimiter="|" --tokens --type=8 --objects')
console.log()
console.log('timing stats and message counts. Structured parsing of all messages.')
console.log('jsfix-cmd --dict=data/FIX44.xml --fix=data/examples/FIX.4.4/quickfix/execution-report/fix.txt --stats')
console.log()
console.log('encode a json object to fix format')
console.log('jsfix-cmd --json=data/examples/FIX.4.4/quickfix/execution-report/object.json' +
' --session=data/session.json --type=8 --delimiter="|"')
console.log()
console.log('display field definition')
console.log('jsfix-cmd --dict=data/FIX44.xml --field=MsgType|35')
console.log()
console.log('display field use fix repo dictionary e.g. 271 MDEntrySize QTY Quantity or volume represented by the Market Data Entry.')
console.log('jsfix-cmd --dict=data/fix_repo/FIX.4.4/Base --field=MsgType')
console.log('jsfix-cmd --dict=data/fix_repo/FIX.4.4/Base --field=35')
console.log()
console.log('script to describe field in repository version 4.4')
console.log('npm run repo44 -- --field=8')
console.log()
console.log('script to describe field in fixml')
console.log('npm run fixml -- --field=50')
console.log()
console.log('generate unit test set of files - i.e. randomly generate an object, encode to fix. density 1 is all fields')
console.log('jsfix-cmd --generate --type=AE --density=0.8 --unit --delimiter="|" --session=data/session/test-initiator.json')
console.log('npm run repo44-unit -- --type=AE')
console.log('test script with no repeat groups')
console.log('npm run repo44-unit -- --type=AE --groups=false')
console.log()
console.log('generate a fix log of randomly generated but syntactically correct messages')
console.log('jsfix-cmd --generate --density=0.8 --repeats=50 --script --delimiter="|" --session=data/session/test-initiator.json')
console.log('npm run repo44-script')
console.log('parse above generated script')
console.log('npm run repo44-repscr')
console.log()
console.log('replay example repo fix file of 50 messages.')
console.log('jsfix-cmd --session=data/session/test-initiator.json --fix=data/examples/FIX.4.4/fix.txt --delimiter="|" --stats')
console.log('npm run repo44-replay -- --stats')
console.log('npm run repo44-replay -- --objects')
console.log('npm run repo44-replay -- --tokens')
console.log('npm run repo44-replay -- --structures')
console.log()
console.log('benchmark parse a message')
console.log('jsfix-cmd --delimiter="|" --session=data/session/test-initiator.json --fix=data/examples/FIX.4.4/repo/trade-capture-no-groups/fix.txt --benchmark')
console.log('npm run repo44-bench -- --fix=data/examples/FIX.4.4/repo/trade-capture-no-groups/fix.txt')
console.log()
console.log('compile typescript interfaces - i.e. outputs to src/types/FIX4.4 - requires set and enum sub folders')
console.log('npm run cmd -- --dict=repo40 --compile')
console.log('npm run cmd -- --dict=repo41 --compile')
console.log('npm run cmd -- --dict=repo42 --compile')
console.log('npm run cmd -- --dict=repo43 --compile')
console.log('npm run cmd -- --dict=repo44 --compile')
console.log('npm run cmd -- --dict=repo50 --compile')
console.log('npm run cmd -- --dict=repo50sp1 --compile')
console.log('npm run cmd -- --dict=repo50sp2 --compile')
console.log('npm run cmd -- --dict=repofixml --compile')
console.log('npm run cmd -- --dict=qf44 --compile')
console.log('npm run cmd -- --dict=data/handmade.xml --compile --output=src/types/handmade')
console.log()
}
const help: boolean = argv.h || argv.help
if (help) {
showHelp()
} else {
const cmd: JsfixCmd = new JsfixCmd()
cmd.exec().then((res: boolean) => {
if (!res) {
showHelp()
}
}).catch((e: Error) => {
console.log(`error ${e.message}`)
})
}