わたねこコーリング

野良プログラマ発、日々のアウトプット

Pandas 演習としてのテクニカル指標計算 〜 コポック買い指標の巻

10連休短けぇ… もっと色々やりたかったのに… と萎える連休最終日、ブログでも書いて締めますかね。今回は「コポック買い指標」というちょっと聞き慣れないやつを取り上げてみます。これは、月の平均株価を前年同月と比較して騰落率を算出しておき、さらにその移動平均をとって売買サインとする、というものです。月単位なので当然、中長期な売買向けの指標となりますね。

今回の株価データは10年以上程度の長期データが必要なので、いつもの日経平均プロフィルさんではなく、FRED(セントルイス連邦準備銀行)から2000年以降の日経平均を取り寄せることにします。有名所のデータソースなら Pandas DataReader が使えるので、こいつでサクッと。

df = pdt.DataReader('NIKKEI225', 'fred', '2000/1/1',  '2019/04/30')
df.dropna(how='any', inplace=True) # 欠損値を含む行を削除

取得したデータは日足(といっても終値だけですが)なので、月単位の平均に加工します。

df['year'], df['month'] = df.index.year, df.index.month # 年と月の列を追加
df_monthly = df.groupby(['year', 'month']).mean() # 年月単位で終値平均を算出
df_monthly.sort_values(by=['year', 'month'], inplace=True) # この後の集計作業に備えて年・月でソートしておく

下ごしらえができたので、コポック買い指標を計算。対前年騰落率→加重移動平均算出、という流れです。

WMA_MONTHS = 10 # 対前年騰落率の移動平均月数
WMA_WEIGHTS = np.arange(WMA_MONTHS) + 1 # 加重平均用の重み

df_monthly['prev_year_rate'] = df_monthly.rolling(13).apply(lambda x: x[-1] / x[0] - 1, raw=True) # 対前年騰落率を計算
df_monthly['prev_year_rate_wma'] = df_monthly.prev_year_rate.rolling(WMA_MONTHS).apply(lambda x: np.average(x, weights=WMA_WEIGHTS), raw=True) # 加重移動平均を計算

この指標の使い方はシンプルで、上昇に転じたら買いサイン、下降に転じたら売りサインとされるようです。という訳でこのルールで売買サインも検出しておきます。

def detect_sign(vals):
  if vals[0] > vals[1] and vals[1] < vals[2]:
    # 上昇に転じたら買いサイン
    return 1
  elif vals[0] < vals[1] and vals[1] > vals[2]:
    # 下降に転じたら売りサイン
    return -1
  return 0

df_monthly['sign'] = df_monthly.prev_year_rate_wma.rolling(3).apply(detect_sign, raw=True)

ひととおり計算が終わったので可視化を。月株価とコポック指標を上下にグラフ作画し、それぞれに売買サインに挟まれた保持期間を青く塗り潰してみます。

f:id:mariyudu:20190506172723p:plain
月次株価とコポック指標

今回も、コード一式と実行結果は Google Colaboratory で公開してまつ↓。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 RCI の巻

東証も前代未聞の長いお休みに入った10連休3日目、いかがお過ごしでしょうか。そんな今回は RCI を。順位相関指数と言うやつで、N 日間の株価について昇順に順位を採り、時系列に沿って 1, 2, … N と順当な上昇であれば 100% に、逆に N, N-1, … 1 と下降なら -100% に近づくというものです。直観的で分かりやすいっすよねー。

例によって、日経平均プロフィルさんから株価データ取得のコードは、シリーズ第1回目を参照ください。

株価(終値)の順位計算には、Pandas の Series/DataFrame に rank() というメソッドがあるので、これを活用します。

# 計算期間(日数)
RCI_DAYS = 9

# RCI 計算関数
def calc_rci(prices):
    day_cnt = len(prices)
    # 日付昇順ランク
    rank_day = np.arange(day_cnt) + 1
    # 株価昇順ランク
    rank_price = np.array(pd.Series(prices).rank())
    rci = 1 - (6 * ((rank_day - rank_price)**2).sum()) / (day_cnt * (day_cnt**2 - 1))
    return rci * 100 # パーセント値で返却

# RCI を計算して列追加
df['rci'] = df.close.rolling(RCI_DAYS).apply(calc_rci, raw=True)

直近250日について、株価と RCI のグラフを並べてみます。

f:id:mariyudu:20190429154118p:plain
直近250日の株価と RCI

ついでに、RCI が最小・ゼロ付近・最大の日付について、時系列と株価の順位相関も散布図で見える化して、「うんうん」と納得してみましょー。

f:id:mariyudu:20190429154206p:plain
RCI 散布図

うんうんw。今回も、コード一式と実行結果は Google Colaboratory でどうぞー↓。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 ボラティリティの巻

今回はボラティリティを。価格変動の激しさを捉える指標ですね。「変動」といっても見方は様々で、前回のボリンジャーバンドで算出した株価の標準偏差も「個々の株価と一定期間平均とのバラツキ度合い」なので変動のいち表現と言えます。いっぽうで「前日との価格差」に着目した指標もあり、ヒストリカル・ボラティリティと呼ばれています。例えば一定期間内で株価が前日の1%増しで上昇していった場合、標準偏差ではそれなりの値を示しますが、ヒストリカル・ボラティリティでは変動率に変化が無かったとしてゼロが算出されます。

例によって、日経平均プロフィルさんから株価データ取得のコードは、シリーズ第1回目を参照ください。

ヒストリカル・ボラティリティは、株価変動率(前日価格との比率を自然対数化したもの)の標準偏差で求められるので、下記のようになります。

# 計算期間(日数)
BB_DAYS = 25

# 株価変動率を計算
df['variability'] = df.close.rolling(2).apply(lambda price: np.log(price[1]/price[0]))

# ヒストリカルボラティリティを計算
df['hv'] = df.variability.rolling(BB_DAYS).std()

# 比較用の標準偏差ボラティリティも計算
df['std'] = df.close.rolling(BB_DAYS).std() / df.close.rolling(BB_DAYS).mean()

という訳で、株価・ヒストリカルボラティリティ標準偏差ボラティリティのグラフはこんな感じ。

f:id:mariyudu:20190421130349p:plain
株価・ヒストリカルボラティリティ標準偏差ボラティリティ

コード一式と実行結果は Google Colaboratory でどうぞー↓。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 ボリンジャーバンドの巻

シリーズ3回めはボリンジャーバンドを。個人的には最も使用頻度が高い指標かも。

日経平均プロフィルさんから株価データ取得のコードはここでは割愛。シリーズ第1回目を参照ください。

まずバンドの中心ラインとなる単純移動平均を算出し、同日数での株価標準偏差を算出してプラスとマイナスそれぞれの3本バンドを算出、という流れです。

# 計算期間(日数)
BB_DAYS = 25

# 株価移動平均を計算
df['ma'] = df.close.rolling(BB_DAYS).mean()

# 株価標準偏差を計算
df['sigma'] = df.close.rolling(BB_DAYS).std()

# ボリンジャーバンドを計算
for n in range(1, 4):
  df[f"bb_h{n}"] = df.ma + df.sigma * n
  df[f"bb_l{n}"] = df.ma - df.sigma * n

株価(終値)とボリンジャーバンドを重ね書きした可視化(↓)。

f:id:mariyudu:20190414141548p:plain
ボリンジャーバンドの図(全期間)

全期間だとごちゃごちゃして分かりづらいので、2018年ぶんだけ作画してみます(↓)。

f:id:mariyudu:20190414141644p:plain
ボリンジャーバンドの図(2018年)

今回も、コード一式と実行結果は Google Colaboratory で公開しておきます(↓)。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 MACD の巻

珍しくやる気になってるので、鉄は熱いうちにという訳で今回は MACD の計算でつ。長短2本の指数平滑移動平均の差分をとり、さらにその移動平均をとったものとの差分が売買シグナルになるというオシレータ系の指標です。個人的にはどうも意味合いというか使い所がイメージできないのですが、一般的には広く利用されているポピュラーな手法ですね。

株価データは前回同様に日経平均プロフィルさんの日経平均日足データ CSV を使うのでコードは割愛。

前回書いた指数平滑移動平均計算関数を流用しつつ、MACD の計算はこんな感じ。

# 指数平滑移動平均計算
def calc_ema(prices, period):
    ema = np.zeros(len(prices))
    ema[:] = np.nan # NaN で初期化
    ema[period-1] = prices[:period].mean() # 最初だけ単純移動平均
    for d in range(period, len(prices)):
        ema[d] = ema[d-1] + (prices[d] - ema[d-1]) / (period + 1) * 2
    return ema

# MACD 計算
def calc_macd(prices, period_short, period_long, period_signal):
    ema_short = calc_ema(prices, period_short)
    ema_long = calc_ema(prices, period_long)
    macd = ema_short - ema_long # MACD = 短期移動平均 - 長期移動平均
    signal = pd.Series(macd).rolling(period_signal).mean() # シグナル = MACD の移動平均
    hist = macd - signal # ヒストグラム
    return macd, signal, hist

df['macd'], df['signal'], df['hist'] = calc_macd(df.close, 12, 26, 9)

MACD とシグナルの差分をとってヒストグラムとします。一般的にはこいつがプラスに転じれば買いサイン、マイナスに転じれば売りサイン、という訳ですな。株価、MACD とシグナル、ヒストグラムの順に可視化したグラフがこんな感じです。

f:id:mariyudu:20190406181835p:plain
株価・MACD とシグナル・ヒストグラム

今回も、コード一式と実行結果は Google Colaboratory で公開しておきます(↓)。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 移動平均の巻

Jupyter Notebook 上で主にシストレをテーマにしたデータいぢりを始めて1年あまり経ちますが、いまいち NumPy・Pandas・Matplotlib がマスター出来た気になれないので、公開しながら演習することにしました。プライベートのスニペットバンクにちまちま書き溜めるよりは気合いが入るのじゃないかと…

Python には TA-Lib というテクニカル分析ライブラリがありますが、これを使わずに NumPy や Pandas の機能をフル活用して自前で計算することでデータ処理のスキルを磨こうという狙いです。てな訳で、手始めに移動平均の類から。

尚、計算の対象は日経平均にします。日経平均プロフィルさんが2016年からの日足データ CSV を提供されているので、これを有り難く使わせて頂きます。

CSV_URL = "https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv"
df = pd.read_csv(CSV_URL, encoding="SHIFT-JIS", names=['date', 'close', 'open', 'high', 'low'], header=1);
df = df.dropna() # 最後のコメント行を削除

データフレームの列名は ASCII のほうが何かと便利なので、names= で列名を指定しておきます。では、この日足データフレームに、以下のように25日の各種移動平均を算出しながら列追加していきます。

まずは単純移動平均から。これは Pandas の窓関数平均で一発。

df['sma25'] = df.close.rolling(25).mean()

次に加重移動平均。これは関数を作って窓関数に apply してやります。

def calc_wma(prices):
    weights = np.arange(len(prices)) + 1
    wma = np.sum(weights * prices) / weights.sum()
    return wma

df['wma25'] = df.close.rolling(25).apply(calc_wma)

指数平滑移動平均は、当日株価と前回平均を計算するので窓関数での逐次計算では処理できず、全終値からまるっと計算する手法にしました。ループは極力使いたくなかったのですが、無理に行列指向な処理にするとコードの可読性が落ちそうだったので仕方なく…

def calc_ema(prices, period):
    ema = np.zeros(len(prices))
    ema[:] = np.nan # NaN で初期化
    ema[period-1] = prices[:period].mean() # 最初だけ単純移動平均
    for d in range(period, len(prices)):
        ema[d] = ema[d-1] + (prices[d] - ema[d-1]) / (period + 1) * 2
    return ema

df['ema25'] = calc_ema(df.close, 25)

ついでに株価と移動平均の乖離率[%]も計算してみます。

def calc_devrate(prices):
    sma = prices.mean()
    return (prices[-1] - sma) / sma * 100

df['devrate'] = df.close.rolling(25).apply(calc_devrate)

以上、各種移動平均を計算・列追加されたデータフレームがこちらになりまつ。

f:id:mariyudu:20190330173612p:plain
移動平均を加えたデータフレーム

最後にお約束の可視化グラフを。計算した各種移動平均を株価に重ねて作画してみます。期間が長いのでちと重なりがちですが、それぞれの移動平均線の特色がわかる…ような気がしますw 乖離率もその下に並べて置いときます。

f:id:mariyudu:20190330173408p:plain
移動平均をグラフで可視化

上記のコードは Google Colaboratory で公開してあります(↓)。

https://colab.research.google.com/drive/1Od95PnZsuXXMHNA3HeKwRTsSui9XGqSacolab.research.google.com

「INSERT … SELECT … WHERE NOT EXIST …」を少し深掘りしてみた

MySQL で INSERT する際に、所定条件のレコードが既に存在しない時のみ行いたい、ってのはよくある要求のようです。調べたら、こんなハック↓で何とかなる模様。

stackoverflow.com

成る程、INSERT … SELECT 構文の SELECT を工夫する訳か。ただ自分の場合、上記記事の例に倣うと

INSERT INTO table_listnames (name, address, tele)
SELECT * FROM (SELECT 'Rupert', '', '') AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;


みたいに、複数列に同じ値をセットしたかったのですが、それだと「ERROR 1060 (42S21): Duplicate column name ''」と怒られちゃいます。これには

INSERT INTO table_listnames (name, address, tele)
SELECT * FROM (SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele`) AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

と、きっちり列名を付与してやれば良いみたいです。さて問題は解決したのですが、この SQL、SELECT 文が二重になってて何だか分かったような分かんないような。フツーに

SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele`
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

じゃダメなの? と試してみるとエラー。FROM 句が無いと WHERE 句は使えないのか… それじゃ

SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele` FROM tmp
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

では? 「tmp なんてテーブルねーぞ」と怒られるなぁ(当たり前)。ここで「MySQL テーブル ダミー」等とぐぐってみたら、MySQL には DUAL というダミーテーブルがあるようです↓。

rnk.mitelog.jp

という訳で

INSERT INTO table_listnames (name, address, tele)
SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele` FROM DUAL
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

で上手く行きました! 少しシンプルになったぞいww