A multi-broker algorithmic trading engine written in modern C++23. SlickTrader connects to crypto exchanges over WebSocket/REST, routes market data and order events through a lock-free messaging core, runs trading strategies as hot-loadable plugins, and exposes a real-time web dashboard for monitoring and manual control.
Status: first release, 0.1.0. The Coinbase and Hyperliquid brokers are built in, and more can be built in from their own repositories (see Brokers and out-of-tree brokers). Order execution needs broker credentials (see Broker configuration). The sample config sends Coinbase orders to a local simulator and leaves Hyperliquid order entry disabled.
- Multi-broker support β Coinbase Advanced Trade
and Hyperliquid today, with market data and
order execution modeled as independent, per-broker connectors so more venues can be added
behind the same
IBrokerinterface. - Lock-free messaging core β a single-writer threading model (see
Architecture) built on slick-queue
and a
stream_buffer_multiplexer, avoiding mutexes on the hot path between broker connectors, the strategy manager, and the web server. - Hot-loadable strategy plugins β strategies are compiled as standalone
.dll/.soshared libraries and loaded at engine startup byStrategyLoader; no engine rebuild needed to iterate on strategy logic. See strategies/README.md for the plugin guide, config schema, and a production-styleMarketMakerreference implementation. - Position, PnL & risk management β FIFO-lot position tracking, realized/unrealized PnL, and pre-trade risk validation (position limits, order-value limits, per-second rate limits, max-loss halts) all run single-threaded off the strategy manager for lock-free reads/writes.
- Futures-ready accounting β a per-instrument contract multiplier, taken from the
InstrumentDefinitionEventa broker publishes, scales realized/unrealized PnL and order-value risk checks (ES moves $50 a point, not $1). Per-contract fees (FeeType::PER_UNIT), aDAYtime in force, and zero or negative prices (calendar spreads) are supported; brokers that cannot expressDAY(Coinbase, Hyperliquid) reject it. - Market-by-order books and queue position β for a broker whose feed carries every order,
a strategy subscribes
SubscriptionType::MBOand is handed the instrument's order-by-order book (on_order_book_l3).queue_position()reads from it where one of the strategy's own orders stands: how many orders and how much quantity trade before it. See strategies/README.md. - Trading status β a broker that reports it publishes each instrument's state (pre-open,
open, halted, closed) as an
InstrumentStatusEvent, handed to strategies throughon_instrument_status. - Positions that survive a restart β with
position_storeconfigured, every position, with the open lots FIFO accounting still has to close, is kept in a memory-mapped file: recording a fill never waits for the disk, and a restart carries on from what was held. See Positions across restarts. - Multi-leg fills β a broker reports the fill of an order for a multi-leg instrument (a
futures spread) as the order's own fill followed by one fill per leg (
FillScope), so the order moves at the spread's price while the positions booked are the legs', where they clear. - Live web dashboard β a dependency-free vanilla-JS frontend (
web/static/) served over uWebSockets, with a REST API and a price-ladder UI for manual order entry/cancellation. - Manual + automated strategies side by side β a built-in
ManualStrategyowns orders placed by hand through the dashboard, coexisting with plugin strategies under the same risk/position tracking.
ββββββββββββββββββββββββββ
β TradingEngine β (owns lifecycle, wiring)
ββββββββββββββ¬ββββββββββββ
β
ββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β Broker threads β β StrategyManager β β WebServer β
β (per-broker MD βββeventsβββΆβ (single-writer: βββeventsβββΆβ (uWebSockets, β
β + order conns, ββββordersβββ positions, PnL, βββββctrlββββ REST + WS push, β
β own threads) β β risk, dispatch) β β manual control) β
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
- Each broker owns a market-data connector and an order connector, each running on its own
thread, publishing/consuming events through per-producer lock-free queues multiplexed by a
slick::stream_buffer_multiplexer. StrategyManageris the sole writer toPositionTracker,PnLCalculator, andRiskManagerβ by construction there's a single thread mutating shared trading state, so no locking is required there. It dispatches market data/order events to every loaded strategy and forwards new/modify/cancel order requests to the owning broker's order connector.StrategyBase(which real strategies inherit from) validates every order against theRiskManagerand checksBrokerStatusRegistryfor connector health before it's queued. Order events are broadcast to all strategies, soStrategyBaseattributes each one by the<strategy-name>_prefixgenerate_order_id()stamps onclient_order_idand ignores the rest β without that, one strategy books another's fills into its own position and PnL.WebServerreads the same tracker/calculator/risk state and streams updates to connected browsers over WebSocket, and exposes a REST API for querying and controlling strategies/orders.BrokerStatusRegistryis a lock-free, always-on status board (market-data/order enabled+connected flags per broker) that both the dashboard andStrategyBase::submit_orderconsult to avoid orders getting stuck when a broker connector is down. Strategy plugins linkslick-trader-corestatically and so would otherwise get their own private copy of it, soStrategyLoaderinjects the host's instance into every plugin at load time (alongside the logger) β see strategies/README.md.
- Compiler: a C++23 compiler - MSVC (Visual Studio 2022) on Windows, GCC 14 on Linux, Apple
clang (Xcode 16) on macOS. CI builds and tests all three; strategy plugins are
.dll,.soand.dylibrespectively. - CMake β₯ 3.28
- vcpkg β provides
uWebSocketsandnlohmann_json(bothfind_package(... REQUIRED), not auto-fetched). Configure with vcpkg's toolchain file. slick-logger,slick-orderbook,coinbase-advanced-cppandhyperliquid-cppare used as installed packages when found, and fetched with CMakeFetchContentotherwise. A broker SDK is only looked up when its broker is enabled.slick-net,slick-queue,slick-stream-bufferandslick-stream-buffer-multiplexercome with the SDKs. With no broker built,slick-netisn't needed but the other three have to be installed.slick-stackeris fetched only for the strategy plugins (order_stackerneeds it).- GTest (vcpkg), for the tests only.
Configure (first time, or after CMakeLists.txt changes):
"C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" \
-S . -B build \
-DCMAKE_TOOLCHAIN_FILE=Y:/repo/vcpkg/scripts/buildsystems/vcpkg.cmakeBuild:
"C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" \
--build build --config Debug --target ALL_BUILD -j 16This produces:
build/src/Debug/slick-trader.exeβ the main engine executablebuild/strategies/Debug/*.dllβ strategy plugins (e.g.market_maker.dll,order_stacker.dll)build/tests/Debug/slick-trader-tests.exeβ the regression test suite
ctest --test-dir build -C Debug --output-on-failure
# or run the binary directly:
build/tests/Debug/slick-trader-tests.exeThe suite covers behaviour that only reproduces across a module boundary: that a plugin loaded
through StrategyLoader observes the host's BrokerStatusRegistry, and that a strategy ignores
order events belonging to other strategies. It also checks that every broker built into the binary
is registered under its config name, that each market data subscription a strategy declares
reaches its broker, futures accounting, the market-by-order book and queue position, multi-leg
fills, and positions across a restart - the position store, and a broker's fill journal replayed
without a fill booked twice. An out-of-tree broker's own tests are compiled in too. Requires GTest
(find_package(GTest CONFIG)).
| Option | Default | Effect |
|---|---|---|
SLICK_TRADER_BUILD_STRATEGIES |
ON when top-level, OFF as a subproject | Build the example strategy plugins. When OFF, slick-stacker is not fetched either β it is needed only by order_stacker. The engine is unaffected: plugins are dlopened at runtime, never linked. |
SLICK_TRADER_DISABLE_CXX_MODULE_SCAN |
ON | Skip C++ module dependency scanning for faster MSVC builds |
SLICK_TRADER_FAST_FETCHCONTENT |
ON | Skip FetchContent update checks on every configure |
SLICK_TRADER_SUPPRESS_VS_REGEN |
ON | Disable ZERO_CHECK auto-regeneration for Visual Studio generators |
SLICK_TRADER_ENABLE_PYTHON_STRATEGY |
ON | Look for Python3 (development headers) and, when found, define SLICK_TRADER_PYTHON_SUPPORT. Reserved for script-based strategies: nothing in this release uses it |
SLICK_TRADER_ENABLE_LUA_STRATEGY |
OFF | The same for Lua 5.4 (SLICK_TRADER_LUA_SUPPORT) |
SLICK_TRADER_BUILD_TESTS |
ON when top-level, OFF as a subproject | Build slick-trader-tests and its probe plugin. Requires GTest. |
SLICK_TRADER_ENABLE_COINBASE |
ON | Build the Coinbase broker. OFF skips its SDK too |
SLICK_TRADER_ENABLE_HYPERLIQUID |
ON | Build the Hyperliquid broker. OFF skips its SDK too |
SLICK_TRADER_SANITIZER |
(empty) | GCC and Clang: address,undefined (ASan with UBSan) or thread (TSan). Instruments every target - fetched dependencies and strategy plugins included - in every build type. CI runs both on Linux |
SLICK_TRADER_EXTERNAL_BROKERS |
(empty) | ;-separated directories of out-of-tree brokers to build in. See below. |
Each broker is its own OBJECT library under src/brokers/<name>/, declared with
slick_trader_add_broker() from cmake/slick_trader_broker.cmake,
and registers itself with BrokerRegistry under its config name:
SLICK_TRADER_REGISTER_BROKER("coinbase", CoinbaseBroker);A broker kept in its own repository uses the same two pieces and is built in by pointing the configure step at its directory; no file in this repository changes:
slick_trader_add_broker(mybroker
SOURCES src/mybroker_broker.cpp ...
DEPENDS my-sdk
TESTS tests/test_mybroker.cpp # compiled into slick-trader-tests
)cmake -S . -B build -DSLICK_TRADER_EXTERNAL_BROKERS=/path/to/slick-trader-mybrokerKeep that setting in a CMakeUserPresets.json, which .gitignore excludes, so a local path never
reaches the repository. Configure prints each directory as it adds it, then the brokers that
directory registered and the version its own project() declares, if any. A directory that
registers no broker gets a warning. A brokers[] config entry whose name has no adapter in the
binary is a fatal startup error that lists the brokers the binary does carry.
See strategies/README.md for how to write and build your own strategy plugin.
build/src/Debug/slick-trader.exe config/config_example.jsonEach release also attaches a ready-to-run
package for Windows, Linux and macOS: the engine, its strategy plugins under strategies/, the
dashboard under web/static/, and config/config_example.json pointed at those plugins. Run it
from the package directory: ./slick-trader config/config_example.json.
The engine reads a single JSON config path as its only argument. Copy
config/config_example.json to config/config.json (gitignored β
only the example is checked in) and adjust it for your environment; on startup it will:
- Initialize per-broker market data and order connectors as enabled in the config.
- Scan
strategies.strategy_plugins_folderfor plugins and load each one, matching it to a*.config.jsoninstrategies.strategy_configs_folderby strategy name (see strategies/README.md). - Start the web dashboard on
web_service.web_server_portifweb_service.enabledis true.
Once running, open http://localhost:<web_server_port> (default 3000) for the dashboard.
Shut down gracefully with Ctrl+C (SIGINT/SIGTERM).
Top-level keys in config.json (example):
| Key | Purpose |
|---|---|
log_file_path, log_level |
Log output path and verbosity (trace/debug/info/...) |
event_queue_size, order_request_queue_size, md_request_queue_size |
Lock-free queue capacities (entries) |
brokers[] |
Per-broker {name, enabled, market_data:{...}, order:{...}} β see below |
strategies |
enabled, strategy_plugins_folder, strategy_configs_folder, plus TOB (top-of-book) queue sizing |
position_store |
Optional. path (default state/positions.dat) and capacity (positions, default 1024): keeps positions across restarts β see below |
web_service |
enabled, web_server_port, web_server_cors, web_server_static_path, ladder_config (default order size, per-instrument overrides), config_queue_size/config_control_size (event-bus producer the dashboard publishes strategy parameter updates through) |
- Coinbase β
market_data.websocket_url(defaultwss://advanced-trade-ws.coinbase.com) andorder.rest_url/order.websocket_urlfor the authenticated order channel. Order submission requiresCOINBASE_API_KEY/COINBASE_API_SECRETenvironment variables to be set. - Hyperliquid β
market_data.api_url/order.api_url(defaulthttps://api.hyperliquid.xyz). Order execution needs a wallet private key wired up to the order connector (seehyperliquid_order_connector.cpp); it is disabled (order.enabled: false) in both sample configs until that's configured.
Both brokers' local dev config (config/config.json, gitignored) point at localhost mock
endpoints rather than the real exchanges β check that file if you're running against simulators.
Without position_store positions live in memory: a restart starts every strategy flat, whatever
it holds at the broker. With it β
"position_store": { "path": "state/positions.dat", "capacity": 1024 }β each position (quantity, average price, realized PnL and fees, contract multiplier, and its open FIFO lots) is written to a slot of a memory-mapped file as its fills are booked, and read back at startup before any strategy runs. Realized PnL carries over with it, so a strategy's loss limit keeps counting across the restart.
- Cost. Recording a fill is a copy into mapped memory on the thread that books it: no system call, no wait for the disk.
- What survives. A fill that was booked is in the file even if the process is killed the next
instant; only the machine going down can lose the last moments. A slot holds two copies of its
position and switches between them, so a process that dies mid-update leaves the position as
it was before the fill, never half written. A fill still on its way from a broker's thread to
the strategy thread when the process dies was not booked, and is not there - unless the broker
journals its fills (
OrderFillEvent::journal_seq): it replays its journal at startup, and the store keeps how far into each broker's journal the strategies had got, with each position recording the fill it was last booked from. A replayed fill the strategies had is dropped before it reaches them; one they never had is booked, once. Without a store a replay is dropped whole: the positions started flat. A replay comes before the instruments' definitions do, so a broker's fill can carry its contract multiplier (OrderFillEvent::contract_multiplier), which values it - and the instrument from then on - whatever the tracker knew before. - Limits. A slot keeps 32 open lots; a position built from more keeps the oldest 31 as they are and the rest as one lot at their average price. A file that is not a position store is refused at startup, never overwritten, and startup stops if the store cannot be opened.
- Starting flat. Stop the engine and delete the file.
The store holds what the framework booked. It does not know what a broker holds: an order filled while the engine was down reaches it only if the broker reports that fill after the restart.
The dashboard (web/static/) is a build-tool-free HTML/CSS/vanilla-JS app that connects over
WebSocket for live position/PnL/order updates and hits a REST API for everything else. PnL is
pushed when a subscribed instrument's top of book moves - StrategyManager publishes it on the
event bus as a TOB record - at most four times a second. Key endpoints (see
web_server.cpp):
GET /api/health,GET /api/brokers,GET /api/strategiesGET /api/positions[/:strategy_id],GET /api/pnl[/:strategy_id]GET /api/orders,POST /api/orders,PUT|DELETE /api/orders/:strategy_id/:order_id,DELETE /api/orders/cancel-allPOST /api/strategies/:strategy_id/{control,halt,resume}GET|PUT /api/strategies/:strategy_id/{risk,config}GET|PUT /api/ladder-config
It includes a price-ladder view (slick-ladder, an @slickquant npm package β see
package.json) for click-to-trade manual order entry; manual orders are attributed
to the built-in ManualStrategy. See web/static/CONTROLS_GUIDE.md
and web/static/FEATURES.md for UI details.
include/slick-trader/ Public headers (.hpp), organized by domain:
core/ TradingEngine, StrategyManager, PositionTracker, PositionStore,
PnLCalculator, RiskManager, BrokerRegistry, BrokerStatusRegistry,
StrategyLoader
brokers/ IBroker, the connector interfaces, and the built-in brokers' headers
(coinbase/, hyperliquid/)
strategy/ IStrategy, StrategyBase, ManualStrategy, plugin export macro
events/ Wire event types (trades, quotes, order book, market by order,
order lifecycle)
market_data/ Order book types, queue position
web/ WebServer
utils/ Fixed-point price/qty types, time helpers, fee structures, MappedFile
src/ Implementation (.cpp) mirroring include/ - each broker under
src/brokers/<name>/ - plus main.cpp
cmake/ Build helpers: slick_trader_add_broker() and the external-broker hook
strategies/ Strategy plugins, built as shared libraries (see its README)
tests/ GTest regression suite, plus a probe plugin for cross-module tests
web/static/ Dashboard frontend (HTML/CSS/JS)
config/ Runtime JSON configs (only config_example.json is checked in)
Strategies inherit from StrategyBase, which
provides order submission/cancellation/modification helpers with built-in pre-trade risk checks
and broker-connectivity checks, automatic FIFO position tracking on fills, and order-state
tracking (get_order, get_open_orders, close_all_positions, ...). The full plugin guide β
project setup, CMakeLists.txt wiring, config schema, loading, and troubleshooting β lives in
strategies/README.md. strategies/market_maker.cpp
is a production-style reference implementation (configurable fair value, inventory skew, trend
detection, book-imbalance adjustment, post-only quoting via a per-side state machine).
strategies/order_stacker.cpp is the reference for integrating a
third-party order-management library: it maintains a two-sided ladder of resting orders offset a
configurable number of ticks from the top of book, driving slick-stacker through an executor
adapter over StrategyBase.
| Library | Role |
|---|---|
| slick-logger | Logging |
| slick-orderbook | L2 order book maintenance |
| slick-stacker | Header-only resting-order ladder engine (used by the order_stacker strategy plugin) |
| slick-net | Networking (fetched transitively by the broker SDKs) |
slick-queue / stream_buffer_multiplexer |
Lock-free single-producer event queues, multiplexed for broadcast to strategies/web |
| coinbase-advanced-cpp | Coinbase Advanced Trade API client |
| hyperliquid-cpp | Hyperliquid API client |
| uWebSockets (via vcpkg) | REST + WebSocket server |
| nlohmann/json (via vcpkg) | JSON parsing/serialization |
- Headers use the
.hppextension throughout. - New non-trivial state is single-writer/lock-free by design rather than mutex-protected β see Architecture.
- Bug fixes should come with a regression test.
MIT β see LICENSE.