2. "Implementing QuantLib"の和訳
Chapter VIII The Finite-Difference Framework (有限差分法のフレームワーク)
8.2 The New Framework (新しいフレームワーク)
8.2.5 Schemes and Solvers : 時間軸方向の差分スキーム と ソルバー
古いフレームワークと同様、Schemeクラスは、解こうとしている微分方程式における、時間軸方向の偏微分の差分スキームを提供しています。詳しい説明は、古いフレームワークの該当セクションをご覧下さい。新しいフレームワークでは、古いフレームワークで使われていた陰的オイラー法や陽的オイラー法といった Schemeを、作り直して提供しています。それに加えて、いくつかの新しいスキームも提供していますが、その部分の説明は省略します。詳細を知りたい方は、QuantLibライブラリーの中のプログラムコードをご覧下さい。
古いフレームワークと同様、Schemeクラス群は共通のベースクラスを持ちません。そのクラス群はそれぞれで、step( ) と setStep( )メソッドを、古いフレームワークと同じ Semantics(プログラム構造上の役割) で実装しているものの、それらの間には、お互いを束縛するような継承関係は存在しません。この事は、新しいフレームワークでは出来るだけオブジェクト指向の技法を追及してGeneric ProgrammingやTemplateを控えようとしている中で、やや問題かもしれません。ベースクラスが存在しないので、汎用的なSchemeインスタンスを使う事によって Templateを使わないで済むようなコードを書くことはできません。
この問題(訳注:ベースクラスが無いので共通のインターフェースで様々な Scheme を使うことが出来ない問題)を回避しながら、新しいフレームワークを機能させる為、FdmSchemeDescクラス(構造体)を定義しています (その内容を下記 Listing 8.31 に示します)。特定の Schemeを他のオブジェクトの部品として使いたい場合、この構造体のインスタンス自体が、特定された Schemeの型と、それをインスタンス化するのに必要なパラメータを、メンバー変数に設定した上で、他のオブジェクトに渡されていきます。Schemeの選択肢はクラスの宣言部分の中で enumを使ってリストアップされていますが、最適な方法とは言えないでしょう。この点については後で説明します。
Listing 8.31 : FdmSchemeDescクラスのインターフェース
struct FdmSchemeDesc {
enum FdmSchemeType { HundsdorferType, DouglasType,
CraigSneydType, ModifiedCraigSneydType,
ImplicitEulerType, ExplicitEulerType };
FdmSchemeDesc(FdmSchemeType type, Real theta, Real mu);
const FdmSchemeType type;
const Real theta, mu;
static FdmSchemeDesc Douglas();
static FdmSchemeDesc ImplicitEuler();
static FdmSchemeDesc ExplicitEuler();
static FdmSchemeDesc CraigSneyd();
static FdmSchemeDesc ModifiedCraigSneyd();
static FdmSchemeDesc Hundsdorfer();
static FdmSchemeDesc ModifiedHundsdorfer();
};
このクラスは、enum宣言の他に、静的メソッド群が宣言されており、これらのメソッドはすべて、パラメータ (thetaとmu) が設定された特定の Schemeインスタンスを事前に生成し、それを返します。仮に、独自のインスタンスを生成したい場合は、どのような古い(?)値を使ったインスタンスでも生成できます。
もうひとつ、このクラスも、このセクションで説明するほかのクラスも、クラステンプレートで無い点、注意して下さい。これらのクラスは、FdmLinearOpCompositeクラスとだけしか、一緒に動作しないようにプログラムされています。推察するに、このクラスの開発者は古いフレームワークでの問題点から学んで、この様にしたのでしょう (古いフレームワークでは Schemeクラスを、どのような Operator(差分演算子) とでも一緒に使える用に設計したものの(訳注:テンプレート引数として任意の Operatorを指定できるようにしていた)、結果的には TridiagonalOperatorクラスでしか使われませんでした)。
さて、もしこのような enumを使った選択肢のリストを使うのが気持ち悪いのであれば、次の Listing 8.32 にある FdmBackwardSolverクラスについては、(もっと気持ち悪いプログラム手法を)覚悟して下さい。
Listing 8.32 : FdmBackwardSolverクラスの概要
class FdmBackwardSolver {
public:
FdmBackwardSolver(
const shared_ptr<FdmLinearOpComposite>& map,
const FdmBoundaryConditionSet& bcSet,
const shared_ptr<FdmStepConditionComposite> condition,
const FdmSchemeDesc& schemeDesc);
void rollback(array_type& rhs,
Time from, Time to,
Size steps, Size dampingSteps) {
const Time deltaT = from - to;
const Size allSteps = steps + dampingSteps;
const Time dampingTo =
from - (deltaT*dampingSteps)/allSteps;
if (dampingSteps && schemeDesc_.type !=
FdmSchemeDesc::ImplicitEulerType) {
ImplicitEulerScheme implicitEvolver(map_, bcSet_);
FiniteDifferenceModel<ImplicitEulerScheme>
dampingModel(implicitEvolver, condition_->stoppingTimes());
dampingModel.rollback(rhs, from, dampingTo, dampingSteps, *condition_);
}
switch (schemeDesc_.type) {
case FdmSchemeDesc::HundsdorferType:
{
HundsdorferScheme hsEvolver(schemeDesc_.theta,
schemeDesc_.mu,
map_, bcSet_);
FiniteDifferenceModel<HundsdorferScheme>
hsModel(hsEvolver, condition_->stoppingTimes());
hsModel.rollback(rhs, dampingTo, to, steps, *condition_);
}
break;
// other cases, not shown
}
}
コンストラクターは、Operator、BoundaryCondition、StepCondition、および SchemeDescインスタンスを引数として取り、メンバー変数に保持します。これらの部品は rollback( )メソッドの中で使われます。このメソッドは、①これら部品を使って有限差分モデルを組み立て、②初期値の配列を取り、そして③スタートとエンド時の間で、与えられたステップ数だけ有限差分モデルを走らせます。その中でも特に最初の数ステップは damping steps の操作を行います (訳注:適当な訳語が無いので英語のままにしました。時間ステップの最初の数ステップは、Payoff関数の微分不可能な領域(Strike価格、あるいはノックイン・ノックアウトなどのバリア点)の影響を強く受け、その近辺で不自然な振動を起こす可能性があるので、その影響を緩和する為に特別な操作を行っているもの)。そしてコードを見てわかる通り、実際のrollback( )の動作は、SchemeDescクラスの型にあわせ、switchによる多岐に渡る分岐操作の中で行われます。
びっくりして息が止まりそうでした? はい、分っています。もし新しい Scheme を追加しようとすれば、FdmSchemeDescクラスと rollback( )メソッドの両方とも修正が必要になり、それは open-closedの原則 (訳注: wiki の開放・閉鎖原則 の項を参照) に反します。でも、これはこのコードを書いた人が怠慢だった訳ではありません。仮に、すべての Schemeクラスのベースクラスを作ってそこから派生させたとします。そうすると Strategyパターンのようなテクニックを使って多相性を持たせることになります。ところが、そうすると二律背反が発生します。まず、仮にユーザーが Schemeの選択が出来る様にすると、その Schemeインスタンスは価格エンジンの外から引数として渡す事になります。しかし一方で、Schemeインスタンスを生成するには Operator や BoundaryConditionのインスタンスが必要ですが、それらは価格エンジンの中で生成されています。なぜなら、ユーザーに、価格エンジンの外でそれらを生成するコードを書かせるのは重複になるからです。そうすると、やはりSchemeインスタンスは価格エンジンの中で生成せざるを得ません。混乱しそうですね?
この問題を解決するのに、2通りの方法が思いつきます。ひとつは、なんらかの Factoryを使う事です。Schemeクラスに、例えば、同じ型とパラメータを持つ別のインスタンスを生成する clone( )メソッドのようなものを作り、それに BoundaryCondition と StepConditionを渡す方法です。(注:Factoryクラス群を作ることも出来たでしょうが、そうすると並行した階層構造が作られ、それは決していい事ではありません。) もうひとつの方法は、このクラスのインターフェースを変えてしまい、Schemeクラスが Conditionオブジェクトを持たないようにする方法です。そうすると過去のバージョンとの互換性の問題から、FiniteDiffereneModelクラスを変更する事は出来ないので、新しいモデルクラスを作り直さなければなりません。いずれの方法も可能です。新しいフレームワークが導入された時に、それをレビューした者は、この問題に気付き、いずれかの対応策を提案すべきでした。そのレビューした者とは私でした。長く生きると、いろいろ学習するものです。
さて、FdmBackwardSolverクラスの説明が終われば、いよいよホームストレッチに入ってきます。残りのクラス群は、2つの Layer(訳注:ここでは各種部品を価格エンジンにつなぐ為の介在役のオブジェクトのこと)を加えて、(完成形の)Solverをより便利に使えるようにしています。でも、新しいロジックが沢山加わった訳ではありません。より注意深い読者なら、このセクションの説明方法は、新しいフレームワークの説明の最初に述べたトップダウンの方法から離れてしまった事に気づかれたでしょう。(ここではボトムアップの)この方法の方が、より合理的だと思います。
まず、下記 Listing 8.33 に示す FdmSolverDesc構造体は、(完成形の)Solverを初期化するのに必要な部品を一纏めにし、一体として渡せるようにしたものです。但し Operatorはその部品の中には含まれません。理由は、この直ぐ後で分りますが、より高次のSolver(訳注:ここでの高次とは、使われる部品が特定され、より具体化したオブジェクトの意味)の中で 特定のOperatorを生成すれば、その Solverに別の FdmSolverDescインスタンスを渡す事によって、そのSolverを別の商品でも再利用できるようにする為です。
Listing 8.33 : FdmSolverDescクラスのインターフェース
struct FdmSolverDesc {
const boost::shared_ptr<FdmMesher> mesher;
const FdmBoundaryConditionSet bcSet;
const boost::shared_ptr<FdmStepConditionComposite> condition;
const boost::shared_ptr<FdmInnerValueCalculator> calculator;
const Time maturity;
const Size timeSteps;
const Size dampingSteps;
};
2つ目は、Fdm1DimSolverクラスで、下記 Listing 8.34 に概要を示します。このクラスは Solver回りの配管の面倒を見ます。
Listing 8.34 : Fdm1DimSolverクラスの実装(一部)
class Fdm1DimSolver : public LazyObject {
public:
Fdm1DimSolver(const FdmSolverDesc& solverDesc,
const FdmSchemeDesc& schemeDesc,
const shared_ptr<FdmLinearOpComposite>& op)
: /* ... */ {
// ... get mesher, calculator etc. from solver description ...
FdmLinearOpIterator end = layout->end();
for (FdmLinearOpIterator i = layout->begin(); i != end; ++i) {
initialValues_[i.index()] = calculator->avgInnerValue(i, maturity);
x_[i.index()] = mesher->location(i, 0);
}
}
Real interpolateAt(Real x) const {
calculate();
return (*interpolation_)(x);
}
// ...other inspectors for derivatives
protected:
void performCalculations() const {
Array rhs(initialValues_.size());
std::copy(initialValues_.begin(), initialValues_.end(), rhs.begin());
FdmBackwardSolver(op_, solverDesc_.bcSet, conditions_, schemeDesc_)
.rollback(rhs, solverDesc_.maturity, 0.0,
solverDesc_.timeSteps,
solverDesc_.dampingSteps);
std::copy(rhs.begin(), rhs.end(), resultValues_.begin());
interpolation_ = shared_ptr<CubicInterpolation>(
new MonotonicCubicNaturalSpline(
x_.begin(), x_.end(), resultValues_.begin()));
}
};
このコンストラクターは、引数として、①FdmSolverDescインスタンス、②FdmSchemeDescインスタンス、及び③Operatorインスタンスを取り、一旦メンバー変数に保持し、さらに、計算に使うその他のメンバー変数の値を事前に準備します。特に、SolverDescインスタンスの持つ layout( )メソッドを使って、初期値用の配列に正しいサイズを割当て、Calculator(オプションの行使価値を計算するオブジェクト)を使ってその値を埋めます。さらに、Mesherインスタンスから、指定したグリッド上の対象資産価格を取りだします。
実際の計算は performCalculations( )メソッドの中に実装されています。(そう。このクラスはLazyObjectから派生してますが、それはおかしいです。これについては後で述べます(訳注:performCalculations( )は LazyObjectで宣言された純粋仮想関数で、それを実装しているという事は、このクラスはその派生クラスになる)) 既に述べた通り、動作の内容はほとんどが配管回りの事です (訳注:ここでの動作は、様々なオブジェクトによって、すでに用意されたアルゴリズムが、順番にスムーズに流れていく様にしている意味)。コードは、①まず初期値の配列をコピーし、②次にFdmBackwardSolverのインスタンスを生成し、③それを使ってグリッド上の値をroll backしていき、④計算結果をメンバー変数に保持し、⑤それらを線形補間して一本の線に纏めます。interpolateAt( )などのメソッドを使えば、計算結果をClient Codeでも使えます。この Listingには示していませんが。いくつかのインスペクター関数は、線形補間した曲線の1階と2階の微分の値、および時間軸に対する微分の値を取りだせます。
一歩戻って LazyObjectからの派生についてですが、しばらく考えてみましたが、なぜ LazyObjectの派生クラスにする必要があったのか見出せませんでした。Fdm1DimSolverのコンストラクターは、そもそも自らを (Observerとして) 別のオブジェクト (Observable) に登録する動作を実装していないので、Observerパターンの目的からしておかしな事です (訳注:LazyObject は Observerの派生クラス)。(注:このクラスの派生クラスで登録の動作を実装しているのかも知れないと思い Libraryの中をざっと見ましたが、Fdm1DimSolverクラスの派生クラスは見当たりませんでした) さらに、solverインスタンスは、通常価格エンジンでの計算の際に生成・使用され、計算が終わると捨てられます。まとめて言うと、(performCalculations( )内の)すべての計算をコンストラクターに纏めてしまう事も出来たし、あるいはこのクラスを関数に置き換えて、線形補間された計算結果を返したり、それに対するインスペクター関数を作ったりして済ます事も出来たはずです。
この Fdm1DimSolverクラスには、姉妹クラスがあるのを述べておくべきでしょう。新しいフレームワークでは、Fdm2DimSolverクラスと Fdm3DimSolverクラスが定義されており、同じ内容の計算を行い、それぞれ 2次元 と 3次元 の線形補間の結果を導出します。インターフェースは、殆ど同じで、次元が高くなっている分だけ違いが発生しています。例えば、interpolationAt( )メソッドは、引数として、2次元の場合は x,y、3次元の場合は x,y,z を取っています。
最後に、Operatorを自ら生成して、それを今説明した汎用的な Solverの中で使う様に作られた、特別な Solverクラスについて説明します。次の Listing 8.35 に示す、FdmBlackScholesSolverクラスがそれです。
Listing 8.35 : FdmBlackScholesSolverクラスの実装(一部)
class FdmBlackScholesSolver : public LazyObject {
public:
FdmBlackScholesSolver(
const Handle<GeneralizedBlackScholesProcess>& process,
Real strike,
const FdmSolverDesc& solverDesc,
const FdmSchemeDesc& schemeDesc,
bool localVol = false,
Real illegalLocalVolOverwrite = -Null<Real>())
: /* ... */ {
registerWith(process_);
}
Real valueAt(Real s) const {
calculate();
return solver_->interpolateAt(std::log(s));
}
Real deltaAt(Real s) const {
calculate();
return solver_->derivativeX(std::log(s))/s;
}
Real gammaAt(Real s) const;
Real thetaAt(Real s) const;
protected:
void performCalculations() const {
const boost::shared_ptr<FdmBlackScholesOp> op(
new FdmBlackScholesOp(
solverDesc_.mesher, process_.currentLink(),
strike_, localVol_, illegalLocalVolOverwrite_));
solver_ = boost::shared_ptr<Fdm1DimSolver>(
new Fdm1DimSolver(solverDesc_, schemeDesc_, op));
}
private:
// other data members, not shown
mutable boost::shared_ptr<Fdm1DimSolver> solver_;
};
このクラスのコンストラクターは、引数として、FdmSolverDescと FdmSchemeDesc、およびモデルのパラメータ以外に、BlackScholesProcessへの Handleを取ります。そして、それをメンバー変数に保持し、その Handle(Observableに該当)に自らを(Observerとして)登録します。再度述べますが、このやり方の方が、価格エンジンが計算終了後に Solverを消去するより合理的でしょう。performCalculation( )メソッドは、BlackScholesProcessインスタンスを使って Operatorを生成し、さらにそれを使って 1DSolverを生成し、メンバー変数に保持します。valueAt( )や deltaAt( )といったインスペクター関数は、まず calculate( )メソッドを呼び出し、そこで 1DSolverが生成され、その Solverが持つ対応するメソッドに動作を委託します。その Solverは rollback( )の計算を内部で行います。また、それらのインスペクター関数は、インターフェースの中で使われている対象資産の価格と、Processインスタンスの中で使われているその対数値との間で換算の動作も行います。
既に述べた通り、この Solverは外から SolverDescインスタンスを取りこむので、このSolverを別の商品の価格エンジンでも使えます。但し、その場合は対象資産の価格が同じ確率過程を取る事が前提になります。もう一度この Chapterの最初の Listing 8.17 を見て頂くと、このSolverがプレインバニラの Call と Putオプションの価格計算に使われているのが判ります。境界条件を変える事によって、このSolverをバリアオプションでも使用できますし、実際に使われています。あるいは、初期条件を変える事によって、デジタルオプションやその他の類似の商品でも使う事が出来ます。新しいフレームワークが、継承をベースとする古いフレームワークと比べて、より柔軟なデザインになっている事が判ると思います。少なくとも、柔軟性を持たせる為に、はるかに簡単なデザインになっています。
多次元の Solverについては、説明を省略しますが、同じ様な動作で機能します。興味がある型は、2次元の例としてFdmBatesSolverクラスを、3次元の例として FdmHestonHullWhiteSolverクラスを見てください。諺にもある通り、これらのクラスがどうやってできたのか、もっとたくさんの話題があります。
*********************************************************************************
Solverの説明が終わったので、(有限差分モデルを使った)価格エンジンを構築するためのすべての部品が揃いました。そこで、このChapterを纏めます。価格エンジンの例として示した Listing 8.17 を、もう一度見直して下さい。もう、有限差分モデルによる価格エンジンがどのように機能するか理解できるでしょう。価格エンジンは、まずコンストラクターが、Schemeインスタンスと、その他の必要なパラメータを取り、次に計算を実行する際は、それに必要な Mesher、Initial Condition、StepCondition、BoundaryCondition の各部品を構築し、最後に Solverを使って(アルゴリズムを順番に走らせ)計算結果を取り出す事ができます。
<ライセンス表示>
QuantLibのソースコードを使う場合は、ライセンス表示とDisclaimerの表示が義務付けられているので、添付します。 ライセンス