Skip to content

Commit 3cf2794

Browse files
committed
feat: elevated process priority, realtime thread, cpu isolation, and pinned cpus as config
1 parent 99e485e commit 3cf2794

23 files changed

Lines changed: 351 additions & 136 deletions

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
# LOGGING
12
LOG_LEVEL="info"
23
LOG_PATH="logs/log"
4+
# AUTHENTICATION
35
API_KEY=""
46
FIX_CONFIG_PATH="binance/fixconfig"
57
PRIVATE_KEY_PATH="key.pem"
8+
#
69
SYMBOLS=BTCUSDT,
10+
# CPU PINNING
11+
PX_SESSION_CPU=0
12+
TX_SESSION_CPU=1

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ build-release:
4949
cmake --preset=release
5050
cmake --build --preset=release
5151

52-
## test: 🧪 run google-test
52+
## test: 🧪 run unit tests
5353
.PHONY: test
5454
test:
5555
cmake --preset debug
@@ -74,6 +74,7 @@ run-debug:
7474
## run-release: 🏎️ run the app (prod)
7575
.PHONY: run-release
7676
run-release:
77+
$(call pp,starting app. dont forget to run `scripts/cpu_shield_start.sh`)
7778
build/Release/tradercpp
7879

7980
# CONTAINERISATION RECIPES ----------------------------------------------------

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A proof-of-concept, showcasing some modern c++ and some fintech concepts.
1212
- linux (tested with debian-trixie)
1313
- `stunnel` for TLS encryption (or a local proxy)
1414
- a Binance account, and an Ed25519 token with FIX read permissions enabled
15+
- `sudo` permissions (for elevated process and thread priorities)
1516

1617
## Run
1718
- start an SSL tunnel
@@ -24,7 +25,7 @@ A proof-of-concept, showcasing some modern c++ and some fintech concepts.
2425
- run:
2526
- download release
2627
- `chmod u+x tradercpp`
27-
- `./tradercpp`
28+
- `sudo ./tradercpp`
2829

2930
## Build Requirements
3031
- C++20
@@ -48,7 +49,7 @@ NB: this app uses `make` as a recipe book, but it's not essential:
4849
2. run an SSL tunnel (e.g. `stunnel binance/stunnel_prod.conf`)
4950
3. `make init`
5051
4. `make build-debug`
51-
5. `make withenv RECIPE=run-debug`
52+
5. `sudo make withenv RECIPE=run-debug`
5253

5354
## Test
5455
`make test`
@@ -149,9 +150,26 @@ NB: this app uses `make` as a recipe book, but it's not essential:
149150
- profiling (valgrind/cachegrind)
150151
- profile-guided optimization (pgo)
151152
- load test with mocked FIX server
152-
- set process priority
153-
- NIC affinity
154-
- QoS
153+
- CPU
154+
- ✅ process priority
155+
- ✅ FIX-thread "realtime"
156+
- ✅ FIX-thread CPU affinity
157+
- Disable hyperthreading
158+
- NIC
159+
- wired, kernel-bypass NICs
160+
- NIC IRQ affinity to same CPU as FIX
161+
- hardware queue affinity
162+
- QoS (mark packets)
163+
- AF_XDP (+ Zero-copy mode)
164+
- ~dedicated NIC + DPDK~
165+
- OS
166+
- vacate OS services / move IRQs for all system devices to the last CPU
167+
- RTOS
168+
- BIOS
169+
- disable hyperthreading, turbo boost
170+
- disable C-states deeper than C1 (C1E, C6, etc)
171+
- set cpu governor to "performance"
172+
- Memory locking
155173
- sparse arrays & flat matrix
156174
- memory-mapped files
157175
- (analyse) find Binance's server location for a low-latency connection
@@ -162,6 +180,7 @@ NB: this app uses `make` as a recipe book, but it's not essential:
162180
- RT OS
163181
- ✅ logging
164182
- ✅ fast
183+
- add console target for fatal messages
165184
- error handling
166185
- compiled out 'debug' logging for release builds
167186
- thread name in logs

scripts/cpu_shield_start.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
# ------------------------------------------------------------
3+
# move all OS threads off of trader CPUs
4+
# ------------------------------------------------------------
5+
set -e
6+
7+
sudo cset shield --cpu 0-1 --kthread=on
8+
9+
echo "Isolated CPUs 0 and 1 for the trader app, all OS threads will be moved"

scripts/cpu_shield_stop.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
# ------------------------------------------------------------
3+
# reset CPU isolation, allow OS threads to run on all CPUs
4+
# ------------------------------------------------------------
5+
set -e
6+
7+
sudo cset shield --reset
8+
9+
echo "Reset CPU isolation, OS threads can run on all CPUs"

src/binance/config.cpp

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <format>
44
#include <ranges>
5+
#include <stdexcept>
56
#include <string>
67
#include <vector>
78

@@ -11,25 +12,43 @@
1112
namespace binance {
1213
// TODO(mils): for keys, use `std::vector<unsigned char>` instead of string
1314

14-
/// @brief load Binance configuration from env
15-
/// (static member function)
16-
/// @return Config object
15+
// static function
1716
Config Config::from_env() {
18-
const std::string apiKey = utils::Env::get_env_or_throw("API_KEY");
19-
const std::string privateKey = utils::Env::get_env_or_throw("PRIVATE_KEY_PATH");
20-
const std::string fixConfig = utils::Env::get_env_or_throw("FIX_CONFIG_PATH");
21-
const std::string instStr = utils::Env::get_env_or_throw("SYMBOLS");
22-
23-
spdlog::info("fetched environment variable. key [API_KEY], value [{}]",
24-
apiKey.substr(0, 3) + "..." + apiKey.substr(apiKey.length() - 3));
25-
spdlog::info("fetched environment variable. key [PRIVATE_KEY_PATH], value [{}]",
26-
privateKey);
27-
spdlog::info("fetched environment variable. key [FIX_CONFIG_PATH], value [{}]",
28-
fixConfig);
29-
spdlog::info("fetched environment variable. key [SYMBOLS], value [{}]", instStr);
17+
const std::string api_key = utils::Env::get_env_or_throw("API_KEY");
18+
const std::string private_key = utils::Env::get_env_or_throw("PRIVATE_KEY_PATH");
19+
const std::string fix_config = utils::Env::get_env_or_throw("FIX_CONFIG_PATH");
20+
const std::string px_cpu_str = utils::Env::get_env_or_throw("PX_SESSION_CPU");
21+
const std::string tx_cpu_str = utils::Env::get_env_or_throw("TX_SESSION_CPU");
22+
const std::string inst_str = utils::Env::get_env_or_throw("SYMBOLS");
23+
24+
uint8_t px_cpu;
25+
std::errc px_ec =
26+
std::from_chars(px_cpu_str.data(), px_cpu_str.data() + px_cpu_str.size(), px_cpu)
27+
.ec;
28+
if (px_ec != std::errc()) {
29+
throw std::runtime_error(
30+
std::format("could not parse px cpu, value [{}]", px_cpu_str));
31+
}
32+
33+
uint8_t tx_cpu;
34+
std::errc tx_ec =
35+
std::from_chars(tx_cpu_str.data(), tx_cpu_str.data() + tx_cpu_str.size(), tx_cpu)
36+
.ec;
37+
if (tx_ec != std::errc()) {
38+
throw std::runtime_error(
39+
std::format("could not parse tx cpu, value [{}]", tx_cpu_str));
40+
}
41+
42+
spdlog::info("fetched envar. key [API_KEY], value [{}]",
43+
api_key.substr(0, 3) + "..." + api_key.substr(api_key.length() - 3));
44+
spdlog::info("fetched envar. key [PRIVATE_KEY_PATH], value [{}]", private_key);
45+
spdlog::info("fetched envar. key [FIX_CONFIG_PATH], value [{}]", fix_config);
46+
spdlog::info("fetched envar. key [SYMBOLS], value [{}]", inst_str);
47+
spdlog::info("fetched envar. key [PX_SESSION_CPU], value [{}]", px_cpu_str);
48+
spdlog::info("fetched envar. key [TX_SESSION_CPU], value [{}]", tx_cpu_str);
3049

3150
std::vector<std::string> symbols;
32-
for (auto inst : std::views::split(instStr, ',')) {
51+
for (auto inst : std::views::split(inst_str, ',')) {
3352
if (!inst.empty()) {
3453
symbols.emplace_back(inst.begin(), inst.end());
3554
}
@@ -38,7 +57,7 @@ Config Config::from_env() {
3857
spdlog::info("MAX_DEPTH, value [{}]", MAX_DEPTH);
3958

4059
// copy
41-
return Config{apiKey, privateKey, fixConfig, symbols};
60+
return Config{api_key, private_key, fix_config, symbols, px_cpu, tx_cpu};
4261
};
4362

4463
} // namespace binance

src/binance/config.h

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <charconv>
34
#include <cmath>
45
#include <cstdint>
56
#include <format>
@@ -16,11 +17,27 @@ namespace binance {
1617
/// @brief Binance config parameters, fetched from env
1718
struct Config {
1819
public:
19-
// TODO: MAKE UNIQUE POINTERS
2020
std::string api_key, private_key_path;
2121
const std::string fix_config_path;
2222
const std::vector<std::string> symbols;
23-
//
23+
const uint8_t px_cpu;
24+
const uint8_t tx_cpu;
25+
26+
// Constructor that initializes all const members
27+
Config(std::string api,
28+
std::string private_key,
29+
std::string fix_config,
30+
std::vector<std::string> syms,
31+
uint8_t px,
32+
uint8_t tx)
33+
: api_key(std::move(api)),
34+
private_key_path(std::move(private_key)),
35+
fix_config_path(std::move(fix_config)),
36+
symbols(std::move(syms)),
37+
px_cpu(px),
38+
tx_cpu(tx) {}
39+
40+
/// @brief load Binance configuration parameters from environment variables
2441
static Config from_env();
2542

2643
/// @brief convert a market price to a tick representation, for performance and
@@ -58,9 +75,6 @@ struct Config {
5875

5976
/// @brief 1 == top level, otherwise 5000 is Binance's maximum depth
6077
static constexpr uint16_t MAX_DEPTH = 100;
61-
62-
static constexpr uint8_t PX_SESSION_CPU_AFFINITY = 0;
63-
static constexpr uint8_t TX_SESSION_CPU_AFFINITY = 1;
6478
};
6579

6680
} // namespace binance

src/binance/fix_app.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ namespace binance {
2727
FixApp::FixApp(const std::vector<std::string>& symbols,
2828
std::unique_ptr<IAuth> auth,
2929
const uint16_t MAX_DEPTH,
30-
const uint8_t PX_SESSION_CPU_AFFINITY,
31-
const uint8_t TX_SESSION_CPU_AFFINITY)
30+
const uint8_t px_cpu,
31+
const uint8_t tx_cpu)
3232
: symbols_(symbols),
3333
auth_(std::move(auth)),
3434
MAX_DEPTH_(MAX_DEPTH),
35-
PX_SESSION_CPU_AFFINITY_(PX_SESSION_CPU_AFFINITY),
36-
TX_SESSION_CPU_AFFINITY_(TX_SESSION_CPU_AFFINITY) {}
35+
px_cpu_(px_cpu),
36+
tx_cpu_(tx_cpu) {}
3737

3838
void FixApp::subscribe_to_prices(const FIX::SessionID& session_id) const {
3939
spdlog::info("subscribing to depth. qualifier [{}], id [{}]",
@@ -135,10 +135,11 @@ void FixApp::onLogon(const FIX::SessionID& sessionId) {
135135
utils::Threading::get_os_thread_id());
136136

137137
if (sessionId.getSessionQualifier() == PX_SESSION_QUALIFIER_) {
138-
utils::Threading::set_current_thread_affinity(PX_SESSION_CPU_AFFINITY_);
138+
utils::Threading::set_thread_cpu(px_cpu_);
139+
utils::Threading::set_thread_realtime();
139140
subscribe_to_prices(sessionId);
140141
} else if (sessionId.getSessionQualifier() == TX_SESSION_QUALIFIER_) {
141-
utils::Threading::set_current_thread_affinity(TX_SESSION_CPU_AFFINITY_);
142+
utils::Threading::set_thread_cpu(tx_cpu_);
142143
subscribe_to_trades(sessionId);
143144
} else if (sessionId.getSessionQualifier() == OX_SESSION_QUALIFIER_) {
144145
// do nothing for order session

src/binance/fix_app.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class FixApp final : public FIX::Application, public FIX44::MessageCracker {
2121
FixApp(const std::vector<std::string>& symbols,
2222
std::unique_ptr<IAuth> auth,
2323
const uint16_t MAX_DEPTH,
24-
const uint8_t PX_SESSION_CPU_AFFINITY,
25-
const uint8_t TX_SESSION_CPU_AFFINITY);
24+
const uint8_t px_cpu,
25+
const uint8_t tx_cpu);
2626
~FixApp() override = default;
2727

2828
// Use the FIX44::MessageCracker to pull in the relevant overloads. This resolves the
@@ -51,8 +51,8 @@ class FixApp final : public FIX::Application, public FIX44::MessageCracker {
5151
const std::vector<std::string>& symbols_;
5252
const std::unique_ptr<IAuth> auth_;
5353
const uint16_t MAX_DEPTH_;
54-
const uint8_t PX_SESSION_CPU_AFFINITY_;
55-
const uint8_t TX_SESSION_CPU_AFFINITY_;
54+
const uint8_t px_cpu_;
55+
const uint8_t tx_cpu_;
5656

5757
void onCreate(const FIX::SessionID&) override;
5858
void onLogon(const FIX::SessionID&) override;

src/binance/worker.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ Worker Worker::from_conf(Config& conf) {
3232
std::unique_ptr<IAuth> auth =
3333
std::make_unique<Auth>(conf.api_key, conf.private_key_path);
3434
auto app = std::make_unique<FixApp>(conf.symbols, std::move(auth), conf.MAX_DEPTH,
35-
conf.PX_SESSION_CPU_AFFINITY,
36-
conf.TX_SESSION_CPU_AFFINITY);
35+
conf.px_cpu, conf.tx_cpu);
3736
auto settings = FIX::SessionSettings{conf.fix_config_path};
3837
auto store = std::make_unique<FIX::FileStoreFactory>(settings);
3938
auto log = std::make_unique<FIX::FileLogFactory>(settings);

0 commit comments

Comments
 (0)