2. "Implementing QuantLib"の和訳
Chapter VIII The Finite-Difference Framework (有限差分法のフレームワーク)
8.1 The Old Framework (古いフレームワーク)
8.1.3 Boundary Conditions : 境界条件
既に述べた通り、step( )メソッドの中では、(簡略化された上記 Code Listingの例よりも)さらに多くの作業が実行されています。まず初めに、グリッド(訳注:関数値 \(f_i(t)\) を離散的に並べた配列) のどちらかの端(あるいは両端)において、境界条件による制約をかけなければなりません。例えば、グリッド上のオプション価格(即ち関数値fi(t))について、deep out of the money側の端において 0 となり、deep in the money側の端において、価格の微分(訳注:関数値 \(f_i(t)\) の原資産価格に対する感応度。デルタに相当)が 1 となるような条件をかけたいとします。そのような境界条件は、BoundaryConditionクラスでオブジェクトモデル化されており、その実装内容を下記 Listing 8.3に示します。
Listing 8.3: BoundaryConditionクラステンプレートのインターフェース
template <class Operator>
class BoundaryCondition {
public:
typedef Operator operator_type;
typedef typename Operator::array_type array_type;
enum Side { None, Upper, Lower };
virtual ~BoundaryCondition() {}
virtual void applyBeforeApplying(operator_type&) const = 0;
virtual void applyAfterApplying(array_type&) const = 0;
virtual void applyBeforeSolving(operator_type&,
array_type& rhs) const = 0;
virtual void applyAfterSolving(array_type&) const = 0;
};
ご覧の通り、このクラスは runtime polymorphism(実行時多相性)を取り入れています。その理由は、異なった型の境界条件インスタンスを、複数保持したいというニーズに対応する為でした( MixedSchemeクラスの typedef として bc_set(境界条件の集合を扱う型)が定義されていたのを覚えておられると思います)。そして、その為に最も相応しいコンテナは、同じ性質を持った、従って共通のベースクラスを持ったものがいいという考えに基づくものでした。でも、この理由は、完全に正確ではありません。もし、(状態変数が)1次元に限った場合なら、このクラスを開発した当時でも、(境界条件のコンテナとして) std::pair を使う事が出来たでしょう。しかし、そうしませんでした。std::pair を使っていれば、長い目で見れば、フレームワークがより複雑になる事を避けられたと考えています。 (注: std::tupleを使う事も可能でしたが、そうしませんでした。 std::tuple は C++ の標準化協議会においても、あまり役に立つテンプレートとはみなされていませんでした。それをコンテナとして使おうとすると、テンプレートを使った metaプログラミングが必要となりますが、それは、どうしても必要な場合以外は、避けるべきと考えました)。
このクラスのインターフェースは、やや奇妙な名前が付いています。Enumerationの Sideについては、説明を飛ばしますが (要は、1次元のモデルにおけるグリッドのどちら側かを示すもので、従って意味は自明です)、例えば、applyBeforeApplying( )メソッドなどは、Libraryの中で最悪なメソッド名の有力候補です。なぜそのような名前になったかと言うと、境界条件を‘適用する(applyする)’方法が、色々あるからです。ひとつは、境界条件の適用を、applyTo( )やsolveFor( )メソッドが実行されるまで待って、実行後に境界値を調整する方法です。それぞれ applyAfterApplying( ) と applyAfterSolving( ) が対応します。もうひとつの方法は、事前に Operator(差分演算子)と入力データの配列を準備しておき、計算結果が境界条件を満たすようにする方法です。applyBeforeApplying( ) と applyBeforeSolving( )メソッドが、それに対応します。(applyBeforeApplying( )メソッドは何等かの理由で、単に演算子を取り込む動作しか行っていません。おそらく、既存の条件では配列の値を修正する必要が無かったために、見逃がされたのかもしれません。)
例として、下記 Listing 8.4にある DirichletBCクラスを見てください。このクラスは、基本的なディリクレ境界条件を実装したものです。すなわち、グリッドの端の関数値について、与えられた値をそのまま条件として設定するものです。
Listing 8.4 : DirichletBCクラスの実装内容
class DirichletBC
: public BoundaryCondition<TridiagonalOperator> {
public:
DirichletBC(Real value, Side side);
void applyBeforeApplying(TridiagonalOperator& L) const {
switch (side_) {
case Lower:
L.setFirstRow(1.0,0.0);
break;
case Upper:
L.setLastRow(0.0,1.0);
break;
}
}
void applyAfterApplying(Array& u) const {
switch (side_) {
case Lower:
u[0] = value_;
break;
case Upper:
u[u.size()-1] = value_;
break;
}
}
void applyBeforeSolving(TridiagonalOperator& L,
Array& rhs) const {
switch (side_) {
case Lower:
L.setFirstRow(1.0,0.0);
rhs[0] = value_;
break;
case Upper:
L.setLastRow(0.0,1.0);
rhs[rhs.size()-1] = value_;
break;
}
}
void applyAfterSolving(Array&) const {}
};
コードを見て判る通り、もはやクラステンプレートではありません。この境界条件クラスは、実装時にテンプレート引数となる Operatorを具体的な型で特定しているからです。これは驚くことではありません。なぜなら、境界条件は演算子にどのようにアクセスし、それをどう修正するかを知る必要があるからです。このケースでは、既に説明した、三重対角演算子が指定されています。
コンストラクターは、非常に単純です。引数として①境界値として設定する値と、②それがグリッドの‘どちら側の境界か’という情報を受け取り、それをメンバー変数に保存します。すでに説明した通り、applyBeforeApplying( )メソッドは、\(L.apply(u),\ 即ち\ u'=L∙ u\) の実行結果 \(u'\) が、境界条件を満たすように、演算子 \(L\) を設定しなければなりません。そうする為、‘どちら側の境界か’の情報に従って、対応する単位行列の最初(あるいは最後)の行を設定します。これにより、(1ステップ後の状態変数の配列) \(u'\) の最初(あるいは最後)の要素が、配列 \(u\) の、対応する要素と同じ値になります。この操作により、一つ前のステップの配列 \(u\) が境界条件を満たしていれば、次のステップにおいても境界条件を満たすことになります。このロジックに依存するのは、若干危なっかしい点があります(例えば、時間の経過によって境界値が動くような場合や、何等かの事象の発生により配列の値を修正しなければならないような場合には、このロジックは崩れます)。しかし、この方法が入力データの配列にアクセスする事なく境界値を設定する最善の方法と言えるでしょう。applyAfterApplying( )メソッドの方は、よりシンプルで安全です。このメソッドでは、出力された配列データ \(u'\) の境界値を、境界条件に従って設定するだけです。
applyBeforeSolving( )メソッドは、その姉妹メソッドになる applyBeforeApplying( )と同じ様な動作をしますが、このメソッドは入力データの配列にもアクセスでき、その配列に正しいデータが入っているかどうかのチェックを行えます。最後に、applyAfterSolving( )メソッドについてですが、驚くべき事に、今この説明文を書いている段階においてさえ、中味が空なのです。このクラスのプログラムコードを書き始めた当初、applyBeforeSolving( )も同時に呼び出されるであろうとの想定に頼っていたのかもしれません(訳注:applylAfterSolvingが何もしなくても、applyBeforeSolvingが必要な動作をやってくれていると想定していた)。後から見ればあまりいいアイデアでは無いと思います。なぜなら、特定の Evolution Scheme(差分進化スキーム)を選択した場合、それが、どのメソッドを呼び出すのか判りません。(これにより、applyBeforeApplying( )メソッドが、見た目よりもっと安全で無くなります。)
にもかかわらず、このメソッドが動作する理由は、下記Listing 8.5にある通り、MixedScheme::step( )メソッドの実装方法にあります。各時間ステップにおいて、まず陽的なEvolutionスキームを実行する前と後にすべての境界条件が設定され、さらにその後、陰的 Evolutionスキームの為の Solverを実行する前と後に、すべての境界条件が設定されます。ディリクレ境界条件の場合、その仕組みにより、applyAfterApplying( )メソッドが、applyBeforeApplying( )メソッドが起こしそうな問題を修正するように働きます。また applyBeforeSolving( )メソッドが、たとえ applyAferSolving( )が動作しなかったとしても、必要な動作を済ませている仕組みになっているからです。
仮に、境界条件のクラスが、そのメンバーメソッドをすべて正しく実装していたとすれば、これ(MixedScheme::step( )の動作)は重複した動作という事になるでしょう。つまり境界条件を計算の後にだけ設定するだけで十分でした。しかし、開発当時、おそらく、いくつかの境界条件は、そのすべてを実装する事が出来ないものと想定して、従って(訳注:beforeとafterを両方呼び出しておけば、どちらかが必要な動作を行ってくれるという)安全な選択肢を選んだのではないかと思います。
Listing 8.5 : MixedSchemeクラスのstep( )メソッド
template <class Operator>
void MixedScheme<Operator>::step(array_type& a, Time t) {
if (theta_ != 1.0) { // there is an explicit part
for (Size i=0; i<bcs_.size(); i++)
bcs_[i]->applyBeforeApplying(explicitPart_);
a = explicitPart_.applyTo(a);
for (Size i=0; i<bcs_.size(); i++)
bcs_[i]->applyAfterApplying(a);
}
if (theta_ != 0.0) { // there is an implicit part
for (Size i=0; i<bcs_.size(); i++)
bcs_[i]->applyBeforeSolving(implicitPart_, a);
implicitPart_.solveFor(a, a);
for (Size i=0; i<bcs_.size(); i++)
bcs_[i]->applyAfterSolving(a);
}
}
もし、Libraryの中のこの部分を修正するのであれば、(新しいフレームワークを導入したので、その必要はなさそうですが)この問題をきちんと見直すべきでしょう。残念ながら、解決すべき問題は、簡単ではありません。特定の Evolutionスキームで必要とされ、かつ特定の境界条件にサポートされたメソッドだけを呼び出す為には、その両方について特定の型を知る必要があるからです。
Evolutionスキームと境界条件の依存関係を逆転させ、Evolutionスキームを境界条件に渡すようなロジックは、うまく動作しないでしょう。境界条件の方は、Evolutionスキームが applyTo( )か、それとも solveFor( )メソッドを呼び出すのか、知る術がありません。従って、Evolutionスキームの step( )メソッドの中で動作する事ができません(MixedScheme::step( )のプログラムコードを見ればわかる通り、境界条件の設定は、陽的ステップの計算と陰的ステップの計算の間で実行されています)。
Mediatorパターンあるいは何らかの Visitorパターンを使えばうまく行くかもしれませんが、プログラムコードはより複雑になります(境界条件の数が増えた場合は特にそうです)。仮にC++でマルチメソッドの方法が提供されていれば、それが最善の選択肢であったでしょうが、その場合でも複数の境界条件の設定が必要な場合は、使えなかったでしょう。総合的にみて、今のプログラムのようにすべてのメソッドを呼び出して動作させる方法は、重複した動作を行う可能性があるものの、少なくとも正しく動作します。私が思いつく唯一の代替策は、すべての境界条件を、スキームの計算の後で設定する方法で、そうすると applyBefore…( )メソッドは不要になり取り除くことができます。
境界条件の説明は以上で十分でしょう。これまで説明してきた事項をもとに、t=T 時(オプション満期時)の PayOff関数から時間を順次遡って t=0 まで計算する事が出来ます。その間何も起こらなければですが。残念ながら、そうならないケースが多くあり、次のセクションではそれについて説明します。
<ライセンス表示>
QuantLibのソースコードを使う場合は、ライセンス表示とDisclaimerの表示が義務付けられているので、添付します。 ライセンス