C++ for Quants
  • Home
  • Contact
  • About
Author

cppforquants

cppforquant.com
cppforquants

september-jobs
Jobs

September 2026 – C++ Quant Jobs Selection

by cppforquants September 25, 2026

A strong month for C++ quant hiring, especially in low-latency trading, systematic research infrastructure and pricing. London and New York remain the deepest markets, while Singapore shows solid demand around trading systems and quant analytics. The common denominator: modern C++, Python, Linux and increasingly end-to-end ownership from research through production.

🇬🇧 LONDON

Citi — Quant Developer, VP
⚙️ C++ quant development within Citi’s Markets Quantitative Analysis Rates team, working with multiple datasets and front-office quantitative infrastructure. Citi Careers
Why it made the cut: strong direct match for the C++ + derivatives/markets side of quant development.

Qube Research & Technologies — Quantitative Developer, Digital Assets (C++)
⚙️ C++20/23 on Linux for high-frequency and low-latency crypto trading, working directly with traders and quant researchers. WallStreetQuants
Why it made the cut: unusually explicit modern-C++ role with direct exposure to alpha, execution and low-latency trading.

Quadrature — Quantitative Developer
⚙️ Quant development across automated trading, low-latency stream processing, research infrastructure and execution; the stack includes C++, Python and Rust. Jobijoba
Why it made the cut: broad quant-engineering scope at a systematic trading firm, rather than a narrowly scoped software role.

🇺🇸 New York

Millennium Management — Quantitative Developer, C++ / Low-Latency Systems
⚙️ High-performance C++ and low-latency systems for systematic trading infrastructure.
Why it made the cut: A very direct C++ × quantitative trading role with a strong performance-engineering component.

Point72 — Quant Library Developer, Macro Technology
⚙️ Quantitative libraries and technology supporting Point72’s macro investment platform.
Why it made the cut: Strong fit for developers interested in reusable quant libraries, analytics, and front-office infrastructure rather than pure execution engineering.

Goldman Sachs — Quantitative Developer, Systematic Market Making
⚙️ Quantitative development within Global Banking & Markets, focused on systematic market making.
Why it made the cut: Strong combination of quantitative finance, engineering, and direct exposure to systematic trading..


🇸🇬 Singapore

Luxoft Singapore — C++ Developer, Trading Systems / Quantitative / Risk Calculations
⚙️ C++ development across trading systems, pricing engines, quantitative libraries, Murex integration and risk calculations in a Global Markets environment. LinkedIn
Why it made the cut: Strong fit for readers interested in the pricing/risk side of C++ quant development rather than only low-latency execution.

Optiver — C++ Software Engineer
⚙️ C++ engineering for commodities trading systems, covering pricing, quoting, execution, research tooling, backtesting and performance-sensitive infrastructure. LinkedIn
Why it made the cut: Excellent C++ × quant fit, with direct collaboration with traders and researchers and a clear path from research ideas to live trading systems.

Nicoll Curtin — C++ Engineer, Trading
⚙️ High-performance C++/Rust engineering for low-latency trading infrastructure, covering market data, exchange connectivity, execution and system optimisation. LinkedIn
Why it made the cut: A strong systems-heavy option for developers interested in latency, networking, concurrency and performance-critical trading software.

🇫🇷 Paris

Qube Research & Technologies — Quantitative Developer, C++
⚙️ C++ quantitative development within a systematic investment firm, with direct relevance to research and trading infrastructure. LinkedIn
Why it made the cut: Probably the cleanest pure C++ + systematic quant role in the current Paris selection.

Capital Fund Management (CFM) — Quantitative Developer, Equity & Options Portfolio Construction
⚙️ Quant development for systematic equity and options portfolios, including production and large-scale backtesting environments focused on statistical and volatility arbitrage. LinkedIn
Why it made the cut: Very strong quant-finance content and unusually close proximity to portfolio construction and systematic research.

XRAYS TRADING — IT Quant C++ / C#
⚙️ Front-office quant development on rates products, with C++/C# and direct desk exposure. LinkedIn
Why it made the cut: A good representation of the classic Paris front-office IT quant path: derivatives, pricing, and close interaction with the trading desk.

September 25, 2026 0 comments
Commodities trading
ForwardsLibraries

Commodity Forwards in C++: A Quantlib Implementation

by cppforquants September 18, 2026

Commodity forwards are among the simplest derivatives conceptually: two counterparties agree today on a price for a commodity that will be delivered or settled at a future date. The contract itself is straightforward, but valuing an existing forward requires understanding the distinction between the price written into the contract and the current market forward price for the same delivery. How to value commodity forwards in C++?

In this article, we build a simple commodity-forward valuation workflow in C++ using QuantLib. We start from a set of market quotes across several delivery dates, construct an interpolated forward curve, retrieve the current forward price for a specific maturity, discount the resulting payoff, and calculate the mark-to-market value of an existing contract.

1. Commodity Forward Contracts

A commodity forward is an agreement between two counterparties to buy or sell a specified quantity of a commodity at a predetermined price on a future delivery date.

Consider a crude-oil forward entered into in September with the following terms:

  • Commodity: crude oil
  • Position: long
  • Quantity: 1,000 barrels
  • Delivery: December 2026
  • Forward price: $69 per barrel

The contractual delivery price is therefore:K=$69K = \$69

and the quantity is:Q=1,000.Q=1,000.

In December, the long counterparty is contractually entitled to buy 1,000 barrels for $69 per barrel, regardless of how the market price of crude oil has changed.

When the forward is initially entered into, its contractual price is normally chosen so that the contract has approximately zero value:
K=F0(T)K=F_0(T)

where F0(T)F_0(T) is the market forward price for delivery at TT.

Therefore:V0=0.V_0=0.

This is an important distinction: the forward price and the value of a forward contract are not the same thing.

2. Valuing Existing Commodity Forwards in C++: The Theory

For a simple commodity forward, its value to the long can be written as:

where:

  • QQ is the quantity of commodity,
  • KK is the contractual forward price,
  • Ft(T)F_t(T) is today’s market forward price for delivery at TT,
  • DF(t,T)DF(t,T) is the discount factor between today and delivery.

Suppose:Q=1,000Q=1,000K=69K=69Ft(T)=72F_t(T)=72

and:DF(t,T)=0.99.DF(t,T)=0.99.

The value is: Vt​=1,000×0.99×(72−69)=$2,970

The forward therefore has a positive value of approximately $2,970 to the long counterparty.

3. The Commodity Forward Curve

The remaining question is where:
Ft(T)F_t(T)

comes from.

Commodity markets trade instruments corresponding to different future delivery periods. A simplified set of observable crude-oil market prices might look like:

DeliveryMarket Price
October 2026$71.20
November 2026$70.80
December 2026$70.10
January 2027$69.60

These points describe the current shape of the commodity market across different maturities:
(T1,F1),  (T2,F2),…,(Tn,Fn).(T_1,F_1),\;(T_2,F_2),\ldots,(T_n,F_n).
Collectively, they form a discrete representation of the commodity forward curve.

For exchange-traded commodities, some of the most directly observable prices will actually be futures prices rather than OTC forward quotes.

Futures and forwards provide closely related economic exposure, but they are not identical instruments. Futures are standardized, exchange-cleared contracts whose gains and losses are settled through daily variation margin. Forwards are bilateral OTC contracts whose terms can be customized.

Under simplifying assumptions, futures prices can nevertheless provide useful market inputs for constructing the curve used to mark an OTC forward.

4. Representing the Market Curve with QuantLib

A first step to value commodity forwards in C++ is to represent our market data using QuantLib dates and prices:

#include <ql/quantlib.hpp>

#include <iostream>
#include <vector>

using namespace QuantLib;

int main()
{
    Date today(13, September, 2026);
    Settings::instance().evaluationDate() = today;

    std::vector<Date> deliveryDates = {
        Date(1, October, 2026),
        Date(1, November, 2026),
        Date(1, December, 2026),
        Date(1, January, 2027)
    };

    std::vector<Real> futuresPrices = {
        71.20,
        70.80,
        70.10,
        69.60
    };
}



At this point we have discrete market observations but not yet a continuous curve.

For example, we know the market prices corresponding to November 1 and December 1, but not necessarily the price corresponding to a delivery date such as November 15.

We therefore need interpolation.

5. From the Forward Curve to the Forward Value

Once the market quotes have been converted into a forward curve, valuing the contract is straightforward.

QuantLib’s LinearInterpolation allows us to estimate Ft(T)F_t(T) for a delivery date that falls between the quoted market maturities. For example, if our delivery date lies between the November and December contracts, the interpolated value provides an estimate of the current market forward price for that date. We then compare this value with the contractual price KK agreed when the forward was entered into. Because this difference represents value associated with a future settlement date, it must be discounted back to today. For simplicity, we use a flat 4% QuantLib FlatForward interest-rate curve to obtain the discount factor DF(t,T)DF(t,T); in a production system, this would normally be replaced by an appropriately constructed market discount curve. The value of the forward to the long is therefore given by the formula given in 2.

A positive value means the contract is valuable to the long because it allows the commodity to be purchased below the current market forward price; the value to the short is the opposite. The complete C++ implementation below performs this entire sequence: market quotes → interpolation → current forward price → discount factor → NPV.

6. Complete C++ Example

So, how to price commodity forwards in C++?

#include <ql/quantlib.hpp>

#include <iostream>
#include <vector>

using namespace QuantLib;

Real valueCommodityForward(
    Real quantity,
    Real strike,
    const Date& delivery,
    const LinearInterpolation& forwardCurve,
    const YieldTermStructure& discountCurve,
    const Date& today,
    const DayCounter& dayCounter)
{
    Time t =
        dayCounter.yearFraction(today, delivery);

    Real marketForward =
        forwardCurve(t);

    DiscountFactor df =
        discountCurve.discount(delivery);

    return quantity
         * (marketForward - strike)
         * df;
}

int main()
{
    Date today(13, September, 2026);
    Settings::instance().evaluationDate() = today;

    Actual365Fixed dayCounter;

    std::vector<Date> deliveryDates = {
        Date(1, October, 2026),
        Date(1, November, 2026),
        Date(1, December, 2026),
        Date(1, January, 2027)
    };

    std::vector<Real> futuresPrices = {
        71.20,
        70.80,
        70.10,
        69.60
    };

    std::vector<Time> times;

    for (const auto& date : deliveryDates) {
        times.push_back(
            dayCounter.yearFraction(today, date)
        );
    }

    LinearInterpolation forwardCurve(
        times.begin(),
        times.end(),
        futuresPrices.begin()
    );

    Handle<YieldTermStructure> discountCurve(
        ext::make_shared<FlatForward>(
            today,
            0.04,
            dayCounter
        )
    );

    Real quantity = 1000.0;
    Real strike = 69.0;

    Date delivery(15, November, 2026);

    Real npv = valueCommodityForward(
        quantity,
        strike,
        delivery,
        forwardCurve,
        *discountCurve,
        today,
        dayCounter
    );

    std::cout
        << "Commodity forward NPV: $"
        << npv
        << '\n';

    return 0;
}

First, we define today’s date and provide a small set of commodity market prices for different future delivery dates. Because the market only gives us prices at specific maturities, we use QuantLib’s LinearInterpolation to construct a simple forward curve and estimate Ft(T)F_t(T) for our exact delivery date.

We then create a simple interest-rate curve using QuantLib’s FlatForward with a 4% rate. This gives us the discount factor needed to convert the future value of the contract into today’s value.

Finally, the valueCommodityForward function brings everything together. It retrieves the interpolated market forward price, obtains the discount factor, and calculates the value of the forward based on the formula from the former section.

September 18, 2026 0 comments
montecarlo C++
LibrariesPerformance

Monte Carlo Simulation in C++26 with std::philox_engine

by cppforquants September 12, 2026

Monte Carlo simulation is one of the core tools of quantitative finance. It is used to price derivatives, estimate future exposures, generate market scenarios, and model problems that are difficult to solve analytically. What role plays std::philox_engine in this story?

At the heart of every Monte Carlo simulation is a random number generator. In C++26, the standard library introduces std::philox_engine, a counter-based random number engine designed with large-scale and parallel simulation in mind.A good place to start is the documentation of the library.

In this article, we look at what Philox is, why it is interesting for quantitative finance, and how to use it in a simple Monte Carlo option pricing example. We simulate one million possible terminal stock prices, calculate the payoff of a European call option under each scenario, and estimate the option price from the average discounted payoff.

The goal is not to build a production-grade pricing engine, but to show how a new C++26 feature can fit naturally into a familiar quantitative finance workflow.

1. What Is std::philox_engine?

std::philox_engine is a counter-based pseudo-random number generator introduced in C++26 as part of the <random> library. Unlike traditional generators such as std::mt19937, which evolve a relatively large internal state from one random number to the next, Philox generates values from a combination of a counter and a key. This difference is particularly useful in quantitative finance because counter-based generators are naturally suited to large-scale Monte Carlo simulations:

See the full video here:

Conceptually, the generator works like this:

counter + key
     ↓
 Philox rounds
     ↓
pseudo-random values

Instead of relying entirely on a long sequence of state transitions, Philox transforms a counter through several deterministic mixing rounds. Incrementing the counter produces the next block of pseudo-random numbers.

C++26 provides two predefined Philox engines:

std::philox4x32
std::philox4x64

The first generates blocks based on four 32-bit words, while the second uses four 64-bit words. Both predefined versions use 10 Philox rounds.

For example:

#include <iostream>
#include <random>

int main()
{
    std::philox4x32 rng{42};

    std::cout << rng() << '\n';
    std::cout << rng() << '\n';
    std::cout << rng() << '\n';
}

Here, 42 is used to seed the engine. Each call to rng() returns the next pseudo-random integer from the Philox sequence.

The main advantage of Philox is not simply that it generates random numbers. Its design makes independent parts of the random-number space much easier to access and distribute across different computations.

This makes it especially attractive for workloads such as:

  • Monte Carlo option pricing
  • exposure simulation
  • Value at Risk calculations
  • scenario generation
  • multi-threaded simulations
  • GPU-based quantitative workloads

The C++ standard library specifically describes Philox as suitable for Monte Carlo workloads requiring massively parallel random-number generation, and notes that its design is easy to vectorize and parallelize.

Another important property is reproducibility. Given the same seed and the same counter state, Philox produces the same sequence of pseudo-random values. This is extremely useful when debugging or reproducing quantitative simulations.

It is important, however, to distinguish pseudo-randomness from cryptographic randomness. std::philox_engine is designed for numerical simulation and is not a cryptographically secure random-number generator.

For quantitative developers, the key idea is therefore:

Philox is a random-number engine designed around counters rather than a large sequential state, making it particularly well suited to reproducible and highly parallel Monte Carlo simulation.

2. A Simple Monte Carlo Option Pricing Example

To see std::philox_engine in a quantitative finance context, we can use it to price a simple European call option with Monte Carlo simulation.

Assume:

  • spot price (S_0 = 100)
  • strike (K = 100)
  • risk-free rate (r = 5%)
  • volatility (\sigma = 20%)
  • maturity (T = 1) year
  • N simulated paths (e.g: 1,000,000)

Under the Black–Scholes model, the terminal stock price can be simulated as:

For each simulated terminal stock price STS_T, the payoff of a European call option is:

where KK is the strike price.

After simulating NN paths, we estimate the expected payoff by averaging across all simulations:

Finally, we discount the expected payoff back to today:

where C0C_0 is the estimated option price, rr is the risk-free rate, and TT is the time to maturity.

3. A C++ Implementation Using std::philox_engine?

Here is a suggestion of implementation:

#include <algorithm>
#include <cmath>
#include <iostream>
#include <random>

int main()
{
    constexpr int paths = 1'000'000;

    const double S0 = 100.0;
    const double K = 100.0;
    const double r = 0.05;
    const double sigma = 0.20;
    const double T = 1.0;

    std::philox4x32 rng{42};
    std::normal_distribution<double> normal(0.0, 1.0);

    double payoff_sum = 0.0;

    for (int i = 0; i < paths; ++i)
    {
        const double z = normal(rng);

        const double ST =
            S0 * std::exp(
                (r - 0.5 * sigma * sigma) * T +
                sigma * std::sqrt(T) * z
            );

        payoff_sum += std::max(ST - K, 0.0);
    }

    const double price =
        std::exp(-r * T) *
        payoff_sum / paths;

    std::cout << "Option price: "
              << price << '\n';
}

Here we create the C++26 predefined 32-bit Philox engine and seed it with 42. std::philox4x32 is one of the predefined specializations of std::philox_engine.

We then combine the engine with:

std::normal_distribution<double> normal(0.0, 1.0);

std::normal_distribution transforms the pseudo-random numbers produced by the engine into normally distributed values with mean (0) and standard deviation (1).

Each call to:

const double z = normal(rng);

therefore gives us one random shock (Z), which we use to generate one possible stock price at maturity.

3. Install C++26, Compile and Execute

If you didn’t install a C++26 compiler yet: it’s the moment! With macOS, a simple brew install will do:

➜ brew install gcc

Then, run a quick check:

➜  g++-16 --version
                                             
g++-16 (Homebrew GCC 16.2.0) 16.2.0
Copyright (C) 2026 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


Yes, you can now compile C++26 code.

Once done, put our implementation above in a montecarlo.cpp, and run:

g++-16 -std=c++26 -O3 -Wall -Wextra montecarlo.cpp -o montecarlo


Then run the binary:

➜  ./montecarlo 
                                                   
Option price: 10.4644

So, what did we just do? A visual summary:

Once the Monte Carlo simulation has produced an average payoff at maturity, that value still represents money received in the future rather than money today. To convert it into a present fair value, we discount it using the risk-free rate which is the exponential term applied to the average payoff.

The key intuition is:

So going the other way:

where rr is the continuously compounded risk-free interest rate and TT is the time to maturity, expressed in years.

4. Why choosing std::philox_engine

std::philox_engine is an interesting addition to C++26 because it brings a modern counter-based random number generator directly into the standard library. Unlike traditional stateful generators, Philox derives its output from a counter and key, making different regions of the random sequence easy to address independently. This makes the design particularly suitable for large Monte Carlo workloads, where simulations can eventually be split across CPU threads, vector units or GPUs without requiring a single shared random-number-generator state.

std::philox_engine also combines a small internal state, a very long period, strong statistical properties and hardware-friendly integer operations. C++26 additionally exposes set_counter(), allowing an application to position the generator at a specific counter rather than sequentially advancing through the random stream. This is useful for constructing deterministic independent streams and for reproducing individual simulation paths.

Another advantage of standardization is interoperability. std::philox4x32 and std::philox4x64 behave like other C++ random-number engines and can therefore be passed directly to existing facilities such as std::normal_distribution. A Monte Carlo implementation can adopt Philox without redesigning the rest of its simulation pipeline.

In our European option example we use Philox single-threaded, so these parallelism benefits are not yet being exploited. The purpose is to introduce the C++26 engine in the simplest possible setting while using an RNG design that naturally scales toward much larger parallel Monte Carlo simulations.

September 12, 2026 0 comments
Clickhouse for Quantitative Finance
DatabasesPerformance

ClickHouse for Quantitative Finance: Market Data at Scale in C++

by cppforquants September 6, 2026

Quantitative finance produces a lot of data. Market ticks, order books, historical prices, risk scenarios, P&L series, backtest results and model outputs can quickly grow from millions to billions of rows. At that scale, the database starts to matter: ClickHouse is a high-performance columnar database written in C++ and designed for analytical workloads over very large datasets. For all those reasons, ClickHouse for quantitative finance is a good fit.

Instead of optimizing for frequent row-by-row updates, it is built to scan, aggregate and filter huge amounts of data quickly which makes it particularly interesting for quantitative research, market data analysis and risk systems. In this article, we will look at how ClickHouse can be used to store and query financial market data, why its architecture fits many quant workloads, and how a C++ application can interact with it in practice.

1. What is ClickHouse?

ClickHouse is an open-source, column-oriented database designed for analytical workloads. More specifically, it is an OLAP database (Online Analytical Processing) built to scan, filter and aggregate very large datasets quickly.

This is quite different from a traditional transactional database such as PostgreSQL or MySQL.

In a row-oriented database, the values belonging to one record are typically stored together. This works well when an application frequently reads or updates individual rows.

ClickHouse instead stores data by column. If a table contains:

  • timestamp
  • symbol
  • bid
  • ask
  • volume
  • exchange

and a query only needs timestamp, symbol and bid, ClickHouse can largely avoid reading the other columns. This reduces the amount of data that needs to be read and also allows similar values within each column to be compressed efficiently.

This architecture is particularly effective for queries such as:

SELECT
    symbol,
    avg(price)
FROM trades
WHERE timestamp >= now() - INTERVAL 1 DAY
GROUP BY symbol;

Such a query may need to inspect millions or billions of observations but only a small number of columns.

That is exactly the kind of workload that appears frequently in quantitative finance: querying historical prices, aggregating trades, analysing market data, calculating statistics over time windows, or examining large sets of risk and simulation results.

ClickHouse for quantitative finance is therefore not necessarily a replacement for the transactional database behind an application. It is better thought of as a high-performance analytical engine for datasets where fast reads, filtering and aggregation at scale matter more than frequent row-by-row updates.

How fast? Very fast.

2.Why columnar databases fit quantitative finance

Quantitative finance is naturally data-intensive. A market data table may contain billions of observations across prices, volumes, instruments, venues and timestamps. Risk systems can generate similarly large datasets from Monte Carlo scenarios, sensitivities, exposures and P&L calculations.

The important point is that quants rarely need every field for every observation.

Consider a table containing:

timestamp
symbol
bid
ask
volume
exchange
currency

A query calculating the average spread for a given instrument may only need timestamp, bid and ask. A row-oriented database typically stores all the values belonging to each observation together. A columnar database stores values from the same column together instead.

Conceptually:

Row-oriented

09:30:00 | AAPL | 230.10 | 230.12 | 1500 | NASDAQ
09:30:01 | AAPL | 230.11 | 230.13 | 1200 | NASDAQ
09:30:02 | AAPL | 230.09 | 230.11 | 1800 | NASDAQ

versus:

Column-oriented

timestamp: 09:30:00, 09:30:01, 09:30:02, ...
symbol:    AAPL, AAPL, AAPL, ...
bid:       230.10, 230.11, 230.09, ...
ask:       230.12, 230.13, 230.11, ...
volume:    1500, 1200, 1800, ...

This has several advantages for quantitative workloads.

Reading only what is needed

Many financial queries operate on a subset of the available variables. If we want to calculate the average bid-ask spread:

SELECT avg(ask - bid)
FROM market_data
WHERE symbol = 'AAPL';

the database mainly needs the symbol, bid and ask columns.

With billions of rows, avoiding unnecessary columns can significantly reduce the amount of data that must be read from storage.

Efficient compression

Financial datasets also contain a lot of repeated or highly structured information.

A symbol column may contain the same ticker millions of times. Exchange and currency fields have low cardinality. Timestamps are ordered and numerical values often change gradually.

Storing similar values together makes this data highly compressible. Better compression means less disk space, but more importantly it can mean less data to read from disk when executing analytical queries.

Fast aggregation

Quantitative analysis frequently involves operations such as:

AVG
SUM
MIN
MAX
COUNT
quantiles
GROUP BY

For example:

SELECT
    symbol,
    avg(price),
    max(price),
    min(price)
FROM trades
WHERE timestamp >= '2026-01-01'
GROUP BY symbol;

Instead of retrieving individual transactions one at a time, the objective is to process a very large number of observations and derive statistics from them.

That is exactly the workload column-oriented databases are designed to handle.

A natural match for time-series market data

Market data is also usually append-heavy: that’s why Clickhouse for quantitative finance is interesting.

New ticks arrive continuously:

t1 → price
t2 → price
t3 → price
...
tn → price

Historical observations are then queried repeatedly for research, backtesting, monitoring and analysis.

This pattern — large volumes of data being appended and subsequently scanned or aggregated — fits ClickHouse particularly well.

The same idea applies beyond market ticks. A quant platform might use a columnar database to analyse:

  • historical prices and trades,
  • order-book observations,
  • Greeks and sensitivities,
  • P&L histories,
  • backtest results,
  • Monte Carlo scenarios,
  • counterparty exposures,
  • model predictions,
  • risk metrics.

This does not mean every financial database should be columnar. Transactional systems still need databases optimized for individual inserts, updates and lookups.

But when the problem becomes “analyse hundreds of millions or billions of observations quickly”, the columnar model becomes particularly attractive — and this is the type of problem ClickHouse was built to solve.

3. Accessing ClickHouse from C++

For a C++ quant application, ClickHouse can be accessed using the official clickhouse-cpp client library.

The client is written in C++17 and communicates with ClickHouse through its native binary protocol. It provides direct support for executing SQL queries and sending or receiving data in ClickHouse’s native columnar Block format.

A minimal connection looks like this:

#include <clickhouse/client.h>

using namespace clickhouse;

int main()
{
    Client client(ClientOptions()
        .SetHost("localhost")
        .SetPort(9000));

    client.Execute("SELECT 1");
}

The interesting part for quantitative applications is that the C++ API is also column-oriented.

Imagine that we want to store some market data:

CREATE TABLE market_data
(
    timestamp DateTime64(3),
    symbol String,
    bid Float64,
    ask Float64,
    volume UInt64
)
ENGINE = MergeTree
ORDER BY (symbol, timestamp);

We can construct the corresponding columns directly in C++:

#include <clickhouse/client.h>

using namespace clickhouse;

int main()
{
    Client client(ClientOptions().SetHost("localhost"));

    auto symbol = std::make_shared<ColumnString>();
    auto bid    = std::make_shared<ColumnFloat64>();
    auto ask    = std::make_shared<ColumnFloat64>();
    auto volume = std::make_shared<ColumnUInt64>();

    symbol->Append("AAPL");
    symbol->Append("AAPL");
    symbol->Append("MSFT");

    bid->Append(230.10);
    bid->Append(230.11);
    bid->Append(415.20);

    ask->Append(230.12);
    ask->Append(230.13);
    ask->Append(415.24);

    volume->Append(1500);
    volume->Append(1200);
    volume->Append(900);

    Block block;

    block.AppendColumn("symbol", symbol);
    block.AppendColumn("bid", bid);
    block.AppendColumn("ask", ask);
    block.AppendColumn("volume", volume);

    client.Insert("market_data", block);
}

Instead of inserting one trade or tick at a time, we construct a batch of columns and send the entire block to ClickHouse.

This is a natural model for market data pipelines:

Market feed
    ↓
C++ application
    ↓
Accumulate ticks in memory
    ↓
Build ClickHouse Block
    ↓
Batch insert
    ↓
ClickHouse

For large datasets, clickhouse-cpp also supports a streaming batch pattern using BeginInsert, SendInsertBlock and EndInsert, allowing applications to send multiple blocks without keeping the entire dataset in memory.

Querying market data

Reading data follows the same idea. Query results are returned as blocks containing typed columns.

For example:

client.Select(
    R"(
        SELECT
            symbol,
            avg(ask - bid) AS avg_spread
        FROM market_data
        GROUP BY symbol
    )",
    [](const Block& block)
    {
        auto symbol =
            block[0]->As<ColumnString>();

        auto spread =
            block[1]->As<ColumnFloat64>();

        for (size_t i = 0; i < block.GetRowCount(); ++i)
        {
            std::cout
                << symbol->At(i)
                << " "
                << spread->At(i)
                << '\n';
        }
    });

The SQL computation happens inside ClickHouse, while the C++ application receives only the aggregated result.

This distinction is important.

Rather than loading hundreds of millions of market observations into C++ and calculating statistics locally, we can push filtering and aggregation to ClickHouse:

Bad approach

ClickHouse
   ↓
500 million rows
   ↓
C++
   ↓
Calculate statistics


Better approach

ClickHouse
   ↓
Filter + aggregate
   ↓
Small result
   ↓
C++

For a quant system, C++ can therefore remain responsible for pricing, simulation or trading logic, while ClickHouse handles the large analytical dataset behind it.

This combination is particularly attractive when an application needs to process large volumes of market data without turning the C++ process itself into a database engine.


4. An Example of Low-Latency Architecture with ClickHouse

ClickHouse can fit very well into a low-latency trading or market-data architecture, but usually not in the critical execution path.

A trading system may need to react to market data in microseconds or milliseconds. That part is typically handled in memory, inside C++ processes optimized for deterministic latency.

ClickHouse for quantitative finance instead sits next to the trading system as a fast analytical store.

A simplified architecture could look like this:



The key idea is to separate the hot path from the analytical path.

The hot path

The hot path contains everything that directly affects a trading decision:

Market data
    ↓
C++ feed handler
    ↓
Pricing / signal
    ↓
Risk checks
    ↓
Order

This path should avoid unnecessary network calls, disk access and database queries.

For the most latency-sensitive systems, the relevant state is usually held directly in memory.

The analytical path

At the same time, the same market data can be copied into an in-memory buffer or message queue:

Market data
    ↓
Buffer
    ↓
Batch
    ↓
ClickHouse

The C++ application may accumulate several thousand observations and periodically send them to ClickHouse as a batch.

ClickHouse can then be used for queries such as:

SELECT
    symbol,
    avg(ask - bid) AS avg_spread,
    sum(volume) AS total_volume
FROM market_data
WHERE timestamp >= now() - INTERVAL 5 MINUTE
GROUP BY symbol;

This gives researchers, monitoring systems and risk processes access to very recent data without adding latency to the trading engine itself.

Why batching matters

A common mistake would be to insert every market tick individually:

Tick
 ↓
INSERT
 ↓
Tick
 ↓
INSERT

That creates unnecessary network and database overhead.

A better approach is:

Tick
Tick
Tick
Tick
...
   ↓
In-memory batch
   ↓
Single ClickHouse insert

For example, a C++ process could buffer 10,000 observations before sending them as one block.

This preserves low latency in the producer while allowing ClickHouse to ingest data efficiently.

A typical division of responsibilities

In such an architecture:

ComponentResponsibility
C++Market-data processing, pricing, signals, execution
Memory / queueDecoupling the trading path from persistence
ClickHouseHistorical storage and analytical queries
Python / C++ / dashboardsResearch, monitoring and risk analytics

This separation is important.

ClickHouse is not replacing the low-latency C++ engine. Instead, it provides a high-performance analytical layer around it.

For quantitative systems, this can be a very effective combination:

C++ for decisions in microseconds or milliseconds, ClickHouse for analysing millions or billions of observations shortly afterwards.

5. Conclusion on Clickhouse

So, how to conclude? Maybe with a comparison with other databases and a list of strengths/weaknesses.

Different systems are optimized for different workloads. PostgreSQL is a strong general-purpose relational database, DuckDB is excellent for local analytical work, kdb+ has a long history in high-performance market data, and InfluxDB is designed around time-series workloads.

ClickHouse sits in a different part of the spectrum. Its main strength is large-scale analytical processing: scanning, filtering and aggregating very large datasets quickly.

The comparison below is therefore not about choosing a universal winner, but about understanding where ClickHouse fits relative to other common database choices in quantitative systems.

The summary below highlights the main strengths and weaknesses of ClickHouse and helps place it in the broader context of quantitative finance infrastructure:

September 6, 2026 0 comments
cva
Credit RiskRisk

Credit Valuation Adjustment (CVA): Derive a C++ Implementation

by cppforquants August 22, 2026

Credit Valuation Adjustment (CVA) is the adjustment applied to a derivative’s default-free value to account for this counterparty credit risk. In simple terms, CVA is the present value of the expected loss caused by the possibility that the counterparty defaults while the derivative has positive exposure.

1. Understand the CVA Formula

The term CVA (Credit Valuation Adjustment) is defined as:

The discretized version of that integral is a sum:

Credit Valuation Adjustment (CVA) Components

  • LGD (Loss Given Default): The percentage of exposure lost if the counterparty defaults (1 – Recovery Rate).
  • EE (Expected Exposure): The average positive value or market exposure of the portfolio at future time \(t_{i}\).
  • PD (Probability of Default): The marginal chance that the counterparty defaults during the specific time interval.
  • DF (Discount Factor): The present value factor that discounts future cash flows back to today.
  • R (Recovery Rate) is the recovery rate of the counterparty.

A way to explain it is: at every future date, ask how much we could lose, how likely the counterparty is to default during that period, and what that loss is worth today.

Then add everything up.

First divide the life of the portfolio into dates:


For all of those, it’s possible to calculate the Expected Exposure (EE). If the derivative has positive value, the counterparty owes you money and you are exposed to their default:

This is often obtained by simulating market variables such as interest rates, FX rates, equity prices, etc.

For example:

YearExpected Exposure
1£10m
2£14m
3£9m
4£5m
5£1m

Why?
If the counterparty defaults when they owe you nothing, there is essentially no credit loss. CVA therefore depends on the amount you expect to be exposed to at the time of default.

For a real portfolio, this calculation should also reflect things such as netting and collateral.

Next calculate:


This is the probability that the counterparty survives until ti−1​ and then defaults between ti−1​ and ti​.

For example:

PeriodIncremental PD
Year 0–11.0%
Year 1–21.2%
Year 2–31.4%
Year 3–41.5%
Year 4–51.6%

An important point is that we use the incremental default probability, not simply the cumulative probability of default by each year.

Why?
Default can only happen once. Each interval represents a different possible default time, so we want the probability that default occurs specifically in that interval.

Now apply LGD (Loss Given Default): if the counterparty defaults, you do not necessarily lose the entire exposure.

For example, if the assumed recovery rate is 40%: LGD=1−0.40=60%

If exposure at default is £10m: Expected loss if default occurs=10m×60%=£6m

Why?
Some money may be recovered through bankruptcy proceedings, collateral, restructuring, etc. CVA should measure the expected economic loss, rather than assuming a 100% loss.

But a loss occurring several years from now is not worth the same amount as a loss today.

So multiply by:

For example, if the 3-year discount factor is 0.92: £1m expected loss in year 3→£0.92m present value

Why?
CVA is a present-value adjustment to the value of the derivative today.

So, for each interval, we can calculate:

And finally sum across all periods:

Which can be summarized as:

Which is very close to the definition of expected loss:

2. How does CVA reduce the value of a derivative?

Credit Valuation Adjustment or CVA is not just a risk metric. It is a pricing adjustment that reflects the possibility that the counterparty may default before paying everything it owes.

Consider the derivative from the bank’s perspective.

If the derivative has a positive mark-to-market value, it is an asset for the bank: the counterparty owes money to the bank. If the counterparty defaults at that point, the bank may recover only part of that amount.

If the derivative has a negative mark-to-market value, the bank owes money to the counterparty instead. From the perspective of unilateral CVA, this does not create a loss from the counterparty’s default. This is why CVA focuses on positive exposure.

Suppose a derivative has a risk-free value of £10 million. If the counterparty were guaranteed never to default, the bank could value the derivative at the full £10 million.

Now suppose the present value of the expected loss caused by possible counterparty default is £300,000. That expected loss is the CVA.

The credit-adjusted value of the derivative becomes:

The derivative is worth less because some of its future positive cash flows may never actually be received.

This also explains why two otherwise identical derivatives may have different values when traded with different counterparties. A £10 million receivable from a highly creditworthy counterparty is more valuable than a £10 million receivable from a counterparty with a significant probability of default.

CVA provides a way to incorporate that difference directly into the valuation.

The relationship also gives some useful intuition: PD↑⇒CVA↑⇒Vrisky​↓

If the counterparty becomes more likely to default, CVA increases and the derivative becomes less valuable.

Similarly: EE↑⇒CVA↑⇒Vrisky​↓

If the bank expects to be owed more money in the future, more value is at risk if the counterparty defaults.

Conversely, better collateralisation, stronger netting agreements or an improvement in the counterparty’s credit quality can reduce expected losses and therefore reduce CVA.

So the key idea is simple:

CVA is the monetary value of counterparty credit risk embedded in the price of the derivative.

3. Implementation in C++

There are several ways to implement a CVA calculation in C++, depending on how much of the pricing stack you want to build yourself.

At the simplest level, you can assume that the expected exposure profile, default probabilities and discount factors have already been calculated. CVA then becomes a straightforward aggregation: CVA=LGDi∑​EE(ti​)×PD(ti−1​,ti​)×DF(ti​)

This is a good starting point because it isolates the CVA calculation from the more complex problem of generating future exposures.

A more complete implementation would typically involve one of the following approaches:

  • Precomputed exposure profiles — read EE, PD and discount curves from upstream systems and aggregate them. This is the simplest approach.
  • Monte Carlo simulation — simulate future market states, reprice the portfolio at each future date, and estimate EE(t) from the resulting exposure distribution.
  • QuantLib — use existing pricing engines, yield curves, credit curves and stochastic processes rather than implementing every component from scratch.
  • Production CVA engine — combine trade pricing, netting sets, collateral agreements, market simulation, credit curves and aggregation across thousands or millions of trades.

For illustration, we can start with the first approach.

Suppose the expected exposure profile is:

Year EE Incremental PD DF
1 10.0m 1.0% 0.97
2 14.0m 1.2% 0.94
3 8.0m 1.4% 0.91

A simple C++ implementation is then:

#include <iostream>
#include <vector>
#include <stdexcept>

struct CVAPoint {
    double expectedExposure;
    double defaultProbability;
    double discountFactor;
};

double calculateCVA(
    const std::vector<CVAPoint>& profile,
    double recoveryRate)
{
    if (recoveryRate < 0.0 || recoveryRate > 1.0) {
        throw std::invalid_argument("Invalid recovery rate");
    }

    const double lgd = 1.0 - recoveryRate;

    double cva = 0.0;

    for (const auto& point : profile) {
        cva += point.expectedExposure
             * point.defaultProbability
             * point.discountFactor
             * lgd;
    }

    return cva;
}

int main()
{
    std::vector<CVAPoint> profile = {
        {10'000'000.0, 0.010, 0.97},
        {14'000'000.0, 0.012, 0.94},
        { 8'000'000.0, 0.014, 0.91}
    };

    const double recoveryRate = 0.40;

    const double cva = calculateCVA(profile, recoveryRate);

    std::cout << "CVA = £" << cva << '\n';
}

For the first year, for example: 10,000,000×0.01×0.97×0.60=58,200

The three contributions are approximately: 58,200+94,752+61,152=214,104

so the resulting CVA is approximately:

4. CVA as part of the XVA framework

CVA is one component of the broader XVA framework used to adjust the clean, or risk-free, value of a derivative for costs and risks that are not captured by the classical pricing model.

A useful way to think about the XVA stack is:

This is why CVA should not be viewed as an isolated calculation. It is one part of a much larger framework used by banks to determine the true economic cost of entering into and maintaining a derivative position.

It also explains why the same exposure simulation infrastructure can often support several XVA calculations. Once a system can simulate future portfolio values, collateral and exposure distributions, those simulations can be reused to calculate CVA, DVA, FVA, MVA and other adjustments.

In practice, this is one of the reasons XVA systems can become computationally demanding: a large bank may need to simulate future market states and reprice millions of trades across thousands of counterparties and many future time steps before the different valuation adjustments can be calculated.

5. 10 interview questions about CVA

1. What is CVA?
CVA is the credit valuation adjustment made to a derivative’s value to account for the possibility that the counterparty defaults.

2. Why does CVA reduce a derivative’s value?
Because a positive future payoff is worth less if there is a chance the counterparty will not fully pay it.

3. What is expected exposure?
Expected exposure is the average amount the bank expects to be owed by the counterparty at a future date.

4. What is the difference between EE and PFE?
EE is the average future exposure, while PFE measures a high percentile of potential exposure and focuses more on tail risk.

5. Where do default probabilities come from?
They are generally derived from the counterparty’s credit curve, often using CDS or bond market information.

6. Why use incremental default probabilities?
Because CVA needs the probability that default occurs within each specific time period, without double-counting default risk.

7. How do netting and collateral affect CVA?
They reduce the amount exposed to counterparty default and therefore generally reduce CVA.

8. What is wrong-way risk?
Wrong-way risk occurs when exposure increases at the same time as the counterparty becomes more likely to default.

9. How is CVA calculated with Monte Carlo simulation?
Future market scenarios are simulated, the portfolio is repriced, exposures are calculated, and the resulting expected losses are aggregated.

10. How does CVA fit into XVA?
CVA is the counterparty-credit component of XVA, alongside adjustments for own credit risk, funding, margin and capital costs.

August 22, 2026 0 comments
FRM Syllabus
InterviewJobs

The FRM Syllabus: A Guide for C++ Quant Developers

by cppforquants August 13, 2026

The Financial Risk Manager (FRM) certification covers a lot of key concepts: in this article we will talk about the FRM syllabus. For quant developers, it provides a useful framework for understanding the financial models, products, and risk measures that sit behind many real-world systems.

This guide looks at the FRM from a quant developer’s perspective: which topics matter most, how they connect to real development work, and where C++ projects can help turn theory into practical skills.

1. Why Should a Quant Developer Care About the FRM?

Quant developers are usually hired for their programming ability, mathematical background, and understanding of financial markets. In practice, however, writing good C++ or Python is only part of the job. A developer working on pricing, risk, trading, or portfolio systems also needs to understand what the numbers being calculated actually mean.

This is where the Financial Risk Manager (FRM) curriculum can be useful.

The FRM is not a programming qualification, and it will not teach you how to write high-performance C++, design a pricing library, or optimise a Monte Carlo engine. What it does provide is a structured introduction to many of the financial concepts that quant developers encounter in banks, hedge funds, asset managers, and other financial institutions.

Topics such as derivatives, fixed income, probability, statistics, Value at Risk, Expected Shortfall, market risk, credit risk, counterparty exposure, and stress testing are all directly related to systems that quant developers may eventually be asked to build or maintain.

For example, it is one thing to implement a function that calculates VaR. It is another to understand why the calculation is being performed, what assumptions sit behind it, how the result should be interpreted, and where the model can fail.

That domain knowledge becomes particularly valuable when working closely with quantitative analysts, traders, risk managers, and model validation teams.

For a quant developer, the real value of the FRM syllabus is therefore not necessarily the certification itself. It is the financial and risk-management knowledge behind it.

2. FRM 2026 Syllabus at a Glance

How is the FRM exact structured?

FRM Part I

FRM Part I focuses on the fundamental tools used to understand and measure financial risk.

The four main areas are:

  • Foundations of Risk Management
  • Quantitative Analysis
  • Financial Markets and Products
  • Valuation and Risk Models

For quant developers, Part I is especially relevant because it covers many of the concepts that appear in quantitative libraries and risk systems, including:

  • Probability and statistics
  • Regression and time-series analysis
  • Derivatives and fixed-income products
  • Valuation techniques
  • Volatility and correlations
  • Value at Risk (VaR)
  • Expected Shortfall
  • Monte Carlo methods
  • Risk factors and sensitivities

In many ways, Part I provides the mathematical and financial foundations needed to understand what a quantitative system is actually calculating.

FRM Part II

FRM Part II builds on those foundations and focuses more heavily on applying them to real-world risk management.

The main areas are:

  • Market Risk Measurement and Management
  • Credit Risk Measurement and Management
  • Operational Risk and Resilience
  • Liquidity and Treasury Risk Measurement and Management
  • Risk Management and Investment Management
  • Current Issues in Financial Markets

From a quant developer’s perspective, the most directly relevant topics include:

  • Market risk measurement
  • Scenario analysis
  • Stress testing
  • Value at Risk and Expected Shortfall
  • Credit risk modelling
  • Counterparty exposure
  • Probability of default and loss given default
  • Liquidity risk
  • Portfolio risk
  • Risk aggregation

Part II is closer to the type of work found in production risk infrastructure, where the goal is not simply to value one instrument, but to measure risk across portfolios, counterparties, and changing market conditions.

3. FRM Part I: What Matters to Quant Developers?

FRM Part I is probably the most immediately useful part of the curriculum for a quant developer. It introduces the quantitative tools, financial instruments, valuation techniques, and risk concepts that sit behind many pricing and risk systems.

Some areas have a much stronger connection to quant development than others. Let’s look at each one of the FRM syllabus part 1.

3.1 Quantitative Analysis

Relevance for Quant Developers: ★★★★★

Quantitative Analysis covers much of the mathematical and statistical foundation used throughout quantitative finance. Topics include probability distributions, statistical inference, regression, time-series analysis, volatility estimation, correlation, and simulation methods.

For a quant developer, these concepts appear everywhere.

Typical applications include:

  • Monte Carlo simulation
  • Random number generation
  • Regression models
  • Volatility estimation
  • Correlation and covariance matrices
  • Time-series analysis
  • Statistical model calibration
  • Historical market-data analysis

You may not be responsible for developing every mathematical model yourself, but you will often be responsible for implementing, optimising, testing, or maintaining them.

For example, implementing a Monte Carlo engine requires more than knowing C++. You also need to understand probability distributions, sampling, convergence, correlation, and the statistical meaning of the results.

For this reason, Quantitative Analysis is one of the most valuable FRM areas for a quant developer.

3.2 Financial Markets and Products

Relevance for Quant Developers: ★★★★★

A quant developer also needs to understand the financial instruments represented by the code.

Financial Markets and Products covers areas such as:

  • Futures and forwards
  • Options
  • Swaps
  • Bonds
  • Interest rates
  • Foreign exchange
  • Hedging with derivatives
  • OTC and exchange-traded markets
  • Mortgage-backed securities

These are among the products and market concepts covered within the Part I curriculum.

A class such as:

InterestRateSwap

may look like another C++ object from a software-engineering perspective. From a quantitative-finance perspective, however, you need to understand its legs, cash flows, payment dates, floating-rate resets, discounting, and market-data dependencies.

The same applies to options, futures, bonds, and other instruments.

Understanding the product makes it much easier to:

  • Model trades correctly
  • Design appropriate data structures
  • Understand pricing inputs
  • Interpret Greeks and sensitivities
  • Investigate unexpected results
  • Communicate with traders and quants

This is one of the areas where financial knowledge directly makes you a better quant developer.

3.3 Valuation and Risk Models

Relevance for Quant Developers: ★★★★★

Valuation and Risk Models has perhaps the strongest overlap with traditional quant development.

The area includes topics such as:

  • Value at Risk (VaR)
  • Expected Shortfall
  • Stress testing and scenario analysis
  • Option valuation
  • Fixed-income valuation
  • Hedging
  • Credit risk measures

These are core components of many real-world pricing and risk platforms.

A quant developer working at a bank might encounter systems responsible for:

  • Pricing thousands or millions of trades
  • Calculating portfolio sensitivities
  • Running historical scenarios
  • Calculating VaR or Expected Shortfall
  • Performing stress tests
  • Revaluing portfolios under changing market conditions

This is where mathematical models become software systems.

A formula may fit on a few lines of paper, while its production implementation has to deal with market data, portfolios, numerical methods, performance, concurrency, memory usage, and potentially millions of calculations.

For quant developers interested in pricing engines, risk libraries, derivatives analytics, or front-office systems, this is one of the most relevant parts of the entire FRM curriculum.

4. FRM Part II: What Matters to Quant Developers?

FRM Part II moves away from the foundations and focuses more on how risk is measured and managed across financial institutions.

For quant developers, this is where the curriculum starts to connect more directly with large-scale risk systems, portfolio analytics, counterparty exposure, stress testing, and enterprise infrastructure. Let’s dive into the FRM syllabus part 2.

4.1 Market Risk

Relevance for Quant Developers: ★★★★★

Market Risk is one of the most relevant areas of FRM Part II for quant developers.

It focuses on how changes in market variables such as interest rates, equity prices, foreign exchange rates, credit spreads, and volatility affect portfolios.

Typical topics include:

  • Value at Risk (VaR)
  • Expected Shortfall
  • Stress testing
  • Scenario analysis
  • Backtesting
  • Volatility modelling
  • Correlation
  • Risk-factor modelling
  • Portfolio sensitivities

These concepts appear directly in market-risk platforms and pricing systems.

A quant developer may work on systems that:

  • Generate market scenarios
  • Revalue portfolios
  • Calculate sensitivities
  • Aggregate risk across desks
  • Calculate VaR and Expected Shortfall
  • Run regulatory stress tests
  • Backtest risk models

Market Risk is therefore particularly useful for developers working in banks, trading desks, risk technology teams, and quantitative analytics.

4.2 Credit Risk

Relevance for Quant Developers: ★★★★★

Credit Risk is another highly relevant area, especially for developers working on counterparty risk, XVA, fixed income, or credit derivatives.

Important concepts include:

  • Probability of Default (PD)
  • Loss Given Default (LGD)
  • Exposure at Default (EAD)
  • Credit spreads
  • Credit migration
  • Default correlation
  • Counterparty exposure
  • Credit derivatives
  • Expected exposure
  • Potential Future Exposure (PFE)

These concepts often translate directly into quantitative systems.

For example, a counterparty-risk engine may need to:

  • Simulate future market scenarios
  • Revalue trades at future dates
  • Calculate exposure profiles
  • Apply collateral agreements
  • Aggregate exposure by counterparty
  • Calculate PFE or Expected Exposure
  • Feed results into CVA and other XVA calculations

Credit Risk is especially valuable for quant developers working in large banks, where counterparty-credit and XVA infrastructure can be extremely complex.

4.3 Liquidity and Treasury Risk

Relevance for Quant Developers: ★★★☆☆

Liquidity and Treasury Risk focuses on whether a financial institution can meet its funding and cash-flow obligations under normal and stressed conditions.

Relevant topics include:

  • Funding liquidity
  • Market liquidity
  • Cash-flow forecasting
  • Liquidity stress testing
  • Funding strategies
  • Balance-sheet management
  • Liquidity risk metrics

For quant developers, the relevance depends heavily on the team.

This area is particularly useful for developers working on:

  • Treasury systems
  • Asset and Liability Management (ALM)
  • Funding analytics
  • Liquidity-risk platforms
  • Cash-flow engines
  • Enterprise risk systems

It is less directly relevant to developers working purely on derivatives pricing or low-latency trading systems, but it provides useful knowledge of how banks manage funding and balance-sheet constraints.

4.4 Operational Risk and Resilience

Relevance for Quant Developers: ★★★☆☆

Operational Risk and Resilience is less mathematically focused, but it has more relevance to software engineering than it may initially appear.

Typical areas include:

  • Operational failures
  • Internal controls
  • Cyber and technology risk
  • Model risk
  • Data quality
  • Business continuity
  • System resilience
  • Risk governance

For quant developers working in production environments, many of these issues are very real.

A mathematically correct pricing or risk model is not useful if:

  • The market data is incorrect
  • A batch process fails
  • Results cannot be reproduced
  • A system cannot handle peak workloads
  • Calculations are not properly monitored
  • Model versions are not controlled

This part of the FRM curriculum helps connect quantitative development with the broader operational requirements of running financial systems reliably.

4.5 Risk Management and Investment Management

Relevance for Quant Developers: ★★★☆☆

This area is particularly relevant to developers working in asset management, portfolio analytics, or systematic investment platforms.

Typical concepts include:

  • Portfolio construction
  • Portfolio risk
  • Risk-adjusted performance
  • Factor exposures
  • Asset allocation
  • Performance measurement
  • Investment risk management

For a quant developer, these topics can appear in systems used for:

  • Portfolio optimisation
  • Risk attribution
  • Factor analysis
  • Performance analytics
  • Portfolio construction
  • Asset-allocation models

The relevance is therefore highly role-dependent.

A developer working at an asset manager may use these concepts frequently, while an XVA or derivatives-pricing developer may encounter them much less often.

4.6 Current Issues in Financial Markets

Relevance for Quant Developers: ★★☆☆☆ to ★★★☆☆

Current Issues in Financial Markets focuses on emerging developments and risks affecting the financial industry.

The exact topics change over time, but they may include areas such as:

  • New regulatory developments
  • Changes in market structure
  • Emerging financial risks
  • Technology-related risks
  • Macroeconomic developments
  • New approaches to risk management

For quant developers, the main value is broader industry awareness rather than direct technical knowledge.

Understanding current developments can help explain why banks introduce new models, reporting requirements, data pipelines, or risk calculations.

This section is therefore less likely to help you write better C++ directly, but it can help you understand why the systems around you are changing.

Overall, the most important Part II areas for quant developers are generally Market Risk and Credit Risk, followed by Liquidity, Investment Risk, and Operational Risk depending on the role.

5. FRM vs Building Quant Projects: Where Should You Spend Your Time?

For an aspiring quant developer, the FRM and practical quant projects develop very different skills.

The FRM gives you structured knowledge of financial markets, risk models, derivatives, statistics, and risk management. Building projects teaches you how to turn those concepts into working software.

If your goal is a quant developer role, projects should generally take priority over studying the FRM in isolation.

A hiring manager is more likely to care whether you can:

  • Write clean and efficient C++
  • Implement numerical algorithms correctly
  • Understand financial instruments
  • Work with market data
  • Build pricing and risk models
  • Debug quantitative code
  • Explain the design decisions behind your implementation

This is why the strongest approach is not necessarily to choose between FRM or C++ projects.

Instead, combine them. Learn them and then, implement them. Many topics in the FRM curriculum translate naturally into quant development projects.

For example:

  • Probability and simulation → Build a Monte Carlo simulation framework
  • Options and derivatives → Build a Black-Scholes option pricer
  • Fixed income → Build a yield curve and bond pricing library
  • Value at Risk → Build a historical or Monte Carlo VaR engine
  • Expected Shortfall → Extend your portfolio risk engine
  • Market risk → Build a scenario and stress-testing framework
  • Credit risk → Implement default probability and credit exposure models
  • Counterparty risk → Build a Potential Future Exposure simulator
  • CVA → Build a simple counterparty valuation adjustment engine

This approach gives you both the financial theory and evidence that you can translate it into code.

12. Final Takeaway

The FRM is not a quant development qualification, but parts of its syllabus are highly relevant to the work quant developers do.

Topics such as quantitative analysis, derivatives, valuation, market risk, credit risk, and counterparty exposure all connect directly to pricing libraries, risk engines, portfolio analytics, and other quantitative systems.

The key is to treat the FRM as a source of domain knowledge, not as a substitute for technical skills.

A strong quant developer still needs:

  • Solid C++ and Python
  • Numerical methods
  • Algorithms and data structures
  • Software design
  • Debugging and testing
  • Performance optimisation
  • A good understanding of financial products and models

The most effective approach is to combine both sides.

Use the FRM syllabus to understand the financial concepts, then reinforce that knowledge by implementing them in code.

August 13, 2026 0 comments
Quantlib Architecture
Libraries

QuantLib Architecture: A Tour of Its Core Modules

by cppforquants August 9, 2026

The QuantLib architecture is designed around a clear separation between financial instruments, market data, models, and pricing engines. Instead of tightly coupling each product to a single valuation method, QuantLib lets developers reuse shared term structures, volatility surfaces, and market inputs across options, bonds, swaps, and other instruments.

This modular design is one of the main reasons QuantLib is widely used for quantitative finance and derivatives pricing. In this article, we’ll explore how the core pieces of the QuantLib architecture fit together: from yield curves and volatility structures to pricing engines, numerical methods, and model calibration, and how the same market infrastructure can support multiple valuation workflows.

1. Term Structures

A term structure describes how a financial quantity changes with maturity. In the case of a yield/discount curve, it answers questions such as:

  • What is the discount factor for a cash flow occurring in 3 months?
  • What is the implied zero rate for 5 years?
  • What is the forward rate between years 5 and 7?

The important idea in QuantLib is that a curve is not simply an array of rates. Market data usually gives you only a finite set of quotes—for example, a 3-month deposit rate and swap rates at 1Y, 2Y, 5Y, 10Y, etc. Pricing, however, may require a value at any date.

QuantLib therefore represents the curve as a queryable object. You provide the market instruments and their quotes, and the bootstrapping machinery constructs a YieldTermStructure. You can then ask that object for discount factors, zero rates, or forward rates at arbitrary dates. Interpolation and the conventions used to construct the curve are encapsulated by the term structure rather than being left to every pricing model.

A useful mental model is in the Quantlib architecture framework:

Market quotes → calibration/bootstrapping → queryable term structure → pricing

For example, a discount curve might be bootstrapped from a short-dated deposit followed by a set of interest-rate swaps:

#include <ql/quantlib.hpp>

using namespace QuantLib;

int main() {
    Calendar calendar = TARGET();
    Date today = Date(9, August, 2026);
    Settings::instance().evaluationDate() = today;

    DayCounter dc = Actual365Fixed();

    // Market quotes
    auto depositRate =
        ext::make_shared<SimpleQuote>(0.0250);

    auto swapRate1Y =
        ext::make_shared<SimpleQuote>(0.0270);

    auto swapRate5Y =
        ext::make_shared<SimpleQuote>(0.0320);

    auto swapRate10Y =
        ext::make_shared<SimpleQuote>(0.0350);

    // Convert quotes into RateHelpers.
    auto deposit = ext::make_shared<DepositRateHelper>(
        Handle<Quote>(depositRate),
        Period(3, Months),
        2,
        calendar,
        ModifiedFollowing,
        false,
        Actual360()
    );

    auto swap1Y = ext::make_shared<SwapRateHelper>(
        Handle<Quote>(swapRate1Y),
        Period(1, Years),
        calendar,
        Annual,
        Unadjusted,
        Thirty360(Thirty360::BondBasis),
        Euribor6M()
    );

    auto swap5Y = ext::make_shared<SwapRateHelper>(
        Handle<Quote>(swapRate5Y),
        Period(5, Years),
        calendar,
        Annual,
        Unadjusted,
        Thirty360(Thirty360::BondBasis),
        Euribor6M()
    );

    auto swap10Y = ext::make_shared<SwapRateHelper>(
        Handle<Quote>(swapRate10Y),
        Period(10, Years),
        calendar,
        Annual,
        Unadjusted,
        Thirty360(Thirty360::BondBasis),
        Euribor6M()
    );

    std::vector<ext::shared_ptr<RateHelper>> helpers = {
        deposit, swap1Y, swap5Y, swap10Y
    };

    // Bootstrap the curve from the market instruments.
    auto curve = ext::make_shared<PiecewiseYieldCurve<Discount, LogLinear>>(
        today,
        helpers,
        dc
    );

    // The curve is queryable at arbitrary dates.
    Date fiveYears = calendar.advance(today, Period(5, Years));

    Real discountFactor = curve->discount(fiveYears);
    Rate zeroRate = curve->zeroRate(fiveYears, dc, Continuous).rate();

    std::cout << "5Y discount factor: " << discountFactor << '\n';
    std::cout << "5Y zero rate:       " << zeroRate << '\n';
}

2. Options Pricing

Options pricing is a big big deal in quantitative finance.


In the QuantLib architecture, an Instrument represents the financial contract: its payoff, exercise rules, maturity, and other contractual properties. It does not need to know how its value will be calculated.

The actual valuation is delegated to a PricingEngine.

That separation is useful because the same option can be priced using different numerical methods without changing the definition of the option itself. For example, a European vanilla option can be valued with an analytic Black–Scholes engine, or with a Monte Carlo engine. The VanillaOption remains the same; only the pricing engine changes.

A useful mental model is:

Market data + Instrument → PricingEngine → valuation

Or, more specifically:

What are we pricing? → VanillaOption
How are we pricing it? → PricingEngine

Here’s a simple European call:

#include <ql/quantlib.hpp>

using namespace QuantLib;

int main() {
    Date today(9, August, 2026);
    Settings::instance().evaluationDate() = today;

    Calendar calendar = TARGET();
    DayCounter dc = Actual365Fixed();

    // Market inputs
    Handle<Quote> spot(
        ext::make_shared<SimpleQuote>(100.0)
    );

    Handle<YieldTermStructure> riskFreeCurve(
        ext::make_shared<FlatForward>(
            today, 0.03, dc
        )
    );

    Handle<YieldTermStructure> dividendCurve(
        ext::make_shared<FlatForward>(
            today, 0.00, dc
        )
    );

    Handle<BlackVolTermStructure> volatility(
        ext::make_shared<BlackConstantVol>(
            today, calendar, 0.20, dc
        )
    );

    // Define the option contract.
    auto payoff = ext::make_shared<PlainVanillaPayoff>(
        Option::Call,
        100.0
    );

    Date maturity = calendar.advance(
        today, Period(1, Years)
    );

    auto exercise = ext::make_shared<EuropeanExercise>(
        maturity
    );

    VanillaOption option(payoff, exercise);

    // Choose how to price it.
    auto engine = ext::make_shared<AnalyticEuropeanEngine>(
        Handle<GeneralizedBlackScholesProcess>(
            ext::make_shared<GeneralizedBlackScholesProcess>(
                spot,
                dividendCurve,
                riskFreeCurve,
                volatility
            )
        )
    );

    option.setPricingEngine(engine);

    std::cout << "Option value: "
              << option.NPV()
              << '\n';
}

3. Bonds Pricing

A bond is a sequence of future cash flows: coupons and, eventually, repayment of principal. To value the bond today, those future cash flows need to be discounted back to the valuation date.

In the QuantLib architecture, the FixedRateBond represents the bond contract and its cash flows. It doesn’t itself contain the logic for discounting those cash flows.

The DiscountingBondEngine provides that valuation logic. It takes a YieldTermStructure—such as the curve bootstrapped in the previous section—and uses its discount factors to calculate the present value of the bond’s future cash flows.

So the architecture becomes:

Market quotes → YieldTermStructure → PricingEngine → Instrument value

And importantly, the same curve can be shared by many instruments:

One curve → bonds, swaps, options, and other instruments

Here’s a simple fixed-rate bond using a discount curve:

#include <ql/quantlib.hpp>

using namespace QuantLib;

int main() {
    Date today(9, August, 2026);
    Settings::instance().evaluationDate() = today;

    Calendar calendar = TARGET();
    DayCounter dc = ActualActual(ActualActual::Bond);

    // In practice, this would be the bootstrapped curve
    // from Section 1.
    Handle<YieldTermStructure> discountCurve(
        ext::make_shared<FlatForward>(
            today,
            0.03,
            dc
        )
    );

    // Define the bond.
    Natural settlementDays = 2;
    Date issueDate = today;
    Date maturityDate = calendar.advance(
        issueDate, Period(5, Years)
    );

    Schedule schedule(
        issueDate,
        maturityDate,
        Period(Annual),
        calendar,
        Unadjusted,
        Unadjusted,
        DateGeneration::Backward,
        false
    );

    Real couponRate = 0.04;

    FixedRateBond bond(
        settlementDays,
        100.0,                  // face value
        schedule,
        std::vector<Rate>{couponRate},
        dc
    );

    // Tell the bond how it should be valued.
    auto engine =
        ext::make_shared<DiscountingBondEngine>(
            discountCurve
        );

    bond.setPricingEngine(engine);

    std::cout << "Bond NPV: "
              << bond.NPV()
              << '\n';

    std::cout << "Clean price: "
              << bond.cleanPrice()
              << '\n';

    std::cout << "Dirty price: "
              << bond.dirtyPrice()
              << '\n';
}

Another example in this video:

4. Interest Rate Swaps

An interest rate swap exchanges two streams of interest payments, typically a fixed rate against a floating rate. In a vanilla fixed-for-floating swap, one party pays a fixed rate while receiving a floating rate, usually based on an index such as 6-month Euribor.

From a pricing perspective, the swap is essentially a comparison of two legs:

  • the fixed leg, whose future payments are known from the swap’s fixed rate;
  • the floating leg, whose future payments depend on the evolution of interest rates.

QuantLib architecture represents the contract with VanillaSwap. The actual valuation is delegated to a pricing engine, such as DiscountingSwapEngine.

This is where the term structure from Section 1 becomes particularly useful. The same YieldTermStructure that was bootstrapped from market quotes can be passed into the swap’s pricing engine to discount the swap’s future cash flows.

A useful mental model is:

Curve → discounting and forward-rate information → swap valuation

Or, across the sections you’ve built so far:

Market quotes → YieldTermStructure → financial instrument → PricingEngine → NPV

Here is a simple vanilla swap:

#include <ql/quantlib.hpp>

using namespace QuantLib;

int main() {
    Date today(9, August, 2026);
    Settings::instance().evaluationDate() = today;

    Calendar calendar = TARGET();
    DayCounter dc = Actual365Fixed();

    // In practice, reuse the bootstrapped curve
    // from Section 1.
    Handle<YieldTermStructure> curve(
        ext::make_shared<FlatForward>(
            today,
            0.03,
            dc
        )
    );

    // Swap terms
    Date startDate = calendar.advance(
        today, Period(2, Days)
    );

    Date maturityDate = calendar.advance(
        startDate, Period(5, Years)
    );

    Schedule fixedSchedule(
        startDate,
        maturityDate,
        Period(Annual),
        calendar,
        ModifiedFollowing,
        ModifiedFollowing,
        DateGeneration::Forward,
        false
    );

    Schedule floatingSchedule(
        startDate,
        maturityDate,
        Period(Semiannual),
        calendar,
        ModifiedFollowing,
        ModifiedFollowing,
        DateGeneration::Forward,
        false
    );

    // Build the vanilla fixed-for-floating swap.
    Rate fixedRate = 0.0325;

    VanillaSwap swap(
        VanillaSwap::Payer,       // pay fixed, receive floating
        1'000'000.0,              // notional
        fixedSchedule,
        fixedRate,
        dc,
        floatingSchedule,
        Euribor6M(curve),
        0.0,                       // spread
        Actual360()
    );

    // Use the same curve to discount the swap.
    auto engine =
        ext::make_shared<DiscountingSwapEngine>(curve);

    swap.setPricingEngine(engine);

    std::cout << "Swap NPV: "
              << swap.NPV()
              << '\n';

    std::cout << "Fair fixed rate: "
              << swap.fairRate()
              << '\n';
}

5. Volatility Surface

A volatility surface captures this two-dimensional relationship and it can often look like a smile.

In QuantLib architecture, BlackVarianceSurface represents this market information as a queryable object. Instead of asking for “the volatility,” a pricing engine can effectively ask:

What volatility should I use for this particular maturity and strike?

That makes it conceptually similar to the term structure from Section 1: rather than storing a handful of market observations as disconnected numbers, QuantLib turns them into a reusable object that can be queried by pricing models.

The architecture becomes:

Market volatility quotes → BlackVarianceSurface → PricingEngine → option value

And because the VanillaOption from Section 2 doesn’t change, we can simply replace its flat-volatility input with the surface and reprice it.

Here’s a compact example using a small grid of implied volatilities:

#include <ql/quantlib.hpp>

using namespace QuantLib;

int main() {
    Date today(9, August, 2026);
    Settings::instance().evaluationDate() = today;

    Calendar calendar = TARGET();
    DayCounter dc = Actual365Fixed();

    // Underlying and curves from Section 2.
    Handle<Quote> spot(
        ext::make_shared<SimpleQuote>(100.0)
    );

    Handle<YieldTermStructure> riskFreeCurve(
        ext::make_shared<FlatForward>(
            today, 0.03, dc
        )
    );

    Handle<YieldTermStructure> dividendCurve(
        ext::make_shared<FlatForward>(
            today, 0.00, dc
        )
    );

    // Maturities represented by the surface.
    std::vector<Date> dates = {
        calendar.advance(today, Period(6, Months)),
        calendar.advance(today, Period(1, Years)),
        calendar.advance(today, Period(2, Years))
    };

    // Strikes represented by the surface.
    std::vector<Real> strikes = {
        90.0, 100.0, 110.0
    };

    // Implied volatility matrix:
    //
    //             90%     100%     110%
    //  6M        22%      20%      21%
    //  1Y        21%      19%      20%
    //  2Y        20%      18%      19%
    //
    Matrix vols(dates.size(), strikes.size());

    vols[0][0] = 0.22;
    vols[0][1] = 0.20;
    vols[0][2] = 0.21;

    vols[1][0] = 0.21;
    vols[1][1] = 0.19;
    vols[1][2] = 0.20;

    vols[2][0] = 0.20;
    vols[2][1] = 0.18;
    vols[2][2] = 0.19;

    // Build the volatility surface.
    auto surface = ext::make_shared<BlackVarianceSurface>(
        today,
        calendar,
        dates,
        strikes,
        vols,
        dc
    );

    Handle<BlackVolTermStructure> volatility(surface);

    // The same option from Section 2.
    auto payoff =
        ext::make_shared<PlainVanillaPayoff>(
            Option::Call,
            100.0
        );

    Date maturity = calendar.advance(
        today, Period(1, Years)
    );

    auto exercise =
        ext::make_shared<EuropeanExercise>(maturity);

    VanillaOption option(payoff, exercise);

    auto process =
        ext::make_shared<GeneralizedBlackScholesProcess>(
            spot,
            dividendCurve,
            riskFreeCurve,
            volatility
        );

    auto engine =
        ext::make_shared<AnalyticEuropeanEngine>(process);

    option.setPricingEngine(engine);

    std::cout << "Option value with volatility surface: "
              << option.NPV()
              << '\n';
}

6. Model Calibration

Calibration is the process of choosing a model’s parameters so that the model reproduces prices or quotes observed in the market.

For example, suppose we want to use the Hull–White short-rate model. The model has parameters controlling the behaviour of interest rates, such as mean reversion and volatility. Those parameters aren’t arbitrary: we want to choose them so that the model is consistent with instruments that are actually traded in the market.

Conceptually:

Market quotes → instruments → model → calibration → model parameters

This is different from bootstrapping a curve.

A curve bootstrapping process asks:

What discount factors are consistent with these market instruments?

Calibration asks:

What model parameters make this model reproduce the prices of these market instruments?

Here is a deliberately compact example:

#include <ql/quantlib.hpp>

using namespace QuantLib;

int main() {
    Date today(9, August, 2026);
    Settings::instance().evaluationDate() = today;

    Calendar calendar = TARGET();
    DayCounter dc = Actual365Fixed();

    // Reuse the curve built in Section 1.
    Handle<YieldTermStructure> curve(
        ext::make_shared<FlatForward>(
            today, 0.03, dc
        )
    );

    // Hull-White model.
    auto model = ext::make_shared<HullWhite>(
        curve,
        0.10,   // mean reversion
        0.01    // short-rate volatility
    );

    // Market instruments used for calibration.
    std::vector<ext::shared_ptr<CalibrationHelper>> helpers;

    std::vector<Period> maturities = {
        Period(1, Years),
        Period(2, Years),
        Period(5, Years),
        Period(10, Years)
    };

    std::vector<Volatility> marketVols = {
        0.20,
        0.21,
        0.23,
        0.25
    };

    for (Size i = 0; i < maturities.size(); ++i) {
        auto helper =
            ext::make_shared<SwaptionHelper>(
                maturities[i],
                Period(5, Years),
                Handle<Quote>(
                    ext::make_shared<SimpleQuote>(
                        marketVols[i]
                    )
                ),
                Euribor6M(curve),
                Period(1, Years),
                Thirty360(Thirty360::BondBasis),
                dc,
                curve
            );

        helpers.push_back(helper);
    }

    // Give every calibration instrument the model.
    auto engine =
        ext::make_shared<TreeSwaptionEngine>(
            model,
            50
        );

    for (auto& helper : helpers)
        helper->setPricingEngine(engine);

    // Optimize the model parameters so model prices
    // reproduce the market instruments.
    model->calibrate(
        helpers,
        LevenbergMarquardt(),
        EndCriteria(
            1000,    // max iterations
            100,     // max stationary state
            1e-8,    // root epsilon
            1e-8,    // function epsilon
            1e-8     // gradient norm epsilon
        )
    );

    std::cout << "Mean reversion: "
              << model->a()
              << '\n';

    std::cout << "Volatility: "
              << model->sigma()
              << '\n';
}

August 9, 2026 0 comments
Order Book C++
Data StructuresInterviewPerformance

Data Structures for Order Book: std::map, std::unordered_map, or std::vector?

by cppforquants July 12, 2026

Ask a C++ developer to design a limit order book, and you will almost always get the same answer: std::map<Price, Level>. It is the answer every tutorial gives, the answer that passes the coding screen, and the answer that feels obviously right: an order book is a collection of price levels that must stay sorted, and std::map is the sorted container. Case closed.

Except that if you look at how production trading systems are actually built, you will struggle to find a red-black tree anywhere near the hot path. The container that “obviously” fits the problem is quietly absent from the systems where the problem matters most.

This article explains why, by putting three candidates through the same test: std::unordered_map, std::map, and — the one nobody suggests in interviews — std::vector.


1.Refresher on std::map, std::unordered_map and std::vectors

What are maps?

A std::map is a sorted associative container storing key-value pairs with unique keys, typically implemented as a red-black tree.

Lookups, insertions, and deletions are all O(log n), and iterating gives you elements in key order.

A good summary video:

What are unordered maps?

A std::unordered_map is a hash table storing key-value pairs with unique keys but no ordering guarantee.

Average O(1) lookup, insert, and erase, degrading to O(n) in the worst case (hash collisions).

The trade-offs: it needs a hash function for the key type, iteration order is unspecified, and the node-based bucket implementation means pointer chasing that can hurt cache performance — which is why HFT code often reaches for open-addressing alternatives like absl::flat_hash_map.

What are vectors?


std::vector is C++’s dynamic array: it stores elements in contiguous memory, gives fast indexed access, and automatically grows when you add more elements.

How to use them to create an order book? But, by the way, what’s an order book?



2. Refresher on order books

An order book is the mechanism that allows a market to match buyers and sellers.

A trader can send:

Buy 100 shares at 99.98
Sell 200 shares at 100.02
Buy 50 shares at market
Cancel order #123
Modify order #456

The exchange maintains the book and decides what happens next.

An order book is the live list of buyers and sellers for a financial instrument.

At first, it is often shown as a table:

Bid SizeBid PriceAsk PriceAsk Size
500 99.98 100.02 300
1,200 99.97 100.03 700
800 99.96 100.04 1,500

The bid side represents buyers.
The ask side represents sellers.

Here, “size” means quantity.

So:

500 at 99.98 means buyers want to buy 500 shares at 99.98.
300 at 100.02 means sellers want to sell 300 shares at 100.02.

Each row is also called a price level.

Level 1 is the best available price on each side:

Level 1 bid = 99.98 x 500
Level 1 ask = 100.02 x 300

Level 2 is the next best price:

Level 2 bid = 99.97 x 1,200
Level 2 ask = 100.03 x 700

Level 3 is the next one after that:

Level 3 bid = 99.96 x 800
Level 3 ask = 100.04 x 1,500

A more natural way to visualize the book is vertically:

            ASK SIDE

Level 3     100.04 x 1,500
Level 2     100.03 x 700
Level 1     100.02 x 300     <- best ask

            spread = 0.04

Level 1      99.98 x 500     <- best bid
Level 2      99.97 x 1,200
Level 3      99.96 x 800

            BID SIDE

The best bid is the highest price someone is willing to buy at.
The best ask is the lowest price someone is willing to sell at.

3. Model an Order Book in C++: Various Attempts

Attempt 1: hash the price levels with std::unordered_map

The message flow suggests an obvious first design. Cancels dominate, and cancels are lookups — so we optimize for lookup. A hash map from price to level gives us O(1) access to any level, and std::unordered_map is sitting right there in the standard library:

cpp

std::unordered_map<Price, Level> bids_;
std::unordered_map<Price, Level> asks_;

Adds are O(1). Cancels are O(1). Modifies are O(1). On paper we’ve made the dominant operations constant-time, and for a few minutes this feels like a solved problem.

Then we implement best_bid().

There is no “first element” in a hash map. Hashing deliberately destroys ordering — that’s what makes it fast — so the only way to find the highest bid is to scan every populated level. The book’s single most frequent query, the one strategy code calls on effectively every tick, has become O(n) over the entire side. Depth walks are worse: there’s no notion of “the next level down” at all; we’d re-scan or sort on demand.

We didn’t build a slow order book. We built something that structurally isn’t an order book. An order book’s defining property is that its levels are ordered — it’s in the name — and we chose the one container whose entire design premise is discarding order. The lookup speed was real, but we optimized the operation that was never going to be the bottleneck and broke the one that defines the product.

Attempt 2: the tree with std::map

So ordering is non-negotiable. The standard library’s ordered associative container is std::map, a red-black tree, and it fixes everything the hash map broke:

std::map<Price, Level, std::greater<Price>> bids_;  // begin() is best bid
std::map<Price, Level> asks_;                        // begin() is best ask

Best bid is bids_.begin() — O(1). Depth walks are in-order traversal. Adds, cancels, and modifies are O(log n), and with a few hundred populated levels, log n is under ten comparisons. This is the textbook answer, it’s correct, and it’s what most order book implementations you’ll find online actually use.

Now replay a day of market data through it and watch what the hardware does.

A full implementation of that version that would make you pass the quant interview in C++ has been proposed in our first article on order books:

using OrderId   = uint64_t;
using Qty       = int64_t;        // signed for partial fills math
using Px       = int64_t;         // price in ticks
enum Side { Buy, Sell };

struct Order {
  OrderId id;
  Side side;
  Px price;
  Qty qty;            // remaining
  uint64_t ts;        // exchange/seq time for tie-breaks
  // intrusive list pointers for O(1) erase
  Order* prev = nullptr;
  Order* next = nullptr;
};

struct Level {
  Px price;
  Order* head = nullptr;
  Order* tail = nullptr;
  inline void push_back(Order* o);
  inline void erase(Order* o);
  bool empty() const { return head == nullptr; }
};

// price → level; bids need descending, asks ascending
using BookSide = std::map<Px, Level, std::greater<Px>>;     // bids
using BookSideAsk = std::map<Px, Level, std::less<Px>>;     // asks

struct OrderBook {
  BookSide bids;
  BookSideAsk asks;
  std::unordered_map<OrderId, Order*> by_id;  // direct handle for cancel/replace

  // API
  void add_limit(OrderId id, Side side, Px px, Qty qty, uint64_t ts);
  void cancel(OrderId id);
  void replace(OrderId id, Px new_px, Qty new_qty, uint64_t ts); // cancel+add semantics
  void match_market(Side side, Qty qty);
  // helpers
  Level& level(BookSide& s, Px px);
  Level& level(BookSideAsk& s, Px px);
};

Attempt 3: keep it sorted, make it contiguous

If pointer chasing is the disease, contiguity is the cure. A sorted std::vector<Level> with binary search keeps the ordering guarantee but lays every level out in a single flat allocation:

cpp

std::vector<Level> bids_;  // sorted; back() is best bid

Lookup is std::lower_bound — still O(log n) comparisons, but now the search touches a handful of cache lines in one array instead of five scattered heap nodes, and the hardware prefetcher can see where we’re going. Best bid is back(). Insertion of a new level requires shifting elements — O(n) in theory — but here the workload rescues us: adds cluster near the touch, so if the vector is sorted with the best price at the back, the memmove is almost always a few elements. In practice this structure embarrasses the tree on a realistic feed.

And yet, benchmark it honestly and there’s a residue we can’t scrub out. Every operation still begins with a search — a binary search is a series of dependent loads, each one’s address unknown until the previous compare resolves, so the pipeline stalls on each step. We’ve made searching cheap. We haven’t asked whether we need to search at all.

Attempt 4: stop searching

Every structure so far has treated the price as an opaque key — something to hash, compare, or binary-search for. But a price in a limit order book is none of those things. Exchanges don’t accept arbitrary prices: every instrument trades on a fixed grid, an integer number of ticks. Two consecutive price levels don’t just happen to be close — they differ by exactly one tick, always. That’s not a statistical tendency we can exploit; it’s a hard constraint the venue enforces on every order.

Once you see prices as grid positions rather than keys, the search problem dissolves. If we know the tick size and pick an anchor price for index zero, then the level for any price isn’t something we find — it’s something we compute.

index = (price − anchor) / tick

One subtraction, one division by a constant, one indexed load into a flat array. No hash function, no comparisons, no dependent loads waiting on the previous step to resolve. The lookup that cost the tree five scattered pointer dereferences and cost the sorted vector a pipeline-stalling binary search is now cheaper than either structure’s first step.

4. A Proposition of Implementation for Attempt 4

In the approach we’re presenting, prices are ticks on a fixed grid, so the book is a flat array of price levels where a price is converted to an index by one subtraction — no hashing, no tree, no search — with a cached best index and each level holding a FIFO of pool-allocated orders linked intrusively.

Adds, cancels, and executions each cost an arithmetic index or an O(1) ID lookup plus an O(1) unlink, touching two or three cache lines and zero heap allocations on the hot path.

Let’s start with an order_book.hpp:

// order_book.hpp — tick-indexed limit order book
//
// Design (see article): contiguous array of price levels indexed by tick
// offset, intrusive doubly-linked FIFO per level, pre-allocated order pool,
// open-addressing map for OrderId -> Order*. Zero heap allocation on the
// hot path after construction.
//
// Conventions:
//   - Price is an integer number of ticks (scale at the feed decoder).
//   - OrderId 0 and ~0 are reserved (empty / tombstone sentinels in IdMap).

#pragma once

#include <cassert>
#include <cstddef>
#include <cstdint>
#include <vector>

using Price   = std::int64_t;
using Qty     = std::int64_t;
using OrderId = std::uint64_t;

enum class Side : std::uint8_t { Bid, Ask };

// ---------------------------------------------------------------------------
// Order: 64 bytes, cache-line aligned. prev/next are intrusive — the order
// *is* its own list node, so joining/leaving a level allocates nothing.
// ---------------------------------------------------------------------------
struct alignas(64) Order {
    OrderId id;
    Qty     qty;
    Price   price;
    Side    side;
    Order*  prev;
    Order*  next;
};
static_assert(sizeof(Order) == 64, "one order per cache line");

// ---------------------------------------------------------------------------
// OrderPool: all orders live in one contiguous slab allocated at startup.
// alloc/release are a free-list push/pop — no new/delete on the hot path.
// ---------------------------------------------------------------------------
class OrderPool {
public:
    explicit OrderPool(std::size_t capacity) : slots_(capacity) {
        free_.reserve(capacity);
        for (std::size_t i = capacity; i-- > 0;)
            free_.push_back(&slots_[i]);
    }

    Order* alloc() {
        assert(!free_.empty() && "pool exhausted — size it to the session");
        Order* o = free_.back();
        free_.pop_back();
        return o;
    }

    void release(Order* o) { free_.push_back(o); }

private:
    std::vector<Order>  slots_;
    std::vector<Order*> free_;
};

// ---------------------------------------------------------------------------
// Level: FIFO queue of resting orders at one price. head is oldest (first to
// fill), tail is where adds append — price-time priority falls out of the
// list order. total_qty is maintained incrementally so top-of-book snapshots
// never walk the list.
// ---------------------------------------------------------------------------
struct Level {
    Order* head      = nullptr;
    Order* tail      = nullptr;
    Qty    total_qty = 0;

    bool empty() const { return head == nullptr; }

    void push_back(Order* o) {
        o->prev = tail;
        o->next = nullptr;
        if (tail) tail->next = o; else head = o;
        tail = o;
        total_qty += o->qty;
    }

    // O(1) given the order pointer — no search. This is why cancels, the
    // dominant message type, stay cheap.
    void unlink(Order* o) {
        if (o->prev) o->prev->next = o->next; else head = o->next;
        if (o->next) o->next->prev = o->prev; else tail = o->prev;
        total_qty -= o->qty;
    }
};

// ---------------------------------------------------------------------------
// BookSide: the tick-indexed array. Price -> level is one subtraction and
// one indexed load; no hashing, no comparisons, no pointer chasing.
// best_ caches the top of book; when the top level empties we scan linearly
// toward the interior — adjacent prices are adjacent cache lines, and the
// next populated level is almost always within a few ticks.
// ---------------------------------------------------------------------------
template <bool IsBid>
class BookSide {
public:
    static constexpr std::size_t kLevels = std::size_t{1} << 16;
    static constexpr std::size_t kNone   = ~std::size_t{0};

    explicit BookSide(Price anchor)
        : levels_(kLevels), anchor_(anchor) {}

    void add(Order* o) {
        const std::size_t idx = index(o->price);
        levels_[idx].push_back(o);
        if (best_ == kNone || better(idx, best_)) best_ = idx;
    }

    void remove(Order* o) {
        const std::size_t idx = index(o->price);
        levels_[idx].unlink(o);
        if (idx == best_ && levels_[idx].empty()) advance_best();
    }

    void reduce(Order* o, Qty by) {
        o->qty -= by;
        levels_[index(o->price)].total_qty -= by;
    }

    bool  empty()      const { return best_ == kNone; }
    Price best_price() const { return anchor_ + static_cast<Price>(best_); }
    Qty   best_qty()   const { return levels_[best_].total_qty; }
    const Order* best_order() const { return levels_[best_].head; }

private:
    std::size_t index(Price p) const {
        assert(p >= anchor_ &&
               static_cast<std::size_t>(p - anchor_) < kLevels &&
               "price outside book range — re-anchor on the slow path");
        return static_cast<std::size_t>(p - anchor_);
    }

    static bool better(std::size_t a, std::size_t b) {
        if constexpr (IsBid) return a > b;   // best bid = highest price
        else                 return a < b;   // best ask = lowest price
    }

    void advance_best() {
        if constexpr (IsBid) {
            while (best_ != 0) {
                --best_;
                if (!levels_[best_].empty()) return;
            }
        } else {
            while (best_ + 1 < kLevels) {
                ++best_;
                if (!levels_[best_].empty()) return;
            }
        }
        best_ = kNone;   // side is empty
    }

    std::vector<Level> levels_;   // one flat allocation, made once
    Price              anchor_;   // price at index 0
    std::size_t        best_ = kNone;
};

// ---------------------------------------------------------------------------
// IdMap: OrderId -> Order*, open addressing with linear probing. Flat slot
// array — one hash, then a short contiguous probe. Sized 2x expected load
// at construction; erases leave tombstones (fine for a trading session,
// rebuild between sessions if reusing).
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// IdMap: OrderId -> Order*, open addressing with linear probing. Flat slot
// array — one hash, then a short contiguous probe. Sized 2x expected load
// at construction; erases leave tombstones (fine for a trading session,
// rebuild between sessions if reusing).
// ---------------------------------------------------------------------------
class IdMap {
public:
    explicit IdMap(std::size_t expected) {
        std::size_t cap = 1;
        while (cap < expected * 2) cap <<= 1;
        slots_.assign(cap, Slot{kEmpty, nullptr});
        mask_ = cap - 1;
    }

    void insert(OrderId id, Order* o) {
        std::size_t i = hash(id) & mask_;
        while (slots_[i].key != kEmpty && slots_[i].key != kTomb)
            i = (i + 1) & mask_;
        slots_[i] = Slot{id, o};
    }

    Order* find(OrderId id) const {
        std::size_t i = hash(id) & mask_;
        while (slots_[i].key != kEmpty) {
            if (slots_[i].key == id) return slots_[i].val;
            i = (i + 1) & mask_;
        }
        return nullptr;
    }

    void erase(OrderId id) {
        std::size_t i = hash(id) & mask_;
        while (slots_[i].key != kEmpty) {
            if (slots_[i].key == id) {
                slots_[i].key = kTomb;
                slots_[i].val = nullptr;
                return;
            }
            i = (i + 1) & mask_;
        }
    }

private:
    static constexpr OrderId kEmpty = 0;
    static constexpr OrderId kTomb  = ~OrderId{0};

    struct Slot { OrderId key; Order* val; };

    static std::size_t hash(OrderId id) {   // splitmix64 finalizer
        std::uint64_t x = id;
        x ^= x >> 33; x *= 0xff51afd7ed558ccdULL;
        x ^= x >> 33; x *= 0xc4ceb9fe1a85ec53ULL;
        x ^= x >> 33;
        return static_cast<std::size_t>(x);
    }

    std::vector<Slot> slots_;
    std::size_t       mask_;
};

// ---------------------------------------------------------------------------
// OrderBook: ties the pieces together. The three feed-driven mutations map
// onto it directly:
//   Add     -> pool alloc, arithmetic index, list append   (0 allocations)
//   Cancel  -> id lookup, O(1) unlink, pool release        (0 allocations)
//   Execute -> id lookup, reduce or remove                 (0 allocations)
// ---------------------------------------------------------------------------
class OrderBook {
public:
    struct Quote { Price price; Qty qty; bool valid; };

    OrderBook(Price anchor, std::size_t max_live_orders)
        : bids_(anchor), asks_(anchor),
          pool_(max_live_orders), ids_(max_live_orders) {}

    void add(OrderId id, Side side, Price price, Qty qty) {
        Order* o = pool_.alloc();
        *o = Order{id, qty, price, side, nullptr, nullptr};
        if (side == Side::Bid) bids_.add(o); else asks_.add(o);
        ids_.insert(id, o);
    }

    void cancel(OrderId id) {
        if (Order* o = ids_.find(id)) remove(o);
    }

    // Execution reported by the feed against a resting order.
    void execute(OrderId id, Qty exec_qty) {
        Order* o = ids_.find(id);
        if (!o) return;
        if (exec_qty >= o->qty) {
            remove(o);
        } else if (o->side == Side::Bid) {
            bids_.reduce(o, exec_qty);
        } else {
            asks_.reduce(o, exec_qty);
        }
    }

    Quote best_bid() const {
        return bids_.empty() ? Quote{0, 0, false}
                             : Quote{bids_.best_price(), bids_.best_qty(), true};
    }

    Quote best_ask() const {
        return asks_.empty() ? Quote{0, 0, false}
                             : Quote{asks_.best_price(), asks_.best_qty(), true};
    }

private:
    void remove(Order* o) {
        if (o->side == Side::Bid) bids_.remove(o); else asks_.remove(o);
        ids_.erase(o->id);
        pool_.release(o);
    }

    BookSide<true>  bids_;
    BookSide<false> asks_;
    OrderPool       pool_;
    IdMap           ids_;
};

How to test this approach?

Create a demo.cpp:

#include "order_book.hpp"
#include <cstdio>

static void print_top(const OrderBook& book, const char* tag) {
    auto b = book.best_bid();
    auto a = book.best_ask();
    std::printf("%-28s  bid: ", tag);
    if (b.valid) std::printf("%lld x %lld", (long long)b.qty, (long long)b.price);
    else         std::printf("--");
    std::printf("   ask: ");
    if (a.valid) std::printf("%lld x %lld", (long long)a.qty, (long long)a.price);
    else         std::printf("--");
    std::printf("\n");
}

int main() {
    // Anchor at tick 10'000, capacity for 1M live orders.
    OrderBook book(/*anchor=*/10'000, /*max_live_orders=*/1'000'000);

    book.add(1, Side::Bid, 10'100, 500);
    book.add(2, Side::Bid, 10'101, 300);   // better bid
    book.add(3, Side::Bid, 10'101, 200);   // joins queue behind id 2
    book.add(4, Side::Ask, 10'103, 400);
    book.add(5, Side::Ask, 10'102, 250);   // better ask
    print_top(book, "after adds");

    book.cancel(2);                        // partial drain of best bid level
    print_top(book, "cancel id 2");

    book.cancel(3);                        // best bid level empties -> scan inward
    print_top(book, "cancel id 3");

    book.execute(5, 100);                  // partial execution at best ask
    print_top(book, "execute 100 vs id 5");

    book.execute(5, 150);                  // fills remainder -> level empties
    print_top(book, "execute 150 vs id 5");

    book.cancel(1);
    book.cancel(4);
    print_top(book, "book emptied");

    return 0;
}

Let’s compile and run:

g++ -std=c++20 -O2 -Wall -Wextra -o demo demo.cpp && ./demo

Which gives:

after adds                    bid: 500 x 10101   ask: 250 x 10102
cancel id 2                   bid: 200 x 10101   ask: 250 x 10102
cancel id 3                   bid: 500 x 10100   ask: 250 x 10102
execute 100 vs id 5           bid: 500 x 10100   ask: 150 x 10102
execute 150 vs id 5           bid: 500 x 10100   ask: 400 x 10103
book emptied                  bid: --   ask: --
July 12, 2026 0 comments
Best C++ libraries for parallel processing
LibrariesPerformance

Best C++ Libraries for Parallel Programming

by cppforquants June 11, 2026

One of the most important topics in C++ is parallel programming. While the C++ Standard Library provides foundational concurrency primitives such as std::thread, std::mutex, and std::async, or more recent SIMD additions, many real-world applications benefit from higher-level abstractions. Modern parallel programming libraries offer task schedulers, work-stealing runtimes, dependency graphs, distributed execution models, and performance-portable frameworks that dramatically simplify the development of scalable systems. What are the best C++ libraries for parallel programming?

1. OpenMP

OpenMP (Open Multi-Processing) is an open standard for shared-memory parallel programming that allows developers to parallelize code using compiler directives, library routines, and environment variables. It’s one of the best C++ libraries for parallel programming.

It was first introduced in 1997 by the OpenMP Architecture Review Board (ARB), a consortium of hardware and software companies that included organizations such as Intel, IBM, Hewlett-Packard, and others. The goal was to create a portable and vendor-neutral standard for exploiting multiple CPU cores on shared-memory systems.

Monte Carlo pricing is a classic example of an embarrassingly parallel workload. By distributing simulation paths across multiple CPU cores, OpenMP can significantly reduce execution times with only a few additional lines of code.

Let’s create a “monte_carlo.cpp” file:

#include <omp.h>
#include <cmath>
#include <random>
#include <vector>
#include <iostream>

double simulate_option_price(
    double spot,
    double strike,
    double rate,
    double vol,
    double maturity,
    int num_paths)
{
    double payoff_sum = 0.0;

    #pragma omp parallel
    {
        std::mt19937 rng(42 + omp_get_thread_num());
        std::normal_distribution<> normal(0.0, 1.0);

        double local_sum = 0.0;

        #pragma omp for
        for (int i = 0; i < num_paths; ++i)
        {
            double z = normal(rng);

            double st =
                spot * std::exp(
                    (rate - 0.5 * vol * vol) * maturity +
                    vol * std::sqrt(maturity) * z);

            local_sum += std::max(st - strike, 0.0);
        }

        #pragma omp atomic
        payoff_sum += local_sum;
    }

    return std::exp(-rate * maturity) * payoff_sum / num_paths;
}

int main()
{
    double price = simulate_option_price(
        100.0,
        100.0,
        0.05,
        0.20,
        1.0,
        10'000'000);

    std::cout << "Option Price: " << price << '\n';
}

In the code above, each thread is responsible for a portion of the Monte Carlo simulations. Because individual simulation paths are completely independent, they can be executed concurrently on multiple CPU cores before their results are aggregated into a final option price estimate.

Compiling the Example

OpenMP is implemented through compiler support rather than as a standalone library. When the compiler encounters OpenMP directives such as #pragma omp parallel or #pragma omp for, it generates the necessary multithreaded code and links against the OpenMP runtime.

To compile the example using GCC:

g++ -O3 -fopenmp monte_carlo.cpp -o monte_carlo

The -fopenmp flag enables OpenMP support and links the OpenMP runtime library. Without this flag, the compiler will ignore the OpenMP directives and execute the code sequentially.

On macOS, the default Apple Clang compiler does not always include OpenMP support. In this case, developers typically install LLVM or GCC through Homebrew and compile the program using an OpenMP-enabled compiler.

Then execute the code:

./monte_carlo

The simulation above will be split on different threads before an aggregation step:

2.oneTBB

oneTBB (formerly Intel Threading Building Blocks) is a task-based parallel programming library created by Intel and first released in 2006. Rather than managing threads directly, developers express work as tasks, allowing oneTBB’s scheduler to efficiently distribute computation across multiple CPU cores.

Widely used in high-performance computing, quantitative finance, and scientific applications, oneTBB provides parallel algorithms, concurrent containers, and a work-stealing scheduler designed to simplify scalable multicore development.

A bank needs to recompute a risk metric for 50,000 portfolios after a market move. Since each portfolio can be processed independently, the workload is naturally parallel. Instead of manually creating and managing threads, oneTBB distributes the portfolios across available CPU cores and balances the work automatically.

#include <oneapi/tbb/parallel_for.h>
#include <vector>

struct Portfolio
{
    std::string portfolio_id;
    std::vector<double> trade_dv01s;
};

double compute_risk(const Portfolio& portfolio)
{
    double dv01 = 0.0;

    for(double trade_dv01 : portfolio.trade_dv01s)
    {
        dv01 += trade_dv01;
    }

    return dv01;
}

int main()
{
    std::vector<Portfolio> portfolios(50000);
    std::vector<double> risks(portfolios.size());

    oneapi::tbb::parallel_for(
        size_t(0),
        portfolios.size(),
        [&](size_t i)
        {
            risks[i] = compute_risk(portfolios[i]);
        });

    return 0;
}

In this example, each portfolio can be evaluated independently, making the workload embarrassingly parallel. The parallel_for algorithm automatically divides the portfolio universe into smaller chunks and schedules them across available CPU cores. Unlike traditional thread-based approaches, developers do not need to manage thread creation, synchronization, or load balancing manually. This allows applications to scale efficiently on multicore systems while keeping the code concise and maintainable.

3.TaskFlow

Taskflow is a modern C++ parallel programming library that allows developers to express applications as task dependency graphs (DAGs) rather than individual threads or loops. It automatically schedules tasks, manages dependencies, and executes workflows efficiently across available CPU cores, making it particularly well-suited for data pipelines, simulations, and complex computational workflows. Taskflow is one the best C++ libraries for parallel programming.

The project was first presented publicly in 2019 as “Cpp-Taskflow: Fast Task-Based Parallel Programming Using Modern C++”.


The following example models a simple risk analytics pipeline. Market data must be loaded before risk calculations can begin, while independent calculations can run in parallel. Once all computations are complete, a report is generated

#include <taskflow/taskflow.hpp>

int main() {

    tf::Executor executor;
    tf::Taskflow taskflow;

    auto load_market_data = taskflow.emplace([]{
        std::cout << "Loading market data\n";
    });

    auto calculate_greeks = taskflow.emplace([]{
        std::cout << "Calculating Greeks\n";
    });

    auto calculate_var = taskflow.emplace([]{
        std::cout << "Computing VaR\n";
    });

    auto generate_report = taskflow.emplace([]{
        std::cout << "Generating report\n";
    });

    load_market_data.precede(calculate_greeks);
    calculate_greeks.precede(calculate_var);
    calculate_var.precede(generate_report);

    executor.run(taskflow).wait();
}

Unlike OpenMP and oneTBB, which primarily focus on parallel loops and tasks, Taskflow allows developers to express entire applications as dependency graphs. Independent tasks can execute concurrently, while dependent tasks automatically wait for their prerequisites to complete. This approach is particularly useful for data pipelines, machine learning workflows, risk calculations, and other complex computational processes.

4.HPX

HPX is a modern C++ runtime system designed for scalable parallel and distributed applications. It extends the C++ standard library with asynchronous programming primitives such as futures, parallel algorithms, and task scheduling, allowing developers to write code that can scale from a laptop to a large computing cluster with minimal changes.

Typical Use Cases

  • Scientific computing
  • Distributed simulations
  • Numerical methods
  • Large-scale graph processing
  • HPC applications
  • Quantitative finance workloads requiring cluster-scale execution

Imagine a trading platform receives market data from multiple exchanges. Instead of processing each feed sequentially, HPX can launch asynchronous tasks and combine the results once all feeds have been processed.

#include <hpx/hpx_main.hpp>
#include <hpx/include/async.hpp>

std::vector<Tick> process_feed(const std::string& exchange);

int main()
{
    auto nyse = hpx::async(process_feed, "NYSE");
    auto nasdaq = hpx::async(process_feed, "NASDAQ");
    auto cboe = hpx::async(process_feed, "CBOE");

    auto nyse_ticks = nyse.get();
    auto nasdaq_ticks = nasdaq.get();
    auto cboe_ticks = cboe.get();

    merge_market_data(
        nyse_ticks,
        nasdaq_ticks,
        cboe_ticks
    );
}

In this example, market data from multiple exchanges is processed concurrently using HPX futures. Each feed is handled asynchronously, allowing the application to utilize available computing resources efficiently while avoiding unnecessary blocking. Once all tasks complete, the results are merged into a unified market view.

In summary, HPX is one of the best C++ libraries for parallel programming!

5. A Summary of Pros and Cons

The libraries covered in this article address different parallel programming challenges, from simple loop parallelism to task scheduling, workflow orchestration, and distributed execution. The best choice depends on the complexity of your workload and how much control you need over execution.

LibraryStrengthsWeaknesses
OpenMPEasy to learn, simple loop parallelism, broad compiler supportLimited flexibility for complex task dependencies
oneTBBTask-based programming, automatic load balancing, scalable runtimeMore concepts to learn than OpenMP
TaskflowElegant workflow graphs (DAGs), intuitive dependency managementSmaller ecosystem and fewer learning resources
HPXFutures, asynchronous execution, distributed computing supportSteeper learning curve and more advanced programming model

Choosing the Right Library

  • OpenMP is ideal when you need to parallelize loops with minimal code changes.
  • oneTBB is a strong choice for applications composed of many independent tasks.
  • Taskflow excels at modelling complex workflows with explicit dependencies.
  • HPX is designed for highly scalable asynchronous applications that may span multiple machines.

In short: OpenMP focuses on loops, oneTBB on tasks, Taskflow on workflows, and HPX on asynchronous and distributed execution. Together, they represent a progression from straightforward multicore programming to advanced parallel and distributed systems.

June 11, 2026 0 comments
Llama.cpp Internals
AI

Llama.cpp Internals: The Secret Behind Fast LLMs

by cppforquants May 7, 2026

Modern AI is supposed to require massive GPUs, enormous cloud clusters, and billions of dollars in infrastructure. Yet somehow, llama.cpp can run surprisingly capable large language models on a laptop CPU, a MacBook, or even embedded hardware. In quant finance, low-latency systems are built around the same principles that make llama.cpp fast: cache efficiency, predictable memory access, SIMD acceleration, aggressive optimization, and minimizing unnecessary abstraction layers. High-frequency trading firms spend years optimizing nanoseconds out of market data pipelines, order routing systems, and pricing engines. Modern LLM inference is increasingly facing similar constraints. What are the secrets of the llama.cpp internals?

1.What is Lama.cpp?

llama.cpp is a high-performance C/C++ inference engine designed to run large language models (LLMs) efficiently on local hardware.

Originally created to run Meta’s LLaMA models on consumer CPUs, it has evolved into one of the most widely used runtimes for local AI inference. The features combine efficient quantization support, multi-model compatibility, advanced token sampling strategies, privacy-focused local inference, and highly optimized CPU/GPU execution.

2.What’s inside llama.cpp?

So, what are those llama.cpp internals? It begins with tokenization, where input text is split into discrete tokens and mapped to numerical IDs. These tokens are then converted into dense vector embeddings that represent semantic meaning in a form the model can efficiently process. The embeddings flow through the Transformer network, which is the core of the model and is responsible for most of the computation through stacked layers of self-attention and feed-forward operations. The Transformer outputs logits over the entire vocabulary, which are then passed through a sampling strategy to select the next token. This token is appended to the context, and the process repeats in an autoregressive loop until the output is complete.

To make this efficient on consumer hardware, llama.cpp relies on key systems-level optimizations such as quantized weights and the KV cache, which reuses previously computed attention states to avoid redundant work during long context generation.

KV-Caching is really where the secret sauce is, avoiding to recompute the attention over the entire input sequence at every new token generation step by storing and reusing previously computed key and value tensors from earlier tokens:

The consequences on performance are impressive both in terms of latency:

But there is no free lunch: the speed comes at the cost of increased memory usage, since the KV cache must store intermediate key and value tensors for every token in the context, and this footprint grows linearly with sequence length.

The breakdown of memory needed by token is unforgiving:

3.Install llama.cpp

You can install llama.cpp in a few different ways depending on whether you want control, speed of setup, or deployment flexibility.

The most common approach is to build it from source. This gives you full control over compilation flags and performance optimizations, and produces the core binaries like llama-cli and llama-server.

git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp

cmake -B build
cmake --build build -j

Once built, you can immediately run a model using the CLI by pointing it to a GGUF file and providing a prompt for generation.

./build/bin/llama-cli -m models/model.gguf -p "Explain risk parity in finance" -n 200

If you want to skip compilation entirely, prebuilt binaries (when available for your platform) let you run inference directly. This is useful for quick testing or experimentation without setting up a build environment.

./llama-cli -m model.gguf -p "What is CAPM?" -n 200

For a more isolated and reproducible setup, you can run llama.cpp inside Docker. This avoids local dependency issues and is often used for deployment or server environments.

docker run -it --rm \
-v $(pwd)/models:/models \
ghcr.io/ggml-org/llama.cpp:latest \
llama-cli -m /models/model.gguf -p "Explain portfolio optimization" -n 200

To expose inference as a service, you can run the built-in server mode inside Docker and interact with it via HTTP, which is useful for integrating LLMs into applications or internal tools.

docker run -it --rm \
-p 8080:8080 \
-v $(pwd)/models:/models \
ghcr.io/ggml-org/llama.cpp:latest \
llama-server -m /models/model.gguf --host 0.0.0.0 --port 8080

You can then query it like a simple API endpoint, which is often how it is integrated into larger systems.

curl http://localhost:8080/completion -d '{
"prompt": "Explain Sharpe ratio",
"n_predict": 150
}'

On supported hardware, you can optionally enable GPU or accelerator backends during compilation to improve performance significantly. On Apple Silicon this uses Metal acceleration, while NVIDIA systems can use CUDA.

cmake -B build -DGGML_METAL=ON
cmake --build build -j

4. Some Use Cases for Quantitative Finance

In quantitative finance, llama.cpp internals matter and running models locally with llama.cpp is not about replacing trading systems, but about accelerating research, analysis, and decision support with low-latency, privacy-preserving inference.

One of the most immediate use cases is research assistance for strategy development. A local LLM can be used to explain or prototype ideas like portfolio optimization, factor models, or risk parity without sending sensitive research data to external APIs. Optimized llama.cpp internals play a big role!

Want to explain something in your portfolio optimization process?

./llama-cli -m model.gguf \
-p "Explain how mean-variance optimization is used in portfolio construction and derive the objective function" \
-n 300

Another practical use case is market microstructure analysis. Models can help summarize or interpret order book dynamics, liquidity conditions, or short-term price signals, which are often difficult to reason about quickly during research.

./llama-cli -m model.gguf \
-p "How does order book imbalance relate to short-term price movement in high-frequency trading?" \
-n 250

llama.cpp is also useful for risk analysis and scenario reasoning. Quant teams can use it to generate explanations or structured breakdowns of risk metrics, stress testing approaches, and portfolio exposure.

./llama-cli -m model.gguf \
-p "Compare VaR and CVaR and explain when each measure can fail under extreme market conditions" \
-n 300

A more advanced application is integrating llama.cpp into internal tools for research workflows. Since it can run as a local server, it can power internal copilots that sit next to proprietary datasets, allowing analysts to query models without exposing sensitive information externally.

./llama-server -m model.gguf --host 0.0.0.0 --port 8080

Finally, in low-latency environments, llama.cpp can be embedded directly into C++ systems for fast inference. While it is not used for execution-critical trading decisions, it can support real-time analytics, research dashboards, or decision-support tools where response time and data locality matter.

llama_model * model = llama_load_model_from_file("model.gguf", llama_model_default_params());
llama_context * ctx = llama_new_context_with_model(model, llama_context_default_params());

const char * prompt = "Explain pairs trading using a Kalman filter";
llama_eval(ctx, prompt, strlen(prompt), 0, 8);

char output[1024];
llama_get_next_token(ctx, output, sizeof(output));
printf("%s\n", output);

Overall, the value of llama.cpp in quantitative finance comes from its ability to bring LLM inference closer to the data and the system: reducing latency, improving privacy, and enabling tight integration with existing C++-based research and infrastructure stacks.

5. Alternatives to llama.cpp

While llama.cpp is one of the most popular runtimes for local LLM inference, especially on CPUs, there are several alternatives depending on whether you prioritize GPU throughput, production serving, or ecosystem tooling.

One major alternative is vLLM, which is designed for high-throughput GPU inference. It uses techniques like paged attention to efficiently manage memory during batching and is widely used in production LLM serving systems where throughput matters more than CPU efficiency.

pip install vllm

python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3-8B-Instruct

Another option is TensorRT-LLM, which is highly optimized for NVIDIA GPUs. It focuses on maximizing inference speed using low-level kernel optimizations and is commonly used in enterprise-grade deployments where GPU performance is critical.

trtllm-build --model llama-3-8b
trtllm-run --engine model.engine

For more general-purpose deep learning workflows, PyTorch (with Hugging Face Transformers) remains the most flexible option. It is not optimized for CPU inference like llama.cpp, but it is widely used for prototyping, fine-tuning, and research.

from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3-8B")

inputs = tokenizer("Explain portfolio optimization", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=200)

For Apple devices, MLX is an emerging alternative optimized specifically for Apple Silicon. It provides a more native experience on macOS compared to CUDA-centric stacks and is designed for efficient local inference.

import mlx_lm

model, tokenizer = mlx_lm.load("mlx-community/Llama-3-8B")
mlx_lm.generate(model, tokenizer, "Explain risk parity", max_tokens=200)

Finally, Ollama provides a higher-level abstraction over local LLMs, including llama.cpp under the hood in many cases. It focuses on developer experience, making it easy to run models locally with minimal setup.

ollama run llama3 "Explain the Sharpe ratio"

In summary:

  • llama.cpp → best for CPU inference, low-latency local systems, edge deployment thanks to llama.cpp internals
  • vLLM / TensorRT-LLM → best for high-throughput GPU serving
  • Transformers (PyTorch) → best for research and flexibility
  • MLX → best for Apple Silicon native performance
  • Ollama → best for simple developer experience

Each tool occupies a different layer of the LLM stack, from research flexibility to production-scale inference to lightweight local execution

May 7, 2026 0 comments
  • 1
  • 2
  • 3
  • …
  • 11

@2025 - All Right Reserved.


Back To Top
  • Home
  • Contact
  • About