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

Clement D.

Clement D.

Hedge Fund for C++ Developers
Jobs

Best Hedge Funds For Quantitative Developers

by Clement D. July 10, 2025

In 2025, a select cohort of hedge funds and prop trading firms is fiercely competing for elite quantitative developers—those adept in coding, statistics, and machine learning. Firms like Citadel, D.E. Shaw, Two Sigma, and others are leading the charge, offering six‑figure base salaries and performance bonuses tied directly to alpha generated. What are the best hedge funds for quantitative developers?

1. Citadel

Citadel and Citadel Securities continue their aggressive recruitment, launching intensive internship pipelines with record-low acceptance rates (0.4%) to secure the next generation of quant talent. Summer interns can earn as much as $5,000 per week—an early indicator of the hyper-competitive environment. With approximately $65 billion in assets under management, the firm is deeply reliant on advanced technology, and quant developers play a central role in driving trading decisions, building low-latency systems, and maintaining scalable infrastructure. Citadel’s recruitment is global, with open roles in major financial hubs like New York, London, Miami, Gurugram, and Hong Kong.

Citadel’s hiring funnel is notoriously selective. Their internship program, a key gateway to full-time roles, had a 0.4% acceptance rate this year—more competitive than top-tier tech firms. Interns can earn up to $24,000 per month, reflecting the high value Citadel places on early talent. These internships are intensive and structured to transition into permanent positions quickly.

Full-time quantitative developer roles offer some of the highest compensation in the industry, with total packages ranging from $200,000 to over $700,000 per year, and a median near $550,000. Citadel Securities, the firm’s market-making division, offers similarly lucrative packages for developer positions focused on execution engines and infrastructure.

The firm places a premium on engineers with strong coding ability in C++, Python, and systems-level programming, as well as deep understanding of algorithms, data structures, and statistics. Citadel is expanding in regions like India, particularly targeting IIT graduates for roles in equity derivatives technology.

2. D.E. Shaw

D.E. Shaw remains one of the most prestigious and desirable hedge funds hiring quantitative developers. Founded in 1988, the firm has built its reputation on rigorous research, engineering excellence, and a collaborative, low-ego culture that appeals strongly to top STEM graduates and seasoned engineers alike. With offices in New York, London, and Hyderabad, D.E. Shaw offers global opportunities for quant devs to work on high-impact systems supporting both systematic and discretionary trading strategies.

Quantitative developers at D.E. Shaw are deeply embedded in cross-functional teams, partnering closely with researchers and portfolio managers. They build and optimize everything from execution platforms and backtesting frameworks to pricing engines and large-scale data ingestion systems. The firm’s approach is highly academic, often drawing in PhDs in computer science, physics, and mathematics, but equally welcoming experienced software engineers from top tech firms.

The firm’s hiring process is known for being intellectually demanding but fair, focusing on algorithmic problem solving, systems design, and real-world coding skills. Compensation is highly competitive, with total packages for junior developers often exceeding $400,000 and rising quickly with experience. Unlike some more aggressive competitors, D.E. Shaw places a greater emphasis on long-term innovation and internal mobility, rather than rapid iteration.

D.E. Shaw continues to prioritize talent development through structured mentorship, technical training, and a strong internal engineering culture. The firm is particularly attractive to candidates who value technical depth, thoughtful problem solving, and a strong sense of intellectual camaraderie. In 2025, it remains a top-tier choice for quant developers seeking a high-impact, research-driven engineering career in finance.

3. Two Sigma

In 2025, Two Sigma continues to distinguish itself as one of the most engineering-driven hedge funds hiring quantitative developers. Based in New York with a global presence, the firm operates at the intersection of finance, data science, and cutting-edge software engineering. Unlike some peers that prioritize trading speed above all, Two Sigma is renowned for its research-first culture and thoughtful approach to building scalable, maintainable systems that support a wide range of data-driven investment strategies.

Quant developers at Two Sigma are more than infrastructure engineers—they build the platforms, tools, and pipelines that power research and trading. From developing custom machine learning frameworks to managing terabytes of alternative data, their work enables researchers to test hypotheses at scale and deploy production strategies with minimal friction. This blend of software craftsmanship and statistical rigor makes Two Sigma a magnet for developers from Google, Meta, and top academic institutions.

The hiring process is structured around deep technical assessments, covering data structures, algorithms, distributed systems, and applied ML. Interviews are known for being intense but well-organized, with an emphasis on real-world engineering challenges rather than trick questions. Compensation is highly attractive, with total packages for mid-level developers typically ranging from $400K to $600K, along with generous perks and equity-like incentives.

Two Sigma’s engineering culture is known for its clean code, peer reviews, mentorship, and internal tooling excellence. It is particularly appealing to developers who want to work in a rigorous yet collaborative environment where the long-term quality of systems matters as much as short-term gains. For quantitative developers who value a balance of intellectual depth, modern software practices, and strong research collaboration, Two Sigma remains one of the most desirable destinations in 2025.

4. Jump Trading

Jump Trading ranks among the top hedge funds aggressively hiring quantitative developers, particularly those with expertise in low-latency systems and high-performance computing. Headquartered in Chicago with key offices in London, Singapore, and New York, Jump operates as a technology-centric trading firm where developers play a foundational role in shaping the firm’s competitive edge in high-frequency markets.

Quantitative developers at Jump are responsible for building ultra-low-latency trading infrastructure, co-located exchange connectivity, and high-throughput data pipelines. The work is performance-critical—developers routinely optimize nanosecond-level latency in C++, tune networking stacks, and architect systems that process millions of messages per second. This makes Jump a prime destination for engineers who thrive on precision, speed, and scale.

Jump’s hiring process is notoriously rigorous. The firm recruits from the most elite technical talent pools—top-tier CS programs, Olympiad medalists, and systems engineers from Google, Meta, and Nvidia. Interviews emphasize C++ mastery, concurrency, networking, and real-time system design. Candidates should expect deep-dive technical sessions with a strong focus on engineering fundamentals and execution under pressure.

Compensation at Jump is among the highest in the industry. Total packages for experienced quant devs can reach $700K to $1M+, with highly lucrative performance-based bonuses. Even junior roles offer salaries that rival or exceed those at top tech companies. The firm’s flat structure means that developers can see their work deployed quickly and directly affect P&L.

What sets Jump apart culturally is its research-driven, R&D-focused environment. The firm funds open-source work, sponsors academic research, and even explores crypto markets and digital asset infrastructure through its affiliate, Jump Crypto. For quantitative developers who want to work on bleeding-edge systems in a highly autonomous, deeply technical environment, Jump Trading offers one of the most exciting opportunities in 2025.

5. Hudson River Trading

Hudson River Trading (HRT) stands out as one of the most sought-after firms for quantitative developers seeking a balance between technical excellence, compensation, and culture. Headquartered in New York, HRT is a major player in high-frequency trading, operating across equities, futures, options, and crypto markets. The firm is widely respected for its engineering-first mindset and flat organizational structure, where developers work shoulder-to-shoulder with researchers to build and optimize trading systems from scratch.

Quantitative developers at HRT contribute directly to all layers of the trading stack, including strategy simulation platforms, real-time risk engines, and execution frameworks. The environment demands both creativity and precision—developers frequently write latency-critical C++, build robust Python backtests, and design resilient systems capable of reacting to live market conditions within microseconds. HRT’s infrastructure is primarily built in-house, giving engineers full ownership and the ability to innovate quickly.

The hiring process is designed to identify world-class problem solvers and systems thinkers. Candidates are tested on advanced algorithms, computational efficiency, concurrency, and low-level debugging. HRT regularly recruits from elite programming competitions like the ICPC and Codeforces, and from top computer science programs globally. The interview process is technical and fast-paced, but also fair and transparent.

Compensation is highly competitive. Total compensation for quantitative developers commonly ranges from $350K to $700K+, including generous year-end bonuses tied to firm performance. Despite the fast-moving markets it operates in, HRT is known for maintaining a healthier work-life balance than many of its peers, avoiding the “burnout” culture associated with some HFT firms.

What truly sets HRT apart is its emphasis on high-quality, elegant code and long-term technical investment. The firm fosters a strong sense of developer autonomy and deeply values mentorship, documentation, and code reviews. For quant developers who want to work on mission-critical systems in an environment that values intellect over hierarchy, Hudson River Trading remains a top-tier choice in 2025.

July 10, 2025 0 comments
C++ for matrix calculation
Libraries

Best C++ Libraries for Matrix Computations

by Clement D. July 5, 2025

Matrix computations are at the heart of many quantitative finance models, from Monte Carlo simulations to risk matrix evaluations. In C++, selecting the right library can dramatically affect performance, readability, and numerical stability. Fortunately, there are several powerful options designed for high-speed computation and scientific accuracy. Whether you need dense, sparse, or banded matrix support, the C++ ecosystem has you covered. Some libraries prioritize speed, others emphasize syntax clarity or Python compatibility. What are the best C++ libraries for matrix computations?

Choosing between Eigen, Armadillo, or Blaze depends on your project goals. If you’re building a derivatives engine or a backtesting framework, using the wrong matrix abstraction can slow you down. In this article, we’ll compare the top C++ matrix libraries, focusing on performance, ease of use, and finance-specific suitability. By the end, you’ll know exactly which one to use for your next quant project. Let’s dive into the best C++ libraries for matrix computations.

1. Eigen

Website: eigen.tuxfamily.org
License: MPL2
Key Features:

  • Header-only: No linking required
  • Fast: Competes with BLAS performance
  • Clean API: Ideal for prototyping and production
  • Supports: Dense, sparse, and fixed-size matrices
  • Thread-safe: As long as each thread uses its own objects

Use Case: General-purpose, widely used in finance for risk models and curve fitting.

Eigen is a header-only C++ template library for linear algebra, supporting vectors, matrices, and related algorithms. Known for its performance through expression templates, it’s widely used in quant finance, computer vision, and machine learning.

Here is a snippet example:

#include <iostream>
#include <Eigen/Dense>

int main() {
    // Define 2x2 matrices
    Eigen::Matrix2d A;
    Eigen::Matrix2d B;

    // Initialize matrices
    A << 1, 2,
         3, 4;
    B << 2, 0,
         1, 2;

    // Matrix addition
    Eigen::Matrix2d C = A + B;

    // Matrix multiplication
    Eigen::Matrix2d D = A * B;

    // Print results
    std::cout << "Matrix A + B:\n" << C << "\n\n";
    std::cout << "Matrix A * B:\n" << D << "\n";

    return 0;
}

This is probably one of the best C++ libraries for matrix computations.

2. Armadillo

Website: arma.sourceforge.net
License: MPL2
Key Features:

  • Readable syntax: Ideal for research and prototyping
  • Performance-boosted: Uses LAPACK/BLAS when available
  • Supports: Dense, sparse, banded matrices
  • Integrates with: OpenMP, ARPACK, SuperLU
  • Actively maintained: Trusted in academia and finance

Use Case: Quant researchers prototyping algorithms with familiar syntax.

Armadillo is a high-level C++ linear algebra library that offers Matlab-like syntax, making it exceptionally easy to read and write. Under the hood, it can link to BLAS and LAPACK for high-performance computations, especially when paired with libraries like Intel MKL or OpenBLAS.

Here’s a quant finance-style example using Armadillo to perform a Cholesky decomposition on a covariance matrix, which is a common operation in portfolio risk modeling, Monte Carlo simulations, and factor models.

#include <iostream>
#include <armadillo>

int main() {
    using namespace arma;

    // Simulated 3-asset covariance matrix (symmetric and positive-definite)
    mat cov = {
        {0.10, 0.02, 0.04},
        {0.02, 0.08, 0.01},
        {0.04, 0.01, 0.09}
    };

    // Perform Cholesky decomposition: cov = L * L.t()
    mat L;
    bool success = chol(L, cov, "lower");

    if (success) {
        std::cout << "Cholesky factor L:\n";
        L.print();
        
        // Simulate a standard normal vector for 3 assets
        vec z = randn<vec>(3);

        // Generate correlated returns: r = L * z
        vec returns = L * z;

        std::cout << "\nSimulated correlated return vector:\n";
        returns.print();
    } else {
        std::cerr << "Covariance matrix is not positive-definite.\n";
    }

    return 0;
}

3. Blaze

Website: blaze.mathjs.org
License: BSD
Key Features:

  • Highly optimized: Expression templates + SIMD
  • Parallel execution: Supports OpenMP, HPX, and pthreads
  • BLAS backend optional
  • Supports: Dense/sparse matrices, vectors, custom allocators
  • Flexible integration: Can plug into existing quant platforms

Use Case: Performance-critical applications like Monte Carlo engines.

Blaze is a high-performance C++ math library that emphasizes speed and scalability. It uses expression templates like Eigen but leans further into parallelism, making it ideal for latency-sensitive finance applications such as pricing engines, curve fitting, or Monte Carlo simulations.

Imagine you simulate 1,000 paths for a European call option across 3 assets. Here’s how you could compute portfolio payoffs using Blaze:

#include <iostream>
#include <blaze/Math.h>

int main() {
    using namespace blaze;

    constexpr size_t numPaths = 1000;
    constexpr size_t numAssets = 3;

    // Simulated terminal prices (rows = paths, cols = assets)
    DynamicMatrix<double> terminalPrices(numPaths, numAssets);
    randomize(terminalPrices);  // Random values between 0 and 1

    // Portfolio weights (e.g., long 1.0 in asset 0, short 0.5 in asset 1, flat in asset 2)
    StaticVector<double, numAssets> weights{1.0, -0.5, 0.0};

    // Compute payoffs: each row dot weights
    DynamicVector<double> payoffs = terminalPrices * weights;

    std::cout << "First 5 simulated portfolio payoffs:\n";
    for (size_t i = 0; i < 5; ++i)
        std::cout << payoffs[i] << "\n";

    return 0;
}

4. xtensor

Website: xtensor.readthedocs.io
License: BSD
Key Features:

  • Numpy-like multidimensional arrays
  • Integrates well with Python via xtensor-python
  • Supports broadcasting and lazy evaluation

Use Case: Interfacing with Python or for higher-dimensional data structures.

xtensor is a C++ library for numerical computing, offering multi-dimensional arrays with NumPy-style syntax. It supports broadcasting, lazy evaluation, and is especially handy for developers needing interoperability with Python (via xtensor-python) or high-dimensional operations in quant research.

  • Python interop through xtensor-python
  • Header-only, modern C++17+
  • Syntax close to NumPy
  • Fast and memory-efficient
  • Broadcasting, slicing, views supported

Here is an example for moving average calculation:

#include <iostream>
#include <xtensor/xarray.hpp>
#include <xtensor/xview.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xio.hpp>

int main() {
    using namespace xt;

    // Simulated closing prices (1D tensor)
    xarray<double> prices = {100.0, 101.5, 103.2, 102.0, 104.1, 106.3, 107.5};

    // Window size for moving average
    std::size_t window = 3;

    // Compute moving averages
    std::vector<double> ma_values;
    for (std::size_t i = 0; i <= prices.size() - window; ++i) {
        auto window_view = view(prices, range(i, i + window));
        double avg = mean(window_view)();
        ma_values.push_back(avg);
    }

    // Print result
    std::cout << "Rolling 3-period moving averages:\n";
    for (auto val : ma_values)
        std::cout << val << "\n";

    return 0;
}

Ready for the last entries for our article on the best C++ libraries for matrix computations?

5. FLENS (Flexible Library for Efficient Numerical Solutions)

Website: github.com/michael-lehn/FLENS
License: BSD
Key Features:

  • Thin wrapper over BLAS/LAPACK for speed
  • Supports banded and triangular matrices
  • Good for structured systems in quant PDEs
  • Integrates well with Fortran-style scientific computing

Use Case: Structured financial models involving PDE solvers.

FLENS is a C++ wrapper around BLAS and LAPACK designed for clear object-oriented syntax and high numerical performance. It provides clean abstractions over dense, sparse, and banded matrices, making it a good fit for quant applications involving curve fitting, linear systems, or differential equations.Object-oriented, math-friendly syntax

Let’s solve a system representing a regression problem (e.g., estimating betas in a factor model):

#include <flens/flens.cxx>
#include <iostream>

using namespace flens;

int main() {
    typedef GeMatrix<FullStorage<double> >   Matrix;
    typedef DenseVector<Array<double> >      Vector;

    // Create matrix A (3x3) and vector b
    Matrix A(3, 3);
    Vector b(3);

    A = 1.0,  0.5,  0.2,
        0.5,  2.0,  0.3,
        0.2,  0.3,  1.0;

    b = 1.0, 2.0, 3.0;

    // Solve Ax = b using LAPACK
    Vector x(b); // solution vector
    lapack::gesv(A, x);  // modifies A and x

    std::cout << "Solution x:\n" << x << "\n";

    return 0;
}
July 5, 2025 0 comments
volatility smile in C++
Volatility

Volatility Smile in C++: An Implementation for Call Options

by Clement D. July 4, 2025

Implied volatility isn’t flat across strikes: it curves. It’s like a smile: the volatility smile.

When plotted against strike price, implied volatilities for call options typically form a smile, with lower volatilities near-the-money and higher volatilities deep in- or out-of-the-money. This shape reflects real-world market dynamics, like the increased demand for tail-risk protection and limitations of the Black-Scholes model.

In this article, we’ll implement a simple C++ tool to compute this volatility smile from observed market prices, helping us visualize and understand its structure.

1. What’s a Volatility Smile?

A volatility smile is a pattern observed in options markets where implied volatility (IV) is not constant across different strike prices — contrary to what the Black-Scholes model assumes. When plotted (strike on the x-axis, IV on the y-axis), the graph forms a smile-shaped curve.

  • At-the-money (ATM) options tend to have the lowest implied volatility.
  • In-the-money (ITM) and out-of-the-money (OTM) options show higher implied volatility.

If you plot IV vs. strike price, the shape curves upward at both ends, resembling a smile.

Implied volatility is lowest near the at-the-money (ATM) strike and increases for both in-the-money (ITM) and out-of-the-money (OTM) options.

This convex shape reflects typical market observations and diverges from the flat IV assumption in Black-Scholes.

For call options, the left side of the volatility smile (low strikes) is in-the-money, the center is at-the-money, and the right side (high strikes) is out-of-the-money.

2. In-the-money (ITM), At-the-money (ATM), Out-of-the-money (OTM)

These terms describe the moneyness of an option:

Option TypeIn-the-money (ITM)At-the-money (ATM)Out-of-the-money (OTM)
CallStrike < SpotStrike ≈ SpotStrike > Spot
PutStrike > SpotStrike ≈ SpotStrike < Spot
  • ITM: Has intrinsic value — profitable if exercised now.
  • ATM: Strike is close to spot price — most sensitive to volatility (highest gamma).
  • OTM: No intrinsic value — only time value.

Here are two quick scenarios:

In-the-Money (ITM) Call – Profitable Scenario:

A trader buys a call option with a strike price of £90 when the stock is at £95 (already ITM).
Later, the stock rises to £105, and the option gains intrinsic value.
🔁 Trader profits by exercising the option or selling it for more than they paid.

Out-of-the-Money (OTM) Call – Profitable Scenario:

A trader buys a call option with a strike price of £110 when the stock is at £100 (OTM, cheaper).
Later, the stock jumps to £120, making the option now worth at least £10 intrinsic.
🔁 Trader profits from the big move, even though the option started OTM.

3. Why this pattern?

The volatility smile arises because market participants do not believe asset returns follow a perfect normal distribution. Instead, they expect more frequent extreme moves in either direction, what statisticians call “fat tails.” To account for this, traders are willing to pay more for options that are far out-of-the-money or deep in-the-money, especially since these positions offer outsized payoffs in rare but impactful events. This increased demand pushes up the implied volatility for these strikes.

Moreover, options at the tails often serve as hedging tools.

  • For example, portfolio managers commonly buy far out-of-the-money puts to protect against a market crash. Similarly, speculative traders might buy cheap out-of-the-money calls hoping for a large upward movement. This consistent demand at the tails drives up their prices, and consequently, their implied volatilities.
  • In contrast, at-the-money options are more frequently traded and are typically the most liquid. Because their strike price is close to the current market price, they tend to reflect the market’s consensus on future volatility more accurately. There’s less uncertainty and speculation around them, and they don’t carry the same kind of tail risk premium.

As a result, the implied volatility at the money tends to be lower, forming the bottom of the volatility smile.

4. Implementation in C++

Here is an implementation in C++ using Black-Scholes:

#include <iostream>
#include <cmath>
#include <vector>
#include <iomanip>

// Black-Scholes formula for a European call option
double black_scholes_call(double S, double K, double T, double r, double sigma) {
    double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T));
    double d2 = d1 - sigma * std::sqrt(T);

    auto N = [](double x) {
        return 0.5 * std::erfc(-x / std::sqrt(2));
    };

    return S * N(d1) - K * std::exp(-r * T) * N(d2);
}

// Bisection method to compute implied volatility
double implied_volatility(double market_price, double S, double K, double T, double r,
                          double tol = 1e-5, int max_iter = 100) {
    double low = 0.0001;
    double high = 2.0;

    for (int i = 0; i < max_iter; ++i) {
        double mid = (low + high) / 2.0;
        double price = black_scholes_call(S, K, T, r, mid);

        if (std::abs(price - market_price) < tol) return mid;
        if (price > market_price) high = mid;
        else low = mid;
    }

    return (low + high) / 2.0; // best guess
}

// Synthetic volatility smile to simulate market prices
double synthetic_volatility(double K, double S) {
    double base_vol = 0.15;
    return base_vol + 0.0015 * std::pow(K - S, 2);
}

int main() {
    double S = 100.0;     // Spot price
    double T = 0.5;       // Time to maturity in years
    double r = 0.01;      // Risk-free rate

    std::cout << "Strike\tMarketPrice\tImpliedVol\n";

    for (double K = 60; K <= 140; K += 2.0) {
        double true_vol = synthetic_volatility(K, S);
        double market_price = black_scholes_call(S, K, T, r, true_vol);
        double iv = implied_volatility(market_price, S, K, T, r);

        std::cout << std::fixed << std::setprecision(2)
                  << K << "\t" << market_price << "\t\t" << std::setprecision(4) << iv << "\n";
    }

    return 0;
}

We aim to simulate and visualize a volatility smile by:

  1. Generating synthetic market prices for European call options using a non-constant volatility function.
  2. Computing the implied volatility by inverting the Black-Scholes model using a bisection method.
  3. Printing the results to observe how implied volatility varies across strike prices.

We compute implied volatility by numerically inverting the Black-Scholes formula:

double implied_volatility(double market_price, double S, double K, double T, double r)

We seek the value of [math]\sigma[/math] such that:

[math] C_{\text{BS}}(S, K, T, r, \sigma) \approx \text{Market Price} [/math]

We use the bisection method, iteratively narrowing the interval [math][\sigma_{\text{low}}, \sigma_{\text{high}}][/math] until the difference between the model price and the market price is within a small tolerance.

Now, here is the plot of your volatility smile using the output from the C++ program.

After compiling our code:

mkdir build
cd build
cmake ..
make

We can run it:

./volatilitysmile

And get:

Strike  MarketPrice     ImpliedVol
60.00   72.18           2.0000
62.00   68.21           2.0000
64.00   64.09           2.0000
66.00   59.85           1.8840
68.00   55.55           1.6860
70.00   51.23           1.5000
72.00   46.92           1.3260
74.00   42.68           1.1640
76.00   38.53           1.0140
78.00   34.51           0.8760
80.00   30.64           0.7500
82.00   26.95           0.6360
84.00   23.45           0.5340
86.00   20.17           0.4440
88.00   17.11           0.3660
90.00   14.28           0.3000
92.00   11.70           0.2460
94.00   9.38            0.2040
96.00   7.36            0.1740
98.00   5.70            0.1560
100.00  4.47            0.1500
102.00  3.73            0.1560
104.00  3.44            0.1740
106.00  3.57            0.2040
108.00  4.06            0.2460
110.00  4.91            0.3000
112.00  6.10            0.3660
114.00  7.64            0.4440
116.00  9.55            0.5340
118.00  11.83           0.6360
120.00  14.48           0.7500
122.00  17.49           0.8760
124.00  20.86           1.0140
126.00  24.57           1.1640
128.00  28.59           1.3260
130.00  32.89           1.5000
132.00  37.44           1.6860
134.00  42.19           1.8840
136.00  47.07           2.0000
138.00  52.03           2.0000
140.00  57.01           2.0000


As expected, implied volatility is lowest near the at-the-money strike (100) and rises symmetrically for deep in-the-money and out-of-the-money strikes forming the characteristic smile shape:

The code for this article is accessible here:

https://github.com/cppforquants/volatilitysmile

July 4, 2025 0 comments
two sum problem C++
Interview

Hash Maps for Quant Interviews: The Two Sum Problem in C++

by Clement D. June 29, 2025

In the article of today, we talk about an interview question, simple on the surface, that’s given quite a lot as interview question for quants positions: the two sum problem.

What is the two sum problem in C++?

🔍 Problem Statement

Given a list of prices and a target value, return all the indices of price pairs in the list that sum up to the target.

Example:

nums = {2, 7, 11, 15}, target = 9  

Output: [{0, 1}]

// because nums[0] + nums[1] = 2 + 7 = 9


1. The Naive Implementation in [math] O(n^2) [/math] Time Complexity


The simple way of doing is to loop through the list of prices twice to test every possible sums:

std::vector<std::pair<int, int>> twoSum(const std::vector<int>& nums, int target) {
    std::vector<std::pair<int, int>> results = {};
    for (int i = 0; i < nums.size(); ++i) {
        for (int j = i + 1; j < nums.size(); j++) {
            if (nums[i]+nums[j] == target){
                results.push_back({i, j});
            };
        };
    };
    return results;
}

This can be used with the list of prices given in our introduction:


int main() {
    std::vector<int> nums = {2, 7, 11, 15};
    int target = 9;
    auto results = twoSum(nums, target);

    for (const auto& pair: results) {
        std::cout << "Indices: " << pair.first << ", " << pair.second << std::endl;
    };


    return 0;
}

Running the code above prints the solution:

➜  build git:(main) ✗ ./twosum
Indices: 0, 1

The time complexity is [math] O(n^2) [/math] because we’re looping twice over the list of prices.

And that’s it, we’ve nailed the two sum problem in C++!

But…. is it possible to do better?

2. The Optimal Implementation in [math] O(n) [/math] Time Complexity

The optimal way is to use a hash map to store previously seen numbers and their indices, allowing us to check in constant time whether the complement of the current number (i.e. target - current) has already been encountered.

This reduces the time complexity from O(n²) to O(n), making it highly efficient for large inputs.

std::vector<std::pair<int, int>> twoSumOptimized(const std::vector<int>& nums, int target) {
    std::vector<std::pair<int, int>> results = {};
    std::unordered_map<int, int> pricesMap;
    for (int i = 0; i < nums.size(); ++i) {

        int diff = target - nums[i];

        if (pricesMap.find(diff) != pricesMap.end()){
            results.push_back({pricesMap[diff], i});

        } 
        pricesMap[nums[i]] = i;

    };
    return results;
}

And we can run it on the exact same example but this time, it’s O(n) as we loop once and hash maps access/storing time complexity are O(1):


int main() {
    std::vector<int> nums = {2, 7, 11, 15};
    int target = 9;

    auto results = twoSumOptimized(nums, target);

    for (const auto& pair: results) {
        std::cout << "Indices: " << pair.first << ", " << pair.second << std::endl;
    };

    return 0;
}

3. A Zoom on Unordered Hash Maps in C++

In C++, std::unordered_map is the go-to data structure for constant-time lookups. Backed by a hash table, it allows you to insert, search, and delete key-value pairs in average O(1) time. For problems like Two Sum, where you’re checking for complements on the fly, unordered_map is the natural fit.

Here’s a quick comparison with std::map:

Featurestd::unordered_mapstd::map
Underlying StructureHash tableRed-Black tree (balanced BST)
Average Lookup TimeO(1)O(log n)
Worst-case Lookup TimeO(n) (rare)O(log n)
Ordered Iteration❌ No✅ Yes
C++ Standard IntroducedC++11C++98
Typical Use CasesFast lookups, cache, setsSorted data, range queries

Use unordered_map when:

  • You don’t need the keys to be sorted
  • You want maximum performance for insert/lookup
  • Hashing the key type is efficient and safe (e.g. int, std::string)

Let me know if you’d like to add performance tips, custom hash examples, or allocator benchmarks.

The code for the article is available here:

https://github.com/cppforquants/twosumprices

June 29, 2025 0 comments
smart pointers in C++ for financial data
Data Structures

Smart Pointers for C++ Financial Data Structures: An Overview

by Clement D. June 29, 2025

Since C++11, smart pointers have become essential tools for managing memory safely and efficiently.
They eliminate the need for manual new/delete while enabling shared or exclusive ownership semantics. What are smart pointers for C++ financial data?

1. Raw Pointers vs Smart Pointers: The Evolution

C++ traditionally relied on raw pointers (T*) for dynamic memory management. While flexible, raw pointers come with risks: memory leaks, dangling references, and double deletes — especially in complex quant systems where ownership isn’t always clear.

Smart pointers, introduced in C++11, offer a safer alternative by wrapping raw pointers with automatic lifetime management.

C++11 introduced smart pointers, which wrap raw pointers with automatic memory management based on ownership models:

  • std::unique_ptr<T>: sole ownership, cannot be copied.
  • std::shared_ptr<T>: shared ownership via reference counting.
  • std::weak_ptr<T>: non-owning reference to break cycles.
  • std::make_shared<T>() / std::make_unique<T>(): factory functions for safe allocation.

Consider this classic raw pointer pattern, here with a dummy YieldCurve object:

YieldCurve* curve = new YieldCurve(...);
// use curve
delete curve; // ❌ must remember to call this, or risk a leak

With std::shared_ptr, you simplify and secure ownership:

auto curve = std::make_shared<YieldCurve>(...); // ✅ deleted automatically when last owner goes out of scope

2. Heap-Allocated Financial Structures and Smart Pointers

In financial systems, data structures like price histories, risk vectors, or trade buckets are often heap-allocated.

It’s either because of their size or because they need to be shared across multiple components or threads. Managing their lifetime correctly is crucial to avoid memory leaks or invalid access.

Let’s take a practical example of such structures: std::vector. A vector is stored on the stack, while its internal elements are managed on the heap.

void computePnL() {
    std::vector<double> pnl = {120.5, -75.2, 30.0};  // stack-allocated vector
    double total = 0.0;
    for (double p : pnl) {
        total += p;
    }
    std::cout << "Total PnL: " << total << std::endl;
} // ✅ `pnl` is automatically destroyed when it goes out of scope

So a value-based vector would look like this in memory:

The stack contains basic metadata about the vector and a pointer to the heap block where the values are actually defined.

How would a vector smart shared pointer look like?

It would be a pointer defined on the stack pointing to a vector object on the heap that contains a pointer towards the actual values also on the heap. Yes, it’s a bit nested!

When the last std::shared_ptr to a heap-allocated object goes out of scope or is reset, the internal reference count drops to zero.

At that moment, unlike raw pointers, the managed object (in this case, the std::vector<double>) is automatically destroyed, and its memory is released. This ensures safe, deterministic cleanup without manual delete calls.

Let’s see with an example for shared pointers:

#include <iostream>
#include <memory>
#include <vector>

void createVector() {
    auto pnl = std::make_shared<std::vector<double>>(std::initializer_list<double>{120.5, -75.2, 30.0});
    std::cout << "Inside createVector(), use_count: " << pnl.use_count() << std::endl;
} // 🔥 When pnl goes out of scope, vector is destroyed (ref count drops to 0)

int main() {
    createVector();
    std::cout << "Back in main()" << std::endl;
}

When pnl goes out of scope, vector is destroyed (ref count drops to 0) automatically.

Now, let’s talk about std::unique_ptr<T>:

While shared_ptr supports shared ownership with reference counting, unique_ptr enforces strict single ownership.

It cannot be copied, only moved making ownership transfer explicit and safe.

#include <iostream>
#include <memory>
#include <vector>

std::unique_ptr<std::vector<double>> createVector() {
    auto pnl = std::make_unique<std::vector<double>>(std::initializer_list<double>{100.0, -20.0, 50.0});
    std::cout << "Inside createVector(), vector size: " << pnl->size() << std::endl;
    return pnl; // ownership is transferred (moved) to the caller
}

int main() {
    auto vecPtr = createVector(); // vecPtr now owns the vector
    std::cout << "Back in main(), first value: " << vecPtr->at(0) << std::endl;

    // auto copy = vecPtr; ❌ This won't compile: unique_ptr cannot be copied
    auto moved = std::move(vecPtr); // ✅ Ownership moved
    if (!vecPtr)
        std::cout << "vecPtr is now null after move.\n";
}

This shows how unique_ptr ensures exclusive ownership: once moved, the original pointer becomes null, and the resource is safely destroyed when the final owner goes out of scope.

Now, let’s talk about std::weak_ptr<T>:

Unlike shared_ptr, a weak_ptr does not contribute to the reference count. It’s designed for non-owning references that safely observe a shared_ptr, managed resource, especially useful to break cyclic references in shared ownership scenarios (like parent ↔ child graphs or caches).

Here’s a minimal example:

#include <iostream>
#include <memory>
#include <vector>

void observeVector() {
    std::shared_ptr<std::vector<int>> data = std::make_shared<std::vector<int>>(std::initializer_list<int>{1, 2, 3});
    std::weak_ptr<std::vector<int>> weakData = data; // 👀 weak observer

    std::cout << "use_count: " << data.use_count() << std::endl;

    if (auto locked = weakData.lock()) { // Try to temporarily access
        std::cout << "Accessed value: " << locked->at(0) << std::endl;
    }
    
    data.reset(); // destroy the shared_ptr

    if (auto locked = weakData.lock()) {
        std::cout << "Still alive.\n";
    } else {
        std::cout << "Resource expired.\n";
    }
}

std::weak_ptr is ideal for temporary, non-owning access to a shared resource. It lets you safely check if the object is still alive without extending its lifetime: perfect for avoiding cyclic references and building efficient observers or caches.

Said differently: std::weak_ptr is a smart pointer specifically designed to observe the lifecycle of a std::shared_ptr without affecting its ownership or reference count.

3. Overhead of Using Smart Pointers in HFT

While smart pointers offer safety and convenience, they do introduce runtime overhead, especially std::shared_ptr.

🏎️ unique_ptr, by contrast, is zero-overhead in release builds — it’s just a thin RAII wrapper around a raw pointer with no reference counting.

🔁 shared_ptr maintains a reference count (typically via an internal control block). Each copy or reset updates this count atomically, which adds thread-safe synchronization costs.

🔒 weak_ptr shares this control block, adding some memory overhead, though access via .lock() is generally efficient.

In performance-critical, low-latency systems (e.g. HFT), overuse of shared_ptr can introduce cache pressure and latency spikes. It’s crucial to profile and use them only where ownership semantics justify the cost.

Each std::shared_ptr has an associated control block (on the heap) storing:

  • use_count (shared references)
  • weak_count

Every copy, assignment, or destruction updates use_count atomically:

std::shared_ptr<T> a = b;  // atomic increment

These atomic ops:

  • May invalidate CPU cache lines
  • Cause false sharing if multiple threads access nearby memory
  • Require synchronization fences, introducing latency

Each shared_ptr object requires a heap allocation for its control block. This:

  • Adds malloc/free overhead
  • Increases memory fragmentation
  • May cause non-contiguous accesses, leading to cache misses

Also, destroying a shared_ptr with many references (e.g., in a tree or graph) may lead to:

  • Long chain deletions
  • Blocking deallocation spikes
  • Potential STW-like moments under multi-threaded pressure

4. Summary

Here’s a clear summary table of the key points we’ve covered:

TopicKey PointsNotes / Code Snippet
Smart Pointer Typesunique_ptr, shared_ptr, weak_ptrmake_shared<T>() preferred over raw new
unique_ptrSingle ownership, zero overhead, auto-destroyCannot be copied, only moved
shared_ptrReference-counted ownership, auto-destroyCopies increment ref count atomically
weak_ptrNon-owning observer of a shared_ptrUse .lock() to access if still alive
Stack vs Heap Allocationstd::vector<T> is heap-allocated but wrapper can be stack-allocatedstd::array<T, N> is truly stack-allocated
Why Smart Pointers?Automate memory management, avoid leaks, support ownership semanticsUseful when passing around heap structures like vectors or trees
Ownership Model ExampleReturn shared_ptr from a function, pass to othersReuse across functions and threads safely
Dangling Pointer ExampleRaw pointer to stack-allocated object leads to UBstd::shared_ptr copies avoid this
Diagram Conceptsshared_ptr → vector → heap dataMemory is layered; ownership tracked by control block
Control Block (shared_ptr)Stores ref counts, lives on heapIntroduces atomic ops and heap churn
Overhead IssuesCache pressure, false sharing, heap fragmentation, latency spikesProfiling tools: perf, Valgrind, VTune, Heaptrack
Monitoring Smart Pointer UsageAtomic op count, heap allocations, cache miss ratesAvoid in tight loops or critical paths
Use CasesLong-lived financial structures (e.g. Yield Curves, Risk Matrices)Enables clean sharing, esp. across threads

The code for the article is available here:

https://github.com/cppforquants/smartpointers/tree/main

June 29, 2025 0 comments
Greek Vega Calculation C++
Greeks

Option Greeks: Vega Calculation in C++ Explained

by Clement D. June 28, 2025

Vega plays a vital role for trades. It measures how much the price of an option changes with respect to changes in volatility. In this article, we’ll break down the concept of Vega, explore its importance in options trading, and walk through a clean and efficient implementation in C++, the language of choice for many high-performance financial systems. Whether you’re building a derivatives pricing engine or just deepening your understanding of options, this guide will provide both the theory and the code to get you started. Let’s work on this Vega calculation in C++!

1. What’s Vega?

Let’s derive Vega from the Black-Scholes formula for a European call option.

This is Black-Scholes for a European call option:

[math] \Large C = S \cdot N(d_1) – K e^{-rT} \cdot N(d_2) [/math]

Where:

  • [math] S [/math]: spot price
  • [math] K [/math]: strike price
  • [math] r [/math]: risk-free rate
  • [math] T [/math]: time to maturity (in years)
  • [math] \sigma [/math]: volatility
  • [math] N(\cdot) [/math]: cumulative distribution function of the standard normal distribution

With:

[math] \Large d_1 = \frac{\ln(S/K) + (r + \frac{1}{2}\sigma^2)T}{\sigma \sqrt{T}} [/math]

And:

[math] \Large d_2 = d_1 – \sigma \sqrt{T} [/math]

To compute Vega, we differentiate the Black-Scholes price with respect to [math] \sigma [/math], valid for both call and put optiions:

[math] \Large \text{Vega} = \frac{\partial C}{\partial \sigma} [/math]

After applying calculus and simplifying:

[math] \Large \text{Vega} = S \cdot \phi(d_1) \cdot \sqrt{T} [/math]

Where [math] \phi(d) [/math] is the standard normal probability density function:

[math] \Large \phi(d) = \frac{1}{\sqrt{2\pi}} e^{-d^2 / 2} [/math]

All the terms are positive (spot price, density function and square root of time to maturity).

✅ Summary:

  • Vega is positive for both call and put options as all the terms of the formula are positive.
  • It reaches its maximum at-the-money, where [math] S \approx K [/math].
  • Vega declines as expiration approaches.
  • It is essential for volatility trading strategies.

2. Implementation in C++: A Vanilla Snippet

Important assumptions of the following implementation of the vega calculation in C++:

  • The option is European (no early exercise).
  • We use the Black-Scholes model.

Let’s simply implement the formula defined in the previous section in a vega.cpp file:

#include <iostream>
#include <cmath>

// Compute standard normal PDF without hardcoding pi
double normal_pdf(double x) {
    static const double inv_sqrt_2pi = 1.0 / std::sqrt(2.0 * std::acos(-1.0));
    return inv_sqrt_2pi * std::exp(-0.5 * x * x);
}

// Compute d1 for Black-Scholes formula
double d1(double S, double K, double r, double T, double sigma) {
    return (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T));
}

// Compute Vega
double vega(double S, double K, double r, double T, double sigma) {
    double d1_val = d1(S, K, r, T, sigma);
    return S * normal_pdf(d1_val) * std::sqrt(T);
}

// Example usage
int main() {
    double S = 100.0;     // Spot price
    double K = 100.0;     // Strike price
    double r = 0.05;      // Risk-free rate
    double T = 1.0;       // Time to maturity (years)
    double sigma = 0.2;   // Volatility

    double v = vega(S, K, r, T, sigma);
    std::cout << "Vega: " << v << std::endl;

    return 0;
}

Let’s use a reasonable CMakeLists.txt to be able to compile the file above:

cmake_minimum_required(VERSION 3.10)
project(vega)
set(CMAKE_CXX_STANDARD 17)
add_executable(vega ../vega.cpp)

Let’s create a build directory and compile:

mkdir build
cd build
comake ..
make

Then run the executable:

➜  build make       
[ 50%] Building CXX object CMakeFiles/vega.dir/vega.cpp.o
[100%] Linking CXX executable vega
[100%] Built target vega

➜  build ./vega
Vega: 37.524

Interpretation:

Vega = 37.52 means that if the implied volatility increases by 1 percentage point (0.01), the option’s price increases by approximately 0.3752 units.

So for a 5-point move in volatility (e.g. from 20% to 25%), the option price would rise by ~1.88 units.

Why is it so “high”?

Because:

  • The option is at-the-money.
  • The time to expiry is 1 year.
  • Vega is proportional to [math]S \cdot \sqrt{T}[/math].
  • The PDF value [math]\phi(d_1)[/math] is near its peak at [math]d_1 \approx 0[/math].

If you changed any of the following:

  • Time to maturity ↓ → Vega ↓
  • Move far in- or out-of-the-money → Vega ↓
  • Lower spot price → Vega ↓

3. How would you calculate it for an American-Style option?

Since there’s no closed-form solution for American-style options, we would estimate Vega using a numerical approach:

General Steps

  1. Choose a pricing model that supports American options:
    • Most common: binomial tree
  2. Define base parameters:
    Spot price [math]S[/math], strike [math]K[/math], rate [math]r[/math], time [math]T[/math], and volatility [math]\sigma[/math]
  3. Pick a small increment for volatility (e.g., [math]\epsilon = 0.01[/math])
  4. Compute option prices at [math]\sigma + \epsilon[/math] and [math]\sigma – \epsilon[/math]
    using your American option pricing model (e.g., binomial tree):

[math] \Large V_+ = \text{Price}(\sigma + \epsilon) [/math]

[math] \Large V_- = \text{Price}(\sigma – \epsilon) [/math]

Estimate Vega using central difference:

[math] \Large \text{Vega} \approx \frac{V_+ – V_-}{2 \epsilon} [/math]

This is just to give you a taste of it, we will do a vega calculation in C++ for American-style options in another article.

Code of the article with Readme for compilation and execution:

https://github.com/cppforquants/vega/tree/main

June 28, 2025 0 comments
Citadel article
Jobs

Citadel: Top Performing Hedge Fund for C++ Quant Developers

by Clement D. June 27, 2025

Founded by Ken Griffin in 1990, Citadel has evolved into a global powerhouse, consistently delivering top-tier returns across its multi-strategy portfolios. But behind its alpha-generating engine lies an often underappreciated force: the C++ quantitative developers who architect the low-latency systems, model risk in real time, and turn market chaos into structured opportunity.

1. The Rise of Citadel

Founded in 1990 by Ken Griffin with just over $4 million in capital, Citadel has grown into one of the most successful hedge funds in history. Headquartered in Chicago, it now manages over $60 billion in assets and operates across the globe, including offices in New York, London, Hong Kong, and beyond.

Citadel is not a single-strategy fund—it’s a multi-strategy powerhouse, deploying capital across equities, fixed income, commodities, credit, and quantitative strategies. This diversification, paired with robust risk management, has made the firm highly resilient through market cycles.

What sets Citadel apart isn’t just its performance—though returns like 38% in 2022 certainly command attention—but its deep integration of technology and talent. The firm runs on infrastructure built for speed, scale, and precision, and this is where quantitative developers come in.

Its flagship Wellington fund has outperformed many rivals, even in turbulent years. Citadel’s ability to consistently extract alpha is often attributed to its collaborative model, where engineers, researchers, and portfolio managers work shoulder-to-shoulder.

The firm’s reputation as a developer-first hedge fund has drawn some of the world’s top C++ talent. At Citadel, engineering is not support—it’s strategy.

With a culture that values innovation, ruthless execution, and data-driven decision making, Citadel has become more than a hedge fund—it’s a financial technology juggernaut.

2. C++ at Citadel

Citadel operates at nanosecond precision. That level of performance demands fine-grained control over memory, concurrency, and CPU cache: all things C++ excels at. Unlike higher-level languages, C++ lets developers write code that talks directly to the hardware, eliminating unnecessary overhead.

The firm’s systems are responsible for processing millions of market messages per second, reacting to market events in real time, and executing trades across global venues. This wouldn’t be possible without the deterministic behavior and performance characteristics of C++.

But Citadel’s use of C++ isn’t limited to execution systems. Quant researchers rely on C++ libraries to backtest strategies, model derivatives, and run complex simulations at scale. Its computational efficiency makes it ideal for workloads that can’t afford latency or garbage collection pauses.

Citadel also invests heavily in modern C++, adopting features from C++17 and C++20 where they offer measurable gains in safety, readability, or performance. Engineers are encouraged to write clean, maintainable code—but never at the cost of latency.

Citadel’s tech culture rewards those who can profile, benchmark, and optimize. The best C++ quants here don’t just code—they engineer alpha.

3. Salaries at Citadel

  • Citadel Securities Quantitative Developer / Research Engineer roles have a base salary range of $250,000–$350,000, with discretionary incentive bonuses on top teamblind.com+12citadelsecurities.com+12teamblind.com+12.
  • Central Risk Services Quant Dev positions offer a lower base range of $150,000–$300,000, plus discretionary bonuses and full benefits investopedia.com+3citadel.com+3glassdoor.com+3teamblind.com+1fnlondon.com+1.
  • According to Levels.fyi, software engineering roles (which often include quant roles) at Citadel report total compensation between $369K (L1) up to $586K (L5), with a U.S. median of $435K citadelsecurities.com+11levels.fyi+11indeed.com+11teamblind.com+1indeed.com+1.
  • On Glassdoor/Indeed, reported base pay averages range widely:
    • Glassdoor reports ~$224K/year average for Quant Dev teamblind.comreddit.com+7glassdoor.com+7teamblind.com+7.
    • Indeed reports around $170K/year, with ranges from $85K–$257K teamblind.com+4indeed.com+4glassdoor.com+4teamblind.com+5teamblind.com+5levels.fyi+5.
  • Reddit/industry anecdotes suggest: “Quant Developers: $300k–1M”
    Contributions near alpha can push compensation toward the high end fnlondon.comreddit.com+5reddit.com+5levels.fyi+5.
  • QuantNet survey indicates for top-tier quant roles (2–5 years of experience):
    • First-year total comp spans $350K–$625K (base $150K–$300K + sign-on $50K–$200K + bonus $75K–$150K) fnlondon.com+11quantnet.com+11reddit.com+11.
    • By 5 years, total comp may reach $800K–$1.2M with solid performance.
  • TeamBlind insights highlight that senior quant devs at Citadel can expect $650K–$900K total compensation, especially for C++ specialists quantnet.com+1reddit.com+1reddit.com+11teamblind.com+11teamblind.com+11efinancialcareers.com+3businessinsider.com+3reddit.com+3.

Summary Snapshot

Role / SeniorityBase SalaryTotal Compensation
Entry Quant/Research Engineer$150K–$300K$250K–$400K+
Junior SWE / Quant at Citadel Securities$250K–$350K$350K–$600K (Levels.fyi data)
U.S. Median SWE @ Citadel (all levels)—$435K
Mid-level (2–5 yrs experience)$150K–$300K base$350K–$625K
Senior (5+ yrs, high-performers)N/A$800K–$1.2M+
Top-tier C++ Quant Dev (High perf, senior)—$650K–$900K (and above)

✅ Key Takeaways

  1. Base ranges: Typically $150K–$300K depending on team and experience.
  2. Total compensation: Often $350K–$600K early in career, with senior/high-impact quants exceeding $800K and potentially hitting $1M+.
  3. Bonuses & incentives: Discretionary, performance-based, and can include multi-year vesting or trading revenue share.
  4. C++ expertise is highly prized—especially for roles touching low-latency systems—often commanding top-end packages.

4. Conclusion on Citadel

Citadel is a technology-driven trading powerhouse where engineering excellence meets financial strategy. For C++ developers, it’s a rare environment where your code directly moves markets, influences multi-billion-dollar portfolios, and demands world-class technical execution.

Its use of cutting-edge C++, relentless focus on performance, and collaborative culture put it at the frontier of quantitative finance. From optimizing nanosecond latency to building scalable real-time infrastructure, developers at Citadel solve problems that few other places even attempt.

Compensation reflects this intensity: total packages rival top tech firms and reward measurable impact. But beyond the pay, Citadel offers a sense of ownership and precision that appeals to the most driven minds in systems programming and quantitative research.

For those who thrive under pressure, care deeply about system-level performance, and want to work alongside the best in finance and engineering: Citadel is the summit. And C++ is one of the best languages that gets you there.

June 27, 2025 0 comments
cost-of-carry futures
Futures

Pricing Futures Using Cost-of-Carry in C++

by Clement D. June 26, 2025

Futures contracts derive their value from the underlying asset, but their price is not always equal to the spot price. The cost-of-carry model explains this difference by accounting for interest rates, storage costs, and dividends. It’s a fundamental concept in pricing futures and understanding arbitrage relationships. In this article, we’ll break down the formula and implement a clean, flexible version in C++. Whether you’re pricing equity, commodity, or FX futures, this model offers a solid foundation. How to calculate the cost-of-carry in C++?

1. What’s a Future?

A future is a standardized financial contract that obligates the buyer to purchase, or the seller to sell, an underlying asset at a predetermined price on a specified future date.

Unlike forwards, futures are traded on exchanges and are marked to market daily, meaning gains and losses are settled each day until the contract expires. They are commonly used for hedging or speculation across a wide range of assets, including commodities, equities, interest rates, and currencies.

Futures contracts help investors manage risk by locking in prices, and they also play a key role in price discovery in global markets.

2. The Cost-of-Carry Formula

The cost-of-carry model provides a theoretical price for a futures contract based on the current spot price of the asset and the costs (or benefits) of holding the asset until the contract’s expiration. These costs include financing (via interest rates), storage (for physical goods), and dividends or income lost by holding the asset instead of investing it elsewhere.

The formula is:

[math] \Large F_t = S_t \cdot e^{(r + u – q)(T – t)} [/math]

Where:

  • [math] F_t [/math]: Theoretical futures price at time [math] t [/math]
  • [math] S_t [/math]: Spot price of the underlying asset
  • [math] r [/math]: Risk-free interest rate (annualized)
  • [math] u [/math]: Storage cost (as a percentage, annualized)
  • [math] q [/math]: Dividend yield or convenience yield (annualized)
  • [math] T – t [/math]: Time to maturity in years

This formula assumes continuous compounding. In practice:

  • For equity index futures, [math] u = 0 [/math], but [math] q > 0 [/math]
  • For commodities like oil or gold, [math] u > 0 [/math], and [math] q = 0 [/math]
  • For FX futures, [math] r [/math] and [math] q [/math] represent the interest rate differential between two currencies

The cost-of-carry explains why futures can trade at a premium or discount to the spot price, depending on these inputs.

An example of this cost, when the spot price stays the same:

Now how to calculate the cost-of-carry in C++?

3. A Flexible C++ Implementation

Below is a self-contained example that takes spot price, interest rate, storage cost, dividend yield, and time to maturity as inputs and outputs the futures price.

Here’s a complete, ready-to-run C++ program:

#include <iostream>
#include <cmath>

struct FuturesPricingInput {
    double spot_price;
    double risk_free_rate;
    double storage_cost;
    double dividend_yield;
    double time_to_maturity; // in years
};

double price_futures(const FuturesPricingInput& input) {
    double exponent = (input.risk_free_rate + input.storage_cost - input.dividend_yield) * input.time_to_maturity;
    return input.spot_price * std::exp(exponent);
}

int main() {
    FuturesPricingInput input {
        100.0,   // spot_price
        0.05,    // risk_free_rate (5%)
        0.01,    // storage_cost (1%)
        0.02,    // dividend_yield (2%)
        0.5      // time_to_maturity (6 months)
    };

    double futures_price = price_futures(input);
    std::cout << "Futures price: " << futures_price << std::endl;

    return 0;
}

Let’s write that code in a futures.cpp file, create a CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(futures)
set(CMAKE_CXX_STANDARD 17)
add_executable(futures ../futures.cpp)

And compile it:

mkdir build 
cd build
cmake ..
make

The result is the price of our future:

➜  build ./futures 
Futures price: 102.02
June 26, 2025 0 comments
Gamma Greek Calculation
Greeks

Options Greeks in C++: Derive and Calculate Gamma

by Clement D. June 26, 2025

The Greeks are a set of metrics that describe these sensitivities. Among them, Gamma measures how fast Delta (the sensitivity of the option price to the underlying price) changes as the underlying price itself changes. So, how to calculate gamma?

In this article, we’ll focus on deriving the Gamma of a European option under the Black-Scholes model and implementing its calculation in C++. Whether you’re building a pricing engine or refining a hedging strategy, Gamma plays a key role in capturing the curvature of your position’s risk profile.

1. What is Gamma?

Gamma is one of the key Greeks in options trading. It measures the rate of change of Delta with respect to changes in the underlying asset price.

In other words:

  • Delta tells you how much the option price changes when the underlying asset moves by a small amount.
  • Gamma tells you how much Delta itself changes when the underlying price moves.

Interpretation:

  • High Gamma means Delta is very sensitive to price changes — common for near-the-money options with short time to expiry.
  • Low Gamma indicates Delta is more stable — typical for deep in-the-money or far out-of-the-money options.

2. The Gamma Formula

In the context of options pricing, Gamma arises naturally from the Black-Scholes Partial Differential Equation, which describes how the value of an option V(S,t) evolves with respect to the underlying asset price S and time t:

[math]\Large \frac{\partial V}{\partial t} + \frac{1}{2} \sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + r S \frac{\partial V}{\partial S} – r V = 0[/math]

In this equation:

  • [math]\frac{\partial V}{\partial S}[/math] is Delta
  • [math]\frac{\partial^2 V}{\partial S^2}[/math] is Gamma
  • [math]r[/math] is the risk-free rate
  • [math]\sigma[/math] is the volatility

From the Black-Scholes PDE, we isolate Gamma as the second derivative of the option value with respect to the underlying price. Under the Black-Scholes model, the closed-form expression for Gamma is:

[math]\Large \Gamma = \frac{N'(d_1)}{S \cdot \sigma \cdot \sqrt{T}}[/math]

Where:

  • [math]N'(d_1)[/math] is the standard normal probability density at [math]d_1[/math]
  • [math]S[/math] is the spot price of the underlying asset
  • [math]\sigma[/math] is the volatility
  • [math]T[/math] is the time to expiration

To derive the closed-form for Gamma, we start from the Black-Scholes formula for the price of a European call option:

[math]\Large C = S N(d_1) – K e^{-rT} N(d_2)[/math]

Here, the dependence on the spot price [math]S[/math] is explicit in both [math]S N(d_1)[/math] and [math]d_1[/math] itself, which is a function of [math]S[/math]:

[math]\Large d_1 = \frac{\ln(S/K) + (r + \frac{1}{2} \sigma^2)T}{\sigma \sqrt{T}}[/math]

To compute Gamma, we take the second partial derivative of [math]C[/math] with respect to [math]S[/math]. This involves applying the chain rule twice due to [math]d_1[/math]’s dependence on [math]S[/math].

After simplification (and canceling terms involving [math]d_2[/math]), what remains is:

[math]\Large \Gamma = \frac{N'(d_1)}{S \cdot \sigma \cdot \sqrt{T}}[/math]

This elegant result shows that Gamma depends only on the standard normal density at [math]d_1[/math], scaled by spot price, volatility, and time.

3. Implementation in Vanilla C++

Now that we’ve derived the closed-form formula for Gamma, let’s implement it in C++. The following code calculates the Gamma of a European option using the Black-Scholes model. It includes:

  • A helper to compute the standard normal PDF
  • A function to compute d1d_1d1​
  • A gamma function implementing the formula
  • A main() function that runs an example with sample inputs

Here’s the complete implementation to calculate gamma:

#include <iostream>
#include <cmath>

// Standard normal probability density function
double norm_pdf(double x) {
    return (1.0 / std::sqrt(2 * M_PI)) * std::exp(-0.5 * x * x);
}

// Compute d1 used in the Black-Scholes formula
double compute_d1(double S, double K, double r, double sigma, double T) {
    return (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T));
}

// Compute Gamma using the closed-form Black-Scholes formula
double gamma(double S, double K, double r, double sigma, double T) {
    double d1 = compute_d1(S, K, r, sigma, T);
    return norm_pdf(d1) / (S * sigma * std::sqrt(T));
}

int main() {
    double S = 100.0;     // Spot price
    double K = 100.0;     // Strike price
    double r = 0.05;      // Risk-free interest rate
    double sigma = 0.2;   // Volatility
    double T = 1.0;       // Time to maturity in years

    double gamma_val = gamma(S, K, r, sigma, T);
    std::cout << "Gamma: " << gamma_val << std::endl;

    return 0;
}

After compilation, running the executable will calculate gamma:

➜  build ./gamma        
Gamma: 0.018762

Code of the article with Readme for compilation and execution:

https://github.com/cppforquants/gamma/tree/main

June 26, 2025 0 comments
automatic diff
Greeks

Using Automatic Differentiation for Greeks Computation in C++

by Clement D. June 25, 2025

Traders and risk managers rely on accurate values for Delta, Gamma, Vega, and other Greeks to hedge portfolios and manage exposure. Today, we’re going to see how to use automatic differentiation for greeks calculation.

Traditionally, these sensitivities are calculated using finite difference methods, which approximate derivatives numerically. While easy to implement, this approach suffers from trade-offs between precision, performance, and stability. Enter Automatic Differentiation (AD): a modern technique that computes derivatives with machine precision and efficient performance.

In this article, we explore how to compute Greeks using automatic differentiation in C++ using the lightweight AD library Adept.

1. Background: Greeks and Finite Differences

The Delta of a derivative is the rate of change of its price with respect to the underlying asset price:

[math] \Large {\Delta = \displaystyle \frac{\partial V}{\partial S}} [/math]

With finite differences, we might compute:

[math] \Large { \Delta \approx \frac{V(S + \epsilon) – V(S)}{\epsilon} } [/math]

This introduces:

  • Truncation error if ε is too large,
  • Rounding error if ε is too small,
  • And 2N evaluations for N Greeks.

2. Automatic Differentiation: Another Way

Automatic Differentiation (AD) is a technique for computing exact derivatives of functions expressed as computer programs. It is not the same as:

  • Numerical differentiation (e.g., finite differences), which approximates derivatives and is prone to rounding/truncation errors.
  • Symbolic differentiation (like in computer algebra systems), which manipulates expressions analytically but can become unwieldy and inefficient.

AD instead works by decomposing functions into elementary operations and systematically applying the chain rule to compute derivatives alongside function evaluation.

🔄 How It Works

  1. Operator Overloading (in C++)
    AD libraries in C++ (like Adept or CppAD) define a custom numeric type—say, adouble—which wraps a real number and tracks how it was computed. Arithmetic operations (+, *, sin, exp, etc.) are overloaded to record both the value and the derivative.
  2. Taping
    During function execution, the AD library records (“tapes”) every elementary operation (e.g., x * y, log(x), etc.) into a computational graph. Each node stores the local derivative of the output with respect to its inputs.
  3. Chain Rule Application
    Once the function is evaluated, AD applies the chain rule through the computational graph to compute the final derivatives.
    • Forward mode AD: computes derivatives with respect to one or more inputs.
    • Reverse mode AD: computes derivatives of one output with respect to many inputs (more efficient for scalar-valued functions with many inputs, like in ML or risk models).

🎯 Why It’s Powerful

  • Exact to machine precision (unlike finite differences)
  • Fast and efficient: typically only 5–10x the cost of the original function
  • Compositional: works on any function composed of differentiable primitives, no matter how complex

📦 In C++ Quant Contexts

AD shines in quant applications where:

  • Analytical derivatives are difficult to compute or unavailable (e.g., path-dependent or exotic derivatives),
  • Speed and accuracy matter (e.g., calibration loops or sensitivity surfaces),
  • You want to avoid hard-coding Greeks and re-deriving math manually.

3. C++ Implementation Using CppAD

Install CppAD and link it with your C++ project:

git clone https://github.com/coin-or/CppAD.git
cd CppAD
mkdir build
cd build
cmake ..
make
make install

    Then link the library to your CMakeLists.txt:

    cmake_minimum_required(VERSION 3.10)
    project(CppADGreeks)
    
    set(CMAKE_CXX_STANDARD 17)
    
    # Find and link CppAD
    find_path(CPPAD_INCLUDE_DIR cppad/cppad.hpp PATHS /usr/local/include)
    find_library(CPPAD_LIBRARY NAMES cppad_lib PATHS /usr/local/lib)
    
    if (NOT CPPAD_INCLUDE_DIR OR NOT CPPAD_LIBRARY)
        message(FATAL_ERROR "CppAD not found")
    endif()
    
    include_directories(${CPPAD_INCLUDE_DIR})
    link_libraries(${CPPAD_LIBRARY})
    
    # Example executable
    add_executable(automaticdiff ../automaticdiff.cpp)
    target_link_libraries(automaticdiff ${CPPAD_LIBRARY})

    And the following code is in my automaticdiff.cpp:

    #include <cppad/cppad.hpp>
    #include <iostream>
    #include <vector>
    #include <cmath>
    
    template <typename T>
    T norm_cdf(const T& x) {
        return 0.5 * CppAD::erfc(-x / std::sqrt(2.0));
    }
    
    int main() {
        using CppAD::AD;
    
        std::vector<AD<double>> X(1);
        X[0] = 105.0;  // Spot price
    
        CppAD::Independent(X);
    
        double K = 100.0, r = 0.05, sigma = 0.2, T = 1.0;
        AD<double> S = X[0];
    
        AD<double> d1 = (CppAD::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T));
        AD<double> d2 = d1 - sigma * std::sqrt(T);
    
        AD<double> price = S * norm_cdf(d1) - K * CppAD::exp(-r * T) * norm_cdf(d2);
    
        std::vector<AD<double>> Y(1);
        Y[0] = price;
    
        CppAD::ADFun<double> f(X, Y);
    
        std::vector<double> x = {105.0};
        std::vector<double> delta = f.Jacobian(x);
    
        std::cout << "Call Price: " << CppAD::Value(price) << std::endl;
        std::cout << "Delta (∂Price/∂S): " << delta[0] << std::endl;
    
        return 0;
    }
    

    After compiling and running this code, we get a delta:

    ➜  build ./automaticdiff
    Call Price: 13.8579
    Delta (∂Price/∂S): 0.723727

    4. Explanation of the Code

    Here’s the key part of the code again:

    cppCopierModifierstd::vector<AD<double>> X(1);
    X[0] = 105.0;  // Spot price S
    CppAD::Independent(X);       // Start taping operations
    
    // Constants
    double K = 100.0, r = 0.05, sigma = 0.2, T = 1.0;
    AD<double> S = X[0];
    
    // Black-Scholes price formula
    AD<double> d1 = (CppAD::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * std::sqrt(T));
    AD<double> d2 = d1 - sigma * std::sqrt(T);
    AD<double> price = S * norm_cdf(d1) - K * CppAD::exp(-r * T) * norm_cdf(d2);
    
    std::vector<AD<double>> Y(1); Y[0] = price;
    CppAD::ADFun<double> f(X, Y);  // Create a differentiable function f: S ↦ price
    
    std::vector<double> x = {105.0};
    std::vector<double> delta = f.Jacobian(x);  // Get ∂price/∂S

    ✅ What This Code Does (Conceptually)

    Step 1 – You Define a Function:

    The code defines a function f(S) = Black-Scholes call price as a function of spot S. But unlike normal code, you define this using AD types (AD<double>), so CppAD can trace the computation.

    Step 2 – CppAD Records All Operations:

    When you run this line:

    CppAD::Independent(X);

    CppAD starts building a computational graph. Every operation you do on S (which is X[0]) is recorded:

    • log(S / K)
    • d1
    • d2
    • norm_cdf(d1)
    • …
    • final result price

    This is like taping a math program:
    CppAD now knows how the output (price) was built from the input (S).

    Step 3 – You “seal” the function:

    CppAD::ADFun<double> f(X, Y);
    
    
    
    
    
    

    This finalizes the tape into a function:

    [math] \Large{f: S \mapsto \text{price}} [/math]

    🤖 How AD Gets the Derivative

    Using:

    std::vector<double> delta = f.Jacobian(x);

    CppAD:

    • Evaluates the forward pass to get intermediate values (just like normal code),
    • Then walks backward through the graph, applying the chain rule at each node to compute:

    [math] \Large{ \Delta = \frac{\partial \text{price}}{\partial S} } [/math]

    This is the exact derivative, not an approximation.

    ⚙️ Summary: How the Code Avoids Finite Difference Pitfalls

    FeatureYour CppAD CodeFinite Differences
    Number of evaluations1 forward pass + internal backprop2+ full evaluations
    Step size tuning?❌ None needed✅ Must choose ϵ\epsilonϵ carefully
    Derivative accuracy✅ Machine-accurate❌ Approximate
    Performance on multiple Greeks✅ Fast with reverse mode❌ Expensive (1 per Greek)
    Maintenance cost✅ Code reflects math structure❌ Rewriting required for each sensitivity

    June 25, 2025 0 comments
    • 1
    • 2
    • 3
    • 4
    • 5

    @2025 - All Right Reserved.


    Back To Top
    • Home
    • News
    • Contact
    • About