QuEP 5: using replaceable pricing engines in option implementations

Luigi Ballabio

Abstract

A number of pricers are currently implemented in QuantLib which provide option evaluation services. However, they all inhabit a sort of middle abstraction layer between high-level objects such as Instrument and low-level facilities such as FiniteDifferenceModel or MonteCarloModel. This leads to mixing of concerns, loss of abstraction, and a number of unrelated class hierarchies. In turn, this leads me to fear that such hierarchies would be difficult to maintain. Fear leads to Anger. Anger leads to Hate. Hate leads to Suffering.

An implementation of Option classes is proposed which tries to address the above problems (well, except for the Jedi part). In particular, an Option class is introduced in the Instrument hierarchy which serves as base for concrete option instruments. In turn, the latter are decoupled from their actual pricing model by means of an abstract OptionPricingEngine class whose child classes encapsulate the different available choices for option calculation.

Current implementation

The figure below shows a partial sketch of the current QuantLib architecture.

UML diagram

The abstract Instrument class serves as base class for actual financial instruments. It uses the Observer/Observable pattern to determine whether its value needs to be recalculated due to changes in market parameters. Also, it uses the Framework pattern to hide such caching logic from derived classes---the latter only need to provide a performCalculation method which implements the actual calculations.

However, the currently available option pricers do not take advantage of such machinery. Instead, distinct hierarchies of pricers are implemented among which are the two shown above, namely, one derived from SingleAssetOption which includes analytical and finite-difference option pricers, and one derived from McPricer which includes Monte Carlo option pricers.

Disadvantages:

Option classes are tightly coupled with their calculation model. For instance, three unrelated European option classes are shown in the above diagram which are dependent on the used model (analytic, finite-difference, or Monte Carlo) and do not share any common "European option" abstraction.

Separate hierarchies are implemented which are not related and would not be easily integrated with the Instrument hierarchy.

Housekeeping code had to be implemented again in SingleAssetOption for the caching of calculated results instead of reusing the code already available in Instrument. Also, separating the class hierarchies led to the duplication of data members in SingleAssetOption and McEuropean.

Option pricers take their parameters (e.g., the underlying price, the risk-free rate, etc.) as doubles even though such values will be obtained from more complex classes such as MarketElements or TermStructures. Using the pricers requires to repeatedly write the code for obtaining the required scalar values.

The inner status of a given pricer should not be modified by an implied volatility calculation. Therefore, a temporary copy of the pricer must be obtained whose parameters (namely, the volatility of the underlying price) can be modified. This requires every pricer class to implement both a clone() method which returns a copy of itself of the right type and a setVolatility() method for manually changing the parameters of the copy.

Proposed implementation

The proposed implementation of the option pricing framework is shown in the following diagram.

UML diagram

An Option class is introduced which inherits from Instrument. Such class contains a pricing engine to which calculations are delegated.

An abstract OptionPricingEngine class acts as a base class for calculation engines. Such class provides abstract methods for setting the calculation parameters, validating the latter, and getting calculation results.

Two abstract classes Arguments and Results are introduced. Such classes are empty and have the purpose of defining a single base type for calculation parameters and results, respectively. Also, a number of derived classes is provided which group together different sets of related parameters and results. Such classes have no methods and are simple structures which allow unrestricted access to their data members. No other interface is needed since their only purpose is to pack together data to be passed to and returned from the engine.

Concrete engines contain the appropriate structures containing their required parameters and provided results. Such structures are assembled by multiply inheriting from the available Arguments and Results subclasses. For instance, parameters and results for an European option engine could be declared as:

class EuropeanOptionParameters : public OptionParameters,
                                 public UnderlyingParameters,
                                 public MarketParameters {};

class EuropeanOptionResults : public OptionValue,
                              public OptionGreeks {};

class EuropeanEngine : public OptionPricingEngine {
  private:
    EuropeanOptionParameters parameters_;
    EuropeanOptionResults results_;
  public:
    Arguments* parameters() { return &parameters_; }
    Results* results() { return &results_; }
    ...
};

Option implements the Instrument interface by delegating the parameter passing to its subclasses and the calculation to the contained engine. Its performCalculations method can be implemented as:

void Option::performCalculations() const {

    // the method below is purely virtual
    // implementation is delegated to derived classes
    setupEngine();

    // sanity check and calculations are delegated to the engine:
    // this checks the values in the Arguments struct...
    engine_->validateParameters();
    // .. and this calculates and store results in the Results struct
    engine_->calculate();

    // get the results:
    // Option only needs to fulfill Instrument requirements, i.e.,
    // set the NPV value.

    // downcast the Results struct to the appropriate type...
    const OptionValue* results =
        dynamic_cast<const OptionValue*>(engine_->results());
    // ... check that the cast succeeded...
    QL_ENSURE(results != 0, "results do not contain an option value slot");
    // ... and read the results.
    NPV_ = results->value;

}

Concrete options will contain objects such as MarketElements, TermStructures, and Dates. It will be their job to deduce from such objects the appropriate value for the engine parameters. Such logic will be implemented in the setupEngine methods. For instance, an European option could implement the latter as:

class EuropeanOption : public Option {
  private:
    RelinkableHandle<TermStructure> riskFreeCurve_;
    RelinkableHandle<MarketElement> underlyingValue_;
    RelinkableHandle<MarketElement> underlyingVol_;
    Date exerciseDate_;
    ...
};

void EuropeanOption::setupEngine() const {

    // downcast the Arguments struct to the appropriate type...
    UnderlyingParams* underArgs =
        dynamic_cast<UnderlyingParams*>(engine_->parameters());
    QL_ENSURE(underArgs != 0, ...);
    // ... and set the values
    underArgs->value = underlyingValue_->value();
    underArgs->volatility = underlyingVol_->value();

    // do the same for other parameter sets:
    MarketParams* marketArgs =
        dynamic_cast<marketParams*>(engine_->parameters());
    ...
    marketArgs->riskFreeRate = riskFreeCurve_->discount(exerciseDate_);

    ...
}

Also, additional results can be read from the engine by extending the base performCalculation method:

void EuropeanOption::performCalculations() const {

    // the main logic is in the parent class method
    Option::performCalculations();

    // now read additional results
    const OptionGreeks* results =
        dynamic_cast<const OptionGreeks*>(engine_->results());
    QL_ENSURE(...);
    delta_ = results->delta;
    gamma_ = results->gamma;
    ...
}

Finally, it can be noted that implied volatility calculations can be performed by feeding the engine different volatility values and analyzing the returned option value. This does not modify the inner state of the option, since previous results were already stored in its own variables and will not be recalculated until the Instrument machinery assesses the need to do so. This eliminates the need for cloning the option.

Conclusion

A new implementation of the option pricing framework was proposed which addresses the disadvantages of the current implementation, namely:

decoupling of option classes from calculation is achieved by encapsulating the latter into a pricing engine; an option class can be abstracted from its calculation model and instantiated by passing the latter, as in:

Handle<OptionPricingEngine> engine1(new AnalyticEuropeanEngine()),
                            engine2(new FDEuropeanEngine(timeSteps)),
                            engine3(new MCEuropeanEngine(samples));

EuropeanOption opt1(..., engine1);
EuropeanOption opt2(..., engine2);
EuropeanOption opt3(..., engine3);

integration of options into the Instrument hierarchy is achieved;

separation of concerns is achieved by delegating housekeeping and result caching to Instrument, market data storage and interpretation to concrete options, and calculation to pricing engines; this also eliminates code duplication;

passing of high-level abstractions such as MarketElements and TermStructures is achieved; their interpretation is performed independently of the calculation model;

implied volatility calculation is performed internally by the option, independently of the calculation model and partly of the option class; no cloning of the option is required.

Proof of concept

A rough proof of concept is available in the library. The base classes are implemented, as well as an analytic European engine and a concrete option class by the unsatisfactory name of PlainOption. The implementation can be found in the following files:

Feedback

Feedback on the above proposal should be posted to the QuantLib-dev mailing list.