C++ によるディーラーモデルの実装
ディーラーの行動をモデル化し人工的に金融市場のシミュレートする ディーラーモデル について簡単に紹介し,C++ による実装例を提示します.(バージョンは C++11 以降を想定)
外国為替市場の概要
外国為替市場の取引のうち,EBS 市場の指値注文が今回の対象です.
ディーラーモデルとは
ディーラーモデルの研究では,金融市場に参加するディーラーたちの戦略を定義し,マクロな現象として観測される暴騰・暴落などの金融市場価格の動的なふるまいとの関係を探ります.順張り・逆張り戦略取るディーラーの比率を変えたり,利益確定や損切りの閾値を変えたり,政府による為替介入を導入できたりと,発展性が高いのが特徴です.文献例を 3 つ挙げます.
- Yamada, Kenta, Hideki Takayasu, Takatoshi Ito, and Misako Takayasu. 2009. “Solvable Stochastic Dealer Models for Financial Markets.” Physical Review. E, Statistical, Nonlinear, and Soft Matter Physics 79 (5 Pt 1): 051120. http://dx.doi.org/10.1103/PhysRevE.79.051120
- 松永健太, 山田健太, 高安秀樹, and 高安美佐子. 2012. “スプレッドディーラーモデルの構築とその応用.” 人工知能学会論文誌 27 (6): 365–75. http://dx.doi.org/10.1527/tjsai.27.365
- Kiyoshi Kanazawa, Takumi Sueshige, Hideki Takayasu, and Misako Takayasu. 2018. Derivation of the boltzmann equation for financial brownian motion: Direct observation of the collective motion of high-frequency traders. Physical review letters, 120 (13): 138301 https://doi.org/10.1103/PhysRevLett.120.138301
本稿は最初に提案されたディーラーモデルである Yamada et al. (2009) のモデルを取り上げます.
仮定(単純化制約)
-
ディーラーは常に bid(買い) と ask(売り) の両方の指値注文を出す
-
すべてのディーラーのスプレッドは同じで,注文価格を更新しても保たれる.つまり,ディーラー
の 時刻i での bid,ask の注文価格をそれぞれt とすると,b_i(t),a_i(t) によらずスプレッドが一定値i,t を取る.L a_i(t) - b_i(t) = L 本稿では
をスプレッドと呼ぶことにする.L -
スプレッドが一定のとき,ディーラーの bid と ask の中央値を
とすると,下記の関係が成り立つ.p_i を mid-price と呼ぶ.p_i \begin{aligned} p_i(t)&=\frac{a_i(t)+b_i(t)}{2}\\ a_i(t)&=p_i(t)+\frac{L}{2}\\ b_i(t)&=p_i(t)-\frac{L}{2} \end{aligned} -
各ディーラーの注文価格更新を,mid-price の更新として反映する
アルゴリズム
- 各ディーラーの注文価格更新
- 取引成立条件の確認
取引成立時は 3 に進み,不成立なら 1 に戻る - 取引処理:
- 市場価格の決定
- tick を進める
- 取引成立したディーラーの再注文
- tick
max_tick となるまで上記を繰り返す\geq - 市場価格時系列を出力
1) ディーラーの注文価格更新ルール
ディーラーの戦略には順張り・逆張り,利益確定・損切りなどの様々なものがありますが,本稿では簡単な戦略として
- ランダムウォーク戦略(基礎モデル / null モデル)
- トレンドフォロー戦略(全員が同じ順張り戦略を取る)
を取り上げ,実装例を提示します.
2) 取引成立条件
最も安い売値 (best_ask) が最も高い買値 (best_bid) 以下になったら,それぞれの価格を付けたディーラー間で取引が成立します.
\mathrm{best\_ask}(t) = \underset{i}{\min}\ a_i(t) \mathrm{best\_bid}(t) = \underset{i}{\max}\ b_i(t) -
なら取引成立\mathrm{best\_ask}(t) \leq \mathrm{best\_bid}(t)
3) 取引成立後の処理
- 市場価格の決定:
で取引が成立したとき,そのときの tick (\mathrm{best\_ask}(t), \mathrm{best\_bid}(t) ) の市場価格n はそれらの中央値に決まるP(n)
P(n) = \frac{\mathrm{best\_ask}(t)+\mathrm{best\_bid}(t)}{2} - tick を進める:
tick += 1
- 再注文:取引成立したディーラーの mid-price を市場価格に揃える
プログラムの主要な関数
-
run_simulation()
:シミュレーションを統括する関数.次の2つを順次実行-
update_dealer_orders()
:各ディーラーの注文価格を更新 -
try_transaction()
:取引成立判定 + 取引成立時の処理
-
-
output_vector()
:市場価格時系列をファイル出力するための関数
ランダムウォークモデル
ランダムウォークモデルはディーラーの注文価格の更新がランダムに行われるモデルで,基礎モデルや null モデルとして位置づけられます.このモデルに市場価格の影響やディーラーの戦略を加えたものとの差分を見ることに興味があります.
数式
各ディーラーの mid-price (ask と bid の中央値) が次のように更新されます.
実装例
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <random>
// 各ディーラーの注文価格更新
// スプレッド(=ask-bid)を一定に保ったまま,bid/ask 価格をランダムにずらす
void update_dealer_orders(std::vector<double> &dealer_mid_prices, int N, double dp, std::mt19937 &gen)
{
std::uniform_real_distribution<double> uni(0.0, 1.0);
// For each dealer, randomly update the mid-price
for (int i = 0; i < N; ++i)
{
if (uni(gen) < 0.5)
dealer_mid_prices[i] += dp;
else
dealer_mid_prices[i] -= dp;
}
}
// 最良(最安)売値: best_ask = min(ask)
// 最良(最高)買値: best_bid = max(bid)
// 取引成立条件 : best_ask <= best_bid(最安の売値に買い手が付いたら)
// 関数の戻り値 : 取引が成立したか否か
bool try_transaction(int N, double spread, std::vector<double> &dealer_mid_prices, std::vector<double> &market_prices, int &tick)
{
double half_spread = spread / 2;
// calculate best_bid, best_ask
double best_ask = dealer_mid_prices[0] + half_spread;
double best_bid = dealer_mid_prices[0] - half_spread;
int best_ask_dealer = 0;
int best_bid_dealer = 0;
for (int i = 1; i < N; ++i)
{
// Determine if i is the best_ask_dealer
double ask = dealer_mid_prices[i] + half_spread;
if (ask < best_ask)
{
best_ask = ask;
best_ask_dealer = i;
// best_ask になると best_bid にはならないので (bid[i] < bid[0] <= best_bid)
// best_bid 判定をスキップ
continue;
}
// update best_bid = max(bid)
double bid = dealer_mid_prices[i] - half_spread;
if (bid > best_bid)
{
best_bid = bid;
best_bid_dealer = i;
}
}
bool is_transaction_successful = best_ask <= best_bid;
// 取引成立の場合のみ,(1)市場価格を決定し,(2)約定したディーラーに再注文をかける
if (is_transaction_successful)
{
// 市場価格の決定 + tick 更新
double market_price = (best_ask + best_bid) / 2;
market_prices[tick] = market_price;
++tick;
// 再注文:市場価格の周りに bid/ask を等幅で配置(mid_price を市場価格に揃える)
dealer_mid_prices[best_ask_dealer] = market_price;
dealer_mid_prices[best_bid_dealer] = market_price;
printf("tick:%d, price:%f\r", tick, market_price);
}
return is_transaction_successful;
}
void run_simulation(int N, double spread, double p0, double dp,
int max_tick, std::vector<double> &market_prices, int seed = 127)
{
std::mt19937 gen(seed);
// ディーラーごとの bid/ask の中央値
std::vector<double> dealer_mid_prices(N, p0);
int tick = 0;
update_dealer_orders(dealer_mid_prices, N, dp, gen);
while (true)
{
bool is_successful = try_transaction(N, spread, dealer_mid_prices, market_prices, tick);
if (is_successful)
{
if (tick >= max_tick)
break;
continue; // 複数組のディーラーがマッチし得る.取引が成立しなくなるまで取引判定を繰り返す
}
// 取引成立しなかったら,各ディーラーが注文価格を更新
update_dealer_orders(dealer_mid_prices, N, dp, gen);
}
}
void output_vector(std::vector<double> &v, std::string path)
{
std::ofstream ofs(path);
for (auto x : v)
ofs << x << std::endl;
}
int main()
{
// パラメータ定義
int N = 4; // ディーラー数
double spread = 0.01; // ask, bid の価格差(spread=ask-bid, ディーラー・時間によらず一定)
double p0 = 100.0; // ask, bid の中央値の初期値(ask0=p0+spread/2, bid0=p0-spread/2)
double dp = 0.001; // ランダムな価格変化の大きさ
int max_tick = 10000; // シミュレーションの tick 数
int seed = 127; // 乱数のシード値
// シミュレーションの実行
std::vector<double> market_prices(max_tick, p0); // 各時刻の市場価格
run_simulation(N, spread, p0, dp, max_tick, market_prices, seed);
// 市場価格時系列のファイル出力
std::string path = "market_prices_random_walk";
output_vector(market_prices, path);
return 0;
}
トレンドフォローモデル
ランダムウォークモデル(基礎モデル)にトレンドフォロー効果を追加したモデルです.ここでは,ディーラーが直近の市場価格変化に追従するように注文価格を更新していきます.
数式
各ディーラーの mid-price (ask と bid の中央値) が次のように更新されます.
のようにも表せます.
第二式は直近の価格差の加重平均で,直近
-
のときM=1 \langle \Delta P\rangle_1 = \Delta P(n) -
のときM=2 \langle \Delta P\rangle_2 = \frac{1}{3}\left(2\Delta P(n)+\Delta P(n-1)\right)
トレンドフォロー項のパラメータ
-
: 順張り戦略d>0 -
: トレンド無視d=0 -
: 逆張り戦略d<0
簡単のため,全てのディーラーが同じトレンドフォロー強度
実装例 1
可読性を高めるため,数式中のトレンドフォローに関する変数を下記で置き換えています.
-
→M trend_window
-
→d trend_follow_intensity
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <random>
// 各ディーラーの注文価格更新
// スプレッド(=ask-bid)を一定に保ったまま,bid/ask 価格をランダムにずらす
// 過去の市場価格のトレンド
void update_dealer_orders(std::vector<double> &dealer_mid_prices, int N, double dp,
std::vector<double> &market_prices, int trend_window, double trend_follow_intensity, double dt,
int tick, std::mt19937 &gen)
{
// Calculate trend follow term
double trend = 0;
int m = trend_window;
if (tick < trend_window) // tick < M の場合,ある分の市場価格を使ってトレンドを計算
m = tick - 1;
for (int k = 0; k < m; ++k)
{
double dP = market_prices[tick - 1 - k] - market_prices[tick - 1 - k - 1];
trend += (trend_window - k) * dP; // weight=M-k
}
trend /= trend_window * (trend_window + 1) / 2; // divide by total weights (=1+2+...+trend_window)
trend *= trend_follow_intensity * dt; // amplify
// For each dealer, randomly update the mid-price
std::uniform_real_distribution<double> uni(0.0, 1.0);
for (int i = 0; i < N; ++i)
{
if (uni(mt) < 0.5)
dealer_mid_prices[i] += trend + dp;
else
dealer_mid_prices[i] += trend - dp;
}
}
// 最良(最安)売値: best_ask = min(ask)
// 最良(最高)買値: best_bid = max(bid)
// 取引成立条件 : best_ask <= best_bid(最安の売値に買い手が付いたら)
// 関数の戻り値 : 取引が成立したか否か
bool try_transaction(int N, double spread, std::vector<double> &dealer_mid_prices, std::vector<double> &market_prices, int &tick)
{
double half_spread = spread / 2;
// calculate best_bid, best_ask
double best_ask = dealer_mid_prices[0] + half_spread;
double best_bid = dealer_mid_prices[0] - half_spread;
int best_ask_dealer = 0;
int best_bid_dealer = 0;
for (int i = 1; i < N; ++i)
{
// Determine if i is the best_ask_dealer
double ask = dealer_mid_prices[i] + half_spread;
if (ask < best_ask)
{
best_ask = ask;
best_ask_dealer = i;
// best_ask になると best_bid にはならないので (bid[i] < bid[0] <= best_bid)
// best_bid 判定をスキップして次のディーラーに移る
continue;
}
// update best_bid = max(bid)
double bid = dealer_mid_prices[i] - half_spread;
if (bid > best_bid)
{
best_bid = bid;
best_bid_dealer = i;
}
}
bool is_transaction_successful = best_ask <= best_bid;
// 取引成立の場合のみ,(1)市場価格を決定し,(2)約定したディーラーに再注文をかける
if (is_transaction_successful)
{
// 市場価格の決定 + tick 更新
double market_price = (best_ask + best_bid) / 2;
market_prices[tick] = market_price;
++tick;
// 再注文:市場価格の周りに bid/ask を等幅で配置(mid_price を市場価格に揃える)
dealer_mid_prices[best_ask_dealer] = market_price;
dealer_mid_prices[best_bid_dealer] = market_price;
printf("tick:%d, price:%f\r", tick, market_price);
}
return is_transaction_successful;
}
void run_simulation(int N, double spread, double p0, double dp,
int trend_window, double trend_follow_intensity, double dt,
int max_tick, std::vector<double> &market_prices, int seed = 127)
{
std::mt19937 gen(seed);
// ディーラーごとの bid/ask の中央値
std::vector<double> dealer_mid_prices(N, p0);
int tick = 0;
update_dealer_orders(dealer_mid_prices, N, dp, market_prices, trend_window, trend_follow_intensity, dt, tick, gen);
while (true)
{
bool is_successful = try_transaction(N, spread, dealer_mid_prices, market_prices, tick);
if (is_successful)
{
if (tick >= max_tick)
break;
continue; // 複数組のディーラーがマッチし得る.取引が成立しなくなるまで取引判定を繰り返す
}
// 取引成立しなかったら,各ディーラーが注文価格を更新
update_dealer_orders(dealer_mid_prices, N, dp, market_prices, trend_window, trend_follow_intensity, dt, tick, gen);
}
}
void output_vector(std::vector<double> &v, std::string path)
{
std::ofstream ofs(path);
for (auto x : v)
ofs << x << std::endl;
}
int main()
{
// パラメータ定義
int N = 4; // ディーラー数
double spread = 0.01; // ask, bid の価格差(spread=ask-bid, ディーラー・時間によらず一定)
double p0 = 100.0; // ask, bid の中央値の初期値(ask0=p0+spread/2, bid0=p0-spread/2)
double dp = 0.001; // [random walk] ランダムな価格変化の大きさ
int trend_window = 2; // [trend follow] 価格変化のトレンド計算に使う期間 (tick 数)
double trend_follow_intensity = 1.25; // [trend follow] トレンドフォローの強さ(d>0:順張り,d=0:トレンド無視,d<0:逆張り)
double dt = 0.01; // [trend follow] 時間刻み
int max_tick = 10000; // シミュレーションの tick 数
int seed = 127; // 乱数のシード値
// シミュレーションの実行
std::vector<double> market_prices(max_tick, p0); // 各時刻の市場価格
run_simulation(N, spread, p0, dp, trend_window, trend_follow_intensity, dt, max_tick, market_prices, seed);
// 市場価格時系列のファイル出力
std::string path = "market_prices_trend_follow";
output_vector(market_prices, path);
return 0;
}
実装例 2(クラスを利用)
DealerModel
クラスを作り,パラメータと関数をそのメンバとすることで,update_dealer_orders()
や try_transaction()
の引数にパラメータを渡す必要がなくなります.
クラスの内部でしか使わない (private) 変数・関数には _
を接頭語として付けています.C++ では _
を後に付ける慣習がありますが,ここでは使用頻度の多い Python の慣習に従っています.
- Google C++ Style Guide: https://google.github.io/styleguide/cppguide.html#Variable_Names
- Python PEP 8: https://peps.python.org/pep-0008/#descriptive-naming-styles
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <random>
class DealerModel
{
private:
// パラメータ
int _N; // ディーラー数
double _spread; // ask, bid の価格差(spread=ask-bid, ディーラー・時間によらず一定)
double _p0; // ask, bid の中央値の初期値(ask0=p0+spread/2, bid0=p0-spread/2)
double _dp; // [random walk] ランダムな価格変化の大きさ
int _trend_window; // [trend follow] 価格変化のトレンド計算に使う期間 (tick 数)
double _trend_follow_intensity; // [trend follow] トレンドフォローの強さ(d>0:順張り,d=0:トレンド無視,d<0:逆張り)
double _dt; // [trend follow] 時間刻み
int _max_tick; // シミュレーションの tick 数
int _seed; // 乱数のシード値
// 内部変数
int _tick;
std::vector<double> _dealer_mid_prices;
std::mt19937 _gen;
// 関数
void _update_dealer_orders();
bool _try_transaction();
public:
// 市場価格時系列
std::vector<double> market_prices;
// コンストラクタ
DealerModel(int N, double spread, double p0, double dp,
int trend_window, double trend_follow_intensity, double dt,
int max_tick, int seed)
{
// パラメータの反映
_N = N;
_spread = spread;
_p0 = p0;
_dp = dp;
_trend_window = trend_window;
_trend_follow_intensity = trend_follow_intensity;
_dt = dt;
_max_tick = max_tick;
_seed = seed;
// 初期化
_tick = 0;
for (int i = 0; i < N; ++i)
_dealer_mid_prices.push_back(p0);
_gen.seed(seed);
for (int tick = 0; tick < max_tick; ++tick)
market_prices.push_back(p0);
};
// シミュレーションを実行
void run();
};
// 各ディーラーの注文価格更新
// スプレッド(=ask-bid)を一定に保ったまま,bid/ask 価格をランダムにずらす
// 過去の市場価格のトレンド
void DealerModel::_update_dealer_orders()
{
// Calculate trend follow term
double trend = 0;
int window = _trend_window;
if (_tick < _trend_window) // tick < M の場合,ある分の市場価格を使ってトレンドを計算
window = _tick - 1;
for (int k = 0; k < window; ++k)
{
double dP = market_prices[_tick - 1 - k] - market_prices[_tick - 1 - k - 1];
trend += (_trend_window - k) * dP; // weight=M-k
}
trend /= _trend_window * (_trend_window + 1) / 2; // divide by total weights (=1+2+...+trend_window)
trend *= _trend_follow_intensity * _dt; // amplify
// For each dealer, randomly update the mid-price
std::uniform_real_distribution<double> uni(0.0, 1.0);
for (int i = 0; i < _N; ++i)
{
if (uni(_gen) < 0.5)
_dealer_mid_prices[i] += trend + _dp;
else
_dealer_mid_prices[i] += trend - _dp;
}
}
// 最良(最安)売値: best_ask = min(ask)
// 最良(最高)買値: best_bid = max(bid)
// 取引成立条件 : best_ask <= best_bid(最安の売値に買い手が付いたら)
// 関数の戻り値 : 取引が成立したか否か
bool DealerModel::_try_transaction()
{
double half_spread = _spread / 2;
// calculate best_bid, best_ask
double best_ask = _dealer_mid_prices[0] + half_spread;
double best_bid = _dealer_mid_prices[0] - half_spread;
int best_ask_dealer = 0;
int best_bid_dealer = 0;
for (int i = 1; i < _N; ++i)
{
// Determine if i is the best_ask_dealer
double ask = _dealer_mid_prices[i] + half_spread;
if (ask < best_ask)
{
best_ask = ask;
best_ask_dealer = i;
// best_ask になると best_bid にはならないので (bid[i] < bid[0] <= best_bid)
// best_bid 判定をスキップして次のディーラーに移る
continue;
}
// update best_bid = max(bid)
double bid = _dealer_mid_prices[i] - half_spread;
if (bid > best_bid)
{
best_bid = bid;
best_bid_dealer = i;
}
}
bool is_transaction_successful = best_ask <= best_bid;
// 取引成立の場合のみ,(1)市場価格を決定し,(2)約定したディーラーに再注文をかける
if (is_transaction_successful)
{
// 市場価格の決定 + tick 更新
double market_price = (best_ask + best_bid) / 2;
market_prices[_tick] = market_price;
++_tick;
// 再注文:市場価格の周りに bid/ask を等幅で配置(mid_price を市場価格に揃える)
_dealer_mid_prices[best_ask_dealer] = market_price;
_dealer_mid_prices[best_bid_dealer] = market_price;
printf("tick:%d, price:%3f\r", _tick, market_price);
}
return is_transaction_successful;
}
void DealerModel::run()
{
// ディーラーごとの bid/ask の中央値
std::vector<double> dealer_mid_prices(_N, _p0);
_update_dealer_orders();
while (true)
{
bool is_successful = _try_transaction();
if (is_successful)
{
if (_tick >= _max_tick)
break;
continue; // 複数組のディーラーがマッチし得る.取引が成立しなくなるまで取引判定を繰り返す
}
// 取引成立しなかったら,各ディーラーが注文価格を更新
_update_dealer_orders();
}
}
void output_vector(std::vector<double> &v, std::string path)
{
std::ofstream ofs(path);
for (auto x : v)
ofs << x << std::endl;
}
int main()
{
// パラメータ定義
int N = 4; // ディーラー数
double spread = 0.01; // ask, bid の価格差(spread=ask-bid, ディーラー・時間によらず一定)
double p0 = 100.0; // ask, bid の中央値の初期値(ask0=p0+spread/2, bid0=p0-spread/2)
double dp = 0.001; // [random walk] ランダムな価格変化の大きさ
int trend_window = 2; // [trend follow] 価格変化のトレンド計算に使う期間 (tick 数)
double trend_follow_intensity = 1.25; // [trend follow] トレンドフォローの強さ(d>0:順張り,d=0:トレンド無視,d<0:逆張り)
double dt = 0.01; // [trend follow] 時間刻み
int max_tick = 10000; // シミュレーションの tick 数
int seed = 127; // 乱数のシード値
// シミュレーションの実行
DealerModel dealerModel(N, spread, p0, dp,
trend_window, trend_follow_intensity, dt,
max_tick, seed);
dealerModel.run();
// 市場価格時系列のファイル出力
std::string path = "market_prices_trend_follow_class";
output_vector(dealerModel.market_prices, path);
return 0;
}
パラメータを変えて並列実行
パラメータを変えて同じシミュレーションを実行する際,並列化して実行時間を減らすことを考えます.ここでは OpenMP を使った単純な並列化の方法を紹介します.
直前のディーラーモデルで trend_window
と trend_follow_intensity
を変えてシミュレーションする場合を考えます.OpenMP で並列化するために必要な工程は次の 4 点です.
- OpenMP のヘッダーファイルをインクルードする
- 並列スレッド数を指定
- 並列処理する
for
文を指定する - コンパイル時に
-fopenmp
のオプションを付ける
1. OpenMP のヘッダーファイルをインクルードする
#include <omp.h>
2. 並列スレッド数を指定
int num_threads = 3; // 並列スレッド数
omp_set_num_threads(num_threads);
omp_set_num_threads
に std::
は不要です.
for
文を指定する
3. 並列処理する // パラメータの候補
int trend_windows[] = {1, 2, 3};
double trend_follow_intensities[] = {0.75, 1.00, 1.25};
// 並列処理する for 文
#pragma omp parallel for
for (auto trend_window : trend_windows)
{
for (auto trend_follow_intensity : trend_follow_intensities)
{
// シミュレーションの実行
DealerModel dealerModel(N, spread, p0, dp,
trend_window, trend_follow_intensity, dt,
max_tick, seed);
dealerModel.run();
// 市場価格時系列のファイル出力
// パラメータをファイル名に含める関数を新たに作る
std::string path_to_save = gen_save_path(dir_to_save, trend_window, trend_follow_intensity);
output_vector(dealerModel.market_prices, path_to_save);
}
}
-fopenmp
のオプションを付ける
4. コンパイル時に g++ -std=c++17 -O3 -fopenmp dealer_model_parallel.cpp -o dealer_model_parallel
最後に
ディーラーモデルが実装できたら,価格差
- ディーラーごとにトレンドフォロー戦略を変える
- 利益確定・損切り戦略を加える
Discussion