What happens when a C++20 trading codebase enforces its own boundaries? Kestrel turns every directory into a static library, makes dependency cycles impossible by design, and bans comments with a script.
Intro
A Comment Breaks the Build
Imagine adding one helpful comment to a pull request, and watching the checks turn red. In Kestrel, that is not a joke. It is a rule, enforced by a script. Today we read the build system, and the house rules that keep a large C++ codebase honest.
- One comment line
- The check fails
Where We Left Off
In episode one we saw the big picture: one engine that runs backtests, paper trading and live trading with the same code. This episode asks a plainer question. How is that code organised so that it stays understandable as it grows? The answer lives in a handful of CMake files and a short list of rules.
- Episode 1: one engine, three modes
- Episode 2: how it is built
The Problem and the Plan
The problem is simple to state. A large codebase needs boundaries that the compiler enforces, not boundaries that live in a wiki. And there should be zero ambiguity about what compiles where. We will walk through the top level CMake, the module helper, the dependency graph, the house rules, explicit registration, and the sanitizers.
- Enforced boundaries
- Zero ambiguity about what compiles where
Top-Level CMake
The Top CMakeLists
The top level file declares project Kestrel with the language set to C plus plus only. It requires the C plus plus 20 standard. If you do not pick a build type, it defaults to RelWithDebInfo, which means optimised code that still carries debug symbols. That choice suits a system where you want realistic speed and usable stack traces at the same time.
- project(Kestrel LANGUAGES CXX)
- C++20
- Default: RelWithDebInfo
Three Options
Three options shape a build. KESTREL_BUILD_TESTS is on by default. KESTREL_WERROR is off by default, so a new compiler warning does not stop a developer who is experimenting. KESTREL_SANITIZE takes either address comma undefined, or thread, and switches on the matching sanitizers. We will come back to why one of those is trusted and the other is not.
- KESTREL_BUILD_TESTS: ON
- KESTREL_WERROR: OFF
- KESTREL_SANITIZE: address,undefined or thread
Why Werror Is Opt-In
Why make Werror opt in? Because the same source tree is built by different people on different compilers. Turning warnings into errors by default makes every compiler upgrade a surprise outage. Instead, the strict mode is something you request, for example in a gate, while everyday builds still print every warning loudly.
- Off for daily work
- On for gates
Dependencies
Two System Libraries
Kestrel has very few external libraries. OpenSSL three, specifically libcrypto, provides SHA-256. And zlib reads and writes the gzip compressed bar files in the history store. Both are expected to come from the system under slash usr.
- OpenSSL 3 libcrypto: SHA-256
- zlib: gzip history files
- Both from /usr
Configure Fails on Conda
Here is an unusual rule. If either library resolves to a conda path, configure fails on purpose. Picture a machine where a data science environment is active. The build quietly links one version of zlib, and the binary later runs against another. That mismatch produces crashes that are miserable to debug, so the build refuses to start instead.
- Resolved path inside conda?
- Configure stops
libcurl Is Never Linked
Notice what is missing. libcurl is never linked. It is loaded with dlopen at runtime, and only for calls to the FYERS broker. That keeps research binaries, which run fully offline, free of any networking library dependency. Episode nineteen shows how that loader works.
- dlopen at runtime
- Only for FYERS calls
- Episode 19
Third-Party Headers
Two header libraries complete the picture: nlohmann slash json for configuration and wire messages, and doctest for tests, which we leave to episode sixteen. A target called kestrel_third_party adds them as SYSTEM include directories. The compiler treats SYSTEM headers as foreign, so their warnings are ignored, and our warning flags stay meaningful for our own code.
- nlohmann/json
- doctest
- Included as SYSTEM
The kestrel_module Helper
From Directory to Library
Now the heart of the build. Every directory under src becomes one static library, named kestrel underscore and the directory name. So src slash pricing becomes kestrel_pricing, and src slash oms becomes kestrel_oms. Twenty-one directories, twenty-one libraries, one helper function creating them all.
- src/pricing -> kestrel_pricing
- src/oms -> kestrel_oms
The Helper Function
Here is the helper, exactly as it appears. It parses two lists of arguments, public dependencies and private dependencies. Then it globs the cpp files, creates the static library, sets the include path, and links. Let us take the interesting lines one at a time.
- Verbatim from CMake
- Six lines of body
CONFIGURE_DEPENDS
Globbing is usually frowned upon, because CMake only runs it at configure time. The word CONFIGURE_DEPENDS fixes the main complaint. On each build, CMake re-checks the glob, and if a new cpp file appeared, it reconfigures. So you add a file to src slash pricing, build, and it is simply part of the library. No list to edit, no forgotten entry.
- Glob re-checked each build
- New files picked up
One Include Root
Line five sets a single include root, the src directory, and marks it public. Every module therefore includes headers the same way, starting with the module name, such as pricing slash something. There are no relative dot dot paths and no per-library include folders. When you read an include line, you immediately know which module it comes from.
- PUBLIC include: src/
- #include "pricing/..." everywhere
Warnings Stay Private
Line six links in two groups. Public dependencies travel along to anyone who links this library. But kestrel_warnings is private, so the warning flags apply when compiling this module and are not forced on its dependents. That matters because flags that leak can make a downstream target fail for reasons it cannot control. Private dependencies are an implementation detail.
- kestrel_warnings is PRIVATE
- Not leaked to dependents
The Warning Flags
What is inside kestrel_warnings? It is an INTERFACE library carrying six flags: Wall, Wextra, Wpedantic, Wshadow, Wnon-virtual-dtor, and Woverloaded-virtual. The last three target classic C plus plus traps. Shadowed variables, base classes without a virtual destructor, and overloads that accidentally hide a virtual function. And if you ask for it, Werror is added on top.
- -Wall -Wextra -Wpedantic
- -Wshadow
- -Wnon-virtual-dtor
- -Woverloaded-virtual
The Module Graph
Dependencies Are Edges
Each module declares its dependencies when it calls the helper, so the library graph is written down in plain sight. There is no other way for one module to see another module's code, because without the link line, the include root alone would not give you the symbols. Let us build the graph from the bottom up.
- PUBLIC_DEPS and PRIVATE_DEPS
- Each arrow is a declared edge
The Foundation Layer
At the bottom sits common, which depends on nothing. Seven modules sit directly on it: config, data, net, pricing, trading, wire and archive. Each of those pulls in common and nothing else. That gives us a wide, shallow base of small modules.
- common at the bottom
- config, data, net, pricing, trading, wire, archive -> common
The Middle Layers
Above that, store depends on data, and history depends on store, so downloading bars sits on top of reading bars. The sim module, which holds the simulated venue, depends on trading and pricing. Alpha depends on pricing and sim. And oms, the order management layer, depends on trading and pricing.
- store -> data; history -> store
- sim -> trading, pricing
- alpha -> pricing, sim
- oms -> trading, pricing
The Top Layer
Then the upper floors. Gateway depends on oms and wire. Engine pulls together alpha, archive, data, gateway, sim and store. Portal sits on engine, history, store and net. And runhost depends on engine, config and control. Notice how the engine reaches the order machinery only through gateway.
- gateway -> oms, wire
- engine -> alpha, archive, data, gateway, sim, store
- portal -> engine, history, store, net
- runhost -> engine, config, control
A Forbidden Arrow
Now try to break it. Suppose someone wants the strategy runner to call the broker directly. They draw an arrow from trader to broker. By design, kestrel_trader contains no broker code, which is how strategy logic stays separate from anything that can send a real order. The arrow is not allowed to exist, and on screen we mark it with a red X.
- Hypothetical: trader -> broker
- Red X
Cycles Are Impossible
What about cycles, where module A needs B and B needs A? With this layering they cannot arise. Each module may only depend on modules below it, so the graph reads from the bottom up like a build order. If you feel the urge to add an upward arrow, that is a signal that some shared piece belongs in a lower module.
- Static libraries link in order
- Cycle = a design error
House Rules: Zero Comments
The Zero-Comment Rule
Now the rule everyone asks about. Every authored file in Kestrel has zero comments. That covers C plus plus, CMake, JavaScript, shell scripts and JSON. A comment can drift out of date silently, but a name that is wrong breaks something visible.
- Every authored file
- C++, CMake, JS, shell, JSON
The Checker Script
The rule is enforced by tools slash check_no_comments dot sh. It greps for slash slash and slash star in C, JavaScript, CSS and JSON files. It greps for the HTML comment opener in HTML files. And in CMake and shell files it looks for lines that start with a hash. Any hit fails the check.
- tools/check_no_comments.sh
- C/JS/CSS/JSON: // and /*
- HTML: <!--
- CMake/shell: # lines
The URL Problem
This creates a funny consequence. The literal text https colon slash slash contains a double slash, so the grep would flag any string that holds it. The string can never appear in the C plus plus source. A grep does not understand quotes, and the rule accepts that simplicity.
- https:// contains //
- Literal can never appear in C++
The Workaround
The workaround is to build the scheme from two pieces. Write the string https, then add the three characters colon slash slash. Neither piece contains a double slash on its own, so the checker is satisfied, and the resulting string is identical at runtime. It looks slightly odd, but now you know why.
- std::string("https") + "://"
- Built from two pieces
Toolchain and Conventions
The g++ 11 Limits
The project builds with g plus plus eleven, and that sets a ceiling on the library. There is no std::format, no expected header, no std::jthread, no atomic of shared pointer, and no string contains. So you will see snprintf style formatting, hand-rolled result types, plain threads with explicit joins, and find compared against npos. These are not style choices. They are what the compiler offers.
- No std::format
- No <expected>
- No std::jthread
- No atomic<shared_ptr>
- No string::contains
Small Conventions
A few conventions complete the picture. Every header uses pragma once rather than include guards. Prices, quantities and money are all plain doubles. And internal time is an int64 count of nanoseconds since the Unix epoch. We unpack those vocabulary types in the next episode, so for now just notice that they are decided once and applied everywhere.
- #pragma once everywhere
- Prices, quantities, money: double
- Time: int64 nanoseconds
Explicit Registration
Self-Registration Fails
Many C plus plus projects register plug-ins with a static object whose constructor runs at startup. That works until the code lives in a static library. The linker only pulls in object files that something refers to, and a registration object that nobody refers to is dropped. The constructor never runs, and the plug-in silently vanishes.
- Static initialisers register types
- Linker drops unused objects
Three Explicit Calls
Kestrel avoids the trap by making registration explicit. Every application calls three functions at startup: registerStoreDataSource, registerBuiltinAlphas, and registerBuiltinVenues. Because main calls them directly, the linker must keep the code behind them. The cost is three visible lines per program. The benefit is that nothing depends on link order luck.
- registerStoreDataSource
- registerBuiltinAlphas
- registerBuiltinVenues
Pitfall: Forgetting a Call
The common pitfall is forgetting one of these calls in a new application. The program builds fine, then at runtime a config names a type, and the factory reports unknown type. The error message lists every known type, so you can see at a glance what is missing from the registry. Check the startup calls first.
- Runtime error: unknown type
- Message lists every known type
Sanitizers
ASan and UBSan Are the Gate
On to sanitizers. Setting KESTREL_SANITIZE to address comma undefined enables AddressSanitizer and UndefinedBehaviorSanitizer, and that combination is the gate. AddressSanitizer catches out of bounds access and use after free. UndefinedBehaviorSanitizer catches things like signed overflow. For a system that handles money and orders, these are cheap insurance.
- KESTREL_SANITIZE=address,undefined
- This is the gate
Why Not TSan
What about ThreadSanitizer? The option exists, but GCC eleven produces false positives around condition_variable wait_for, which Kestrel uses for timed waits. A tool that cries wolf trains people to ignore it. So thread sanitizer output is not relied on as a gate, and the team is honest about that limit.
- GCC 11 TSan false positives
- condition_variable::wait_for
- Not relied on
Recap
Recap: What the Build Enforces
Let us recap the build. Each source directory becomes a static library through one helper. Dependencies are declared, layered and acyclic, with a single include root. Warnings stay private, and Werror is opt in. System libraries come from slash usr, and libcurl is loaded only at runtime.
- One directory, one static library
- Acyclic graph, one include root
- Private warnings, opt-in Werror
Recap: The House Rules
And the house rules. Zero comments, enforced by a script, which is why URLs are built from two strings. The g plus plus eleven limits decide which library features exist. Registration is explicit with three startup calls. And address plus undefined sanitizers are the gate. Educational content only, not financial advice.
- Zero comments, script enforced
- g++ 11 limits, pragma once
- Explicit registration, ASan/UBSan gate