The Research-to-Production Gap in Algorithmic Trading Is a Binding Problem in Disguise
Source: isocpp
The framing in most discussions about Python and C++ in trading is performance: Python is for strategy research, C++ is for execution, and the question is how to connect them without paying too much. Richard Hickling’s recent post on ISOcpp.org applies C++26 reflection to this problem, showing how automatic binding generation can replace the manual glue between Python strategy code and C++ pricing engines.
The latency story is real but somewhat overplayed. For high-frequency trading, microseconds matter and any cross-language boundary is suspect. For most algorithmic trading, though, the bottleneck is rarely the function call overhead through pybind11. It is something less visible and more damaging: the research-to-production gap, the growing divergence between what a quant has built in Python and what actually runs in the C++ production system.
How the gap opens
A typical workflow in a quantitative trading team looks roughly like this. A researcher prototypes a pricing model or signal generator in Python, using numpy and scipy for the numerics and pandas for data handling. Backtesting runs in Python. When the strategy shows promise, it gets handed to a developer who implements the same model in C++ for the production execution system. The two implementations are supposed to be equivalent, but they are maintained separately and diverge quietly over time.
The Python version gets updated as the researcher refines the model: a new risk factor gets added, the calibration routine changes, a second-order Greek becomes relevant. Each of those changes generates a task to update the C++ implementation and, separately, a task to update the Python bindings that expose the C++ version for backtesting. In practice, the binding update is the task that slips. It requires context about both the C++ internals and the pybind11 layer, it is unglamorous, and it is invisible until someone runs a backtest and finds the Greek they expected to see is not there.
The result is a system where the Python research environment is quietly decoupled from the C++ production environment. Backtesting uses one version of the model; execution uses another. The gap is not malicious or even intentional; it accumulates through ordinary work prioritization.
Where the C++ side of the model lives
The component that most commonly sits at the center of this gap is the pricer: a C++ class that takes market data and parameters and computes theoretical prices and sensitivities. A Black-Scholes pricer is the simple case, but production pricers carry local volatility surfaces, stochastic correlation models, American exercise adjustments. They have methods for price, delta, gamma, vega, theta, rho, and possibly a dozen more output quantities depending on the product.
Every one of those methods requires a corresponding .def() call in the pybind11 binding code:
pybind11::class_<Pricer>(m, "Pricer")
.def("price", &Pricer::price)
.def("delta", &Pricer::delta)
.def("gamma", &Pricer::gamma)
.def("vega", &Pricer::vega)
.def("theta", &Pricer::theta);
// rho was added last quarter and is still not here
This is not a pathological case. It is what routine pricer maintenance looks like. A researcher asks for second-order cross-Greeks. The C++ developer adds vanna() and volga() to the pricer. The backtest environment does not see them until someone updates the bindings, tests them, and ships the change. By that point, the researcher may have worked around the gap using a finite-difference approximation in Python, which is slower and less accurate, and which may or may not get cleaned up once the proper binding lands.
What reflection changes operationally
C++26 static reflection, which I covered in more technical depth in an earlier post on the P2996 proposal, allows a generic binding function to enumerate a class’s public methods at compile time and register each with pybind11 without manual enumeration:
template <typename T>
void bind_class(pybind11::module_& m, const char* name) {
auto cls = pybind11::class_<T>(m, name);
template for (constexpr auto fn : std::meta::member_functions_of(^T)) {
if constexpr (std::meta::is_public(fn) &&
!std::meta::is_constructor(fn) &&
!std::meta::is_destructor(fn)) {
cls.def(
std::meta::identifier_of(fn).data(),
&T::[:fn:]
);
}
}
}
When vanna() and volga() are added to the pricer in C++, the next build automatically makes them available in Python. The binding file does not need to be touched. More precisely, there is no binding file to forget about.
The operational shift this enables is meaningful in a domain where model APIs change frequently and the cost of binding drift is measured in bad backtests and eroded researcher trust. Keeping the Python research environment synchronized with C++ production no longer requires a separate maintenance task; it is a structural property of the build.
The trading-specific complications
Option pricers accumulate overloaded methods for good reasons. A barrier option pricer might expose price(double spot, double vol) for European-equivalent pricing and price(double spot, double vol, double barrier_level) for the full barrier. Reflection-based binding cannot automatically resolve this case: &T::[:fn:] is ambiguous when two methods share a name, and pybind11 needs a cast or an explicit selection to disambiguate. These cases require manual .def() calls or an annotation system that encodes the selection policy in the C++ source.
Return value semantics matter more in trading systems than in general library code. A C++ pricer that returns const VolSurface* might be returning a pointer to an internally cached surface that must not be modified, or a pointer the caller should assume ownership of. pybind11 exposes six return value policies to distinguish these cases, and reflection has no way to infer the right one from the type signature alone. As with overloads, annotations embedded in the source are the path forward, likely through user-defined attributes once P1854 moves further through the committee.
Documentation transfer is a quieter problem. Quant researchers depend on docstrings when using a Python API. A reflection-based generator can expose the C++ method name but not the Doxygen comments that describe parameter units, sign conventions, or assumptions. A pricer that silently expects annualized volatility while a researcher passes daily volatility is a correctness bug that better documentation would prevent. The binding infrastructure cannot supply what was never encoded in a machine-readable form.
What the actual ceiling is
For the portion of a typical trading system C++ API that is mechanically bindable, that is, non-overloaded public methods with straightforward return types, automatic reflection-based binding eliminates the maintenance task entirely. For production codebases where the majority of pricer API surface falls in that category, the research-to-production synchronization problem shrinks from an ongoing operational concern to a smaller set of precisely-defined edge cases.
Hickling’s trading framing is useful precisely because it grounds the reflection discussion in a domain where the cost of binding drift is concrete and familiar. The problem is not abstract engineering hygiene; it is the difference between a backtest that uses the model your execution system runs and one that uses a slightly different approximation of it. Reflection does not eliminate that gap entirely, but it removes the mechanism by which the gap opens most often.