2. "Implementing QuantLib"の和訳
Chapter-V Parameterized Models and Calibration
5.3 CalibratedModel クラス
CalibratedModelクラスの実装内容を下記 Listing 5.5に示します。
class CalibratedModel : public virtual Observer,
public virtual Observable {
public:
CalibratedModel(Size nArguments)
: arguments_(nArguments),
constraint_(new PrivateConstraint(arguments_)),
shortRateEndCriteria_(EndCriteria::None) {}
void update() {
generateArguments();
notifyObservers();
}
Disposable<Array> params() const;
virtual void setParams(const Array& params);
void calibrate(
const vector<shared_ptr<CalibrationHelper> >&,
OptimizationMethod& method,
const EndCriteria& endCriteria,
const Constraint& constraint = Constraint(),
const vector<Real>& weights = vector<Real>());
EndCriteria::Type endCriteria();
protected:
virtual void generateArguments() {}
vector<Parameter> arguments_;
shared_ptr<Constraint> constraint_;
EndCriteria::Type shortRateEndCriteria_;
private:
class PrivateConstraint;
class CalibrationFunction;
};
Disposable<Array> CalibratedModel::params() const {
Size size = 0, i;
for (i=0; i<arguments_.size(); i++)
size += arguments_[i].size();
Array params(size);
Size k = 0;
for (i=0; i<arguments_.size(); i++) {
for (Size j=0; j<arguments_[i].size(); j++, k++) {
params[k] = arguments_[i].params()[j];
}
}
return params;
}
void CalibratedModel::setParams(const Array& params) {
Array::const_iterator p = params.begin();
for (Size i=0; i<arguments_.size(); ++i) {
for (Size j=0; j<arguments_[i].size(); ++j, ++p) {
QL_REQUIRE(p!=params.end(),"too few parameters");
arguments_[i].setParam(j, *p);
}
}
QL_REQUIRE(p==params.end(),"too many parameters");
generateArguments();
notifyObservers();
}
void CalibratedModel::calibrate(
const vector<shared_ptr<CalibrationHelper> >& instruments,
OptimizationMethod& method,
const EndCriteria& endCriteria,
const Constraint& additionalConstraint,
const vector<Real>& weights) {
Constraint c =
additionalConstraint.empty()) ?
*constraint_ :
CompositeConstraint(*constraint_,additionalConstraint);
CalibrationFunction f(this, instruments, weights);
Problem prob(f, c, params());
shortRateEndCriteria_ = method.minimize(prob, endCriteria);
setParams(prob.currentValue());
notifyObservers();
}
このクラスの中心は、calibrate( )メソッドです。その他のメソッドやメンバー変数はすべてこのメソッドの実行をサポートする為に用意されています。(注: 実際には、その内の2つの publicメソッドは、直接的にあるいは間接的に calibrate( )メソッドだけにしか使われていません。従って、publicのセクションではなく protectedのセクションに含めるべきでした。エラリー・クイーン(推理小説作家)の流儀に従っているかどうかわかりませんが、必要な証拠はすべてそこにあるので、プログラムコードを読んで、どれがその2つか見つけて下さい。)
このクラスのインスタンスが保持するメンバー変数は、まず、①Parameterクラスのインスタンスの配列(ここではなぜか parameters_ という変数名ではなく arguments_ という変数名が使われていますが)、②関数で使われるパラメータに対する constraints(制約条件)、③EndCriteria::Typeと呼ばれる enumeration型の変数で、Calibrationがどのように終了したか教えてくれるもの (注: 所謂、うまく成功して終了したのか、それとも再計算回数の最大値に達して終了したのか)です。③のメンバー変数名 (shortRateEndCriteria_) は、このクラスが当初 Short-Rateモデルで使われていた頃の名残が残っています。既に述べたように、このクラスは暫くの間ほっておかれたため、古い名残が残っている訳です。
コンストラクターは、引数として、モデルのパラメータの数を取得し、その値を使って、メンバー変数を初期化します。まず、その数の要素を持つパラメータ(配列)のメンバー変数が設定され、次に (そのパラメータ配列を引数として) PrivateConstraintクラス (この内部クラスについては、後で説明します)のインスタンスを生成し、最後にEndCriteria (訳注:Optimizerアルゴリズムの終了条件を設定するオブジェクト) を設定するメンバー変数を、Noneで初期化します。
ここで、注意すべきプログラムの傷があります。コンストラクターは、派生クラスでのみ使われる事を意図しているにも関わらず、publicで宣言されています(おそらく見過ごされたのでしょう)。従って、このクラスが純粋仮想関数を宣言していない事と相まって、CalibratedModelクラスのインスタンスを生成する事が可能になっています。しかし、そのようなインスタンスを生成しても、パラメータを使える値を設定する方法が無いので、使い物になりません (注: 保持されている Parameterクラスのインスタンスは、デフォールトで生成されているのみで、何の動作も行いません)。この問題点を修正しようとすると、過去のバージョンのプログラムが動かなくなる可能性がありますが、そもそもこのインスタンスを使ったプログラムは動かないでしょうから、ほっておいてもいいという理屈も成り立ちます。次回以降のバージョンアップの時に検討したいと思います。
モデルが (Observableからデータの変更の)通知を受け取ると、update( )メソッドが、必要な計算を行った後に、モデル自体の Observerオブジェクトに通知を転送します。これらの動作は、仮想関数である generateArguments( )をオーバーライドして実装されます。この関数名は、その中でパラメータのインスタンスを生成するような印象を与えるので、誤解を与えるかも知れません。しかし、すでに Calibrationが終わったパラメータを書き換えてはいけないので、(オーバーライドする際に)そうしないようにして下さい。このメソッドは、Calibrationの必要が無いパラメータ(例えば、Short-Rate Modelで使われる金利の Term Structureパラメータ)を生成するか、若干の事前計算などの為に使われるものです (訳注: 従ってオーバーライドする際は、そのように使うべき)。これについては、Hestonモデルの具体例の続きの中で説明していきます。
params( )と setParams( )メソッドはそれぞれパラメータの値を読み出したり、書き込んだりするために使われます。まず読みだす為には、params( )メソッドを使います。このメソッドは、まずメンバー変数である Parameterインスタンスが保持するすべての関数で使われるパラメータの数をかぞえ、その数の配列数を持つ Arrayインスタンスを生成して、そこにパラメータ値をコピーします (注: Arrayインスタンスはpush-backの操作ができないので、この動作は、一回のループ処理では行えません)。同様の方法で、setParams( )メソッドは、引数として渡されてきたパラメータの値を読み込み、メンバー変数であるParameterインスタンスの配列に、必要な数のパラメータをコピーしていきます。
さて、私が、先ほど protectedの方に移すべきであったメソッドがある、と言った時、それが params( ) とsetParams( )メソッドと推察された方は正解です。お好きなアルコールで祝杯をあげて下さい(但し、飲酒プログラミングは止めてください)。この2つのメソッドは、calibrate( )メソッドの中からだけ呼ばれるべきです。このメソッドを使って、ユーザーが独自に新しいプログラムを作ろうとしても、そこでは関数で使われるパラメータの数や、またどのパラメータが、各 Parameterインスタンス配列のどの要素に該当するか知ることが出来ません。従って、このメソッドを修正しようにも出来る訳がなく、かつしたとしてもほとんど役に立たないでしょう。
すでに申し上げた通り、calibrate( )メソッドは、このクラスの中心ですが、ほとんどの作業を別のオブジェクトに委託し、それらを統合管理しているだけです。その動作を簡単に説明すると、このメソッドは(数学的な)“最小値問題”を構築し、それを解く形で Calibrationされたパラメータを導出します。その最小値問題を構築する為の材料は、① OpimizationMethodの派生クラスのインスタンス (calibrate( )メソッドを呼び出す時に引数として渡される。例えば Simplex法や Levenberg-Marquardt法などが使われる)、② 最小値を求める対象となる関数、あるいは目的関数 (内部クラスである CalibratedFunctionクラスのインスタンスで、Calibrationエラーの値を返す)、③ パラメータ値の制約条件 (Constraint)で、デフォールト値は PrivateConstraintインスタンスに設定され、(引数として渡される)追加の制約条件がある場合は、それを加える。
calibrate( )メソッドは、これらすべてを集め、最小値問題のインスタンスを生成し、その解を出すプロセスを実行します。それが終了すると、EndCriteriaが保存され、ターゲット値との誤差を最小とするようなパラメータの値 (プログラムコードの中で、prob.currentValue( )から返される値)をメンバー変数(arguments_)に書き込み、最後に Observerオブジェクトに通知します。EndCriteriaの内容 (最小値問題が正常終了したか、失敗したか、失敗した場合はその理由)については、endCriteria( )メソッドを使って取り出す事が出来ます。もしこのクラスを今から設計し直すとすれば、EndCriteriaを返すメソッドは、これではなく calibrate( )メソッドにしたかもしれません (特にそれに拘っている訳ではありませんが)。EndCriteriaの情報は、モデルの中に保持するのが合理的かもしれないという理由からです。
現時点では、calibrate( )メソッドは、例外発生時の安全対策が十分ではありません。もし Calibrationプロセスの途中で例外処理が発生した場合、モデルに設定されているパラメータの値は、最小値を計算する途中で変更された値のままになります (その計算プロセスが例外処理の発生原因の可能性が高い)。Calibration前のパラメータの値を、Calibrationがスタートする前に、どこかに一時保管し、例外処理が発生した場合は、そのパラメータを使って例外処理が発生する前の状態に戻す操作を行えば、強固な安全対策となるかも知れません。しかし、適切な EndCriteriaを設定しておくのがおそらくベストの対応でしょう。 Calibrationが失敗した場合には、まさにそのプロセスに移行するわけですから。
Calibrationの全体プロセスの中の最後のパートは、次の Listing 5.6に示すように、PrivateConstraintと CalibratedFunctionという2つの内部クラスの中で実装されています。
Listing 5.6: Inner classes of the CalibratedModel class.
class CalibratedModel::PrivateConstraint : public Constraint {
private:
class Impl : public Constraint::Impl {
const vector<Parameter>& arguments_;
public:
Impl(const vector<Parameter>& arguments);
bool test(const Array& params) const {
for (Size i=0; i<arguments_.size(); i++) {
Array testParams(/* `select the correct subset` */);
if (!arguments_[i].testParams(testParams))
return false;
}
return true;
}
};
public:
PrivateConstraint(const vector<Parameter>& arguments);
};
class CalibratedModel::CalibrationFunction
: public CostFunction {
public:
CalibrationFunction(
CalibratedModel* model,
const vector<shared_ptr<CalibrationHelper> >& instruments,
const vector<Real>& weights)
: model_(model, no_deletion), instruments_(instruments),
weights_(weights) {}
virtual Disposable<Array> values(const Array& params) const {
model_->setParams(params);
Array values(instruments_.size());
for (Size i=0; i<instruments_.size(); i++) {
values[i] = instruments_[i]->calibrationError()
*sqrt(weights_[i]);
}
return values;
}
virtual Real value(const Array& params) const;
private:
shared_ptr<CalibratedModel> model_;
const vector<shared_ptr<CalibrationHelper> >& instruments_;
vector<Real> weights_;
};
PrivateConstraintクラスは、Parameterインスタンスに設定されている制約条件を取りまとめる事によって機能します。プログラムの構成は params( )や setParams( )と似ています、保存されている Parameter配列の中の各データ内容を、Loopを使って見に行き、各モデルのパラメータに対応する関数で使われるパラメータの集まりを特定し、Parameterインスタンスの testParams( )メソッドを呼び出して、各データの内容をチェックします。合成された制約条件は、その中の各制約条件がすべて満たされたときのみ、満たされた事になります。
最後の、CalibratedFunctionクラスですが、与えられたパラメータを使って計算された Calibration誤差の値を計算します。コンストラクターは、引数として、Calibrationの対象となるモデル (のインスタンス)へのポインター、Calibrationで使われる市場価格のある金融商品の配列、およびウェイトの配列を取り、それぞれメンバー変数に保存します。なぜか、モデルのポインターは、CalibratedFunctionによって消されてしまう事がないように、shared_ptrインスタンスへ保存されています。しかし、単純にポインターを保持するだけで十分であったかも知れません。
このクラスは、CostFunctionクラスから継承されており、(ベースクラスで宣言された)values( ) と value( )メソッドの両方の実装が必要です。前者は、誤差の集合 (各金融商品ごとの誤差の配列) を返しますが、後者はその全体を1つの値にして返します。Optimizationの方法によって、いずれかのメソッドが使われます。この2つのメソッドの実装内容は似ているので、前者の実装内容のみを Listingの中で示しています。このメソッドは、まず引数で受取った Parameterインスタンスをモデルに設定し、次にメンバー変数に保存されている CalibrationHelperインスタンス(の配列)を使って、Calibration誤差を計算します。このプロセスが作動するためには、各 Helperインスタンスに、Calibration対象の‘モデル’を使った価格エンジンを設定する必要があります。これら全体のプロセスをプログラムコード化しようとすると、次のような形になるでしょう。
shared_ptr<HestonModel> model(...);
vector<shared_ptr<CalibrationHelper> > helpers(...);
shared_ptr<PricingEngine> engine =
make_shared<AnalyticHestonEngine>(model, ...);
for (i=0; i<helpers.size(); ++i)
helpers[i]->setPricingEngine(engine);
model->calibrate(helpers, ...);
すなわち、Helperインスタンスは、モデル価格を計算する為に、まず PricingEngineを設定し、次に PricingEngineがモデルを使って計算を行います。従って、values( )メソッドの中で setParams( )を呼び出してパラメータを変化させると、モデル価格も動き、その結果 Calibration誤差の値も変化します。values( )メソッドは、商品ごとの Calibration誤差を返しますが、value( )メソッドは、それらの2乗和を返します。
最後の注意点ですが、今の所、CalibratedFunctionクラスは、CalibratedModelの friendクラスとして宣言されています。しかし CalibratedFunctionクラスは publicメソッドである setParams( )メソッドにしかアクセスしていないので、実際にはその必要はありません。さらに言えば、仮にC++11の環境で開発をしていれば、setParams( )が protectedとして宣言されていたとしても、friendである必要はなかったでしょう。新基準では、内部クラスからは、その外部クラスのメソッドへは publicであろうとなかろうと、アクセス可能になっています。
5.3.1 具体例:Hestonモデル(続き)
Hestonモデルの具体例の説明の続きです。HestonModelクラスのプログラムコードを下記 Listing 5.7に示します。
Listing 5.7: Implementation of the HestonModel class.
class HestonModel : public CalibratedModel {
public:
HestonModel(const shared_ptr<HestonProcess>& process)
: CalibratedModel(5), process_(process) {
arguments_[0] = ConstantParameter(process->theta(),
PositiveConstraint());
arguments_[1] = ConstantParameter(process->kappa(),
PositiveConstraint());
arguments_[2] = ConstantParameter(process->sigma(),
PositiveConstraint());
arguments_[3] =
ConstantParameter(process->rho(),
BoundaryConstraint(-1.0, 1.0));
arguments_[4] = ConstantParameter(process->v0(),
PositiveConstraint());
generateArguments();
registerWith(process_->riskFreeRate());
registerWith(process_->dividendYield());
registerWith(process_->s0());
}
Real theta() const { return arguments_[0](0.0); }
Real kappa() const { return arguments_[1](0.0); }
Real sigma() const { return arguments_[2](0.0); }
Real rho() const { return arguments_[3](0.0); }
Real v0() const { return arguments_[4](0.0); }
shared_ptr<HestonProcess> process() const {
return process_;
}
protected:
void generateArguments() {
process_.reset(
new HestonProcess(process_->riskFreeRate(),
process_->dividendYield(),
process_->s0(),
v0(), kappa(), theta(),
sigma(), rho()));
}
shared_ptr<HestonProcess> process_;
};
すでにご存知かも知れませんが、このモデルは5つのパラメータ、\( \theta , \kappa , \sigma , \rho , v_0 \) を持ちます。このモデルの対象資産の Process (確率過程)は、リスクフリー金利、現時点の株価、および配当利回りにも依存しています。Chapter 6で説明する理由から、QuantLibライブラリでは、これらの値を別々の HestonProcessクラスにグループ分けしています(簡潔にするため、ここではそのインターフェースを示しませんが、これらをモデルパラメータを保持する containerとして使っています)。
HestonModelのコンストラクターは、(モデルが前提としている)Processクラスのインスタンスを引数として取り、それをメンバー変数に保存した上で、Calibrationの対象となるパラメータの設定を行っています。最初に、パラメータの数 (5) をベースクラスのコンストラクターに渡し、次にモデルの各パラメータについて Parameterインスタンスを生成して、メンバー変数 arguments_にそれぞれ保存します。パラメータはすべて定数パラメータであり、その内の rho の制約条件は-1から+1の範囲内であること、その他のパラメータは、すべて正の値であることが制約条件です。各パラメータの初期値は、Processのインスタンスから取り込まれます。このクラスではインスペクター関数として theta( )、kappa( )、sigma( )、rho( )および v0( )が定義されており、それぞれの現時点の各パラメータ値を取り出します。各メソッドは、対応する Parameterインスタンスの t=0 時点 (引数のデフォールト値) の値を返しますが、このモデルのパラメータはすべて定数なので時間の指定は特にいつでも問題ありません。
各パラメータの設定が終わると、generateArguments( )メソッドが呼び出されます。このメソッドは、Calibrationの途中、パラメータの値が変更される都度呼び出され、そして保持されている Processインスタンスを新しいインスタンスに入れ替えます (その時、金利の Term Structure と Quote(市場価格)はそのままで、モデルパラメータは新しいものになります)。この動作を行う理由は、PricingEngineが Model から Processインスタンスを取り出して使う場合に、常に更新されたものが用意されているようにする為です。しかし、この Processインスペクター (訳注: Processインスタンスの情報を取りだすメソッド)は、メソッドが呼び出される都度、新しい Processインスタンスを生成すべきでは無いのではないかと思っています。もし Processインスタンスの役割が、モデルパラメータとイールドカーブのデータを保持するだけなら、それらのデータ用のインスペクターを定義しておけば、PricingEngineがそれらを直接使う事ができます。そうすれば、Calibrationのステップ毎に、新しく複雑なインスタンスを生成する手間を省けるかもしれません。もし読者の方が、今の実装方法ではなく、そういった動作が出来る新たなプログラムコードを開発・実験されるなら、歓迎します。
最後に、コンストラクターは関係するいくつかの Observableオブジェクトに対して、自らを Observerとして登録します。但し、Processインスタンスについては、generateArguments( )メソッドでインスタンスが入れ替わるので、このインスタンスに登録する必要性はありません。その代わり、このインスタンスの内部にあるいくつかの Handleは、Processインスタンスが入れ替わる度にデータが変わる為、これらの Handleに対し直接登録します。
すでに説明したいくつかのインスペクター関数を含め、以上でモデルの実装は完成です。Calibrationの計算プロセスの仕組みは、CalibratedModelクラスの派生クラスで行われ、それがうまく作動するために必要なただ一つの事は、PricingEngineが HestonModelのインスタンスを取り込み、それを使って CalibrationHelperクラスのインスタンスに保存されている VanillaOptionインスタンスの価格計算をする事です。
この Chapterで、QuantLibライブラリの中で提供されている AnalyticHestonEngine について説明できなかった事をお許し下さい。このクラスは Hestonモデルを使ってヨーロピアンオプションの価格式の解析解を実装するもので、コードのコメント欄には5件の論文と本を引用しており、650行にも及ぶコードです。ここで使われている数学の知識が無い人にとっては、まったく訳の分からない内容かもしれません。もし興味があれば、QuantLibライブラリのソースコードで確認して下さい。
<ライセンス表示>
QuantLibのソースコードを使う場合は、ライセンス表示とDisclaimerの表示が義務付けられているので、添付します。 ライセンス