QuantLib 开发指南

QuantLib 开发指南

面向后续开发者的实践指南

本文档面向需要:

  • 在 QuantLib 基础上开发新功能
  • 扩展现有模块(新工具、新引擎、新模型)
  • 理解库设计规范以保持代码一致性
  • 优化现有实现并贡献到上游

1. 快速入门

1.1 环境搭建

# 1. 克隆仓库
git clone <repo-url> QuantLib
cd QuantLib

# 2. 配置构建(推荐 CMake + Ninja)
cmake --preset linux-gcc-ninja-release
# 或 Debug 模式
cmake --preset linux-gcc-debug

# 3. 编译
cmake --build build/linux-gcc-ninja-release -j$(nproc)

# 4. 运行测试
ctest --test-dir build/linux-gcc-ninja-release
# 或运行特定测试
./build/linux-gcc-ninja-release/test-suite/quantlib-test-suite --run_test=EuropeanOption

# 5. 运行基准测试
./build/linux-gcc-ninja-release/test-suite/quantlib-benchmark

1.2 最小应用示例

#include <ql/quantlib.hpp>
#include <iostream>

using namespace QuantLib;

int main() {
    // 1. 设置全局评估日期
    Settings::instance().evaluationDate() = Date(15, June, 2024);

    // 2. 构造市场数据
    Handle<Quote> spot(ext::make_shared<SimpleQuote>(100.0));
    Handle<YieldTermStructure> rTS(flatRate(0.05, Actual365Fixed()));
    Handle<YieldTermStructure> qTS(flatRate(0.02, Actual365Fixed()));
    Handle<BlackVolTermStructure> volTS(flatVol(0.20, Actual365Fixed()));

    // 3. 构造过程
    auto process = ext::make_shared<BlackScholesMertonProcess>(
        spot, qTS, rTS, volTS);

    // 4. 构造工具
    auto payoff = ext::make_shared<PlainVanillaPayoff>(Option::Call, 100.0);
    auto exercise = ext::make_shared<EuropeanExercise>(Date(20, December, 2024));
    VanillaOption option(payoff, exercise);

    // 5. 设置引擎并定价
    option.setPricingEngine(
        ext::make_shared<AnalyticEuropeanEngine>(process));

    std::cout << "NPV: " << option.NPV() << std::endl;
    std::cout << "Delta: " << option.delta() << std::endl;
    return 0;
}

2. 核心设计模式与如何扩展

2.0 开发新功能流程总览

graph TD
    Start["Determine Requirement"] --> Step1{"Need new Instrument?"}
    Step1 -->|Yes| NewInst["Create Instrument subclass"]
    Step1 -->|No| Step2{"Need new pricing method?"}

    NewInst --> Args["Define arguments/results"]
    Args --> EngineTypedef["Define engine typedef"]
    EngineTypedef --> Step2

    Step2 -->|Yes| NewEngine["Create PricingEngine subclass"]
    Step2 -->|No| Step3{"Need new model/process?"}

    NewEngine --> RegisterWith["registerWith Handles in ctor"]
    RegisterWith --> CalcImpl["Implement calculate()"]
    CalcImpl --> Step3

    Step3 -->|Yes| NewModel["Create Model/Process subclass"]
    Step3 -->|No| Step4{"Need new market data?"}

    NewModel --> Calibrate["Implement calibrate"]
    Calibrate --> Step4

    Step4 -->|Yes| NewTS["Create TermStructure or Index"]
    Step4 -->|No| Done["Register files in CMakeLists.txt"]

    NewTS --> ImplMethod["Implement Impl methods"]
    ImplMethod --> Done

    Done --> Test["Write tests"]
    Test --> Format["Run clang-format"]
    Format --> Build["Build and test pass"]

2.1 Observer/Observable 模式

何时使用: 任何需要根据输入数据变化自动重计算的对象。

实现规范:

// === 定义新的 Observable ===
class MyObservable : public virtual Observable {
public:
    void modify() {
        // ... 修改内部状态 ...
        notifyObservers();  // 通知所有注册的 Observer
    }
};

// === 定义新的 Observer(同时是 Observable)===
class MyObserver : public virtual Observer, public virtual Observable {
public:
    MyObserver(const Handle<SomeObservable>& h) {
        registerWith(h);  // 注册为 h 的观察者
    }

    void update() override {
        // 当被观察对象变化时调用
        // 标记自己为 dirty
        notifyObservers();  // 传播通知给自己的观察者
    }
};
sequenceDiagram
    participant Q as SimpleQuote
    participant H as Handle
    participant PEC as YieldCurve
    participant Idx as Index
    participant Cpn as FltCoupon
    participant Swap as VanillaSwap

    Q->>Q: setValue(newRate)
    Q->>Q: notifyObservers()
    Q->>H: update()
    H->>PEC: update()
    PEC->>PEC: LazyObject update
    PEC->>PEC: calculated = false
    PEC->>Idx: notifyObservers()
    Idx->>Cpn: update()
    Cpn->>Cpn: calculated = false
    Cpn->>Swap: notifyObservers()
    Swap->>Swap: calculated = false

    Note over Swap: Auto-recalculate on next NPV()

关键规则:

  • registerWith() 必须在构造时调用,确保对象在销毁时自动解注册
  • notifyObservers() 在状态变化后调用
  • 不要在 update() 中做重量级计算 — 只标记 dirty,计算延迟到访问时

2.2 LazyObject 模式

何时使用: 任何需要按需计算并缓存结果的对象。

stateDiagram-v2
    [*] --> Dirty : construct
    Dirty --> Calculating : calculate
    Calculating --> Clean : success
    Calculating --> Failed : throws exception
    Clean --> Dirty : update
    Failed --> Dirty : update
    Dirty --> Frozen : freeze
    Clean --> Frozen : freeze
    Frozen --> Clean : unfreeze

实现规范:

class MyLazyCalculator : public LazyObject {
public:
    Real result() const {
        calculate();          // 触发 LazyObject 框架
        return cachedResult_; // 返回缓存结果
    }

protected:
    void performCalculations() const override {
        // 从依赖数据(通过 Handle 访问)计算结果
        cachedResult_ = expensiveComputation(inputHandle_->value());
        // 注:cachedResult_ 声明为 mutable
    }

private:
    Handle<InputData> inputHandle_;
    mutable Real cachedResult_;
};

关键规则:

  • calculate() 必须在任何访问计算结果的方法中首先调用
  • performCalculations() 必须声明为 const(结果成员声明为 mutable
  • 如果计算可能抛出异常,calculate() 会自动管理 failed_ 标志
  • 子类不直接调用 performCalculations() — 总是通过 calculate()

2.3 添加新的 Instrument

sequenceDiagram
    participant Client
    participant Inst as Instrument
    participant LO as LazyObject
    participant Eng as PricingEngine

    Client->>Inst: NPV()
    Inst->>LO: calculate()

    alt expired
        Inst->>Inst: setupExpired
    else not expired
        Inst->>Inst: performCalculations
        Inst->>Eng: reset and setupArguments
        Eng->>Eng: validate
        Eng->>Eng: calculate
        Note over Eng: Step1 query Handles for market data
        Note over Eng: Step2 run numerical method
        Note over Eng: Step3 write results
        Inst->>Eng: fetchResults
    end

    Inst-->>Client: NPV
// === 1. 定义工具类 ===
class MyInstrument : public Instrument {
public:
    // 嵌套类型 — 引擎接口
    class arguments;
    class results;
    class engine;

    MyInstrument(/* 工具参数 */);

    bool isExpired() const override { /* ... */ }

protected:
    void setupArguments(PricingEngine::arguments*) const override;
    void fetchResults(const PricingEngine::results*) const override;
    void setupExpired() const override { NPV_ = 0.0; }
};

// === 2. 定义 arguments 和 results ===
class MyInstrument::arguments : public virtual PricingEngine::arguments {
public:
    // 工具参数(引擎需要的数据)
    Real strike;
    Date maturity;
    void validate() const override { /* 检查参数有效性 */ }
};

class MyInstrument::results : public Instrument::results {
public:
    // 引擎输出
    Real delta, gamma;
    void reset() override {
        Instrument::results::reset();
        delta = gamma = Null<Real>();
    }
};

// === 3. 定义 engine typedef ===
class MyInstrument::engine 
    : public GenericEngine<MyInstrument::arguments, MyInstrument::results> {};

2.4 添加新的 PricingEngine

// 解析引擎示例
class MyAnalyticEngine : public MyInstrument::engine {
public:
    MyAnalyticEngine(const Handle<YieldTermStructure>& curve)
    : curve_(curve) {
        registerWith(curve_);  // 关键:注册为 Handle 的观察者
    }

    void calculate() const override {
        // 1. 从 arguments_ 读取工具参数
        Real strike = arguments_.strike;

        // 2. 从 Handle 查询市场数据
        DiscountFactor df = curve_->discount(arguments_.maturity);

        // 3. 执行计算并写入 results_
        results_.value = computeNPV(strike, df);
        results_.delta = computeDelta(strike, df);
    }

private:
    Handle<YieldTermStructure> curve_;
};

// FD 引擎示例
class MyFdEngine : public MyInstrument::engine {
public:
    void calculate() const override {
        // 构造 FdmSolverDesc...
        // 创建 FdmBackwardSolver + 时间离散...
        // 调用 solver.rollback()...
    }
};

// MC 引擎示例
class MyMcEngine : public MyInstrument::engine,
                   public McSimulation<SingleVariate, PseudoRandom, Statistics> {
public:
    MyMcEngine(const ext::shared_ptr<StochasticProcess>& process)
    : process_(process) {
        registerWith(process_);
    }

    void calculate() const override {
        McSimulation::calculate(1e-4, 1000000);  // MC 循环
        results_.value = mcModel_->sampleAccumulator().mean();
    }

protected:
    // McSimulation 要求的纯虚函数
    ext::shared_ptr<PathPricer<Path>> pathPricer() const override;
    ext::shared_ptr<PathGenerator> pathGenerator() const override;
    TimeGrid timeGrid() const override;
};

引擎开发检查清单:

  • [ ] 构造函数中 registerWith() 所有 Handle 成员
  • [ ] calculate() 声明为 const,写入 mutable results_
  • [ ] 从 arguments_ 读取,向 results_ 写入
  • [ ] 错误处理:异常会自动被 Instrument 捕获并设置 failed_

2.5 添加新的 TermStructure

// === 1. 直接插值曲线(最简单)===
class MyInterpolatedCurve : public InterpolatedDiscountCurve<LogLinear> {
public:
    MyInterpolatedCurve(const std::vector<Date>& dates,
                        const std::vector<DiscountFactor>& dfs,
                        const DayCounter& dc)
    : InterpolatedDiscountCurve<LogLinear>(dates, dfs, dc) {}
};

// === 2. 自举曲线(通过 BootstrapHelper)===
class MyPiecewiseCurve 
    : public PiecewiseYieldCurve<Discount, LogLinear, IterativeBootstrap> {
public:
    MyPiecewiseCurve(const Date& refDate,
                     const std::vector<ext::shared_ptr<RateHelper>>& helpers,
                     const DayCounter& dc)
    : PiecewiseYieldCurve<Discount, LogLinear, IterativeBootstrap>(
          refDate, helpers, dc) {}
};

// === 3. 装饰器(包装已有曲线并修改行为)===
class MySpreadedCurve : public YieldTermStructure {
public:
    MySpreadedCurve(const Handle<YieldTermStructure>& base,
                    const Handle<Quote>& spread)
    : base_(base), spread_(spread) {
        registerWith(base_);
        registerWith(spread_);
    }

protected:
    DiscountFactor discountImpl(Time t) const override {
        return base_->discount(t) * std::exp(-spread_->value() * t);
    }

private:
    Handle<YieldTermStructure> base_;
    Handle<Quote> spread_;
};

2.6 添加新的 StochasticProcess

class MyProcess : public StochasticProcess1D {
public:
    MyProcess(Real x0, Real speed, Real level, Real sigma)
    : x0_(x0), speed_(speed), level_(level), sigma_(sigma) {}

    // 必须实现的纯虚函数
    Real x0() const override { return x0_; }
    Real drift(Time t, Real x) const override {
        return speed_ * (level_ - x);  // OU 漂移
    }
    Real diffusion(Time t, Real x) const override {
        return sigma_;  // 常数扩散
    }

    // 可选:提供解析矩(绕过离散化)
    Real expectation(Time t0, Real x0, Time dt) const override {
        return level_ + (x0 - level_) * std::exp(-speed_ * dt);
    }
    Real variance(Time t0, Real x0, Time dt) const override {
        return sigma_ * sigma_ / (2 * speed_) * (1 - std::exp(-2 * speed_ * dt));
    }

private:
    Real x0_, speed_, level_, sigma_;
};

2.7 添加新的 CalibratedModel

class MyModel : public CalibratedModel {
public:
    MyModel(Real a, Real sigma)
    : CalibratedModel(2) {  // 2 个参数
        arguments_[0] = ConstantParameter(a, PositiveConstraint());
        arguments_[1] = ConstantParameter(sigma, PositiveConstraint());
        generateArguments();
    }

    // 暴露参数
    Real a() const { return arguments_[0](0.0); }
    Real sigma() const { return arguments_[1](0.0); }

    // 如果需要,实现校准辅助函数
    void generateArguments() override {
        // 当参数更新时,更新模型内部状态
    }
};

// === 校准 Helper ===
class MyModelHelper : public BlackCalibrationHelper {
public:
    MyModelHelper(const Handle<Quote>& volatility,
                  /* 工具特定参数 */)
    : BlackCalibrationHelper(volatility, /* ... */) {
        // 可能需要在此构造工具并设置引擎
    }

    Real modelValue() const override {
        calculate();  // LazyObject
        return engineValue_;
    }

    void performCalculations() const override {
        // 从模型参数计算校准值
        engineValue_ = /* ... */;
    }

private:
    mutable Real engineValue_;
};

3. 开发规范

3.1 命名约定

约定 示例 说明
类名 PascalCase BlackScholesProcess, AnalyticEuropeanEngine 各单词首字母大写
方法名 camelCase netPresentValue(), setPricingEngine() 首字母小写
私有成员后缀 _ calculated_, frozen_, rate_ 尾部下划线
命名空间 QuantLib:: 所有公开 API
实现细节 QuantLib::detail:: 内部实现不公开
文件名小写 blackscholesprocess.hpp 与类名对应,全小写
纯虚函数后缀 Impl*Impl() discountImpl(), performCalculations()
getter/setter value() / setValue(x) 使用前置 set 而非重载

3.2 类型使用

// 始终使用 QuantLib 类型别名,而非原始类型
Real price = 100.5;          // double
Integer count = 42;          // int
Size length = vec.size();    // std::size_t
Time t = 0.5;                // Real (年)
Rate r = 0.05;               // Real
Volatility vol = 0.20;       // Real
DiscountFactor df = 0.95;    // Real
Spread sp = 0.001;           // Real

// 智能指针:始终使用 ext::shared_ptr
ext::shared_ptr<YieldTermStructure> curve(new FlatForward(...));
auto curve2 = ext::make_shared<FlatForward>(...);

// 空值检查
Handle<YieldTermStructure> handle;
if (handle.empty()) { /* ... */ }  // Handle 的空值检查
// 不要直接比较 shared_ptr 为 nullptr

// 空值哨兵
if (value == Null<Real>()) { /* ... */ }
// 不要使用 NaN 或 -DBL_MAX

3.3 错误处理

// 前置条件:使用 QL_REQUIRE
QL_REQUIRE(volatility > 0.0, "volatility must be positive: " << volatility);
QL_REQUIRE(dates.size() == rates.size(),
           "dates and rates must have same size");

// 后置条件:使用 QL_ENSURE
QL_ENSURE(NPV_ != Null<Real>(), "NPV not calculated");

// 不可恢复错误:使用 QL_FAIL
QL_FAIL("unsupported exercise type");

3.4 Handle 使用规范

// === 类内部持有 Handle ===
class MyClass {
public:
    // 通过 Handle 接收,注册为 Observer
    MyClass(const Handle<YieldTermStructure>& curve) 
    : curve_(curve) {
        registerWith(curve_);  // 必须!否则不会收到通知
    }

private:
    Handle<YieldTermStructure> curve_;  // 值成员(不是引用)
};

// === 创建和链接 Handle ===
// 方式1:创建后立即链接
RelinkableHandle<YieldTermStructure> handle;
auto curve = ext::make_shared<FlatForward>(...);
handle.linkTo(curve);

// 方式2:通过工厂函数
Handle<YieldTermStructure> h = flatRate(0.05, Actual365Fixed());

// === Handle 解引用 ===
DiscountFactor df = curve_->discount(date);  // operator->()
// 如果 Handle 为空,解引用会抛出异常

3.5 构造时注册 Observer

3.6 CMake 集成

新文件必须注册到 ql/CMakeLists.txt(和可选的 ql/Makefile.am)。

# ql/CMakeLists.txt 中
set(QL_SOURCES
    # ... 已有文件 ...
    mymodule/myclass.cpp
)

set(QL_HEADERS
    # ... 已有文件 ...
    mymodule/myclass.hpp
    mymodule/all.hpp
)

CI 工作流 filelists.yml 会自动验证文件是否已注册。


5. 性能优化指南

5.1 通知图优化

// 对于具有大量现金流的 Swap 和 Bond:
// 使用 simplifyNotificationGraph 减少通知传播开销
#include <ql/instruments/simplifynotificationgraph.hpp>

Swap swap(/* ... */);
swap.setPricingEngine(/* ... */);
simplifyNotificationGraph(swap);  // 优化通知图

5.2 LazyObject 通知控制

// 默认:第一个通知后不再转发(QL_FASTER_LAZY_OBJECTS 时默认启用)
LazyObject::Defaults::instance().forwardFirstNotificationOnly();

// 批量更新时禁用通知
ObservableSettings::instance().disableUpdates(true);
// ... 执行多个修改 ...
ObservableSettings::instance().enableUpdates();  // 一次性触发所有通知

5.3 编译优化

# 使用 interprocedural optimization(IPO / LTO)
cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON

# 启用 native 指令优化
cmake -DCMAKE_CXX_FLAGS="-march=native -mtune=native"

# 使用 Ninja 加速构建
cmake -G Ninja

# 禁用不需要的组件
cmake -DQL_BUILD_TEST_SUITE=OFF 
      -DQL_BUILD_EXAMPLES=OFF 
      -DQL_BUILD_BENCHMARK=OFF

5.4 运行时优化技巧

  1. 重用工具和引擎: 不要在循环中反复创建 — 使用 Handle<Quote> 更新参数后引擎自动重计算

    // 不好:每次循环创建新引擎
    for (auto price : prices) {
       VanillaOption option(payoff, exercise);
       option.setPricingEngine(ext::make_shared<AnalyticEuropeanEngine>(process));
       std::cout << option.NPV() << std::endl;
    }
    
    // 更好:复用,通过 Quote 更新
    auto spotQuote = ext::make_shared<SimpleQuote>(100.0);
    Handle<Quote> spot(spotQuote);
    auto process = ext::make_shared<BlackScholesProcess>(spot, ...);
    VanillaOption option(payoff, exercise);
    option.setPricingEngine(ext::make_shared<AnalyticEuropeanEngine>(process));
    for (auto price : prices) {
       spotQuote->setValue(price);  // 自动触发重计算
       std::cout << option.NPV() << std::endl;
    }
  2. 使用 freeze()/unfreeze() 冻结计算

    option.freeze();  // 阻止重计算
    // ... 进行多个不影响结果的修改 ...
    option.unfreeze();  // 恢复通知
  3. 选择合适的日期表示: 除非需要亚日精度,不要开启 QL_HIGH_RESOLUTION_DATE

  4. 选择合适的插值方法: 构建大量曲线时,LogLinear(分段常数远期利率)比 Cubic 更快


6. 测试开发

6.1 添加测试

// test-suite/mymoduletests.cpp
#include <ql/qldefines.hpp>
#include "utilities.hpp"
#include <ql/module/myclass.hpp>

using namespace QuantLib;

BOOST_AUTO_TEST_SUITE(TestMyModule)

BOOST_AUTO_TEST_CASE(testMyFeature) {
    BOOST_TEST_MESSAGE("Testing my feature...");

    // Setup
    Date refDate(15, June, 2024);
    Settings::instance().evaluationDate() = refDate;

    // Execute
    MyClass obj(/* 参数 */);
    Real result = obj.compute();

    // Verify
    Real expected = 0.05;
    Real tolerance = 1e-10;

    if (std::fabs(result - expected) > tolerance)
        BOOST_ERROR("Failed: result=" << result << ", expected=" << expected);
}

BOOST_AUTO_TEST_SUITE_END()

6.2 测试工具

测试辅助函数在 test-suite/utilities.hpp 中:

// 检查表达式是否抛出异常
BOOST_CHECK_EXCEPTION(expr, Error, ExpectedErrorMessage("msg"));

// 浮点数容差检查
BOOST_CHECK_CLOSE(actual, expected, tolerance);

// 使用测试夹具
struct CommonVars {
    CommonVars() {
        // 设置共享测试数据
    }
    // 共享数据成员
};

BOOST_FIXTURE_TEST_CASE(testName, CommonVars) {
    // 使用共享数据
}

7. 常见陷阱

7.1 忘记 registerWith()

// 错误:忘记注册 Handle,不会收到通知
class BadEngine : public VanillaOption::engine {
    Handle<YieldTermStructure> curve_;
public:
    BadEngine(const Handle<YieldTermStructure>& c) : curve_(c) {
        // 缺少: registerWith(curve_);
    }
};

// 正确
class GoodEngine : public VanillaOption::engine {
    Handle<YieldTermStructure> curve_;
public:
    GoodEngine(const Handle<YieldTermStructure>& c) : curve_(c) {
        registerWith(curve_);  // 必须!
    }
};

7.2 Handle 的循环引用

// 错误模式:A 持有 B 的 Handle,B 持有 A 的 Handle
// 会导致 shared_ptr 循环引用,内存泄漏

// 解决方案:使用弱引用或确保至少一个方向不持有 Handle
// 通常 TermStructure → BootstrapHelper 方向不需要 Handle

7.3 虚继承遗忘

// 当多个基类都继承 Observable 时:
// 错误:非虚继承导致菱形问题
class MyClass : public Observable, public Observer { };  // 错误的

// 正确:在基类链中使用虚继承
class MyClass : public virtual Observable, public virtual Observer { };
// LazyObject 已使用虚继承,直接继承 LazyObject 即可

7.4 日期与 Time 混淆

// 错误:混淆 Date(日历年月日)和 Time(年分数)
Date d(15, June, 2024);
Real t = 0.5;  // Time = 0.5 年

// TermStructure 提供转换
Time t = curve->timeFromReference(d);
Date d2 = curve->referenceDate() + Integer(t * 365.25);  // 近似反向

7.5 析构顺序问题

// 构造时 Observer 注册的逆序析构是安全的:
// 析构时 Observer::unregisterWithAll() 会自动解注册。

// 但如果手动管理生命周期,确保:
// 1. Observable 在 Observer 之后销毁
// 2. 使用 shared_ptr 管理 Observable

8. 贡献流程

  1. ql/ 中的适当模块子目录添加 .hpp/.cpp 文件
  2. 在对应模块中更新 all.hpp(自动生成或手动编辑)
  3. ql/CMakeLists.txt 中注册新文件
  4. test-suite/ 中添加测试
  5. 运行 clang-format:.clang-format 在项目根目录
  6. 编译并运行完整测试套件
  7. 确保所有测试通过且没有新增警告

发表评论