@sotatech/nest-quickfix
v0.11.0
Published
A powerful NestJS implementation of the FIX (Financial Information eXchange) protocol. Provides high-performance, reliable messaging for financial trading applications with built-in session management, message validation, and recovery mechanisms.
Maintainers
Readme
Nest QuickFIX
A NestJS library that embeds a FIX (Financial Information eXchange) protocol engine directly into your application. Run a FIX acceptor (server) or initiator (client) over raw TCP, handle incoming messages with decorators, and broadcast outbound messages to groups of sessions — all with familiar NestJS patterns.
Features
- FIX acceptor & initiator — TCP server accepting counterparties, or TCP client connecting out to a broker/exchange, from the same module
- Session management — logon/logout handshake, heartbeat supervision, sequence validation, resend/replay and gap-fill recovery
- Decorator-based handlers —
@OnLogon(),@OnFixMessage(MsgType.NewOrderSingle), ... on any controller method - Room-based broadcasting — group sessions into rooms and send a message to all of them with
fixService.to(room).send(msg) - FIX field dictionary — 680+ typed field-tag and value enums (
Fields,MsgType,Side,OrdType,TimeInForce, ...) with full tag/name coverage of FIX 4.4 - Byte-correct message framing — ASCII
BodyLength(9) framing, configured width, checksum and maximum-frame validation across fragmented/coalesced TCP chunks - Bounded reconnection — serialized reconnect attempts, Logon timeout and stale-socket protection for the initiator
- Sync & async configuration —
FIXModule.register()andFIXModule.registerAsync()
Requirements
- Node.js 18+ (20+ recommended)
- NestJS 11 (
@nestjs/commonand@nestjs/coreare peer dependencies)
Installation
# Using npm
npm install @sotatech/nest-quickfix
# Using yarn
yarn add @sotatech/nest-quickfix
# Using pnpm
pnpm add @sotatech/nest-quickfix@nestjs/common, @nestjs/core, reflect-metadata and rxjs must be present in the host application (they already are in any NestJS app).
Quick Start
1. Register the module
Acceptor (listen for incoming FIX connections)
import { Module } from '@nestjs/common';
import { FIXModule, Fields, Message } from '@sotatech/nest-quickfix';
@Module({
imports: [
FIXModule.register({
config: {
application: {
name: 'MyFixGateway',
dictionary: 'FIX44',
protocol: 'ascii',
reconnectSeconds: 30,
tcp: {
host: '0.0.0.0',
port: 9876,
},
type: 'acceptor',
},
BeginString: 'FIX.4.4',
BodyLengthChars: 10,
EncryptMethod: 0,
HeartBtInt: 30,
ResetSeqNumFlag: true,
SenderCompID: '*', // '*' accepts any sender
TargetCompID: 'BROKER',
},
auth: {
// Receives the raw Logon message — extract any field you need
validateCredentials: async (message: Message) => {
const username = message.getField(Fields.Username);
const password = message.getField(Fields.Password);
return username === 'username' && password === 'password';
},
// Optional: allow-list per Account (tag 1) from the Logon message
getAllowedSenderCompIds: async (account: string) => [
'CLIENT1',
'CLIENT2',
],
},
session: {
maxSessions: 0, // 0 = unlimited
messageStoreCapacity: 10_000,
maxMessageBytes: 1_048_576,
logonTimeoutMs: 10_000,
},
}),
],
})
export class AppModule {}Initiator (connect out to an acceptor)
FIXModule.register({
config: {
application: {
name: 'MyFixClient',
dictionary: 'FIX44',
protocol: 'ascii',
reconnectSeconds: 30,
tcp: {
host: 'broker.example.com',
port: 9876,
},
type: 'initiator',
},
BeginString: 'FIX.4.4',
BodyLengthChars: 10,
EncryptMethod: 0,
HeartBtInt: 30,
ResetSeqNumFlag: true,
SenderCompID: 'CLIENT1',
TargetCompID: 'BROKER',
// Optional credentials — sent as tags 553/554 in the Logon message
Username: 'username',
Password: 'password',
reconnect: {
enabled: true,
interval: 5000, // ms between attempts
maxAttempts: 5,
},
},
});Async configuration
FIXModule.registerAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
config: config.get('fix'),
auth: {/* ... */},
}),
});2. Handle FIX events in a controller
import { Controller } from '@nestjs/common';
import {
Message,
Field,
Fields,
MsgType,
Session,
FixService,
OnLogon,
OnLogout,
OnConnected,
OnDisconnected,
OnFixMessage,
} from '@sotatech/nest-quickfix';
@Controller()
export class TradingController {
constructor(private readonly fixService: FixService) {}
@OnLogon()
async onLogon(session: Session, message: Message) {
// Group the session into a room for targeted broadcasting
session.join('CLIENTS');
// Announce trading session status right after logon
const status = new Message(
new Field(Fields.MsgType, MsgType.TradingSessionStatus),
new Field(Fields.TradingSessionID, '1'),
);
await this.fixService.to(session.getSessionId()).send(status);
}
@OnFixMessage(MsgType.NewOrderSingle)
async onNewOrder(session: Session, message: Message) {
const clOrdId = message.getField(Fields.ClOrdID);
const symbol = message.getField(Fields.Symbol);
const side = message.getField(Fields.Side);
const qty = message.getField(Fields.OrderQty);
// ... route the order to your matching logic
}
@OnFixMessage() // no filter: receives every inbound application message
async onAnyMessage(session: Session, message: Message) {
console.log('IN', message.toFieldNameObject());
}
@OnLogout()
async onLogout(session: Session, message: Message) {
console.log(`Logout: ${session.getSessionId()}`);
}
@OnConnected()
async onConnected(session: Session) {}
@OnDisconnected()
async onDisconnected(session: Session) {}
}3. Send messages from anywhere via FixService
import { Injectable } from '@nestjs/common';
import {
FixService,
Message,
Field,
Fields,
MsgType,
Side,
OrdType,
TimeInForce,
RejectMessage,
} from '@sotatech/nest-quickfix';
@Injectable()
export class OrderGateway {
constructor(private readonly fixService: FixService) {}
async sendOrder() {
const order = new Message(
new Field(Fields.MsgType, MsgType.NewOrderSingle),
new Field(Fields.ClOrdID, 'ORDER-123'),
new Field(Fields.Symbol, 'AAPL'),
new Field(Fields.Side, Side.Buy),
new Field(Fields.OrderQty, 100),
new Field(Fields.Price, 150.5),
new Field(Fields.OrdType, OrdType.Limit),
new Field(Fields.TimeInForce, TimeInForce.Day),
);
// Target can be a room name or a session id ("SENDER->TARGET")
await this.fixService.to('CLIENTS').send(order);
}
async reject(refSeqNum: number, reason: string) {
const reject = new RejectMessage(refSeqNum, reason, MsgType.NewOrderSingle);
await this.fixService.to('CLIENT1->BROKER').send(reject);
}
}You never set BeginString, SenderCompID, TargetCompID, MsgSeqNum, SendingTime, BodyLength or CheckSum yourself — the library fills them in per session.
Configuration reference
FIXModuleOptions
| Option | Type | Description |
| ----------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------- |
| config | FIXConfig | Connection and session configuration (below) |
| auth | object | Optional. Authentication hooks for the acceptor |
| auth.validateCredentials | (message: Message) => boolean \| Promise<boolean> | Receives the raw Logon message; return false to reject |
| auth.getAllowedSenderCompIds (optional) | (account: string) => string[] \| Promise<string[]> | Allowed SenderCompIDs for the given Account (tag 1) |
| session.maxSessions | number | Optional. Max concurrent sessions, 0 = unlimited |
| session.messageStoreCapacity | number | Optional. Outbound entries retained per session for replay; default 10_000 |
| session.maxMessageBytes | number | Optional. Maximum inbound frame/buffer size; default 1_048_576 (1 MiB) |
| session.logonTimeoutMs | number | Optional. Initiator timeout waiting for peer Logon; default 10_000 ms |
FIXConfig
| Field | Type | Required | Description |
| ------------------------------------------------ | --------------------------- | -------- | ----------------------------------------------------------------------------------- |
| application.type | 'acceptor' \| 'initiator' | yes | Run as server or client |
| application.name | string | yes | Application name |
| application.dictionary | string | yes | FIX dictionary, e.g. 'FIX44' |
| application.protocol | 'ascii' | yes | Wire encoding |
| application.reconnectSeconds | number | yes | Seconds between reconnects |
| application.tcp.host | string | yes | Bind host (acceptor) or remote host (initiator) |
| application.tcp.port | number | yes | TCP port |
| BeginString | string | yes | FIX version, e.g. 'FIX.4.4' |
| SenderCompID | string \| '*' | yes | Your comp ID; '*' = accept any (acceptor) |
| TargetCompID | string \| '*' | yes | Counterparty comp ID |
| HeartBtInt | number | yes | Heartbeat interval, seconds |
| EncryptMethod | number | yes | 0 = none |
| ResetSeqNumFlag | boolean | yes | Reset sequence numbers on logon |
| BodyLengthChars | number | yes | Exact tag 9 width used by the engine; serialization fails if the value does not fit |
| Username / Password | string | no | Credentials (acceptor-side validation reference) |
| TargetSubID | string | no | Target sub ID |
| LastSentSeqNum / LastReceivedSeqNum | number | no | Restore sequence numbers |
| reconnect.enabled / interval / maxAttempts | | no | Initiator reconnect policy |
Decorators
| Decorator | Handler signature | Fires when |
| ------------------------- | -------------------- | ------------------------------------------------- |
| @OnLogon() | (session, message) | Logon handshake completed |
| @OnLogout() | (session, message) | Logout received |
| @OnConnected() | (session) | TCP connection established |
| @OnDisconnected() | (session) | TCP connection closed |
| @OnFixMessage(msgType?) | (session, message) | Inbound message; optionally filtered by MsgType |
Handlers can be placed on methods of any @Controller() or @Injectable() provider discovered by NestJS.
Sessions and rooms
Every connection is represented by a Session:
session.getSessionId(); // "SENDER->TARGET"
session.join('roomA'); // join a room
session.leave('roomA'); // leave a room
session.getRooms(); // list joined rooms
session.getConfig(); // SessionConfig
await session.sendMessage(message); // send to this session onlyFixService.to(target) accepts either a room name (broadcast to all sessions in the room) or a session id (send to one session). If no live session exists, send() rejects with the exported FixTargetNotFoundError. Broadcasts use an independent message clone per recipient; if any write fails, the returned promise rejects.
Sequence counters start at LastSentSeqNum + 1 and LastReceivedSeqNum + 1. A peer gap is buffered and requested with ResendRequest; application messages are replayed with PossDupFlag/OrigSendingTime, while admin ranges are represented by SequenceReset-GapFill. The store is in memory and bounded by messageStoreCapacity. If a requested application sequence has already been evicted, the engine sends Logout and closes the session instead of silently gap-filling business data. Restarting the process does not restore stored messages.
Message API
const msg = new Message(
new Field(Fields.MsgType, MsgType.ExecutionReport),
new Field(Fields.OrderID, 'X1'),
);
msg.getField(Fields.OrderID); // typed getter
msg.setField(Fields.Price, 42.5);
msg.hasField(Fields.Price); // true
msg.toString(); // minimal-width BodyLength + CheckSum computed
msg.toString({ bodyLengthChars: 10 }); // exact tag 9 width used by the engine
msg.clone(); // deep copy of the field map
msg.createReverse(); // copy with SenderCompID/TargetCompID swapped
msg.toJSON(); // { fields: { 35: '8', ... } }
msg.toFieldNameObject(); // { MsgType: '8', OrderID: 'X1', ... }
Message.fromJSON(json); // rebuild from JSONFields is the tag-number enum (Fields.ClOrdID === 11). Value enums (MsgType, Side, OrdType, TimeInForce, and 600+ more) type the field values.
Events
FIXAcceptor, FIXInitiator and Session are EventEmitters:
import {
SessionEvents,
InitiatorEvents,
AcceptorEvents,
} from '@sotatech/nest-quickfix';
// e.g. injected FIXInitiator
initiator.on(InitiatorEvents.LOGGED_ON, () => {
/* ... */
});
initiator.on(InitiatorEvents.RECONNECT_FAILED, ({ attempt, error }) => {
/* ... */
});
session.on(SessionEvents.MESSAGE_IN, (msg) => {
/* ... */
});Architecture
flowchart LR
subgraph External["External Counterparties"]
CP["FIX Client / Broker"]
end
subgraph Lib["@sotatech/nest-quickfix"]
Acceptor["FIXAcceptor - TCP server"]
Initiator["FIXInitiator - TCP client"]
SessionMgr["SessionManager"]
SessionObj["Session - heartbeat, seq nums"]
Parser["FIXMessageParser - SOH framing, checksum"]
Explorer["FixMetadataExplorer - decorator discovery"]
Rooms["RoomManager"]
end
subgraph App["Your Application"]
Ctrl["Controller handlers"]
Svc["Services via FixService"]
end
CP -->|"TCP / FIX over SOH"| Acceptor
Initiator -->|"TCP / FIX over SOH"| CP
Acceptor --> SessionMgr
SessionMgr --> SessionObj
SessionObj --> Parser
Parser --> Explorer
Explorer --> Ctrl
Svc --> Rooms
Rooms --> SessionObjFlow inbound: TCP bytes → FIXMessageParser (frame by tag 9 byte length, validate field order + checksum) → Session (sequence validation/recovery, admin messages) → your decorated handlers. Waiting-gap and duplicate messages are not emitted to application handlers. Flow outbound: FixService → RoomManager → Session → required header fields + in-memory replay store + checksum → socket.
Sample application
A complete working acceptor lives in samples/nest-quickfix-sample. It consumes the library via a local file: link:
cd samples/nest-quickfix-sample
pnpm install
pnpm start:dev
# FIX acceptor now listens on localhost:9876Point any FIX 4.4 client (e.g. QuickFIX/J's Banzai, or a raw telnet-style script) at it to see logon, heartbeat and message handling in action.
Development
pnpm install # install dependencies
pnpm build # compile to dist/
pnpm lint # eslint (flat config)
pnpm format # prettier
pnpm test # jest unit tests
pnpm test:cov # coverage with enforced thresholdsThe package is published with scripts/publish.sh (builds and publishes dist/ with public access).
Roadmap
Potential future extensions:
- Persistent session store (Redis) for sequence numbers and message recovery
- TLS/SSL transport
- FIX 5.0 session layer (the field dictionary already covers 5.0 tags)
Contributing
- Fork and clone the repo
pnpm install, create a feature branch- Make sure
pnpm buildandpnpm lintpass - Open a pull request
Please report issues at github.com/sotaaaaa/nest-quickfix/issues.
License
MIT
