sondahub / FIX test server
FIX test server
A FIX acceptor, SONDAHUB, for FIX 4.2, 4.4 and 5.0 SP2 over FIXT.1.1, with the session layer done properly — sequence numbers kept across reconnects, gaps answered with ResendRequests, resends with PossDupFlag, heartbeats, TestRequests and Rejects with their reasons — in front of a small exchange that fills your orders against live currency quotes and streams market data. Through a small bridge, or typed by hand over WebSocket.
Free, nothing to install but the bridge. Nothing here is a real market.
Connect
- Over TCP
- through the bridge: node bridge.mjs fix [port], then localhost:9976
- WebSocket
- wss://api.sondahub.com/fix
- Versions
- FIX.4.2, FIX.4.4, FIXT.1.1 with FIX 5.0 SP2 (DefaultApplVerID 9)
- TargetCompID
- SONDAHUB
- Sign-in
- none, or Username/Password sonda/sondahub, viewer/viewer
- In Sonda
- a FIX item sending to localhost:9976, TargetCompID SONDAHUB
An acceptor in front of a small exchange: currency pairs quoted from the bank API’s live rates, orders filled against those quotes. The bridge listens on localhost:9976 and carries every session over one WebSocket — one bridge, one exchange, with your sessions’ sequence numbers kept across reconnects. The bridge prints each Logon, order, fill and reject, with the reason.
curl -O https://sondahub.com/industrial/bridge.mjs node bridge.mjs fix FIX on localhost:9976 → wss://api.sondahub.com/fix — localhost:9976, TargetCompID SONDAHUB every run of the bridge is a world of its own the hub is a FIX acceptor, SONDAHUB — FIX.4.2, FIX.4.4, FIXT.1.1 (FIX 5.0 SP2) — in front of an exchange of currency pairs quoted from the live rates FIX session CLIENT1 -> SONDAHUB logged on (FIX.4.4, heartbeat 30 s, next in 2 out 2) from 127.0.0.1:52280 CLIENT1: order ord-1 (O1) — buy 100,000 EUR/USD at market CLIENT1: ord-1 filled — 100,000 EUR/USD at 1.0846
[DEFAULT] ConnectionType=initiator HeartBtInt=30 ReconnectInterval=5 FileStorePath=store StartTime=00:00:00 EndTime=00:00:00 UseDataDictionary=Y DataDictionary=FIX44.xml [SESSION] BeginString=FIX.4.4 SenderCompID=CLIENT1 TargetCompID=SONDAHUB SocketConnectHost=localhost SocketConnectPort=9976 ResetOnLogon=Y
import socket, simplefix
s = socket.create_connection(("localhost", 9976))
m = simplefix.FixMessage()
m.append_pair(8, "FIX.4.4")
m.append_pair(35, "A")
m.append_pair(49, "CLIENT1")
m.append_pair(56, "SONDAHUB")
m.append_pair(34, 1)
m.append_utc_timestamp(52)
m.append_pair(98, 0)
m.append_pair(108, 30)
m.append_pair(141, "Y")
s.sendall(m.encode())
p = simplefix.FixParser()
p.append_buffer(s.recv(4096))
print(p.get_message())
8=FIX.4.4|35=A|49=CLIENT1|56=SONDAHUB|34=1|52=20261007-14:30:00.000|98=0|108=30|141=Y 8=FIX.4.4|35=V|49=CLIENT1|56=SONDAHUB|34=2|52=20261007-14:30:00.000|262=md-1|263=0|264=1|267=2|269=0|269=1|146=1|55=EUR/USD 8=FIX.4.4|35=D|49=CLIENT1|56=SONDAHUB|34=3|52=20261007-14:30:00.000|11=ord-1|55=EUR/USD|54=1|60=20261007-14:30:00.000|38=100000|40=1
No bridge? Any WebSocket client — Sonda’s WebSocket request too — can talk to wss://api.sondahub.com/fix with messages typed as text, one each, fields separated by |; BodyLength(9) and CheckSum(10) may be left out, and the answers come back the same way with them filled in. SendingTime(52) has to be within two minutes of now.
In LockFlare Sonda
Sonda’s FIX item opens a TCP session, so it goes through the bridge: run node bridge.mjs fix, make a new FIX item, and type localhost:9976 where it asks where to send — no tls://. Then, under Session:
| Session | Set it to |
|---|---|
| FIX version | FIX.4.4 — or FIX.4.2, or FIXT.1.1 for FIX 5.0 SP2 |
| SenderCompID | Anything of yours: SONDA, CLIENT1… Each one is a session of its own, with its own sequence numbers. |
| TargetCompID | SONDAHUB — exactly. Anything else is a Logout. |
| SenderSubID, TargetSubID | Empty. |
| User, Password | Empty — or sonda / sondahub to trade, viewer / viewer to watch only. |
| Heartbeat | 30, or what you like: the hub heartbeats at the same interval and sends a TestRequest when Sonda goes quiet. |
| Sequence numbers | Ticked (ResetSeqNumFlag) for a clean start every logon. Untick it to see the numbers carry across reconnects — and what was sent while Sonda was away comes back on a resend. |
| TLS | Off, both boxes: the bridge speaks plain TCP. |
| Logon fields | None — except for FIXT.1.1, where the Logon must carry DefaultApplVerID (1137) = 9: add it here if Sonda does not send it already. |
Connect, and the Logon comes back under Messages. Then send these from Send — Sonda writes the header (BeginString, BodyLength, MsgSeqNum, the CompIDs, SendingTime) and the CheckSum:
35=x | 320=list-1 | 559=4
35=V | 262=md-1 | 263=1 | 264=1 | 267=2 | 269=0 | 269=1 | 146=1 | 55=EUR/USD
35=D | 11=ord-1 | 55=EUR/USD | 54=1 | 38=100000 | 40=1
35=D | 11=lim-1 | 55=EUR/USD | 54=1 | 38=50000 | 40=2 | 44=0.9 | 59=1 35=F | 41=lim-1 | 11=lim-2 | 55=EUR/USD | 54=1
The order comes back as two ExecutionReports, New and then Filled at the offer; the limit as New, resting until you cancel it. TransactTime (60) can go too — the hub does not insist on it. On FIX.4.2 a NewOrderSingle also needs HandlInst (21) = 1. A misspelled TargetCompID, a SendingTime off by minutes or a missing tag answer with a Reject or a Logout that says why, under Messages and in the bridge’s window.
The session
The first message is a Logon, or the connection closes. Then every message is numbered: a number above the one expected keeps the message and sends a ResendRequest; one below it without PossDupFlag(43)=Y is a Logout, “MsgSeqNum too low, expecting X but received Y”; a garbled message — wrong BodyLength or CheckSum — is ignored, as FIX says. SendingTime(52) more than two minutes off is a Reject and a Logout. Silence past HeartBtInt gets a TestRequest, and silence after that a Logout.
| Message | What the server does |
|---|---|
| Heartbeat (0) | Sent every HeartBtInt seconds when nothing else is; with TestReqID(112) when it answers a TestRequest. |
| TestRequest (1) | Answered with a Heartbeat carrying its TestReqID(112). |
| ResendRequest (2) | Answered by resending the application messages from BeginSeqNo(7) to EndSeqNo(16) (0 for all) with PossDupFlag(43)=Y, and a SequenceReset-GapFill over the session messages. |
| Reject (3) | A session-level reject: taken and logged. |
| SequenceReset (4) | GapFillFlag(123)=Y moves the expected number to NewSeqNo(36); without it, a reset that ignores MsgSeqNum. |
| Logout (5) | Answered with a Logout, then the connection closes. |
| Logon (A) | Starts the session: EncryptMethod(98)=0, HeartBtInt(108), ResetSeqNumFlag(141)=Y to start the numbers at 1; Username(553) and Password(554) when you sign in. FIXT.1.1 needs DefaultApplVerID(1137)=9. |
Reject reasons (SessionRejectReason, 373)
| Code | Meaning |
|---|---|
| 1 | Required tag missing |
| 4 | Tag specified without a value |
| 5 | Value is incorrect (out of range) for this tag |
| 9 | CompID problem |
| 10 | SendingTime accuracy problem |
| 11 | Invalid MsgType |
| 13 | Tag appears more than once |
Trading
| Message | What the server does |
|---|---|
| NewOrderSingle (D) | A market (40=1) or limit (40=2, with Price 44) order on a currency pair, TimeInForce(59) day, GTC, IOC or FOK: acknowledged New, then filled against the live quotes. |
| OrderCancelRequest (F) | Cancels a resting order: an ExecutionReport Canceled, or an OrderCancelReject(9) when it is unknown or done. |
| OrderCancelReplaceRequest (G) | Changes a resting order’s quantity or price: an ExecutionReport Replaced, or an OrderCancelReject. |
| OrderStatusRequest (H) | An ExecutionReport with the order’s status (ExecType I from 4.4 on). |
| MarketDataRequest (V) | Snapshot (263=0) or subscribe (263=1) to bids and offers (269=0, 1) on pairs: MarketDataSnapshotFullRefresh(W), or MarketDataIncrementalRefresh(X) with MDUpdateType(265)=1; 263=2 unsubscribes. |
| SecurityListRequest (x) | The pairs that trade, as a SecurityList(y). From FIX 4.4. |
A market order fills at the offer (buying) or the bid (selling) — above 1,000,000 in two fills, half now and half half a second later. A limit order rests until the quotes reach it, then fills at the quote. IOC and FOK fill now or are canceled. Orders are refused with an ExecutionReport Rejected and OrdRejReason(103): 1 unknown symbol, 3 over the 50,000,000 limit, 6 duplicate ClOrdID, 13 a quantity that is not a whole number above 0 (FIX 4.2 says 0, broker option, with the reason in Text). From FIX 4.4 a fill is ExecType F and a status report ExecType I; on FIX 4.2 a fill is ExecType 1 or 2, and a status report says ExecTransType 3. Market data entries are sized 1,000,000.
Sign-in
| Who | May |
|---|---|
| (nobody) no Username(553) | Trades and watches. |
| sonda Password(554) sondahub | Trades and watches. |
| viewer Password(554) viewer | Watches: market data, status, the security list. Orders, cancels and replaces get a BusinessMessageReject, not authorized (380=6). |
| Anyone else or a wrong password | A Logout, and the connection closes. |
Questions
Which CompIDs?
TargetCompID(56) is SONDAHUB; SenderCompID(49) is yours to pick. Each SenderCompID is a session of its own, with its sequence numbers kept for as long as the bridge’s connection lives: log on again without ResetSeqNumFlag and the numbers go on, and what was sent while you were away comes when you ask for it.
Why port 9976?
FIX has no standard port, so the bridge takes 9976, or the next free one for a second exchange; give it one — node bridge.mjs fix 9876 — and it takes exactly that or stops.
Are the prices real?
No: they are the bank API’s exchange rates, walking a little every second. Nothing here is a real market, a real order or real money.
What is not offered?
Encryption and TLS, order types beyond market and limit, TimeInForce beyond day, GTC, IOC and FOK, quotes, allocations, and drop copies. A message type it does not take is a BusinessMessageReject, unsupported.