C++ client library for Binance USD-M Futures API. Built on C++23, Boost.Beast/ASIO/Cobalt, OpenSSL, and Glaze JSON.
@startuml
skinparam componentStyle rectangle
skinparam packageStyle frame
package "binapi2::fapi" {
[client] as CLI
note right of CLI : Pure container.\nNo threads, no io_context,\nno executor ownership.
package "REST" {
[rest::pipeline] as PIP
[market_data_service] as MDS
[account_service] as ACS
[trade_service] as TRS
[convert_service] as CVS
[user_data_stream_service] as UDS
}
package "WebSocket API" {
[websocket_api::client] as WSA
}
package "Streams" {
[market_streams] as MS
[user_streams] as US
[local_order_book] as LOB
}
package "Transport" {
[http_client] as HTTP
[websocket_client] as WS
}
package "Pure Functions" {
[signing] as SIGN
[query (to_query_map)] as QRY
[json_opts / decode] as DEC
[endpoint_traits] as ET
}
package "Types" {
[request_tags] as RTAG
[subscriptions] as SUB
[result<T> / error] as RES
[types] as TYP
}
}
package "User-provided Execution" {
[cobalt::main] as CMAIN
[io_thread] as IOT
[thread_pool / io_context] as POOL
}
cloud "Binance" {
[REST API\nfapi.binance.com] as BREST
[WebSocket Streams\nfstream.binance.com] as BSTREAM
[WebSocket API\nws-fapi.binance.com] as BWSAPI
}
MDS --> PIP
ACS --> PIP
TRS --> PIP
CVS --> PIP
UDS --> PIP
CLI --> MDS
CLI --> ACS
CLI --> TRS
CLI --> CVS
CLI --> UDS
PIP --> HTTP
PIP --> SIGN
PIP --> QRY
PIP --> DEC
PIP ..> ET
WSA --> WS
WSA --> SIGN
MS --> WS
US --> WS
LOB --> MS
LOB --> PIP
HTTP --> BREST
WS --> BSTREAM
WS --> BWSAPI
CMAIN ..> CLI : drives coroutines
IOT ..> CLI : run_sync bridge
POOL ..> CLI : spawn + future
@enduml| Layer | Components | Owns executor? |
|---|---|---|
| 1. Pure functions | signing.hpp, query.hpp, json_opts.hpp, detail/decode.hpp |
No |
| 2. Async I/O | transport::http_client, transport::websocket_client |
No -- uses this_coro::executor |
| 3. Protocol | rest::pipeline, websocket_api::client, streams::market_streams, streams::user_streams |
No |
| 4. Execution | detail::io_thread, cobalt::main, thread_pool, manual io_context |
Yes -- user-provided |
| 5. Facade | client |
No -- pure container |
All I/O is built on Boost.Cobalt C++20 coroutines. Every method returns
cobalt::task<result<T>>. There are no sync wrappers anywhere in the library.
@startuml
skinparam classAttributeIconSize 0
package "Transport (async only)" {
class http_client {
+async_request(...) : cobalt::task<result<http_response>>
.. config-only constructor ..
.. no sync wrappers ..
}
class websocket_client {
+async_connect(...) : cobalt::task<result<void>>
+async_write_text(...) : cobalt::task<result<void>>
+async_read_text() : cobalt::task<result<string>>
+async_close() : cobalt::task<result<void>>
.. config-only constructor ..
.. no sync wrappers ..
}
}
package "Pipeline" {
class "rest::pipeline" as pip {
+async_execute<Request>(request) : cobalt::task<result<response_type>>
+async_execute<Response>(verb, path, query, signed) : cobalt::task<result<Response>>
}
}
package "Services" {
class "rest::service" as svc {
#pipeline_ : pipeline&
}
class "account_service" as acs {
+async_execute<Request>(request) : cobalt::task<result<response_type>>
.. requires is_account_request<Request> ..
}
}
acs --|> svc
svc --> pip : delegates
pip --> http_client : async_request
@endumlTransport: http_client::async_request() uses co_await at each I/O step
(resolve, connect, TLS handshake, write, read) -- non-blocking, yields to the event
loop between steps.
No callbacks: the old callback_type<T> = std::function<void(result<T>)> pattern
is removed. Async methods return cobalt::task<result<T>> which is awaitable.
No sync wrappers: there are no execute() methods that wrap async. Bridging to
sync is done by the user via io_thread::run_sync() or cobalt::spawn + use_future.
Usage:
// Async (in cobalt::main):
boost::cobalt::main co_main(int, char*[]) {
fapi::client c(cfg);
auto result = co_await c.market_data.async_execute(exchange_info_request{});
co_return 0;
}
// Sync bridge (via io_thread):
fapi::detail::io_thread io;
fapi::client c(cfg);
auto result = io.run_sync(c.market_data.async_execute(exchange_info_request{}));The core design pattern: request types carry all the information needed to dispatch an API call.
@startuml
skinparam classAttributeIconSize 0
class "rest::pipeline" as PIP {
-cfg_ : config&
-http_ : http_client&
--
+async_execute<Request>(request) : task<result<response_type>>
+async_execute<Response>(verb, path, query, signed) : task<result<Response>>
}
class "rest::endpoint_traits<Request>" as ET <<specialization>> {
+{static} response_type_t : type alias
+{static} endpoint : endpoint_metadata&
}
class "endpoint_metadata" as EM {
+name : string_view
+method : http::verb
+path : string_view
+security : security_type
+signed_request : bool
}
class "to_query_map<T>" as QM <<function>> {
Reflects struct fields via glz::reflect<T>
Skips nullopt optionals
Stringifies enums via to_string()
Returns query_map
}
class "account_service" as ACS {
+async_execute<Request>(request) : task<result<response_type>>
.. requires is_account_request<Request> ..
--
using account_information_request = ...
using balances_request = ...
... (type aliases)
}
class "rest::service" as SVC {
#pipeline_ : pipeline&
}
ACS --|> SVC
SVC --> PIP : delegates
PIP ..> ET : resolves endpoint
PIP ..> QM : serializes request
ET --> EM
@endumlHow it works:
service::async_execute(request)is constrained by a per-service concept (e.g.,is_account_request<Request>) that checks the request type's tag- Delegates to
pipeline::async_execute(request)which looks upendpoint_traits<Request>at compile time to_query_map(request)usesglz::reflect<T>to serialize the request struct fields into aquery_map- Pipeline handles signing, query string encoding, HTTP transport, and JSON response deserialization
Service concepts and tags: Each request type carries a tag from request_tags.hpp
(e.g., rest_account_tag, rest_market_data_tag). Service tags live in
endpoint_traits -- not in request structs -- because glaze reflection breaks with
struct inheritance/using declarations.
@startuml
skinparam classAttributeIconSize 0
class "websocket_api::client" as WSC {
+async_connect() : task<result<void>>
+async_close() : task<result<void>>
+async_execute<Request>(request) : task<result<ws_response<response_type>>>
+async_session_logon() : task<result<ws_response<logon_result>>>
}
class "ws::endpoint_traits<Request>" as WET <<specialization>> {
+{static} response_type_t : type alias
+{static} method : string_view (RPC method name)
+{static} auth : ws_auth_mode (optional)
}
enum ws_auth_mode {
inject
none
signed_base
api_key_only
}
note right of WSC
Auth mode dispatch via if constexpr:
- inject: inject_auth(request) for signed requests with user params
- none: send as-is (unsigned market data)
- signed_base: make_signed_request_base() for parameterless signed
- api_key_only: {apiKey: ...} for user data stream management
end note
WSC ..> WET : resolves method + response + auth
@endumlasync_execute is the single generic entry point. Auth mode is resolved at compile time
from ws::endpoint_traits. session_logon is the only named method (custom auth flow
that cannot use the generic dispatch).
@startuml
actor "User\n(coroutine)" as User
participant "service" as SVC
participant "pipeline" as PIP
participant "endpoint_traits" as ET
participant "to_query_map" as QM
participant "signing" as S
participant "http_client" as H
participant "Binance REST" as B
User -> SVC: co_await async_execute(kline_request{symbol, interval, ...})
SVC -> PIP: co_await async_execute(request)
PIP -> ET: endpoint_traits<kline_request>::endpoint
ET --> PIP: {GET, "/fapi/v1/klines", unsigned}
PIP -> QM: to_query_map(request)
QM --> PIP: query_map{"symbol":"BTCUSDT","interval":"1h",...}
PIP -> S: inject_auth_query + sign_query (if signed)
PIP -> H: co_await async_request(method, target, body, content_type, api_key)
H -> H: co_await async_resolve()
note right: yields to event loop
H -> H: co_await async_connect()
H -> H: co_await async_handshake()
H -> H: co_await async_write()
H -> H: co_await async_read()
H --> PIP: result<http_response>
PIP -> PIP: decode_response<vector<kline>>
PIP --> User: result<vector<kline>>
@endumlStreams use cobalt::generator for typed async iteration:
@startuml
actor User
participant "market_streams" as MS
participant "stream_traits" as ST
participant "websocket_client" as WS
participant "Binance WSS" as B
User -> MS: subscribe(book_ticker_subscription{.symbol = "BTCUSDT"})
MS -> ST: stream_traits<Subscription>::target(cfg, sub)
ST --> MS: "/ws/btcusdt@bookTicker"
MS -> WS: co_await async_connect(host, target)
WS -> B: TLS + WSS handshake
B --> WS: connected
MS --> User: generator<result<book_ticker_stream_event_t>>
loop co_await generator
B --> WS: JSON frame
WS --> MS: raw text
MS -> MS: glz::read<Event>(payload)
MS --> User: co_yield result<Event>::success(event)
end
@endumlstream_traits<Subscription> maps subscription types to target URLs and event types
at compile time. The generator yields result<Event> until an error or disconnect.
User streams return a cobalt::generator<result<user_stream_event_t>> where
user_stream_event_t is a std::variant of 10 event types:
auto stream = c.user_streams().subscribe(listen_key);
while (stream) {
auto event = co_await stream;
if (!event) break;
std::visit(overloaded{
[](const order_trade_update_event_t& e) { /* ... */ },
[](const account_update_event_t& e) { /* ... */ },
[](const auto&) {}
}, *event);
}@startuml
participant "local_order_book" as LOB
participant "market_streams" as MS
participant "rest::pipeline" as REST
participant "Binance" as B
LOB -> MS: subscribe(depth_subscription{symbol})
MS -> B: WSS subscribe depth stream
LOB -> LOB: buffer incoming depth events
LOB -> REST: co_await async_execute(order_book_request{symbol, limit})
REST -> B: GET /fapi/v1/depth
B --> REST: snapshot (lastUpdateId = S)
REST --> LOB: order_book_response
LOB -> LOB: discard buffered events where u <= S
LOB -> LOB: apply remaining buffered events
LOB -> LOB: synced = true
loop continuous (async_run coroutine)
B --> MS: depth diff event
MS --> LOB: apply_event(event)
LOB -> LOB: update bids/asks maps
LOB -> LOB: notify snapshot_callback
end
@endumllocal_order_book::async_run(symbol, depth_limit) is a coroutine that runs the
entire sync algorithm. It takes references to market_streams and rest::pipeline.
Request types with a 1:1 endpoint mapping have endpoint_traits (REST) or
ws::endpoint_traits (WebSocket API) specializations. These are dispatched generically
via async_execute(request).
Shared request types (used by multiple endpoints) retain named service methods:
| Shared Type | Endpoints | Service |
|---|---|---|
kline_request |
klines, mark_price_klines, premium_index_klines | market_data |
futures_data_request |
open_interest_statistics, top_long_short_account_ratio, top_trader_long_short_ratio, long_short_ratio, taker_buy_sell_volume | market_data |
download_id_request |
download_id_transaction, download_id_order, download_id_trade | account |
download_link_request |
download_link_transaction, download_link_order, download_link_trade | account |
batch_orders_request |
batch_orders, modify_batch_orders | trade |
to_query_map<T>(request) uses compile-time reflection via glz::reflect<T> to
automatically build URL query parameters from request struct fields:
std::string-> passed as-isstd::uint64_t,int->std::to_string()bool->"true"/"false"- fapi enums ->
types::to_string()(e.g.,order_side::buy->"BUY") std::optional<T>where value is nullopt -> skipped entirely- Works with both
glz::meta-annotated and plainreflectableaggregates
All methods are async-only (cobalt::task<result<T>>). Sync access is achieved via
user-provided bridging (e.g., io_thread::run_sync()).
| Access Mode | Transport | Authentication | Latency | Use Case |
|---|---|---|---|---|
| REST Request | HTTPS | API key header + signed query (Ed25519 or HMAC) | Medium | Account queries, order placement, market data snapshots |
| WebSocket Stream | WSS | None (market) / Listen key (user) | Low | Real-time market data, account events |
| WebSocket API | WSS | Session logon (Ed25519) or per-request sig (4 auth modes) | Lowest | Low-latency trading without HTTP overhead |
| Local Order Book | WSS + REST | None | Low | Synchronized local depth book |
Services inherit from rest::service which holds a pipeline& reference. Each
derived service provides a constrained async_execute that only accepts request types
tagged for that service (via concepts like is_account_request, is_market_data_request).
Public endpoints. No authentication required.
Generic (via async_execute): ping_request, server_time_request,
exchange_info_request, order_book_request, recent_trades_request,
aggregate_trades_request, continuous_kline_request, index_price_kline_request,
book_ticker_request, price_ticker_request, ticker_24hr_request,
mark_price_request, funding_rate_history_request, open_interest_request,
historical_trades_request, basis_request, price_ticker_v2_request,
delivery_price_request, composite_index_info_request, index_constituents_request,
asset_index_request, insurance_fund_request, adl_risk_request,
rpi_depth_request, trading_schedule_request
Parameterless request types: balances_request_t, klines_request_t, etc.
Named methods for shared request types: klines, mark_price_klines,
premium_index_klines, open_interest_statistics, etc.
Signed endpoints.
Generic: account_information_request, balances_request, account_config_request,
position_risk_request, symbol_config_request, income_history_request,
leverage_bracket_request, commission_rate_request, get_multi_assets_mode_request,
get_position_mode_request, rate_limit_order_request, get_bnb_burn_request,
toggle_bnb_burn_request, quantitative_rules_request, pm_account_info_request,
download_id_*_request, download_link_*_request
Signed endpoints.
Generic: new_order_request, modify_order_request, cancel_order_request,
query_order_request, cancel_all_open_orders_request, auto_cancel_request,
query_open_order_request, all_open_orders_request, all_orders_request,
position_info_v3_request, adl_quantile_request, force_orders_request,
account_trade_request, change_position_mode_request,
change_multi_assets_mode_request, change_leverage_request,
change_margin_type_request, modify_isolated_margin_request,
position_margin_history_request, order_modify_history_request,
new_algo_order_request, cancel_algo_order_request, query_algo_order_request,
all_algo_orders_request
Fully generic: quote_request, accept_request, order_status_request
REST management of listen keys: async_start, async_keepalive, async_close
Generic (via async_execute): order_place_request, order_query_request,
order_cancel_request, order_modify_request, position_request,
book_ticker_request, price_ticker_request, algo_order_place_request,
algo_order_cancel_request, account_status_request, account_status_v2_request,
account_balance_request, user_data_stream_start_request,
user_data_stream_ping_request, user_data_stream_stop_request
Named methods: async_session_logon (custom auth flow), async_connect, async_close
Real-time WebSocket market data via generator pattern:
subscribe(subscription)->cobalt::generator<result<Event>>async_connect(subscription)+async_read_event<Event>()for typed connect/readasync_connect(target)+async_read_text()for low-level access
Real-time account events:
subscribe(listen_key)->cobalt::generator<result<user_stream_event_t>>async_connect(listen_key)+async_read_text()for low-level access
Async locally maintained order book:
async_run(symbol, depth_limit)->cobalt::task<result<void>>- Takes
market_streams&andrest::pipeline&references - Thread-safe snapshot via
snapshot()method
@startuml
enum sign_method_t {
ed25519
hmac
}
class config {
+rest_host : string = "fapi.binance.com"
+rest_port : string = "443"
+rest_base_path : string = ""
+websocket_api_host : string = "ws-fapi.binance.com"
+websocket_api_port : string = "443"
+websocket_api_target : string = "/ws-fapi/v1"
+stream_host : string = "fstream.binance.com"
+stream_port : string = "443"
+stream_base_target : string = "/ws"
+api_key : string
+secret_key : string
+ed25519_private_key_pem : string
+sign_method : sign_method_t = ed25519
+recv_window : uint64 = 5000
+user_agent : string = "binapi2-fapi/0.1.0"
+testnet : bool = false
+logger : transport_logger
--
+{static} testnet_config() : config
}
config --> sign_method_t
@enduml| Method | Config | Credentials | Binance status |
|---|---|---|---|
| Ed25519 (default) | sign_method_t::ed25519 |
api_key + ed25519_private_key_pem |
Recommended, required for WS API session.logon |
| HMAC-SHA256 | sign_method_t::hmac |
api_key + secret_key |
Deprecated |
The default is Ed25519. For WS API session logon (session.logon), Ed25519 is
mandatory — HMAC is only accepted for REST signing.
@startuml
enum error_code {
none
invalid_argument
transport
http_status
json
binance
websocket
internal
}
class error {
+code : error_code
+http_status : int
+binance_code : int
+message : string
+payload : string
}
class "result<T>" {
+operator bool() : bool
+operator*() : T&
+err : error
--
+{static} success(T) : result<T>
+{static} failure(error) : result<T>
}
"result<T>" --> error
error --> error_code
@enduml| Dependency | Purpose | Type |
|---|---|---|
| Boost.ASIO | Async I/O, event loop | Required |
| Boost.Beast | HTTP/WebSocket protocol | Required |
| Boost.Cobalt | C++20 coroutines (async transport) | Required |
| OpenSSL | TLS (HTTPS/WSS) + HMAC-SHA256 + Ed25519 signing | Required |
| ZLIB | Response compression | Required |
| spdlog / fmt | Logging, formatting | Required |
| Glaze | Compile-time JSON serialization + struct reflection | Submodule (header-only) |
| Google Test | Unit tests | Test-only |
Build: CMake 3.20+, C++23 compiler (GCC 13+ / Clang 17+), Boost 1.84+.