Skip to content
Get Started

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

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:

  1. Answer oncustomrequest: ignore requests shorter than 32 bytes or with another magic (return 0). For another version, answer start and control requests with NCBOA_ERROR_VERSION.
  2. 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.
  3. Publish the state with packState() and setcustomreport() after every request and whenever something changes, with a new timestamp each time. The window enables itself after the first new state.
  4. List every instrument the application can trade in symbols; the window offers only these as legs.
  5. Number spread trades from 0 per run and serve them by sequence number.
Minimal application side
#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):

customId = algoId << 8 | slot
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 >> 8 is the algo ID (ncboa::customIdAlgo(); ncboa::customIdSlot() gives the slot, below NCBOA_SLOTS). An order whose algo ID is not in algos[] 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:

  1. Register a remotelistener and start the remotesession with a user of the account.
  2. In oncustomreport, read the state as described in Reading the State.
  3. Send requests with sendcustomrequest: call reset() on the message, set a new requestId, fill the fields.
  4. In oncustomresponse, check magic, type and requestId; status != 0 is a refusal (see Algo Response).
  5. Optionally send a heartbeat every second for faster state updates.
  6. Pull the trade log by sequence number when totalTrades grows.
Start a spreader: buy 2 calendar spreads +ESZ6 -ESH7 at -45.25
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:

  1. connects with a user of the account the Boa Spreader application trades on;
  2. waits until the application is running (its state timestamp changes) and trades the symbol given on the command line, sending a heartbeat every 0.5 s so the state arrives at once;
  3. sends one NCBOA_ALGO_START_REQUEST with the market maker settings;
  4. prints the result: the new algo number, or the refusal code.
compilation
g++ -std=c++17 mmclient.cpp -lnanocondaremote -I nanocondaroot/include -L nanocondaroot/lib -o mmclient
./mmclient <host> <port> <username> <password> ESZ6
mmclient.cpp
#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.