Tips

IT技術系Tips

ブラック・ショールズ式を実装してみた(Python:Sympy)

勉強がてらに作ってみた。
参考にしたサイトは↓
http://www.findai.com/kouza/4009opt.html

とりあえずソース

ソース

import datetime
from sympy.mpmath import log, sqrt, exp
from sympy.statistics import Normal


def black_scholes(原資産, ストライク, 金利, ボラティリティ, 満期日=None, 基準日=None, 期間=0, 配当=0,
                  ポジション=True, コールプット区分=True, 想定元本=1):
    """ ブラック・ショールズ式。
    株価 or 通貨(通貨オプション) or 先物 or 債権 の価格を計算する。
    期間 または 満期日と基準日の組み合わせ の、どちらかが必須。

    満期日と基準日を指定する場合
    2000/07/01 - 2000/01/01 と 2001/01/01 - 2000/7/1 では
    日数が異なるため、期間に違いが出る事に注意。

    @param 原資産: 株価 or 通貨(通貨オプション) or 先物 or 債権 の価格。
    @param ストライク: 権利行使価格。
    @param 金利: リスクフリーレート。百分率。1% なら 0.01を指定。
    @param ボラティリティ: 標準偏差、予想変動率。
    @param 満期日: datetime型。基準日とセットで使用する。例:datetime.strptime("2015-01-01", "%Y-%m-%d")
    @param 基準日: datetime型。満期日とセットで使用する。例:datetime.strptime("2015-01-01", "%Y-%m-%d")
    @param 期間: 数値。1ヶ月なら1/12、2ヶ月なら2/12。5年なら5。
    @param 配当: 株価:配当率、通貨:外国金利、先物:安全利子率、債権:クーポンレート。
    ヨーロピアンコール・オプションの場合、初期値の0で良い。
    @param ポジション: boolean。買い:True、売り:False
    @param コールプット区分: boolean。買う権利:True、売る権利:False
    @param 想定元本: 計算の最後の係数。1ならコールプット価格そのものが算出される。
    @return: 資産価値
    """
    if not 期間 and (満期日 is None or 基準日 is None):
        raise "期間 または 満期日と基準日の両方 を指定して下さい。"

    t = 期間 if 期間 else (満期日 - 基準日).days / 365
    q = 配当
    S = 原資産
    N = lambda d: Normal(0, 1).cdf(d)
    r = 金利
    k = ストライク
    σ = ボラティリティ

    d1 = (log(S / k) + (r - q + (0.5 * σ ** 2)) * t) / (σ * sqrt(t))
    d2 = d1 - σ * sqrt(t)

    C = exp(-q * t) * S * N(d1) - exp(-r * t) * k * N(d2)

    # 売り買いで値反転
    C = C if ポジション else C * -1

    # コールプット区分。ポジションと同じ扱い。
    C = C if コールプット区分 else C * -1

    C *= 想定元本

    return C.evalf()

Sympy

Sympyが特に活躍したのはここ

N = lambda d: Normal(0, 1).cdf(d)

平均が0で分散値が1になる正規分布を作って、正規累積分布関数として使う。
もし業務で金融系の関数書きまくるなら、この正規累積分布関数はやたらめったらいろんなとこで使われると思うから
↑のとこだけ関数の外に出しちゃってもいい。

実行速度だけど、Normal(0, 1)の時点では初期化が走るだけだから
関数の中で使おうが外出しにしようが大差無い。
↓はNormalの初期化のとこのソース

    def __init__(self, mu, sigma):
        self.mu = sympify(mu)
        self.sigma = sympify(sigma)

一応計測してみる

#----------- in -----------#
import timeit


t = timeit.Timer("N = lambda d: Normal(0, 1).cdf(d);N(3.1987)", "from sympy.statistics import Normal")
print(min(t.repeat(3, 10000)))

t2 = timeit.Timer("N(3.1987)", "from sympy.statistics import Normal;N = lambda d: Normal(0, 1).cdf(d)")
print(min(t2.repeat(3, 10000)))


#----------- out -----------#
1.1081136350985616
1.1075455570826307

やっぱり変わらない。
定数ってゆうのが気になるよね。
ランダムにした。

#----------- in -----------#

import timeit


t = timeit.Timer("N = lambda d: Normal(0, 1).cdf(d);N(random.random())", "from sympy.statistics import Normal;import random")
print(min(t.repeat(3, 10000)))

t2 = timeit.Timer("N(random.random())", "from sympy.statistics import Normal;N = lambda d: Normal(0, 1).cdf(d);import random")
print(min(t2.repeat(3, 10000)))


#----------- out -----------#
17.53592729999218
17.445876146899536

うん、ほとんどrandom.random()の実行時間だね。


雑記

いやはや、言葉の意味が難しいね。
ストライクとか想定元本とか知らんし。
計算自体は正規累積分布関数とかsympyに入ってるし難しくはなかったけど。

Pythonでコーディングする時はPycharm使ってるんだけど、警告を示す下線出まくりね。
f:id:kaerouka:20140327095735p:plain
薄いけど、日本語の変数名の下に波線出てる。
ただでさえ言葉の意味がわからんのに、英語使ったり、アルファベット1文字とかにしたら更に分けわなんなくなるから
引数の変数名に日本語使ってみたけど、普通に書いてるつもりが
multi spaces after keyword
とか言われたりするし。

Argument name should be lowercase って警告も出まくり。
日本語にlowerもくそもねーよ。
もし金融関連を扱うなら、IDEの設定がっつり見直すとか、変数周りの規約的なの考えないとダメかも。