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 3. Version 3 changed NcBoaLeg (32 bytes, mult is int32) and
NcBoaSpread (136 bytes, new priceMode). Version 2 had microsecond market maker times in NcBoaAlgoParams (96
bytes). Messages of other versions are refused with NCBOA_ERROR_VERSION, and the Boa Spreader window shows which side
is older.
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. Leg prices (orders, leg fills) use the leg's own format. |
| Spread price | sum(sign x weight x leg price); sign + for BUY legs, - for SELL legs; weight = mult, or mult x security::multiplier with priceMode = NCBOA_PRICE_DOLLARS (the price is then in currency; with mult equal to ratio it is the value of one spread, so price change x spreads = P&L). In the x 1,000,000,000 format when any leg has highPrecisionPrice (the other legs' prices are scaled up by 10,000), otherwise in the x 100,000 format. |
| Ticks | Leg ticks (payup, slop, slip) = the leg's security::tickSize. Spread ticks (all market maker settings) = the greatest common divisor of weight x tickSize over the legs, in the spread price format (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 (3). |
| 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 |
296 |
| 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 (296 bytes) = header (32) + NcBoaSpread (136) + NcBoaAlgoParams (96) + 32 reserved bytes.
NcBoaSpread (136 bytes): legs[4] (NcBoaLeg, 32 bytes each), legCount (1-4), priceMode (uint8:
NCBOA_PRICE_MULT 0 = weights are the legs' mult, NCBOA_PRICE_DOLLARS 1 = weights are mult x
security::multiplier; ratio never enters the price), 6 reserved bytes.
NcBoaLeg fields in memory order:
| Field | Type | Valid | Description |
|---|---|---|---|
symbolId |
uint64 | one of NcBoaState::symbols |
Leg instrument. Each symbol once per spread. |
mult |
int32 | >= 1 | Price multiplier (weight in NCBOA_PRICE_MULT mode). |
ratio |
int16 | >= 1 | Lots per spread. |
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. |
reserved |
8 bytes | 0 | Zeros. |
NcBoaAlgoParams (96 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-32,767 (NCBOA_MAX_TICKS_AWAY) |
Spread ticks from the implied market; + / - stay within the same range. |
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 | SPRD 'B' / 'S'; MM 'M' only |
Spread side; the market maker always quotes both sides ('M'). |
overfill |
uint8 | SPRD | 0 / 1 | NCBOA_OVERFILL_HEDGE / NCBOA_OVERFILL_LEAVE. |
fairLevels |
uint8 | MM | 0-10 | Fair value: 0 = mid price (best bid + best ask) / 2, 1 = top-of-book weighted mid (microprice), 2-10 = weighted over this many book levels (NCBOA_MAX_FAIR_LEVELS). |
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 | Refresh: do not follow the market back by up to this many ticks; 0 = off. |
levels |
uint8 | MM | 0-5 | Levels per side; 0 = 1. |
reserved43 |
uint8 | 0 | Was noReprice; ignored. |
|
pauseUs |
uint32 | MM | 0-60,000,000 | Pull pause after a fill or an exchange delay, microseconds. |
levelQtyStep (offset 48) |
int16 | MM | -1,000 to 1,000 | Level Qty +: spreads added per deeper level; level N quotes qty + (N - 1) x levelQtyStep; 0 = all levels qty. |
reserved50 (offsets 50-53) |
4 bytes | 0 | Were part of sweepWindowUs, sweepLots (removed); ignored. |
|
volPct |
uint16 | MM | 0-1,000 | Volatility %: extra ticks away = spread ticks the mid moved in the last second x volPct / 100; 0 = off. |
maxSpreadTicks |
uint16 | MM | 0-1,000 | Max Spread: pull both sides while the market spread is wider than this many spread ticks; 0 = off. |
tradeFlowTicks |
uint8 | MM | 0-100 | Trade Flow: most the quotes are shifted toward the aggressive trade flow, spread ticks; 0 = off. |
reserved59 (offsets 59-63) |
5 bytes | 0 | Were part of the queue depletion fields (removed); ignored. | |
pullAfterFill |
uint8 | MM | 0 / 1 | Pull On Fill. |
pullOnLimit |
uint8 | MM | 0 / 1 | Pull On Rate. |
reserved66 |
uint8 | 0 | Was imbalancePct (book imbalance pull, removed); ignored. |
|
keepClosing |
uint8 | MM | 0 / 1 | Keep Exits. |
maxExchDelayUs |
uint32 | MM | 0-60,000,000 | Pull all quotes when a leg's exchange time - transaction time exceeds this, microseconds; 0 = off (Max Exch µs). |
reserved72 (8 bytes), customInputs (16 bytes) |
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 algos only with an open position | NCBOA_ERROR_ALGO_ACTIVE when already EXITING, or finished and flat |
|
| 3 | NCBOA_ACTION_REMOVE |
algo | DONE 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, a leg with contract multiplier 0, ratio or multiplier below 1, multipliers so large that spread prices could overflow, bad side, unknown priceMode, 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) |
184 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, including the Volatility % widening, shown up to 32,767. |
algoType |
uint8 | 1 SPRD / 2 MM. |
state |
uint8 | 0 RUNNING, 1 STOPPING, 2 EXITING, 3 DONE (4 is not used). |
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 (184 bytes): one row per spread definition (same legs, ratios, multipliers, sides and price mode).
| 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).
Order Custom IDs¶
Every order the Boa Spreader application sends carries a 64-bit custom ID (customId of the order, visible in the
account's live orders and order callbacks):
| Bits | Field | Use |
|---|---|---|
| 8-39 | algoId |
The algo that sent the order: the same ID as NcBoaAlgoInfo::algoId and the start response. |
| 0-7 | slot |
Internal to the application (it finds the order's slot with it); may change between versions. |
| 40-63 | always 0 |
- Which algo:
customId >> 8is the algo ID (ncboa::customIdAlgo();ncboa::customIdSlot()gives the slot, belowNCBOA_SLOTS). An order whose algo ID is not inalgos[]of the current state was not sent by a running Boa Spreader algo. - Everything else comes from the order: its leg (the symbol), side, price and remaining quantity. Do not derive the price level or quote / hedge role from the slot: an order is placed in any free slot of its leg, and hedge orders use the same slots as quotes. A spreader's quote rests (it would not trade at once on its leg); a hedge is priced to trade at once, or is a market order.
- Algo IDs restart at 1 when the application starts. Orders still working from an earlier run can carry the ID of a new algo; the application itself never mixes them up (it checks the order ID), but a front end that only decodes the custom ID could.
- Your own programs on the same account: set any of bits 40-63 in the custom IDs you use, so they can never be taken for Boa Spreader orders.
- There is no field for your own tag on Boa Spreader's orders.
Showing Leg Orders on a Spread Ladder¶
To show a leg order at a spread price (as the spread DOM does), combine its price with the other legs' current prices on the side a hedge would take:
spread price = sum over legs of sign x mult x price
price = the order's price (stop orders: trigger price) for its own leg,
for every other leg: its ask if the spread buys that leg, its bid if it sells it
- A buy order on a
+leg (or a sell order on a-leg) is a spread buy; the opposite is a spread sell. - Round a spread buy down and a spread sell up to the spread step.
- Quantity in spreads = remaining leg lots / the leg's
ratio.
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, mult, ratio, payupTicks, lean, maxSlipTicks, side, quote,
insideSlop, outsideSlop, 8 reserved bytes. The first leg is quoted, the second is the hedge leg. The price is -45.25 x 100,000
(both legs without highPrecisionPrice).
Example: Start a Market Maker¶
This complete program starts a market maker from a remote machine, the same as filling the Market Maker panel of the Boa Spreader window and clicking START MARKET MAKER. It:
- connects with a user of the account the Boa Spreader application trades on;
- waits until the application is running (its state
timestampchanges) and trades the symbol given on the command line, sending a heartbeat every 0.5 s so the state arrives at once; - sends one
NCBOA_ALGO_START_REQUESTwith the market maker settings; - prints the result: the new algo number, or the refusal code.
g++ -std=c++17 mmclient.cpp -lnanocondaremote -I nanocondaroot/include -L nanocondaroot/lib -o mmclient
./mmclient <host> <port> <username> <password> ESZ6
#include "nanoconda.h"
#include "nanoconda_boa_protocol.h"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
struct BoaClient : nanoconda::remotelistener
{
const char* symbol = nullptr;
ncboa::NcBoaState state;
unsigned long long lastTimestamp = 0;
std::atomic<bool> appRunning{false};
std::atomic<bool> tradingUp{false};
std::atomic<unsigned long long> symbolId{0};
std::atomic<unsigned int> answered{0};
std::atomic<unsigned int> status{0};
std::atomic<unsigned int> algoId{0};
void oncustomreport(char* data, unsigned short len) override
{
const ncboa::NcBoaHeader* h = (const ncboa::NcBoaHeader*) data;
if (len < sizeof(ncboa::NcBoaHeader) || h->magic != NCBOA_PROTOCOL_MAGIC) return;
if (h->type != ncboa::NCBOA_STATE || h->version != NCBOA_PROTOCOL_VERSION) return;
if (!ncboa::unpackState(data, len, &state)) return;
if (lastTimestamp != 0 && state.timestamp != lastTimestamp) appRunning = true;
lastTimestamp = state.timestamp;
tradingUp = state.tradingStatus == 'U' && !state.maxLossHit;
for (int i = 0; i < state.symbolCount; i++)
if (!strcmp(state.symbols[i].symbol, symbol)) symbolId = state.symbols[i].symbolId;
}
void oncustomresponse(char* data, unsigned short len) override
{
const ncboa::NcBoaAlgoResponse* r = (const ncboa::NcBoaAlgoResponse*) data;
if (len < ncboa::NcBoaAlgoResponse::getSize() || r->header.magic != NCBOA_PROTOCOL_MAGIC) return;
if (r->header.type != ncboa::NCBOA_ALGO_START_RESPONSE) return;
status = r->header.status;
algoId = r->algoId;
answered = r->header.requestId;
}
};
static void fillMarketMaker(ncboa::NcBoaAlgoStartRequest& req, unsigned long long symbolId, unsigned int requestId)
{
req.reset();
req.header.requestId = requestId;
req.spread.legCount = 1;
ncboa::NcBoaLeg& leg = req.spread.legs[0];
leg.symbolId = symbolId;
leg.ratio = 1;
leg.mult = 1;
leg.payupTicks = 1;
leg.side = 'B';
leg.quote = 1;
ncboa::NcBoaAlgoParams& p = req.params;
p.algoType = ncboa::NCBOA_ALGO_MARKET_MAKER;
p.side = 'M';
p.qty = 1;
p.maxPosition = 10;
p.maxLoss = 2000;
p.ticksAway = 2;
p.levels = 5;
p.levelStep = 1;
p.maxMsgs = 150;
p.pauseUs = 500000;
p.pullAfterFill = 1;
p.keepClosing = 1;
p.maxExchDelayUs = 2000;
}
static void heartbeat(nanoconda::remotesession* session)
{
ncboa::NcBoaHeartbeat hb;
hb.reset();
session->sendcustomrequest((const char*) &hb, ncboa::NcBoaHeartbeat::getSize());
}
int main(int argc, char** argv)
{
if (argc != 6)
{
printf("usage: %s <host> <port> <username> <password> <symbol>\n", argv[0]);
return 1;
}
nanoconda::reasoncode rc;
nanoconda::remotesession* session = nanoconda::remotesession::init(rc);
BoaClient client;
client.symbol = argv[5];
session->registerListener(&client);
session->start(argv[3], argv[4], argv[1], (unsigned short) atoi(argv[2]));
for (int i = 0; i < 20 && !(client.appRunning && client.symbolId && client.tradingUp); i++)
{
heartbeat(session);
usleep(500000);
}
if (!client.appRunning || !client.symbolId || !client.tradingUp)
{
printf("no running Boa Spreader application trading %s, or trading is not allowed\n", client.symbol);
session->stop();
return 1;
}
ncboa::NcBoaAlgoStartRequest req;
fillMarketMaker(req, client.symbolId, 1);
session->sendcustomrequest((const char*) &req, ncboa::NcBoaAlgoStartRequest::getSize());
for (int i = 0; i < 50 && client.answered != 1; i++) usleep(100000);
if (client.answered != 1) printf("no response\n");
else if (client.status != ncboa::NCBOA_SUCCESS) printf("start refused, status %u\n", client.status.load());
else printf("market maker started on %s: algo %u\n", client.symbol, client.algoId.load());
session->stop();
return client.answered == 1 && client.status == ncboa::NCBOA_SUCCESS ? 0 : 1;
}
The market maker then runs in the application like one started from the window: it appears in the window's algo list
and in NcBoaState::algos of the next state, and it is controlled with NCBOA_ALGO_CONTROL_REQUEST (stop, exit,
change Qty, width or skew) from either side.
The settings in the example and the window inputs they match
| Field | Window input | Value in the example |
|---|---|---|
side |
(none) | 'M': the market maker always quotes both sides; 'B' / 'S' are refused |
qty |
Qty | 1 spread per level |
maxPosition |
Max Pos | 10, must be at least qty |
maxLoss |
Max Loss $ | 2000 in account currency |
ticksAway |
Ticks Away | 2 spread ticks |
levels / levelStep |
Levels / Level Step | 5 levels, 1 tick apart |
maxMsgs |
Msgs/s | 150 |
pauseUs |
Pause µs | 500,000 |
pullAfterFill |
Pull On Fill | 1 = checked |
keepClosing |
Keep Exits | 1 = checked |
maxExchDelayUs |
Max Exch µs | 2,000 |
fairLevels |
Fair Levels | 0 = mid price (left at 0 by reset()) |
pullOnLimit |
Pull On Rate | 0 = unchecked |
Fields left at 0 by reset() are off or default: Refresh, Skew/Fill, Min Edge, Stop Ticks, Level Qty + (all levels qty), Hedge ms. The
meaning and range of every input is in Market Maker and
Adverse-Selection Protection.
Things to know
| Item | Rule |
|---|---|
| Fields that must be set | reset() sets every field to 0. A market maker start is refused with NCBOA_ERROR_PARAMS unless maxPosition is at least qty. The window always sends its defaults, so it never hits this. |
| Symbols | A leg must be one of the symbols the application was started with (-s). They are listed in NcBoaState::symbols with their symbolId; read the state before the first start. |
| Units | Market maker distances are spread ticks; for a one-leg spread that is the leg's tick size. Times are microseconds, Max Loss $ is whole currency units. |
| A stopped application | The platform re-sends the last state about once per second, also after the application has stopped, so wait for a new timestamp before sending a start. Without a running application no response arrives. |
| Same account, same version | The remote user must belong to the account the application trades on, and the request must use protocol version 3 (reset() sets it); otherwise the start is refused or ignored. |
| Callbacks | oncustomreport and oncustomresponse are called on the remote API's own thread; the example passes values to main through std::atomic members. |
Note
Test your implementation against the simulator, and check every refusal in the responses and in
boaspreader.log.