sondahub

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.

Run the bridge
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
QuickFIX initiator settings
[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
Python (simplefix)
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())
Typed by hand over WebSocket: | between fields, BodyLength and CheckSum left out
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:

SessionSet it to
FIX versionFIX.4.4 — or FIX.4.2, or FIXT.1.1 for FIX 5.0 SP2
SenderCompIDAnything of yours: SONDA, CLIENT1… Each one is a session of its own, with its own sequence numbers.
TargetCompIDSONDAHUB — exactly. Anything else is a Logout.
SenderSubID, TargetSubIDEmpty.
User, PasswordEmpty — or sonda / sondahub to trade, viewer / viewer to watch only.
Heartbeat30, or what you like: the hub heartbeats at the same interval and sends a TestRequest when Sonda goes quiet.
Sequence numbersTicked (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.
TLSOff, both boxes: the bridge speaks plain TCP.
Logon fieldsNone — 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:

The pairs that trade (SecurityListRequest)
35=x | 320=list-1 | 559=4
Bids and offers as they move (MarketDataRequest)
35=V | 262=md-1 | 263=1 | 264=1 | 267=2 | 269=0 | 269=1 | 146=1 | 55=EUR/USD
Buy at market (NewOrderSingle)
35=D | 11=ord-1 | 55=EUR/USD | 54=1 | 38=100000 | 40=1
A limit that rests, then its cancel
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.

MessageWhat 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)

CodeMeaning
1Required tag missing
4Tag specified without a value
5Value is incorrect (out of range) for this tag
9CompID problem
10SendingTime accuracy problem
11Invalid MsgType
13Tag appears more than once

Trading

MessageWhat 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

WhoMay
(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.