NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

October 20, 2024
•
NordVarg Team
•

Option Pricing and Greeks: From Black-Scholes to Monte Carlo

Implementing option pricing models and risk calculations for derivatives trading systems—practical approaches for production environments

Quantitative FinanceOptionsDerivativesPricingRisk ManagementC++
16 min read
Share:

Introduction#

Options are the building blocks of modern finance. Pricing them correctly and understanding their risk characteristics (Greeks) is fundamental to derivatives trading. A 1% error in volatility estimation can mean millions in P&L impact on a large portfolio.

We've built option pricing systems handling thousands of instruments with millisecond latency requirements. This post covers the theory, implementation, and production considerations for option pricing and Greeks calculation.

Option Pricing Fundamentals#

Black-Scholes Model#

The classic closed-form solution for European options:

cpp
1// black_scholes.hpp - Black-Scholes pricing with Greeks
2#pragma once
3
4#include <cmath>
5#include <numbers>
6
7class BlackScholes {
8public:
9    struct Inputs {
10        double spot;              // Current stock price
11        double strike;            // Strike price
12        double time_to_expiry;    // Time to expiry (years)
13        double risk_free_rate;    // Risk-free rate
14        double volatility;        // Implied volatility
15        double dividend_yield;    // Continuous dividend yield
16        bool is_call;             // Call or put
17    };
18    
19    struct Greeks {
20        double delta;      // ∂V/∂S
21        double gamma;      // ∂²V/∂S²
22        double vega;       // ∂V/∂σ
23        double theta;      // ∂V/∂t
24        double rho;        // ∂V/∂r
25        double vanna;      // ∂²V/∂S∂σ
26        double volga;      // ∂²V/∂σ²
27        double charm;      // ∂²V/∂S∂t
28    };
29    
30    struct Result {
31        double price;
32        Greeks greeks;
33    };
34    
35    static Result calculate(const Inputs& input) {
36        // Adjust for dividends
37        const double S = input.spot * std::exp(-input.dividend_yield * input.time_to_expiry);
38        const double K = input.strike;
39        const double T = input.time_to_expiry;
40        const double r = input.risk_free_rate;
41        const double sigma = input.volatility;
42        const double q = input.dividend_yield;
43        
44        // Calculate d1 and d2
45        const double sqrt_T = std::sqrt(T);
46        const double d1 = (std::log(S / K) + (r - q + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
47        const double d2 = d1 - sigma * sqrt_T;
48        
49        // Standard normal CDF and PDF
50        const double N_d1 = normal_cdf(d1);
51        const double N_d2 = normal_cdf(d2);
52        const double n_d1 = normal_pdf(d1);
53        
54        // Price calculation
55        double price;
56        if (input.is_call) {
57            price = S * std::exp(-q * T) * N_d1 - K * std::exp(-r * T) * N_d2;
58        } else {
59            price = K * std::exp(-r * T) * (1 - N_d2) - S * std::exp(-q * T) * (1 - N_d1);
60        }
61        
62        // Greeks calculation
63        Greeks greeks;
64        
65        // Delta: ∂V/∂S
66        if (input.is_call) {
67            greeks.delta = std::exp(-q * T) * N_d1;
68        } else {
69            greeks.delta = std::exp(-q * T) * (N_d1 - 1);
70        }
71        
72        // Gamma: ∂²V/∂S² (same for call and put)
73        greeks.gamma = std::exp(-q * T) * n_d1 / (S * sigma * sqrt_T);
74        
75        // Vega: ∂V/∂σ (per 1% change in volatility)
76        greeks.vega = S * std::exp(-q * T) * n_d1 * sqrt_T / 100.0;
77        
78        // Theta: ∂V/∂t (per day)
79        const double theta_term1 = -(S * std::exp(-q * T) * n_d1 * sigma) / (2 * sqrt_T);
80        if (input.is_call) {
81            const double theta_term2 = -r * K * std::exp(-r * T) * N_d2;
82            const double theta_term3 = q * S * std::exp(-q * T) * N_d1;
83            greeks.theta = (theta_term1 + theta_term2 + theta_term3) / 365.0;
84        } else {
85            const double theta_term2 = r * K * std::exp(-r * T) * (1 - N_d2);
86            const double theta_term3 = -q * S * std::exp(-q * T) * (1 - N_d1);
87            greeks.theta = (theta_term1 + theta_term2 + theta_term3) / 365.0;
88        }
89        
90        // Rho: ∂V/∂r (per 1% change in interest rate)
91        if (input.is_call) {
92            greeks.rho = K * T * std::exp(-r * T) * N_d2 / 100.0;
93        } else {
94            greeks.rho = -K * T * std::exp(-r * T) * (1 - N_d2) / 100.0;
95        }
96        
97        // Second-order Greeks
98        
99        // Vanna: ∂²V/∂S∂σ
100        greeks.vanna = -std::exp(-q * T) * n_d1 * d2 / sigma;
101        
102        // Volga (Vomma): ∂²V/∂σ²
103        greeks.volga = S * std::exp(-q * T) * n_d1 * sqrt_T * d1 * d2 / sigma;
104        
105        // Charm: ∂²V/∂S∂t (delta decay)
106        if (input.is_call) {
107            greeks.charm = -q * std::exp(-q * T) * N_d1
108                         - std::exp(-q * T) * n_d1 * (2 * (r - q) * T - d2 * sigma * sqrt_T)
109                           / (2 * T * sigma * sqrt_T);
110        } else {
111            greeks.charm = -q * std::exp(-q * T) * (N_d1 - 1)
112                         - std::exp(-q * T) * n_d1 * (2 * (r - q) * T - d2 * sigma * sqrt_T)
113                           / (2 * T * sigma * sqrt_T);
114        }
115        
116        return {price, greeks};
117    }
118
119private:
120    // Standard normal cumulative distribution function
121    static double normal_cdf(double x) {
122        return 0.5 * std::erfc(-x * std::numbers::inv_sqrt2);
123    }
124    
125    // Standard normal probability density function
126    static double normal_pdf(double x) {
127        constexpr double inv_sqrt_2pi = 0.3989422804014327;
128        return inv_sqrt_2pi * std::exp(-0.5 * x * x);
129    }
130};
131

Numerical Greeks for Complex Models#

For models without closed-form solutions, use finite differences:

cpp
1// numerical_greeks.hpp - Finite difference Greeks
2#pragma once
3
4#include <functional>
5
6class NumericalGreeks {
7public:
8    using PricingFunction = std::function<double(double spot, double vol, double time)>;
9    
10    struct Config {
11        double spot_bump = 0.01;      // 1% bump for delta/gamma
12        double vol_bump = 0.0001;     // 1bp bump for vega
13        double time_bump = 1.0/365.0; // 1 day for theta
14    };
15    
16    struct Greeks {
17        double delta;
18        double gamma;
19        double vega;
20        double theta;
21    };
22    
23    static Greeks calculate(
24        const PricingFunction& pricer,
25        double spot,
26        double vol,
27        double time,
28        const Config& config = {}
29    ) {
30        // Base price
31        const double V = pricer(spot, vol, time);
32        
33        // Delta: first-order central difference
34        const double V_up = pricer(spot * (1 + config.spot_bump), vol, time);
35        const double V_down = pricer(spot * (1 - config.spot_bump), vol, time);
36        const double delta = (V_up - V_down) / (2 * spot * config.spot_bump);
37        
38        // Gamma: second-order central difference
39        const double gamma = (V_up - 2 * V + V_down) / 
40                            std::pow(spot * config.spot_bump, 2);
41        
42        // Vega: first-order central difference
43        const double V_vol_up = pricer(spot, vol + config.vol_bump, time);
44        const double V_vol_down = pricer(spot, vol - config.vol_bump, time);
45        const double vega = (V_vol_up - V_vol_down) / (2 * config.vol_bump * 100);
46        
47        // Theta: backward difference (can't go forward in time easily)
48        const double V_time_minus = pricer(spot, vol, time - config.time_bump);
49        const double theta = -(V - V_time_minus) / config.time_bump * (1.0/365.0);
50        
51        return {delta, gamma, vega, theta};
52    }
53};
54

Advanced Pricing Models#

Binomial Tree for American Options#

American options require early exercise consideration:

cpp
1// binomial_tree.hpp - Binomial tree for American options
2#pragma once
3
4#include <vector>
5#include <algorithm>
6#include <cmath>
7
8class BinomialTree {
9public:
10    struct Inputs {
11        double spot;
12        double strike;
13        double time_to_expiry;
14        double risk_free_rate;
15        double volatility;
16        double dividend_yield;
17        bool is_call;
18        bool is_american;
19        int steps;  // Number of time steps
20    };
21    
22    static double price(const Inputs& input) {
23        const double dt = input.time_to_expiry / input.steps;
24        const double u = std::exp(input.volatility * std::sqrt(dt));
25        const double d = 1.0 / u;
26        const double a = std::exp((input.risk_free_rate - input.dividend_yield) * dt);
27        const double p = (a - d) / (u - d);  // Risk-neutral probability
28        const double discount = std::exp(-input.risk_free_rate * dt);
29        
30        // Build price tree
31        std::vector<double> prices(input.steps + 1);
32        
33        // Terminal nodes
34        for (int i = 0; i <= input.steps; ++i) {
35            const double S_T = input.spot * std::pow(u, input.steps - i) * std::pow(d, i);
36            
37            if (input.is_call) {
38                prices[i] = std::max(0.0, S_T - input.strike);
39            } else {
40                prices[i] = std::max(0.0, input.strike - S_T);
41            }
42        }
43        
44        // Backward induction
45        for (int step = input.steps - 1; step >= 0; --step) {
46            for (int i = 0; i <= step; ++i) {
47                // Continuation value
48                const double continuation = discount * (p * prices[i] + (1 - p) * prices[i + 1]);
49                
50                if (input.is_american) {
51                    // Early exercise value
52                    const double S = input.spot * std::pow(u, step - i) * std::pow(d, i);
53                    const double exercise = input.is_call ? 
54                        std::max(0.0, S - input.strike) :
55                        std::max(0.0, input.strike - S);
56                    
57                    prices[i] = std::max(continuation, exercise);
58                } else {
59                    prices[i] = continuation;
60                }
61            }
62        }
63        
64        return prices[0];
65    }
66};
67

Monte Carlo for Path-Dependent Options#

For exotic options (Asian, barriers, etc.):

cpp
1// monte_carlo.hpp - Monte Carlo option pricing
2#pragma once
3
4#include <random>
5#include <vector>
6#include <cmath>
7#include <algorithm>
8#include <thread>
9#include <numeric>
10
11class MonteCarlo {
12public:
13    struct Inputs {
14        double spot;
15        double strike;
16        double time_to_expiry;
17        double risk_free_rate;
18        double volatility;
19        double dividend_yield;
20        bool is_call;
21        size_t num_paths;
22        size_t num_steps;      // For path-dependent options
23        unsigned int seed;
24    };
25    
26    // Standard European option
27    static double european_option(const Inputs& input) {
28        std::mt19937_64 rng(input.seed);
29        std::normal_distribution<double> normal(0.0, 1.0);
30        
31        const double drift = (input.risk_free_rate - input.dividend_yield - 
32                             0.5 * input.volatility * input.volatility) * input.time_to_expiry;
33        const double vol_sqrt_t = input.volatility * std::sqrt(input.time_to_expiry);
34        const double discount = std::exp(-input.risk_free_rate * input.time_to_expiry);
35        
36        double payoff_sum = 0.0;
37        
38        for (size_t i = 0; i < input.num_paths; ++i) {
39            const double Z = normal(rng);
40            const double S_T = input.spot * std::exp(drift + vol_sqrt_t * Z);
41            
42            double payoff;
43            if (input.is_call) {
44                payoff = std::max(0.0, S_T - input.strike);
45            } else {
46                payoff = std::max(0.0, input.strike - S_T);
47            }
48            
49            payoff_sum += payoff;
50        }
51        
52        return discount * (payoff_sum / input.num_paths);
53    }
54    
55    // Asian option (average price)
56    static double asian_option(const Inputs& input) {
57        std::mt19937_64 rng(input.seed);
58        std::normal_distribution<double> normal(0.0, 1.0);
59        
60        const double dt = input.time_to_expiry / input.num_steps;
61        const double drift = (input.risk_free_rate - input.dividend_yield - 
62                             0.5 * input.volatility * input.volatility) * dt;
63        const double vol_sqrt_dt = input.volatility * std::sqrt(dt);
64        const double discount = std::exp(-input.risk_free_rate * input.time_to_expiry);
65        
66        double payoff_sum = 0.0;
67        
68        for (size_t i = 0; i < input.num_paths; ++i) {
69            double S = input.spot;
70            double price_sum = 0.0;
71            
72            // Generate path
73            for (size_t step = 0; step < input.num_steps; ++step) {
74                const double Z = normal(rng);
75                S *= std::exp(drift + vol_sqrt_dt * Z);
76                price_sum += S;
77            }
78            
79            // Average price
80            const double avg_price = price_sum / input.num_steps;
81            
82            // Payoff based on average
83            double payoff;
84            if (input.is_call) {
85                payoff = std::max(0.0, avg_price - input.strike);
86            } else {
87                payoff = std::max(0.0, input.strike - avg_price);
88            }
89            
90            payoff_sum += payoff;
91        }
92        
93        return discount * (payoff_sum / input.num_paths);
94    }
95    
96    // Barrier option (knock-out)
97    static double barrier_option(
98        const Inputs& input,
99        double barrier,
100        bool is_up_and_out
101    ) {
102        std::mt19937_64 rng(input.seed);
103        std::normal_distribution<double> normal(0.0, 1.0);
104        
105        const double dt = input.time_to_expiry / input.num_steps;
106        const double drift = (input.risk_free_rate - input.dividend_yield - 
107                             0.5 * input.volatility * input.volatility) * dt;
108        const double vol_sqrt_dt = input.volatility * std::sqrt(dt);
109        const double discount = std::exp(-input.risk_free_rate * input.time_to_expiry);
110        
111        double payoff_sum = 0.0;
112        
113        for (size_t i = 0; i < input.num_paths; ++i) {
114            double S = input.spot;
115            bool knocked_out = false;
116            
117            // Generate path and check barrier
118            for (size_t step = 0; step < input.num_steps; ++step) {
119                const double Z = normal(rng);
120                S *= std::exp(drift + vol_sqrt_dt * Z);
121                
122                // Check barrier condition
123                if (is_up_and_out && S >= barrier) {
124                    knocked_out = true;
125                    break;
126                } else if (!is_up_and_out && S <= barrier) {
127                    knocked_out = true;
128                    break;
129                }
130            }
131            
132            // Payoff only if not knocked out
133            if (!knocked_out) {
134                double payoff;
135                if (input.is_call) {
136                    payoff = std::max(0.0, S - input.strike);
137                } else {
138                    payoff = std::max(0.0, input.strike - S);
139                }
140                payoff_sum += payoff;
141            }
142        }
143        
144        return discount * (payoff_sum / input.num_paths);
145    }
146    
147    // Parallel Monte Carlo with variance reduction
148    static double european_option_parallel(const Inputs& input) {
149        const size_t num_threads = std::thread::hardware_concurrency();
150        const size_t paths_per_thread = input.num_paths / num_threads;
151        
152        std::vector<std::thread> threads;
153        std::vector<double> results(num_threads);
154        
155        for (size_t t = 0; t < num_threads; ++t) {
156            threads.emplace_back([&, t]() {
157                Inputs thread_input = input;
158                thread_input.num_paths = paths_per_thread;
159                thread_input.seed = input.seed + t;
160                
161                // Antithetic variates for variance reduction
162                results[t] = european_option_antithetic(thread_input);
163            });
164        }
165        
166        for (auto& thread : threads) {
167            thread.join();
168        }
169        
170        return std::accumulate(results.begin(), results.end(), 0.0) / num_threads;
171    }
172
173private:
174    // Antithetic variates variance reduction
175    static double european_option_antithetic(const Inputs& input) {
176        std::mt19937_64 rng(input.seed);
177        std::normal_distribution<double> normal(0.0, 1.0);
178        
179        const double drift = (input.risk_free_rate - input.dividend_yield - 
180                             0.5 * input.volatility * input.volatility) * input.time_to_expiry;
181        const double vol_sqrt_t = input.volatility * std::sqrt(input.time_to_expiry);
182        const double discount = std::exp(-input.risk_free_rate * input.time_to_expiry);
183        
184        double payoff_sum = 0.0;
185        
186        for (size_t i = 0; i < input.num_paths / 2; ++i) {
187            const double Z = normal(rng);
188            
189            // Path with +Z
190            const double S_T_plus = input.spot * std::exp(drift + vol_sqrt_t * Z);
191            double payoff_plus = input.is_call ? 
192                std::max(0.0, S_T_plus - input.strike) :
193                std::max(0.0, input.strike - S_T_plus);
194            
195            // Antithetic path with -Z
196            const double S_T_minus = input.spot * std::exp(drift - vol_sqrt_t * Z);
197            double payoff_minus = input.is_call ? 
198                std::max(0.0, S_T_minus - input.strike) :
199                std::max(0.0, input.strike - S_T_minus);
200            
201            payoff_sum += (payoff_plus + payoff_minus) / 2.0;
202        }
203        
204        return discount * (payoff_sum / (input.num_paths / 2));
205    }
206};
207

Implied Volatility Calculation#

Market prices embed volatility expectations. Extract IV using Newton-Raphson:

cpp
1// implied_volatility.hpp - IV calculation
2#pragma once
3
4#include "black_scholes.hpp"
5#include <cmath>
6#include <optional>
7
8class ImpliedVolatility {
9public:
10    struct Result {
11        double implied_vol;
12        int iterations;
13        double error;
14    };
15    
16    static std::optional<Result> calculate(
17        double market_price,
18        double spot,
19        double strike,
20        double time_to_expiry,
21        double risk_free_rate,
22        double dividend_yield,
23        bool is_call,
24        double initial_guess = 0.3,
25        double tolerance = 1e-6,
26        int max_iterations = 100
27    ) {
28        double vol = initial_guess;
29        
30        for (int iter = 0; iter < max_iterations; ++iter) {
31            // Calculate price and vega
32            BlackScholes::Inputs input{
33                spot, strike, time_to_expiry,
34                risk_free_rate, vol, dividend_yield, is_call
35            };
36            
37            auto result = BlackScholes::calculate(input);
38            
39            const double price_error = result.price - market_price;
40            
41            // Check convergence
42            if (std::abs(price_error) < tolerance) {
43                return Result{vol, iter, price_error};
44            }
45            
46            // Newton-Raphson update
47            const double vega_decimal = result.greeks.vega * 100;  // Convert from % to decimal
48            
49            if (std::abs(vega_decimal) < 1e-10) {
50                // Vega too small, can't converge
51                return std::nullopt;
52            }
53            
54            vol -= price_error / vega_decimal;
55            
56            // Keep vol positive and reasonable
57            vol = std::clamp(vol, 0.001, 5.0);
58        }
59        
60        // Failed to converge
61        return std::nullopt;
62    }
63    
64    // Vectorized IV calculation for smile/surface
65    static std::vector<double> calculate_smile(
66        const std::vector<double>& market_prices,
67        const std::vector<double>& strikes,
68        double spot,
69        double time_to_expiry,
70        double risk_free_rate,
71        double dividend_yield,
72        bool is_call
73    ) {
74        std::vector<double> implied_vols;
75        implied_vols.reserve(market_prices.size());
76        
77        for (size_t i = 0; i < market_prices.size(); ++i) {
78            auto result = calculate(
79                market_prices[i],
80                spot,
81                strikes[i],
82                time_to_expiry,
83                risk_free_rate,
84                dividend_yield,
85                is_call
86            );
87            
88            if (result) {
89                implied_vols.push_back(result->implied_vol);
90            } else {
91                implied_vols.push_back(std::nan(""));
92            }
93        }
94        
95        return implied_vols;
96    }
97};
98

Portfolio Greeks Aggregation#

For a portfolio of options, aggregate Greeks efficiently:

cpp
1// portfolio_greeks.hpp - Portfolio-level Greeks
2#pragma once
3
4#include <vector>
5#include <unordered_map>
6#include <string>
7#include "black_scholes.hpp"
8
9class PortfolioGreeks {
10public:
11    struct Position {
12        std::string symbol;
13        double quantity;  // Number of contracts (can be negative for short)
14        BlackScholes::Inputs option_params;
15    };
16    
17    struct PortfolioRisk {
18        // Total Greeks
19        double delta;
20        double gamma;
21        double vega;
22        double theta;
23        double rho;
24        
25        // Greeks by underlying
26        std::unordered_map<std::string, double> delta_by_symbol;
27        std::unordered_map<std::string, double> gamma_by_symbol;
28        std::unordered_map<std::string, double> vega_by_symbol;
29        
30        // Risk metrics
31        double total_notional;
32        double total_market_value;
33        double var_1d_95;  // 1-day VaR at 95% confidence
34    };
35    
36    static PortfolioRisk calculate(
37        const std::vector<Position>& positions,
38        double spot_shock = 0.01,      // 1% spot shock for VaR
39        double vol_shock = 0.0001      // 1bp vol shock for VaR
40    ) {
41        PortfolioRisk risk{};
42        
43        for (const auto& pos : positions) {
44            // Calculate Greeks for this position
45            auto result = BlackScholes::calculate(pos.option_params);
46            
47            const double contract_multiplier = 100.0;  // Standard option contract
48            const double position_size = pos.quantity * contract_multiplier;
49            
50            // Aggregate total Greeks
51            risk.delta += result.greeks.delta * position_size;
52            risk.gamma += result.greeks.gamma * position_size;
53            risk.vega += result.greeks.vega * position_size;
54            risk.theta += result.greeks.theta * position_size;
55            risk.rho += result.greeks.rho * position_size;
56            
57            // Aggregate by symbol
58            risk.delta_by_symbol[pos.symbol] += result.greeks.delta * position_size;
59            risk.gamma_by_symbol[pos.symbol] += result.greeks.gamma * position_size;
60            risk.vega_by_symbol[pos.symbol] += result.greeks.vega * position_size;
61            
62            // Market value
63            risk.total_market_value += result.price * position_size;
64            risk.total_notional += pos.option_params.spot * std::abs(position_size);
65        }
66        
67        // Calculate VaR (delta-gamma approximation)
68        risk.var_1d_95 = calculate_var_delta_gamma(positions, spot_shock);
69        
70        return risk;
71    }
72
73private:
74    static double calculate_var_delta_gamma(
75        const std::vector<Position>& positions,
76        double spot_shock
77    ) {
78        // Group positions by underlying
79        std::unordered_map<std::string, std::vector<const Position*>> by_underlying;
80        
81        for (const auto& pos : positions) {
82            by_underlying[pos.symbol].push_back(&pos);
83        }
84        
85        double total_var = 0.0;
86        
87        // Calculate VaR for each underlying
88        for (const auto& [symbol, symbol_positions] : by_underlying) {
89            double delta = 0.0;
90            double gamma = 0.0;
91            double spot = 0.0;
92            
93            for (const auto* pos : symbol_positions) {
94                auto result = BlackScholes::calculate(pos->option_params);
95                const double size = pos->quantity * 100.0;
96                
97                delta += result.greeks.delta * size;
98                gamma += result.greeks.gamma * size;
99                spot = pos->option_params.spot;  // Assume same spot for same symbol
100            }
101            
102            // Delta-gamma approximation for P&L change
103            const double dS = spot * spot_shock;
104            const double pnl_change = delta * dS + 0.5 * gamma * dS * dS;
105            
106            total_var += pnl_change * pnl_change;  // Assuming independence (simplification)
107        }
108        
109        return std::sqrt(total_var) * 1.65;  // 95% confidence (1.65 std devs)
110    }
111};
112

Production Considerations#

Caching and Performance#

cpp
1// option_pricer_cache.hpp - Cached pricing for performance
2#pragma once
3
4#include <unordered_map>
5#include <mutex>
6#include <chrono>
7
8class CachedOptionPricer {
9public:
10    struct CacheKey {
11        double spot;
12        double strike;
13        double vol;
14        double time;
15        bool is_call;
16        
17        bool operator==(const CacheKey& other) const {
18            return spot == other.spot &&
19                   strike == other.strike &&
20                   vol == other.vol &&
21                   time == other.time &&
22                   is_call == other.is_call;
23        }
24    };
25    
26    struct CacheKeyHash {
27        size_t operator()(const CacheKey& k) const {
28            size_t h = std::hash<double>{}(k.spot);
29            h ^= std::hash<double>{}(k.strike) << 1;
30            h ^= std::hash<double>{}(k.vol) << 2;
31            h ^= std::hash<double>{}(k.time) << 3;
32            h ^= std::hash<bool>{}(k.is_call) << 4;
33            return h;
34        }
35    };
36    
37    CachedOptionPricer(std::chrono::seconds ttl = std::chrono::seconds(60))
38        : cache_ttl_(ttl) {}
39    
40    BlackScholes::Result price(const BlackScholes::Inputs& input) {
41        // Create cache key (rounded to reduce cache misses)
42        CacheKey key{
43            round_to_precision(input.spot, 0.01),
44            round_to_precision(input.strike, 0.01),
45            round_to_precision(input.volatility, 0.0001),
46            round_to_precision(input.time_to_expiry, 1.0/365.0),
47            input.is_call
48        };
49        
50        // Check cache
51        {
52            std::shared_lock lock(mutex_);
53            auto it = cache_.find(key);
54            if (it != cache_.end()) {
55                auto age = std::chrono::steady_clock::now() - it->second.timestamp;
56                if (age < cache_ttl_) {
57                    ++cache_hits_;
58                    return it->second.result;
59                }
60            }
61        }
62        
63        // Cache miss - calculate
64        ++cache_misses_;
65        auto result = BlackScholes::calculate(input);
66        
67        // Update cache
68        {
69            std::unique_lock lock(mutex_);
70            cache_[key] = {result, std::chrono::steady_clock::now()};
71        }
72        
73        return result;
74    }
75    
76    void clear_cache() {
77        std::unique_lock lock(mutex_);
78        cache_.clear();
79    }
80    
81    double get_hit_rate() const {
82        uint64_t total = cache_hits_ + cache_misses_;
83        return total > 0 ? static_cast<double>(cache_hits_) / total : 0.0;
84    }
85
86private:
87    struct CacheEntry {
88        BlackScholes::Result result;
89        std::chrono::steady_clock::time_point timestamp;
90    };
91    
92    static double round_to_precision(double value, double precision) {
93        return std::round(value / precision) * precision;
94    }
95    
96    std::unordered_map<CacheKey, CacheEntry, CacheKeyHash> cache_;
97    mutable std::shared_mutex mutex_;
98    std::chrono::seconds cache_ttl_;
99    std::atomic<uint64_t> cache_hits_{0};
100    std::atomic<uint64_t> cache_misses_{0};
101};
102

Key Lessons#

  1. Black-Scholes is Fast: Use it when applicable; closed-form beats numerical methods
  2. Cache Aggressively: Option prices don't change every microsecond
  3. Greeks Matter: Delta hedging is continuous; monitor all Greeks
  4. Numerical Stability: Check for edge cases (deep ITM/OTM, near expiry)
  5. Implied Vol is Critical: Market prices are in vol space, not price space
  6. American Options Need Trees: Early exercise makes closed-form impossible
  7. Monte Carlo for Exotics: Path dependence requires simulation
  8. Aggregate Carefully: Portfolio Greeks need proper aggregation by underlying

Conclusion#

Option pricing and Greeks calculation are fundamental to derivatives trading. The key is choosing the right model for each situation: Black-Scholes for vanilla Europeans, trees for Americans, Monte Carlo for exotics.

In production, performance matters. Cache results, use SIMD when possible, and pre-calculate what you can. But never sacrifice accuracy for speed—a mispriced option can cost millions.


Building derivatives pricing systems? Contact us to discuss your quantitative finance infrastructure.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

OptionsDerivativesPricingRisk ManagementC++

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 26, 2025•11 min read
GPU-Accelerated Portfolio Optimization: When 10 Hours Becomes 10 Seconds
Quantitative Financeportfolio-optimizationGPU
Nov 25, 2025•12 min read
Statistical Arbitrage Strategies: From LTCM's Ashes to Modern Quant Funds
Quantitative Financestatistical-arbitragecointegration
Nov 25, 2025•8 min read
Principal Component Analysis for Yield Curves and Volatility Surfaces
Quantitative FinancePCAyield-curve

Interested in working together?