All reference chapters

9. The environment

Everything a robot runs inside of — the shared library itself, the process-wide initial parameters, the contracts and currencies the engine is monomorphized for, the duration grid, thread placement, the grid-search harness, recipe persistence, the data plane and the connection library, and the ex-post analysis surface. Each topic below opens with what the thing is and the rules that govern it, then its data structures, and shows the code last: the C declarations first, then the C++ wrapper form (namespace tse), then the Python form (module tse). The current ABI version is 6; tse_abi_version() returns it at run time.

Loading the library

The engine is an opaque prebuilt binary: the whole machine lives privately inside the shared library libtse_export and every language sees only the narrow waist of tse_* functions. The C headers live under include/tse/, with tse/tse.h as the umbrella header. The C++ wrapper is one header plus one translation unit (tse.hpp + tse.cpp) over the C ABI; the Python wrapper is a single ctypes module, tse.py.

Link -ltse_export. All four arguments are required: the label, the storage regime (an undefined regime fails), the account currency, and the blotter coreId (negative means no separate blotter thread).

The Python constructor is Account(account_label, regime, currency=Currency.Usd, core_id=-1, lib_path=None). When lib_path is omitted, the TSE_EXPORT_LIB environment variable is consulted, else the default file name on the loader path is used. The default name is platform-specific and may carry the build's version segment: libtse_export[.MAJOR.MINOR].dylib on macOS, .so on Linux, tse_export[.MAJOR.MINOR].dll on Windows; tse.default_lib_filename() returns it.

#include "tse/tse.h"   /* umbrella header */

TseAccountHandle account = tse_account_create("MyAccount", tse_storage_regime_mem, tse_ccy_usd, -1);
/* ... */
tse_account_destroy(account);
#include "tse.hpp"

tse::Account account {"MyAccount", tse::StorageRegime::mem};
// currency defaults to tse::Currency::usd, coreId defaults to -1

Compile your program together with tse.cpp and link the shared library:

g++ -std=c++20 -I tse -I tse/include main.cpp tse/tse.cpp \
    -L tse -ltse_export -Wl,-rpath,tse -o app
import tse

account = tse.Account("MyAccount", tse.StorageRegime.Mem,
                      lib_path="/path/to/libtse_export.dylib")
tse.abi_version()          # 6
a.abi_version()            # the same, through an account

Initial parameters and logging

tse_params.h carries the process-wide initial parameters. They are set once, before any account exists, which is why the call has its own error channel (tse_last_init_error) instead of the per-account tse_last_error.

Every non-empty path is genuinely validated, not just stored: a path that names an existing file is an error; a missing folder is created recursively; the folder is then probed for real read and write rights, and any failure returns tse_error with the reason in tse_last_init_error().

The lifecycle is deliberately simple: call it once at process startup, before creating accounts. Repeated calls are legal and last-writer-wins, but the call is not thread-safe against concurrent account creation — it writes plain process-wide state.

An account keeps the database path it computed at construction time. A later tse_set_initial_params affects only objects created after it: existing accounts keep writing where they started, and already-created database files do not move.

The three fields of TseInitialParams are independent and each empty (NULL or "") field means "do not change":

FieldTypeMeaning
dataFolderPathchar const*NULL or "" = leave unchanged. The folder where the engine writes its databases: the blotter DB files of db accounts and the ex-post SQLite output. Without it the engine falls back to the executable's own directory.
logFolderPathchar const*NULL or "" = leave unchanged. Setting it switches the library's logging from the console sink to a rotating file sink: tse.log in that folder, with the start date-time appended to the file name, rotated daily at 00:00 and additionally at 100 MiB, opened in append mode. The current log level is preserved across the switch.
logLevelint32_t0..6 = TseLogLevel, applied immediately; TSE_LOG_LEVEL_UNCHANGED = leave unchanged, keeping the current one. An out-of-range value fails the whole call.

Log levels live in tse_log.h and can also be changed at any time on their own. The library starts at critical; lower the level explicitly when you want to see more.

ValueCC++ tse::LogLevelPython tse.LogLevel
0tse_log_tracetraceTrace
1tse_log_debugdebugDebug
2tse_log_infoinfoInfo
3tse_log_warningwarningWarning
4tse_log_errorerrorError
5tse_log_criticalcriticalCritical
6tse_log_nonenoneOff
#define TSE_LOG_LEVEL_UNCHANGED (-1)

TseStatus   tse_set_initial_params(TseInitialParams const* params);
char const* tse_last_init_error(void);

void tse_set_log_level(TseLogLevel level);
struct InitialParams final {
    std::string dataFolderPath;
    std::string logFolderPath;
    LogLevel    logLevel;
    bool        applyLogLevel;   // false sends TSE_LOG_LEVEL_UNCHANGED
};

bool        setInitialParams(InitialParams const& params) noexcept;
std::string lastInitError();
void        setLogLevel(LogLevel level) noexcept;
tse.set_initial_params(data_folder="/data", log_folder="/logs",
                       log_level=tse.LogLevel.Info)   # raises TseError on failure
params = tse.make_initial_params(data_folder="", log_folder="", log_level=None)
                                  # log_level None -> tse.LOG_LEVEL_UNCHANGED
a.set_log_level(tse.LogLevel.Off) # Python spells tse_log_none as Off

Contracts

A contract is a tradable instrument declared standalone on the account, with no market-data type and no feed binding; the same contract may later live on several adapters through wiring. The full descriptor now includes the venue and the tick size.

A contract precedes everything that uses it, and it lives as long as the account: declare the contract before any input, rule or robot that names it — the engine refuses a reference to an undeclared symbol with the diagnostic "declare it with tse_add_contract first", and a robot cannot be added to an account that holds no contract at all. Once declared, a contract stays with the account for the account's whole life; there is no removal call, the account owns the declaration, and destroying the account destroys its contracts with it. You never manage a contract's lifetime yourself: nothing you hold has to outlive the account, and no handle you keep refers into it.

FieldType
symbolchar[32]
multiplieruint32_t
instrumentTseInstrument
underlyingTseUnderlying
venueTseVenue
tickSizeint64_t
TseStatus tse_add_contract(TseAccountHandle account, char const* symbol, int32_t multiplier,
                           TseInstrument instrument, TseUnderlying underlying,
                           TseVenue venue, int64_t tickSize);
TseStatus tse_add_contract_id(TseAccountHandle account, char const* symbol, int32_t multiplier,
                              TseInstrument instrument, TseUnderlying underlying,
                              TseVenue venue, int64_t tickSize, uint64_t* outId);
TseStatus tse_get_contract_id(TseAccountHandle account, char const* name, uint64_t* outId);
TseStatus tse_add_contract_struct(TseAccountHandle account, TseContract const* contract);
std::uint64_t id  = account.addContract("AAPL", 1, tse::Instrument::equity,
                                        tse::Underlying::equity, tse::Venue::NASDAQ, 10'000);
std::uint64_t id2 = account.getContractId("AAPL");
cid = a.add_contract("AAPL", 1, tse.Instrument.Equity, tse.Underlying.Equity,
                     tse.Venue.NASDAQ, 10_000)
cid = a.get_contract_id("AAPL")

Identity

The contract id is a hash fold over all of the contract's fields — the symbol, the multiple, the instrument class, the underlying, the venue, the tick size, and the account currency. Two consequences follow. First, the id is not decomposable: no field can be read back out of it, it is an opaque 64-bit identity. Second, equality is total: contracts with equal fields produce an equal id, and changing any field — including the tick size or the multiple — produces a different contract. Databases keyed by contract id therefore do not survive a change of any contract field; regenerate them instead of expecting old rows to match.

The tick pipeline

Prices cross the boundary as doubles but live inside the engine as fixed-point integers in millionths of one price unit (the FinValue encoding; tse_finvalue_scale() returns the exponent, and tse_finvalue_from_double / tse_finvalue_to_double translate). The tickSize field of a contract is expressed in these raw millionth units: 10'000 means a tick of 0.01.

Against that grid the engine applies a three-step rule: a price conforms when its distance to the nearest grid node is at most one raw unit (one millionth) — this absorbs the representation error of decimal fractions in doubles; a conformant price is snapped exactly onto the grid node; a price farther from the grid is rejected with a diagnostic.

The tick is mandatory and constant. A tickSize of zero is refused at the boundary — tse_add_contract returns tse_error with TickSize::Ctor (unsigned) - step must not be 0 in the account's last-error channel, and the wrappers turn the same refusal into tse::Error and tse.TseError; no default is substituted in its place, because the tick is the grid every price of that contract is measured against. Once declared, the tick never changes: it is part of the contract's identity, and no exported call updates it. The engine deliberately does not implement venue tick regimes such as the tiered tables of MiFID II RTS 11, where the step varies with price band and liquidity — one contract carries one step for its whole life. Declaring the step that the venue actually applies, and answering for every price conforming to it, is therefore yours; a mis-declared tick does not fail loudly, it silently snaps or rejects prices against the wrong grid.

Venues

The venue enumeration counts 58 entries — the undefined sentinel, 47 named venues, and ten user_defined_* slots — plus three schedule-group identifiers (US, EU, UK) that name holiday calendars, not exchanges. Each named venue carries a descriptor with its full name and its ISO 10383 MIC where one exists. The regions, compactly: North America (NYSE, NASDAQ, CBOE, CME, ICE, TSX), Latin America (B3, BMV, BCS, BVC, BVL, BYMA), Europe (LSE, EUREX, EURONEXT, MOEX, XETRA, SIX, BME, BIT, OMX, OSLO, WSE, BIST), Asia-Pacific (TSE, HKEX, ASX, SSE, SZSE, NSE, BSE, KRX, SGX, TWSE, IDX, SET, MYX), and ten crypto venues (BINANCE, COINBASE, KRAKEN, BITSTAMP, OKX, BYBIT, BITFINEX, KUCOIN, GEMINI, HTX). The C enum TseVenue mirrors the engine's enumeration one to one, tse_venue_to_string prints a value, and the C++ tse::Venue and Python tse.Venue spell the same names.

Currencies

Today the engine is validated on a single currency, US dollar; the euro value and cross-currency portfolios are a feature in the process of shipping.

The account currency is fixed at creation — it is the third argument of tse_account_create and a constructor argument of the wrappers — and cannot be changed afterwards; it also participates in every contract's identity fold. In a grid search the currency passed to tse_run_grid applies to every grid account the harness creates.

The exported matrix monomorphizes two currency values, and the numeric values are the ISO-4217 numeric codes:

ValueC TseCurrencyC++ tse::CurrencyPython tse.Currency
840tse_ccy_usdusdUsd
978tse_ccy_eureurEur
enum class Currency : std::uint16_t { usd = 840, eur = 978 };
tse.Currency.Usd   # 840
tse.Currency.Eur   # 978

Durations

tse_duration.h defines the 27-value duration grid. Every input and every pattern takes an explicit duration argument; the engine never derives one.

ValueC constantC++ tse::DurationPython tse.Duration
0tse_duration_nanosecondsnanosecondsNanoseconds
1tse_duration_microsecondsmicrosecondsMicroseconds
2tse_duration_millisecondsmillisecondsMilliseconds
3tse_duration_secondssecondsSeconds
4tse_duration_five_secondsfive_secondsFiveSeconds
5tse_duration_ten_secondsten_secondsTenSeconds
6tse_duration_thirty_secondsthirty_secondsThirtySeconds
7tse_duration_minutesminutesMinutes
8tse_duration_two_minutestwo_minutesTwoMinutes
9tse_duration_three_minutesthree_minutesThreeMinutes
10tse_duration_five_minutesfive_minutesFiveMinutes
11tse_duration_ten_minutesten_minutesTenMinutes
12tse_duration_fifteen_minutesfifteen_minutesFifteenMinutes
13tse_duration_thirty_minutesthirty_minutesThirtyMinutes
14tse_duration_hourshoursHours
15tse_duration_two_hourstwo_hoursTwoHours
16tse_duration_three_hoursthree_hoursThreeHours
17tse_duration_six_hourssix_hoursSixHours
18tse_duration_eight_hourseight_hoursEightHours
19tse_duration_twelve_hourstwelve_hoursTwelveHours
20tse_duration_daysdaysDays
21tse_duration_two_daystwo_daysTwoDays
22tse_duration_three_daysthree_daysThreeDays
23tse_duration_weeksweeksWeeks
24tse_duration_monthsmonthsMonths
25tse_duration_quartersquartersQuarters
26tse_duration_yearsyearsYears

A library build does not have to monomorphize the whole grid. Asking for a duration that is not built in fails loud, with an error text that names the requested duration and lists the built-in ones — it has the shape:

duration "<name>" is not built into this library; it monomorphizes
<built-in list>

tse_version.h lets you enumerate what the loaded binary actually supports; both calls follow the count-then-buffer protocol (call with a null buffer to get the count):

uint32_t tse_abi_version(void);
size_t   tse_supported_durations(TseDuration* outDurations, size_t capacity);
size_t   tse_supported_mdtypes(TseMdType* outMdTypes, size_t capacity);
std::uint32_t         abiVersion() noexcept;
std::vector<Duration> supportedDurations();
std::vector<MdType>   supportedMdTypes();
tse.supported_durations()   # [tse.Duration...]
tse.supported_md_types()    # [tse.MdType...]

Thread affinity

Thread placement is uniform across the whole object model: every builder that owns a worker thread takes a coreId parameter, and the parameter is mandatory in C (the wrappers default it to -1). A non-negative value pins that node's thread to the given core; a negative value means no separate thread — the node runs inline on its caller.

  • the account (tse_account_create) — the blotter thread;
  • the execution (tse_exec_create_simulator, tse_exec_create_custom) — the execution worker;
  • every input builder (tse_add_input_ohlcv / _bidask / _trade / _executed) — the input's worker;
  • every pattern builder (tse_add_pattern_comparison / _crossover / _threshold / _peak / _timestamp / _formula) — the pattern's worker.

The per-node core id is part of the saved recipe: tse_save persists it with each input and pattern node, and tse_load restores it, so a robot reloaded in another process keeps its thread placement.

tse_grid.h runs one robot per parameter point, in parallel, and folds each run into a summary. The builder callback receives a fresh account and must configure a complete robot on it from scratch — adapters, execution, contracts, inputs, patterns, rules, robot, data feed — exactly as it would in a standalone program.

The thread policy is fixed inside the library — there is no thread-count parameter. The pool uses nproc - 2 workers pinned to cores 2 .. nproc - 1, keeping cores 0 and 1 reserved for the OS, and the call fails outright when no spare cores remain. Each worker account is created with the label <prefix>_<index>, the given storage regime, the run's currency, and coreId -1 (no separate blotter thread — the worker itself is already pinned). outResults must hold paramCount entries; a point whose builder or summary fails leaves ok at 0 without aborting the other points.

One TseGridResult record is written per parameter point.

FieldType
paramValuedouble
okint
summaryTseSummary
typedef TseStatus (*TseGridBuilder)(TseAccountHandle account, double paramValue, void* userData);

TseStatus tse_run_grid(char const* accountLabelPrefix, TseStorageRegime regime,
                       TseCurrency currency, double const* paramValues, size_t paramCount,
                       TseGridBuilder builder, void* userData, TseGridResult* outResults);
std::vector<GridResult> runGrid(std::string accountLabelPrefix, StorageRegime regime,
                                std::vector<double> paramValues,
                                std::function<void(Account&, double)> builder,
                                Currency currency = Currency::usd);
results = tse.run_grid(account_label_prefix, regime, param_values, builder,
                       currency=tse.Currency.Usd, lib_path=None)
# builder(account, param_value); results[i].param_value, .ok, .summary

Save and load

Persistence is recipe-only, at schema version 3. Saving a robot stores its construction recipe, not live state: the contracts, the inputs (with their market-data type, duration, data length, contract subset and core id, but unbound to any adapter), the patterns, the rules and their order, the risk policies, and the robot itself, together with the account currency and regime tags. Market adapters and the execution are deliberately not saved — after a load you attach fresh ones.

Each contract is saved with its every field, the venue, the tick size and the multiple included. That matters because the contract id is a fold over all of them (see "Identity" above): a contract rebuilt by tse_load hashes to the same identity it had before the save, so tse_get_contract_id returns the value it returned before, and blotter rows and ex-post records keyed by that id keep matching across the round trip.

A processor (an input's data processor, a pattern's formula) is a function pointer and cannot be serialized, so a savable node references a string key instead. Register the key on the account, then build the node with the matching *_by_key builder. The plain (non-keyed) tse_add_input_* / tse_add_pattern_formula builders run fine but make the robot unsavable — tse_save fails on such a robot.

tse_load reconstructs the inputs unbound. The rebinding sequence before start is fixed: create fresh market adapter(s) and an execution, call tse_bind_input for each loaded input — the adapter's type must match the input's saved market-data type, and binding registers the input's contract subset on the adapter — then tse_start. Starting a robot while any loaded input is still unbound is an error.

The round trip is cross-language by construction: the recipe is a SQLite file, and the keys are plain strings. A robot saved from Python loads in C++ or C — create a fresh account, register the same keys (the processors themselves may be reimplemented in the host language), tse_load, bind, start.

TseStatus tse_register_input_processor_ohlcv   (TseAccountHandle, char const* key, TseOhlcvInputProcessor,    void* userData);
TseStatus tse_register_input_processor_bidask  (TseAccountHandle, char const* key, TseBidAskInputProcessor,   void* userData);
TseStatus tse_register_input_processor_trade   (TseAccountHandle, char const* key, TseTradeInputProcessor,    void* userData);
TseStatus tse_register_input_processor_executed(TseAccountHandle, char const* key, TseExecutedInputProcessor, void* userData);
TseStatus tse_register_formula_processor       (TseAccountHandle, char const* key, TseFormulaProcessor,       void* userData);

TseStatus tse_add_input_ohlcv_by_key(TseAccountHandle, char const* label, int32_t dataLength,
                                     TseDuration, char const* key, TseMarketHandle,
                                     char const* const* contractSymbols, size_t contractCount);
/* also _bidask_by_key, _trade_by_key, _executed_by_key */
TseStatus tse_add_pattern_formula_by_key(TseAccountHandle, char const* label, TseDuration,
                                         char const* const* inputLabels, size_t inputCount,
                                         char const* key);

TseStatus tse_save(TseAccountHandle account, char const* robotLabel, char const* path);
TseStatus tse_load(TseAccountHandle account, char const* robotLabel, char const* path);
TseStatus tse_bind_input(TseAccountHandle account, char const* inputLabel, TseMarketHandle market);
account.registerInputProcessor("sma", processor);        // also ...BidAsk / ...Trade / ...Executed
account.registerFormulaProcessor("cross", formula);
account.addInputOhlcvByKey("in", 20, tse::Duration::days, "sma", market, {"AAPL"});
account.addPatternFormulaByKey("pat", tse::Duration::days, {"in"}, "cross");
account.save("Robot", "robot.db");
account.load("Robot", "robot.db");
account.bindInput("in", market);
a.register_input_processor("sma", processor)   # also *_bidask / *_trade / *_executed
a.register_formula_processor("cross", formula)
a.add_input_ohlcv_by_key("in", 20, tse.Duration.Days, "sma", market, ["AAPL"])
a.add_pattern_formula_by_key("pat", tse.Duration.Days, ["in"], "cross")
a.save("Robot", "robot.db"); a.load("Robot", "robot.db"); a.bind_input("in", market)

Market data and connectors

Market data enters the engine through typed adapters (tse_market_create), one push function per market-data type, each addressing the contract by name or by id.

The executed channel (tse_md_executed) and the book channel (tse_md_book) are pure input sources: they carry no price the portfolio could mark against and no venue the simulator could fill on, so a simulator cannot be attached to them and a portfolio subscription on them is refused with a diagnostic.

TseStatus tse_market_push_ohlcv_by_name   (TseMarketHandle, char const* contractName, TseTickOHLCV const*);
TseStatus tse_market_push_ohlcv_by_id     (TseMarketHandle, uint64_t contractId,      TseTickOHLCV const*);
TseStatus tse_market_push_bidask_by_name  (TseMarketHandle, char const* contractName, TseTickBidAsk const*);
TseStatus tse_market_push_bidask_by_id    (TseMarketHandle, uint64_t contractId,      TseTickBidAsk const*);
TseStatus tse_market_push_trade_by_name   (TseMarketHandle, char const* contractName, TseTickTrade const*);
TseStatus tse_market_push_trade_by_id     (TseMarketHandle, uint64_t contractId,      TseTickTrade const*);
TseStatus tse_market_push_executed_by_name(TseMarketHandle, char const* contractName, TseRetained const*);
TseStatus tse_market_push_executed_by_id  (TseMarketHandle, uint64_t contractId,      TseRetained const*);

CSV loaders

tse_dataplane.h keeps the CSV loading helpers for backtest data. The parameterized loaders take a nullable TseCsvOptions; NULL means the per-type defaults.

The *Column fields name columns by role; an empty or NULL role field falls back to that role's default name. indexColumn selects the timestamp column, which matters for dual-timestamp files. The bid/ask roles (bidColumn, bidVolumeColumn, askColumn, askVolumeColumn, lastColumn) apply to the bid/ask loader; priceColumn, volumeColumn and sideColumn apply to the trade loader — volume and side are copied into the tick only (they never form the price serie), and a side cell holds either a numeric TseSide code or the exact textual name mirrored by tse_side_to_string. The OHLCV loader has no role fields of its own: its value columns are fixed at Open/High/Low/Close/Volume. When the same options structure is handed to tse_csv_reader_create (the generic reader of tse_timeserie.h), the header itself says the role-column fields play no part there — only dateFormat, separator, hasHeader, ignoreParseErrors and bufferSize do. ignoreParseErrors drops an unparsable row instead of failing the whole load; bufferSize sizes the reader's I/O buffer, zero meaning the engine-chosen default. Set structSize to sizeof(TseCsvOptions) for forward compatibility.

Defaults per loader: separator ,, date format %m/%d/%Y %H:%M, index column Date; OHLCV value columns Open/High/Low/Close/Volume, bid/ask columns bid/bid_volume/ask/ask_volume/last, trade price column price.

One date-format note that saves an afternoon: the parser's plain %S already consumes an optional fractional part after the seconds. Write %Y-%m-%d %H:%M:%S for both 12:00:05 and 12:00:05.123456; do not write %S.%f.

The options record in full:

FieldTypeMeaning
structSizesize_tset to sizeof(TseCsvOptions)
dateFormatchar const*e.g. "%m/%d/%Y %H:%M"
separatorchar, or ;
hasHeaderintnon-zero if a header row is present
indexColumnchar const*header name of the timestamp column
bidColumnchar const*BidAsk role
bidVolumeColumnchar const*BidAsk role
askColumnchar const*BidAsk role
askVolumeColumnchar const*BidAsk role
lastColumnchar const*BidAsk role
priceColumnchar const*Trade role
volumeColumnchar const*Trade role
sideColumnchar const*Trade role
ignoreParseErrorsintdrops an unparsable row instead of failing the whole load
bufferSizesize_tzero means the engine-chosen default
TseStatus tse_load_ohlcv_csv   (char const* filePath, TseTickOHLCV** outTicks, size_t* outCount);
TseStatus tse_load_ohlcv_csv_ex(char const* filePath, TseCsvOptions const* options, TseTickOHLCV** outTicks, size_t* outCount);
TseStatus tse_load_bidask_csv  (char const* filePath, TseCsvOptions const* options, TseTickBidAsk** outTicks, size_t* outCount);
TseStatus tse_load_trade_csv   (char const* filePath, TseCsvOptions const* options, TseTickTrade** outTicks, size_t* outCount);

void tse_free_ticks(TseTickOHLCV* ticks);
void tse_free_bidask_ticks(TseTickBidAsk* ticks);
void tse_free_trade_ticks(TseTickTrade* ticks);

char const* tse_last_csv_error(void);   /* thread-local text of the last loader failure */
auto ticks  = tse::Account::loadOhlcvCsv("bars.csv");
auto ticks2 = tse::Account::loadOhlcvCsv("bars.csv", options);   // tse::CsvOptions
auto quotes = tse::Account::loadBidAskCsv("quotes.csv", options);
auto trades = tse::Account::loadTradeCsv("trades.csv", options);
opts   = tse.make_csv_options(date_format="%Y-%m-%d %H:%M:%S", index_column="est")
bars   = a.load_ohlcv_csv("bars.csv", opts)      # None -> default options
quotes = a.load_bidask_csv("quotes.csv", opts)
trades = a.load_trade_csv("trades.csv", opts)
tse.last_csv_error()

Custom execution: the go-live seam

The same robot that backtests against the built-in Simulator goes live by swapping one object: create the execution with tse_exec_create_custom instead of tse_exec_create_simulator. A custom execution is not connected to market data; it receives each outgoing order in your fill callback and reports fills — none, one, or several partials — through tse_exec_apply_fill, addressing the order by its client order id. Both creators take requiredDataLength (the resting-order buffer length per contract) and the mandatory coreId. The full order flow, the TseOrder record and the callback contract are covered in the Robot chapter.

TseExecHandle tse_exec_create_custom(TseAccountHandle account, char const* label,
                                     TseFillCallback fillCallback, void* userData,
                                     int32_t requiredDataLength, int coreId);
TseStatus tse_exec_apply_fill(TseExecHandle exec, char const* clientOrderId,
                              double price, double quantity, double fee,
                              int64_t tsExecutionNanoseconds);

The connection library

Feeding a live robot is the job of a separate, standalone shared library: libtse_connection, with its own C surface under tse_connection/ (tse_connection.h is the umbrella) and its own ABI version, tse_connection_abi_version(). It is a deliberately small client stack: one handle, six protocols, one message type.

  • Protocols (TseConnectionProtocol): http, https, ws, wss, udp, udp_multicast.
  • Two ways to consume messages: push (a TseConnectionMessageHandler invoked on the handler threads set by tse_connection_client_set_handler_threads) and pull (tse_connection_client_wait_message, or the combined send-and-wait tse_connection_client_request); waiting comes in park and spin flavors (TseConnectionWaitMode), receiving likewise (TseConnectionReceiveMode in the config).
  • TLS verification (TseConnectionTlsVerifyMode): ca_hostname (verify the chain and the host name), ca (chain only), none; the CA can come inline (tlsCaPem) or from a file (tlsCaFile).
  • A TseConnectionMessage carries the raw bytes plus protocol-specific extras: the HTTP status and headers, the WebSocket opcode, or the UDP sender endpoint.

One documented limitation of the kernel-socket path: a single received UDP datagram is capped at 2048 bytes — a larger datagram is truncated by the kernel and the excess is discarded, for unicast and multicast alike. That is sufficient for typical multicast market data, which stays within an Ethernet MTU. Note what this cap is not: it is not SO_RCVBUF, which only sizes the kernel's receive queue (how many datagrams may wait), never the size of one datagram.

TseConnectionClientHandle tse_connection_client_create(TseConnectionProtocol protocol);
void tse_connection_client_destroy(TseConnectionClientHandle client);

TseConnectionStatus tse_connection_client_set_handler_threads(TseConnectionClientHandle, size_t threadCount);
TseConnectionStatus tse_connection_client_set_message_handler(TseConnectionClientHandle,
                                                              TseConnectionMessageHandler, void* userData);
TseConnectionStatus tse_connection_client_run (TseConnectionClientHandle, TseConnectionConfig const*, int reconnect);
TseConnectionStatus tse_connection_client_stop(TseConnectionClientHandle);

TseConnectionStatus tse_connection_ws_send_text  (TseConnectionClientHandle, char const* utf8, size_t length);
TseConnectionStatus tse_connection_ws_send_binary(TseConnectionClientHandle, uint8_t const* data, size_t length);
TseConnectionStatus tse_connection_ws_close      (TseConnectionClientHandle, char const* reason);
TseConnectionStatus tse_connection_udp_send      (TseConnectionClientHandle, uint8_t const* data, size_t length);
TseConnectionStatus tse_connection_http_request  (TseConnectionClientHandle, uint8_t const* body, size_t length,
                                                  TseConnectionHttpVerb verb);

TseConnectionStatus tse_connection_client_wait_message(TseConnectionClientHandle,
                                                       TseConnectionWaitMode mode, uint64_t timeoutMs,
                                                       TseConnectionMessage const** outMessage);
TseConnectionStatus tse_connection_client_request(TseConnectionClientHandle, uint8_t const* body, size_t length,
                                                  int verbOrOpcode, TseConnectionWaitMode mode,
                                                  uint64_t timeoutMs, TseConnectionMessage const** outMessage);
char const* tse_connection_last_error(TseConnectionClientHandle client);

Ex-post analysis

tse_ex_post.h turns an account's robot-tagged trades into duration-bucketed scoring series — the raw material of candidate selection. Two entry points exist: a one-shot save to disk, and a live object whose features are read back through the API.

tse_ex_post_save builds the per-robot analysis, runs the scoring pass, and writes a two-table SQLite database (serie + scoring) at dbPath. step selects the bucket unit (e.g. tse_duration_days), stepCount is the signed bucket magnitude (negative walks backwards, oldest-last), windowSize is the scoring window length in buckets. tse_ex_post_load opens and validates such a database and returns the stored robot count (outRobotCount may be NULL).

The handle returned by tse_ex_post_create is owned by the caller (destroy it with tse_ex_post_destroy) but borrows the account and becomes invalid when the account is destroyed. The feature readers share one protocol:

  • Two-phase extraction: call with null output buffers first — the required row count is stored into *inoutRowCount and nothing else is touched; then call again with buffers of that size.
  • outValues is a flat buffer of rowCount * tse_ex_post_param_count() floats, laid out row by row (row-major).
  • Row order is variant-major: all buckets of the first half-life (for EWMA) or the first lookback (for level crossings) come first, then the second, and so on; momentum has exactly one variant.
  • Absent values arrive as NaN — gaps are preserved, not interpolated.

The parameter axis is not mirrored as C constants: its width is tse_ex_post_param_count() and the name of each column is tse_ex_post_param_name(index).

The parameter axis — every scoring column a bucket carries — is the following set:

Duration-based score parameters (78)
1. rolling_mean_return27. consequtive_loss53. rolling_skewness
2. ew_mean_return28. confirmed_negative_trend54. regime_flip_rate
3. rolling_median_return29. return_volatility55. net_profit
4. rolling_sharpe30. rolling_kurtosis56. gross_profit
5. return_adjusted31. psi_drift57. gross_loss
6. rolling_calmar32. equity_curve_strength58. total_trades
7. rolling_omega33. hurst_exponent59. avg_trade_net_profit
8. gain_to_pain34. linearity_trend60. max_equity
9. hit_rate35. return_autocorr_lag161. min_equity
10. win_rate36. acf_sum62. max_drawdown
11. profit_factor37. pacf63. return_on_account
12. pf_momentum38. cross_correlation64. profit_factor_raw
13. rolling_rank39. rolling_beta65. entry_efficiency
14. rank_momentum40. mann_kendall_trend66. exit_efficiency
15. top_k_fraction41. theil_sen_slope67. win_loss_ratio
16. cross_sectional_zscore42. directional_consistency68. kelly
17. switch_cost_adjusted43. market_volatility69. mfe_pct
18. thompson_sampling44. feature_correlation70. mfe_abs
19. return_acceleration45. performance_dispersion71. avg_winning_round_trip
20. tail_ratio46. rank_entropy72. avg_losing_round_trip
21. current_drawdown47. step_change73. largest_win
22. drawdown_duration48. information_coefficient74. largest_loss
23. rolling_cvar49. ic_decay_halflife75. consecutive_wins
24. rolling_mae50. rolling_ic_stability76. max_consecutive_wins
25. ulcer_index51. granger_causality77. trading_days
26. equity_curve_breakdown52. expected_holding_duration78. composite
TseStatus tse_ex_post_save(TseAccountHandle account, char const* dbPath,
                           TseDuration step, int32_t stepCount, int32_t windowSize);
TseStatus tse_ex_post_load(TseAccountHandle account, char const* dbPath, size_t* outRobotCount);

TseExPostHandle tse_ex_post_create(TseAccountHandle account, TseDuration step,
                                   int32_t stepCount, int32_t windowSize);
void tse_ex_post_destroy(TseExPostHandle exPost);

TseStatus tse_ex_post_robot_count (TseExPostHandle, size_t* outCount);
TseStatus tse_ex_post_robot_label (TseExPostHandle, size_t robotIndex, char* buf, size_t capacity);
TseStatus tse_ex_post_bucket_count(TseExPostHandle, size_t robotIndex, size_t* outCount);

size_t      tse_ex_post_param_count(void);
char const* tse_ex_post_param_name(size_t paramIndex);

TseStatus tse_ex_post_feature_momentum(TseExPostHandle, size_t robotIndex,
                                       int64_t* outTsNanoseconds, float* outValues, size_t* inoutRowCount);
TseStatus tse_ex_post_feature_ewma(TseExPostHandle, size_t robotIndex,
                                   float const* halfLives, size_t halfLifeCount,
                                   int64_t* outTsNanoseconds, float* outValues, size_t* inoutRowCount);
TseStatus tse_ex_post_feature_level_crossings(TseExPostHandle, size_t robotIndex,
                                              size_t const* lookbacks, size_t lookbackCount,
                                              int64_t* outTsNanoseconds, float* outValues, size_t* inoutRowCount);
tse::ExPost ep {account.createExPost(tse::Duration::days, 1, 30)};
auto [ts, values] = ep.featureEwma(0, {5.0f, 20.0f});
std::size_t n = tse::ExPost::paramCount();
std::string name = tse::ExPost::paramName(0);
ep = a.create_ex_post(tse.Duration.Days, 1, 30)
ts, values = ep.feature_momentum(0)
ep.robot_count(); ep.robot_label(0); ep.bucket_count(0)
ep.param_count(); ep.param_name(0)

Operational practice: the daily cycle

Starting and stopping a trading process deliberately, once per session day, is standard practice in the industry rather than an admission that the software cannot run longer. A process that is brought up before the session and taken down after it starts each day from a known state: caches are cold and therefore consistent with the data that is about to arrive, the day's log and blotter files are delimited by the run itself, memory that accumulated over the session is returned, and any configuration change made overnight takes effect at a single, observable moment instead of drifting in. The engine is built for that rhythm: tse_stop ends a robot's event flow, the account destructor releases its threads and closes its storage, and the next run reconstructs the object graph — from a saved recipe (tse_load) when you want the previous wiring back verbatim.

Doing this by hand every day is the part worth automating, and the operating system already provides the machinery, so there is no reason to build scheduling into the trading process itself. On Linux the conventional instrument is systemd: a unit describing the process, with a timer (or a calendar-scheduled pair of units) starting it before the session and stopping it afterwards, and the unit's restart policy covering an unexpected exit. The supervisor owns the schedule and the restart semantics; the robot owns only its trading logic.

The shipped library

The product is delivered as a dynamic object and is loaded as a dynamic object: on macOS the file is libtse_export.dylib, and it arrives accompanied by the C headers under include/tse/, the C++ wrapper (tse.hpp together with its one translation unit tse.cpp) and the Python module tse.py. Which file is opened at run time is decided exactly as the "Loading the library" section above describes — either by an explicit path argument, or, when none is given, through the TSE_EXPORT_LIB environment variable, falling back to the platform's default file name on the loader path.

Everything the library carries of its own is linked into it statically: the engine's internal static archives are merged into the single shared object at build time, so nothing of the engine has to be shipped beside it. There is one exception, and it is platform-specific: on macOS the standard libraries are resolved by path rather than absorbed, so they have to be present on the client machine.

The library checks its licence over the network, and that exchange is the only one the engine ever initiates on its own behalf — every other packet it sends is one you asked for, through a connector you configured or through a processor of yours that calls out. The check happens lazily, the first time a protected operation runs: a seat is taken, a checkpoint is reached, or the status is queried. What travels is the licence identifier, a salted hash of four machine attributes — platform UUID, hardware model, serial number and MAC address — a digest of the product binary, and the seat accounting the licence is measured by: the backtest and live ceilings, the peaks observed against them, the feature flags, the kind of environment, the hypervisor if the machine runs under one, and a boot identifier. Nothing of yours is in that message and nothing of yours can be: no market data, no contract, no strategy, no order, no trade and no statistic has a field to travel in. Where the check is addressed is yours to set — TSE_LICENSE_HOST, TSE_LICENSE_PORT and TSE_LICENSE_TARGET override the default endpoint, which is how an air-gapped or proxied installation points the library at a licence host of its own. A failed exchange does not stop a running robot: the lease enters a grace phase and work continues, with the condition readable at any time through tse_protection_status and the exchange repeatable on demand through tse_license_heartbeat.

Version 5.0.0.0