Boa Protocol
The Boa Spreader application and the Boa Spreader window talk through a
small binary protocol. It is defined in one header of the package, include/nanoconda_boa_protocol.h (namespace
ncboa), and is open for your own implementations:
| You build | You implement | Works with |
|---|---|---|
| Your own spread application | The application side: answer requests, publish the state. | The Boa Spreader window in the Real-Time GUI, unchanged. |
| Your own front end (office GUI, dashboard, risk tool) | The client side: send requests, read the state. | The Boa Spreader application, unchanged. |
This page describes protocol version 1.
Transport
The protocol uses the custom data channels of the API. The application side is a program with a listener
and a dmasession; the client side is the Boa Spreader window or a Remote API
application (remotesession + remotelistener).
| Message | Direction | Application side | Client side | Max size |
|---|---|---|---|---|
| Request | client → application | listener::oncustomrequest(...) |
remotesession::sendcustomrequest(...) |
504 bytes |
| Response | application → the client that sent the request | Written to responsebuffer; the size is the return value of oncustomrequest |
remotelistener::oncustomresponse(...) |
16,376 bytes |
| State | application → all clients | dmasession::setcustomreport(...) |
remotelistener::oncustomreport(...) |
16,376 bytes |
The state is a persistent report: the platform keeps the latest one and sends it to clients about once per second, also after the application has stopped. See Reading the State.
Encoding
| Item | Rule |
|---|---|
| Byte order and layout | Little-endian, natural alignment, exactly as the structs in the header. Every struct size is checked with static_assert; from other languages, lay out the bytes by the sizes on this page. |
| Reserved fields | Send zeros. reset() of each message clears it and fills the header. |
| Prices | Integers in the API price format: price x 100,000 (NC_PRICE_MULT), or x 1,000,000,000 (NC_PRICE_MULT_HP) for instruments with security::highPrecisionPrice set. All legs of a spread use the same format. |
| Spread price | sum(sign x mult x leg price) in the same integer format; sign + for BUY legs, - for SELL legs. |
| Ticks | Leg ticks (payup, slop, slip) = the leg's security::tickSize. Spread ticks (all market maker settings) = the smallest mult x tickSize across the legs (ncboa::spreadStep()). |
| Quantities | Spreads. Leg lots = spreads x ratio. |
| Money | NcBoaState::sessionPnl x NC_PRICE_MULT; pnl, realizedPnl, unrealizedPnl as double in account currency; maxLoss, accountMaxLoss in whole currency units. |
| Instruments | symbolId = security::symbolId. |
| Time | Nanoseconds since the epoch, UTC. |
| Sides | 'B' / 'S' (market maker start also 'M' = both sides). |
Header
Every message starts with NcBoaHeader (32 bytes).
| Offset | Field | Type | Description |
|---|---|---|---|
| 0 | magic |
uint32 | NCBOA_PROTOCOL_MAGIC = 0x414F424E (bytes "NBOA"). |
| 4 | type |
uint32 | Message type, see below. |
| 8 | status |
uint32 | Responses: result code. Requests: 0. |
| 12 | requestId |
uint32 | Chosen by the client; the response carries the same value. |
| 16 | version |
uint32 | NCBOA_PROTOCOL_VERSION (1). |
| 20 | reserved |
12 bytes | Zeros. |
Note
The GUI routes custom data to its windows by the 4 bytes at offset 0. A different protocol of your own needs its own magic; data with an unknown magic is dropped.
Message Types
| Value | Type | Direction | Struct | Size (bytes) |
|---|---|---|---|---|
| 0 | NCBOA_HEARTBEAT |
request | NcBoaHeartbeat |
64 |
| 40 | NCBOA_ALGO_START_REQUEST |
request | NcBoaAlgoStartRequest |
248 |
| 50 | NCBOA_ALGO_CONTROL_REQUEST |
request | NcBoaAlgoControlRequest |
80 |
| 70 | NCBOA_TRADE_LOG_REQUEST |
request | NcBoaTradeLogRequest |
72 |
| 140 | NCBOA_ALGO_START_RESPONSE |
response | NcBoaAlgoResponse |
72 |
| 150 | NCBOA_ALGO_CONTROL_RESPONSE |
response | NcBoaAlgoResponse |
72 |
| 170 | NCBOA_TRADE_LOG_RESPONSE |
response | NcBoaTradeLogResponse + trades |
72 + 128 per trade |
| 210 | NCBOA_STATE |
state | NcBoaState, packed |
208 + rows, see State |
Message Flow
| Step | From | To | Message | Notes |
|---|---|---|---|---|
| 1 | Application | all clients | NCBOA_STATE |
On changes (at most every 250 ms) and right after every request. |
| 2 | Client | Application | NCBOA_HEARTBEAT |
Every second. No response; the application publishes its state at once. |
| 3 | Client | Application | NCBOA_ALGO_START_REQUEST |
Spread definition + algo settings. |
| 4 | Application | Client | NCBOA_ALGO_START_RESPONSE |
status, and the new algoId on success. The algo appears in the next state. |
| 5 | Client | Application | NCBOA_ALGO_CONTROL_REQUEST |
Stop, exit, remove, change price / qty / width / skew, exit all, account max loss. |
| 6 | Application | Client | NCBOA_ALGO_CONTROL_RESPONSE |
status. |
| 7 | Client | Application | NCBOA_TRADE_LOG_REQUEST |
When NcBoaState::totalTrades is above the number of trades already received. |
| 8 | Application | Client | NCBOA_TRADE_LOG_RESPONSE |
Up to 120 trades; repeat step 7 until all are received. |
Requests are answered in order, one response per start, control and trade log request.
Requests
Heartbeat
NcBoaHeartbeat: the header and 32 reserved bytes. The window sends one every second while it is open (trader users
only). It makes the application publish a fresh state even when the markets are quiet.
Algo Start
NcBoaAlgoStartRequest (248 bytes) = header (32) + NcBoaSpread (104) + NcBoaAlgoParams (80) + 32 reserved bytes.
NcBoaSpread (104 bytes): legs[4] (NcBoaLeg, 24 bytes each), legCount (1-4), 7 reserved bytes.
| Field | Type | Valid | Description |
|---|---|---|---|
symbolId |
uint64 | one of NcBoaState::symbols |
Leg instrument. Each symbol once per spread. |
ratio |
int16 | >= 1 | Lots per spread. |
mult |
int16 | >= 1 | Price multiplier. |
payupTicks |
int16 | >= 0 | Hedge price beyond the market, leg ticks. |
lean |
int16 | >= 0 | Spreads that must rest on this leg before other legs quote against it; 0 = off. |
maxSlipTicks |
int16 | >= 0 | Max leg ticks a hedge may move from its first price; 0 = no limit. |
side |
char | 'B' / 'S' |
Sign of the leg in the spread. |
quote |
uint8 | 0 / 1 | 1 = rest quotes on the leg, 0 = hedge only. At least one leg quoted; a 1-leg spread is always quoted. |
insideSlop |
uint8 | 0-255 | Spreader: inside slop, leg ticks. |
outsideSlop |
uint8 | 0-255 | Spreader: outside slop, leg ticks. |
NcBoaAlgoParams (80 bytes). SPRD = spreader, MM = market maker. The window's ranges are on the
Boa Spreader page; the application accepts the wider ranges below.
| Field | Type | For | Valid | Description |
|---|---|---|---|---|
price |
int64 | SPRD | Target spread price. | |
qty |
int32 | both | 1-100,000 | SPRD: spreads to fill. MM: spreads per level. qty x ratio must fit the clip size of every leg. |
maxPosition |
int32 | MM | qty-100,000 |
Max spread position, long or short. |
maxLoss |
int32 | MM | >= 0 | Algo max loss, currency; 0 = off. |
hedgeTimeoutMs |
int32 | both | >= 0 | Unfilled hedge goes to market after this; 0 = off. |
ticksAway |
int16 | MM | >= 0 | Spread ticks from the implied market. |
maxMsgs |
int16 | both | 0-1024 | Order messages per second; 0 = no algo limit. |
algoType |
uint8 | both | 1 / 2 | NCBOA_ALGO_SPREADER / NCBOA_ALGO_MARKET_MAKER. |
side |
char | both | 'B', 'S'; MM also 'M' |
Spread side; 'M' = both sides. |
overfill |
uint8 | SPRD | 0 / 1 | NCBOA_OVERFILL_HEDGE / NCBOA_OVERFILL_LEAVE. |
skewTicks |
int16 | MM | >= 0 | Spread ticks per qty of position; 0 = off. |
minEdgeTicks |
int16 | MM | >= 0 | Exit at least this far from the average entry; 0 = off. |
stopTicks |
int16 | MM | >= 0 | Exit at market this far against the average entry; 0 = off. |
levelStep |
int16 | MM | >= 0 | Spread ticks between levels; 0 = 1. |
stickyTicks |
int16 | MM | >= 0 | Do not follow the market back by up to this many ticks; 0 = off. |
levels |
uint8 | MM | 0-5 | Levels per side; 0 = 1. |
noReprice |
uint8 | MM | 0 / 1 | 1 = Reprice unchecked. |
pauseMs |
uint16 | MM | Pull pause after a fill, sweep or queue depletion. | |
sweepWindowMs |
uint16 | MM | Sweep counting window. | |
sweepLots |
int16 | MM | >= 0 | Sweep size; 0 = off. |
depletionRatePct |
uint16 | MM | Queue depletion speed threshold; 0 = off. | |
depletionEtaMs |
uint16 | MM | Queue depletion time threshold. | |
pullAfterFill |
uint8 | MM | 0 / 1 | Pull After Fill. |
pullOnLimit |
uint8 | MM | 0 / 1 | Pull On Limit. |
imbalancePct |
uint8 | MM | 0-50 | Book imbalance threshold; 0 = off. |
keepClosing |
uint8 | MM | 0 / 1 | Keep Closing. |
reserved, reserved2, reserved3, customInputs |
zeros |
The meaning of every setting is described in Market Maker and Adverse-Selection Protection.
Algo Control
NcBoaAlgoControlRequest (80 bytes): header, algoId (uint32), action (uint8), 3 reserved bytes, value (int64),
32 reserved bytes.
| Value | Action | algoId |
value |
Applies to | Refused with |
|---|---|---|---|---|---|
| 1 | NCBOA_ACTION_STOP |
algo | any algo (no effect unless RUNNING) | ||
| 2 | NCBOA_ACTION_EXIT |
algo | any algo; DONE / ERROR algos only with an open position | NCBOA_ERROR_ALGO_ACTIVE when already EXITING, or finished and flat |
|
| 3 | NCBOA_ACTION_REMOVE |
algo | DONE / ERROR algo without working orders | NCBOA_ERROR_ALGO_ACTIVE |
|
| 4 | NCBOA_ACTION_PRICE |
algo | new spread price | RUNNING spreader | NCBOA_ERROR_PARAMS |
| 5 | NCBOA_ACTION_EDGE |
algo | ticks away change, signed (result kept in 0-1000) | market maker | NCBOA_ERROR_PARAMS |
| 6 | NCBOA_ACTION_CANCEL_ALL |
0 | EXIT on every algo (EXIT ALL) | ||
| 7 | NCBOA_ACTION_QTY |
algo | new qty, 1-100,000 | RUNNING algo; MM: <= maxPosition; SPRD: not below what is already filled |
NCBOA_ERROR_PARAMS, NCBOA_ERROR_CLIP |
| 8 | NCBOA_ACTION_SKEW |
algo | > 0: one tick higher, < 0: one tick lower (kept in -100..100) | market maker | NCBOA_ERROR_PARAMS (also for 0) |
| 9 | NCBOA_ACTION_ACCOUNT_MAX_LOSS |
0 | 0-100,000,000 currency, 0 = off | account; also clears a reached max loss | NCBOA_ERROR_PARAMS |
An unknown algoId is refused with NCBOA_ERROR_ALGO_NOT_FOUND.
Trade Log
NcBoaTradeLogRequest (72 bytes): header, nextSeqNo (uint32, the first trade wanted = number of trades already
received), 4 reserved bytes, 32 reserved bytes.
Responses
Algo Response
NcBoaAlgoResponse (72 bytes) answers start and control requests: header (type, status, requestId of the
request), algoId (the new algo for a successful start, otherwise the request's algoId), 4 + 32 reserved bytes.
| Status | Value | Meaning |
|---|---|---|
NCBOA_SUCCESS |
0 | Done. |
NCBOA_ERROR_BAD_SPREAD |
8100 | No leg, a repeated symbol, legs with different price formats, ratio or multiplier below 1, bad side, or no quoted leg. |
NCBOA_ERROR_SYMBOL |
8200 | A leg symbol is not traded by the application. |
NCBOA_ERROR_PARAMS |
8300 | A value out of range, or the action does not apply to the algo or its state. |
NCBOA_ERROR_CLIP |
8400 | qty x ratio above the account clip size of a leg. |
NCBOA_ERROR_NO_SLOT |
8500 | 32 algos, or 16 spread definitions with positions. |
NCBOA_ERROR_ALGO_NOT_FOUND |
8600 | No algo with this algoId. |
NCBOA_ERROR_ALGO_ACTIVE |
8700 | The algo cannot be removed or exited in its state. |
NCBOA_ERROR_SESSION_DOWN |
8900 | Start refused: session down, algos blocked or no trading permission. |
NCBOA_ERROR_MAX_LOSS |
8930 | Start refused: the account max loss was reached; set it again to allow starts. |
NCBOA_ERROR_VERSION |
8950 | The request has another protocol version. |
NCBOA_ERROR |
9000 | Other error. |
Trade Log Response
NcBoaTradeLogResponse (72 bytes) followed by numberOfTrades x NcBoaSpreadTrade (128 bytes each).
| Field | Description |
|---|---|
startingSeqNo |
Sequence number of the first trade returned. Higher than nextSeqNo when older trades are no longer kept (the application keeps the last 32,768). |
numberOfTrades |
0-120. |
After a response, the next request uses startingSeqNo + numberOfTrades. When NcBoaState::totalTrades is lower
than the number already received, a new application run has started: clear the trades and start again at 0.
NcBoaSpreadTrade (128 bytes): one completed spread, or one EXIT close.
| Field | Type | Description |
|---|---|---|
timestamp |
uint64 | ns, UTC. |
spreadPrice |
int64 | Spread price of the fills. |
seqNo |
uint32 | 0, 1, 2, ... per application run. |
algoId |
uint32 | Algo. |
qty |
int32 | Spreads. |
algoType |
uint8 | 1 SPRD / 2 MM. |
side |
char | 'B' / 'S', spread side. |
legCount |
uint8 | Legs used in legs. |
legs[4] |
NcBoaLegFill |
Per leg: symbolId, avgPrice, qty (lots), side. |
State
NcBoaState is published with setcustomreport in packed form: the fixed part (208 bytes) followed by only the
used rows. Use ncboa::packState() to build it and ncboa::unpackState() to read it.
| Part | Size (bytes) |
|---|---|
| Header + fixed fields + 128 reserved | 208 |
symbols[symbolCount] (NcBoaSymbolInfo) |
40 each, up to 32 |
algos[algoCount] (NcBoaAlgoInfo) |
120 each, up to 32 |
spreads[spreadCount] (NcBoaSpreadInfo) |
152 each, up to 16 |
Fixed fields
| Field | Type | Description |
|---|---|---|
timestamp |
uint64 | ns, UTC, new on every publish. |
sessionPnl |
int64 | Account session P&L x NC_PRICE_MULT. |
totalTrades |
uint32 | Spread trades of this application run (next seqNo). |
orders / fills / rejects |
uint32 | Orders sent, fills, refused orders (exchange and pre-trade risk) since start. |
symbolCount / algoCount / spreadCount |
uint16 | Rows that follow. |
tradingStatus |
char | 'U' up, 'B' algos blocked, 'P' no trading permission, 'D' session down. |
maxLossHit |
uint8 | 1 = account max loss reached: all algos exited, starts refused. |
accountMaxLoss |
int32 | Account max loss, currency; 0 = off. |
NcBoaSymbolInfo (40 bytes): the instruments the application trades; only these can be legs.
| Field | Description |
|---|---|
symbolId |
Instrument. |
symbol |
Name, zero-terminated (24 bytes). |
position |
Net lots of the symbol across all spreads of the application. |
NcBoaAlgoInfo (120 bytes): one row per algo.
| Field | Type | Description |
|---|---|---|
price / sellPrice |
int64 | SPRD: target price / 0. MM: top buy quote / top sell quote. |
pnl |
double | Algo P&L, currency. |
algoId |
uint32 | |
position |
int32 | Spreads, + long / - short. |
done |
int32 | SPRD: spreads filled. MM: spreads traded. |
qty / maxPosition |
int32 | Current qty / max position. |
unhedged[4] |
int16 | Lots still to hedge per leg, + buy / - sell. |
hedgeAgeMs |
uint32 | Age of the oldest open hedge. |
ticksAway |
int16 | MM: current ticks away. |
algoType |
uint8 | 1 SPRD / 2 MM. |
state |
uint8 | 0 RUNNING, 1 STOPPING, 2 EXITING, 3 DONE, 4 ERROR. |
side |
char | 'B', 'S' or 'M'. |
flags |
uint8 | NCBOA_FLAG_UNHEDGED (0x02) a leg is unhedged, NCBOA_FLAG_HEDGE_MARKET (0x04) a hedge went to market. 0x01 is not used. |
info |
char[16] | Status detail, e.g. MAX POS, FILLED, STUCK ORD (see Algos). |
shiftTicks |
int16 | MM: manual skew (S+ / S-). |
levelStep |
int16 | MM: ticks between levels. |
quotedLevels[2] |
uint8 | MM: levels quoted now, buy / sell. |
spreadIndex |
uint8 | Index of the algo's spread in spreads[] of this message. |
customValues |
32 bytes | Zeros. |
NcBoaSpreadInfo (152 bytes): one row per spread definition (same legs, ratios, multipliers and sides).
| Field | Type | Description |
|---|---|---|
spread |
NcBoaSpread |
The definition (quote flags are 0). |
avgPrice |
int64 | Average entry spread price of the open position. |
realizedPnl / unrealizedPnl |
double | Currency. |
position |
int32 | Spreads. |
spreadsTraded / lotsTraded |
int32 | Totals. |
Reading the State
| Rule | Why |
|---|---|
Check magic, type = NCBOA_STATE and version before unpackState(); ignore anything else. |
Other programs publish their own reports. |
Use a state only when its timestamp differs from the previous one. |
The platform re-sends the last report about once per second, also after the application has stopped. |
Treat the application as not responding after 4 seconds without a new timestamp. |
The window shows its amber bar then. |
Look up algos[i].spreadIndex in the same message. |
Spread rows are renumbered on every publish. |
Implementing the Application Side
The Boa Spreader window works with any application that follows these rules:
- Answer
oncustomrequest: ignore requests shorter than 32 bytes or with another magic (return 0). For anotherversion, answer start and control requests withNCBOA_ERROR_VERSION. - Check the minimum size of each request type before reading it; answer with the matching response type and the
request's
requestId. Heartbeats get no response. - Publish the state with
packState()andsetcustomreport()after every request and whenever something changes, with a newtimestampeach time. The window enables itself after the first new state. - List every instrument the application can trade in
symbols; the window offers only these as legs. - Number spread trades from 0 per run and serve them by sequence number.
#include "nanoconda.h"
#include "nanoconda_boa_protocol.h"
struct MySpreader : nanoconda::listener
{
nanoconda::dmasession* session = nullptr;
ncboa::NcBoaState state;
char packed[sizeof(ncboa::NcBoaState)];
MySpreader() { state.reset(); }
void publish()
{
state.timestamp = nanoconda::getEpoch_ns();
unsigned short bytes = ncboa::packState(&state, packed);
if (session) session->setcustomreport(packed, bytes);
}
unsigned short respond(char* out, unsigned short max, ncboa::Type type, ncboa::Status status, unsigned int requestId, unsigned int algoId)
{
if (max < ncboa::NcBoaAlgoResponse::getSize()) return 0;
ncboa::NcBoaAlgoResponse* response = (ncboa::NcBoaAlgoResponse*) out;
memset(response, 0, sizeof(ncboa::NcBoaAlgoResponse));
response->header.reset(type);
response->header.status = status;
response->header.requestId = requestId;
response->algoId = algoId;
return ncboa::NcBoaAlgoResponse::getSize();
}
ncboa::Status startAlgo(const ncboa::NcBoaAlgoStartRequest* req, unsigned int& algoId) { return ncboa::NCBOA_ERROR; }
ncboa::Status controlAlgo(const ncboa::NcBoaAlgoControlRequest* req) { return ncboa::NCBOA_ERROR; }
unsigned short tradeLog(const ncboa::NcBoaTradeLogRequest* req, char* out, unsigned short max) { return 0; }
unsigned short oncustomrequest(const char* data, unsigned short size, char* out, unsigned short max) override
{
if (size < sizeof(ncboa::NcBoaHeader)) return 0;
const ncboa::NcBoaHeader* header = (const ncboa::NcBoaHeader*) data;
if (header->magic != NCBOA_PROTOCOL_MAGIC) return 0;
unsigned short bytes = 0;
if (header->version != NCBOA_PROTOCOL_VERSION)
{
if (header->type == ncboa::NCBOA_ALGO_START_REQUEST) bytes = respond(out, max, ncboa::NCBOA_ALGO_START_RESPONSE, ncboa::NCBOA_ERROR_VERSION, header->requestId, 0);
if (header->type == ncboa::NCBOA_ALGO_CONTROL_REQUEST) bytes = respond(out, max, ncboa::NCBOA_ALGO_CONTROL_RESPONSE, ncboa::NCBOA_ERROR_VERSION, header->requestId, 0);
return bytes;
}
switch (header->type)
{
case ncboa::NCBOA_ALGO_START_REQUEST:
if (size >= ncboa::NcBoaAlgoStartRequest::getSize())
{
unsigned int algoId = 0;
ncboa::Status status = startAlgo((const ncboa::NcBoaAlgoStartRequest*) data, algoId);
bytes = respond(out, max, ncboa::NCBOA_ALGO_START_RESPONSE, status, header->requestId, algoId);
}
break;
case ncboa::NCBOA_ALGO_CONTROL_REQUEST:
if (size >= ncboa::NcBoaAlgoControlRequest::getSize())
{
const ncboa::NcBoaAlgoControlRequest* req = (const ncboa::NcBoaAlgoControlRequest*) data;
bytes = respond(out, max, ncboa::NCBOA_ALGO_CONTROL_RESPONSE, controlAlgo(req), header->requestId, req->algoId);
}
break;
case ncboa::NCBOA_TRADE_LOG_REQUEST:
if (size >= ncboa::NcBoaTradeLogRequest::getSize()) bytes = tradeLog((const ncboa::NcBoaTradeLogRequest*) data, out, max);
break;
default:
break;
}
publish();
return bytes;
}
};
The full implementation is src/BoaSpreader/boaspreader.cpp (oncustomrequest, publish, startAlgo,
controlAlgo, tradeLog).
Implementing the Client Side
A Remote API application controls the Boa Spreader application the same way the window does:
- Register a
remotelistenerand start theremotesessionwith a user of the account. - In
oncustomreport, read the state as described in Reading the State. - Send requests with
sendcustomrequest: callreset()on the message, set a newrequestId, fill the fields. - In
oncustomresponse, checkmagic,typeandrequestId;status!= 0 is a refusal (see Algo Response). - Optionally send a heartbeat every second for faster state updates.
- Pull the trade log by sequence number when
totalTradesgrows.
ncboa::NcBoaAlgoStartRequest req;
req.reset();
req.header.requestId = ++requestId;
req.spread.legCount = 2;
req.spread.legs[0] = { esz6SymbolId, 1, 1, 1, 0, 0, 'B', 1, 0, 0, {} };
req.spread.legs[1] = { esh7SymbolId, 1, 1, 1, 0, 0, 'S', 0, 0, 0, {} };
req.params.algoType = ncboa::NCBOA_ALGO_SPREADER;
req.params.side = 'B';
req.params.price = -4525000;
req.params.qty = 2;
req.params.maxMsgs = 150;
session->sendcustomrequest((const char*) &req, ncboa::NcBoaAlgoStartRequest::getSize());
Leg fields in order: symbolId, ratio, mult, payupTicks, lean, maxSlipTicks, side, quote,
insideSlop, outsideSlop. The first leg is quoted, the second is the hedge leg. The price is -45.25 x 100,000
(both legs without highPrecisionPrice).
Note
Test your implementation against the simulator, and check every refusal in the responses and in
boaspreader.log.