Scriptone Scriptone

[Pythonによる株式分析]モダンなライブラリで移動平均線とゴールデンクロスを検出する

blog-image

概要

PolarsやPlotly、marimo、kandなどPythonのモダンなライブラリを使って、移動平均線とゴールデンクロスを検出します。

モチベーション

私は給料の一部をRSUの形式で受け取っています。RSUはその銘柄が好調な時は収入源として魅力的ですが卵を1つのカゴに盛って運搬しているのと同じで、その会社の調子が悪くなると損失が大きくなります。なので、卵を複数のカゴに分散することが大事です。現金だけ持つ、1つの銘柄だけでRSUを持つのも望ましくなくリスクに備えて適切に分散をすることが大事です。インデックスファンド(の投資信託)や純金商品などに手早く分散させるのが楽ではありますが、積極的にリスクをとりに行ったり配当金や株主優待を楽しむ上では個別銘柄も楽しいと思います(そもそもRSUも個別銘柄のようなものです)。なので、自身が所有するRSUに対する理解も深めることも念頭に置きつつ、個別銘柄の分析をしていきます。

PythonにはPandasやTa-Lib-Py、Matplotlib、yfinance、Jupyterなど成熟した豊富なライブラリがありそれぞれの連携のしやすさでも非常に便利です。ですが、2020年代に入ってモダンなライブラリが数々登場して、痒いところに手が届いたり速度のメリットがあったりなど従来のライブラリとは違う良さもあります。古いライブラリは成熟しており情報も豊富ですので、新しいライブラリを同時に学びながら分析を楽しんでいきたいと思っています。なお、システムトレードにも利用できる手法ですが、私は人間が分かりやすく管理と把握でき、ファンダメンタルズの問題がないことも確認したいので売買の候補とタイミングを知るためにテクニカル分析を試みます。

使用するライブラリ

ライブラリは以下の通りです。

ライブラリ関連ライブラリ説明
polarspandasデータフレームを高速に扱えるライブラリ
plotlymatplotlib可視化ライブラリでユーザーがインタラクティブにグラフを操作できる
marimojupyterpy形式で動くノートブックでインタラクティブにコードを動かすことができる
kandta-lib-pythonRust製のテクニカル分析の計算ライブラリで、PYO3やWasmを通じてPythonやWebでも活用できる
yfinance-plyfinanceyfinance-rsをラップして作ったPython向けのライブラリでPolarsでYahoo Financeの情報を扱える

ノートとコード

marimoのノートのHTML版はこちらにあります。GitHub上のPython形式のコードにもリンクします

解説

株価情報の取得

株価情報はyfinance-plを用いて取得します。 証券コードに.Tを付与することでyfinance-plからpolarsのDataFrame形式で取得できます。トヨタなら7203.T、ソフトバンクなら9984.Tなどです。私は配当や株主優待を狙って地方銀行ながら経営が安定していると思われる山陰合同銀行(ごうぎん)8381.Tを所持しているので8381.Tを例にします。

import yfinance_pl as yf

ticker = yf.Ticker("8381.T")
hist = ticker.history(period="1y")

periodは1y1mなど期間を入力します。yfinance_plではLiteralを指定しているのでVisual Studio CodeやPyCharmなどを利用していればコード補完で入力できます。histをTSV形式で表示すると下記の通りです(最新の5件です)。

open.amount	open.currency	high.amount	high.currency	low.amount	low.currency	close.amount	close.currency	close_unadj.amount	close_unadj.currency	volume	date
Decimal('1374.0000000000')	JPY	Decimal('1404.0000000000')	JPY	Decimal('1372.0000000000')	JPY	Decimal('1400.0000000000')	JPY	Decimal('1400.0000000000')	JPY	400000	2025-11-21 00:00:00
Decimal('1405.0000000000')	JPY	Decimal('1413.0000000000')	JPY	Decimal('1384.0000000000')	JPY	Decimal('1393.0000000000')	JPY	Decimal('1393.0000000000')	JPY	317800	2025-11-25 00:00:00
Decimal('1409.0000000000')	JPY	Decimal('1412.0000000000')	JPY	Decimal('1399.0000000000')	JPY	Decimal('1407.0000000000')	JPY	Decimal('1407.0000000000')	JPY	387300	2025-11-26 00:00:00
Decimal('1420.0000000000')	JPY	Decimal('1454.0000000000')	JPY	Decimal('1420.0000000000')	JPY	Decimal('1442.0000000000')	JPY	Decimal('1442.0000000000')	JPY	547300	2025-11-27 00:00:00
Decimal('1450.0000000000')	JPY	Decimal('1469.0000000000')	JPY	Decimal('1449.0000000000')	JPY	Decimal('1453.0000000000')	JPY	Decimal('1453.0000000000')	JPY	428800	2025-11-28 00:00:00

移動平均線の計算

移動平均線は当日を含むN日分のデータを日付ごとに移動させながら計算します。短期間の平均であれば5日移動平均線、1ヶ月のトレンドであれば25日移動平均線で計算します。日々、株価が動きますが平均にすることによって日々のばらつきを抑えつつトレンドを把握できます。この計算にはTa-Lib-PythonではなくRust製のkandを使いますが、差異はほとんどなく簡単に計算できます。

import kand as ka
import polars as pl
import yfinance_pl as yf

ticker = yf.Ticker("8381.T")
hist = ticker.history(period="1y")
close = hist["close.amount"].to_numpy().astype("float64")
hist_with_ma = hist.with_columns(
    ma5=pl.Series(ka.sma(close, period=5)),
    ma25=pl.Series(ka.sma(close, period=25)),
)

smaがSimple Moving Average(単純移動平均線)の略であり、periodの数値を変えることで90日、200日などより長期の投資のためのトレンドを算出できます。

hist_with_maの先頭30行をTSV形式で表示すると以下の通りです。

open.amount	open.currency	high.amount	high.currency	low.amount	low.currency	close.amount	close.currency	close_unadj.amount	close_unadj.currency	volume	date	ma5	ma25
Decimal('1147.0000000000')	JPY	Decimal('1155.0000000000')	JPY	Decimal('1142.0000000000')	JPY	Decimal('1151.0000000000')	JPY	Decimal('1195.0000000000')	JPY	212000	2024-11-28 00:00:00	NaN	NaN
Decimal('1151.0000000000')	JPY	Decimal('1169.0000000000')	JPY	Decimal('1145.0000000000')	JPY	Decimal('1163.0000000000')	JPY	Decimal('1207.0000000000')	JPY	222500	2024-11-29 00:00:00	NaN	NaN
Decimal('1173.0000000000')	JPY	Decimal('1213.0000000000')	JPY	Decimal('1169.0000000000')	JPY	Decimal('1211.0000000000')	JPY	Decimal('1257.0000000000')	JPY	493800	2024-12-02 00:00:00	NaN	NaN
Decimal('1214.0000000000')	JPY	Decimal('1235.0000000000')	JPY	Decimal('1208.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1272.0000000000')	JPY	440100	2024-12-03 00:00:00	NaN	NaN
Decimal('1220.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1235.0000000000')	JPY	279400	2024-12-04 00:00:00	1188.2	NaN
Decimal('1200.0000000000')	JPY	Decimal('1213.0000000000')	JPY	Decimal('1198.0000000000')	JPY	Decimal('1209.0000000000')	JPY	Decimal('1255.0000000000')	JPY	241000	2024-12-05 00:00:00	1199.8	NaN
Decimal('1210.0000000000')	JPY	Decimal('1216.0000000000')	JPY	Decimal('1198.0000000000')	JPY	Decimal('1202.0000000000')	JPY	Decimal('1248.0000000000')	JPY	130400	2024-12-06 00:00:00	1207.6	NaN
Decimal('1207.0000000000')	JPY	Decimal('1225.0000000000')	JPY	Decimal('1196.0000000000')	JPY	Decimal('1216.0000000000')	JPY	Decimal('1262.0000000000')	JPY	309500	2024-12-09 00:00:00	1208.6	NaN
Decimal('1232.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1209.0000000000')	JPY	Decimal('1215.0000000000')	JPY	Decimal('1261.0000000000')	JPY	300100	2024-12-10 00:00:00	1206.4	NaN
Decimal('1216.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1204.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1272.0000000000')	JPY	256900	2024-12-11 00:00:00	1213.6	NaN
Decimal('1238.0000000000')	JPY	Decimal('1242.0000000000')	JPY	Decimal('1230.0000000000')	JPY	Decimal('1233.0000000000')	JPY	Decimal('1280.0000000000')	JPY	307600	2024-12-12 00:00:00	1218.4	NaN
Decimal('1223.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1210.0000000000')	JPY	Decimal('1216.0000000000')	JPY	Decimal('1262.0000000000')	JPY	348500	2024-12-13 00:00:00	1221.2	NaN
Decimal('1218.0000000000')	JPY	Decimal('1221.0000000000')	JPY	Decimal('1194.0000000000')	JPY	Decimal('1201.0000000000')	JPY	Decimal('1247.0000000000')	JPY	252900	2024-12-16 00:00:00	1218.2	NaN
Decimal('1205.0000000000')	JPY	Decimal('1215.0000000000')	JPY	Decimal('1193.0000000000')	JPY	Decimal('1195.0000000000')	JPY	Decimal('1240.0000000000')	JPY	215500	2024-12-17 00:00:00	1214.2	NaN
Decimal('1193.0000000000')	JPY	Decimal('1195.0000000000')	JPY	Decimal('1182.0000000000')	JPY	Decimal('1193.0000000000')	JPY	Decimal('1238.0000000000')	JPY	189600	2024-12-18 00:00:00	1207.6	NaN
Decimal('1175.0000000000')	JPY	Decimal('1195.0000000000')	JPY	Decimal('1169.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1235.0000000000')	JPY	363000	2024-12-19 00:00:00	1199	NaN
Decimal('1197.0000000000')	JPY	Decimal('1197.0000000000')	JPY	Decimal('1168.0000000000')	JPY	Decimal('1168.0000000000')	JPY	Decimal('1212.0000000000')	JPY	511500	2024-12-20 00:00:00	1189.4	NaN
Decimal('1171.0000000000')	JPY	Decimal('1204.0000000000')	JPY	Decimal('1171.0000000000')	JPY	Decimal('1204.0000000000')	JPY	Decimal('1250.0000000000')	JPY	403500	2024-12-23 00:00:00	1190	NaN
Decimal('1216.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1209.0000000000')	JPY	Decimal('1222.0000000000')	JPY	Decimal('1268.0000000000')	JPY	320300	2024-12-24 00:00:00	1195.4	NaN
Decimal('1217.0000000000')	JPY	Decimal('1217.0000000000')	JPY	Decimal('1194.0000000000')	JPY	Decimal('1205.0000000000')	JPY	Decimal('1251.0000000000')	JPY	207300	2024-12-25 00:00:00	1197.8	NaN
Decimal('1204.0000000000')	JPY	Decimal('1208.0000000000')	JPY	Decimal('1193.0000000000')	JPY	Decimal('1208.0000000000')	JPY	Decimal('1254.0000000000')	JPY	230600	2024-12-26 00:00:00	1201.4	NaN
Decimal('1209.0000000000')	JPY	Decimal('1220.0000000000')	JPY	Decimal('1205.0000000000')	JPY	Decimal('1220.0000000000')	JPY	Decimal('1266.0000000000')	JPY	252400	2024-12-27 00:00:00	1211.8	NaN
Decimal('1224.0000000000')	JPY	Decimal('1230.0000000000')	JPY	Decimal('1214.0000000000')	JPY	Decimal('1219.0000000000')	JPY	Decimal('1265.0000000000')	JPY	159400	2024-12-30 00:00:00	1214.8	NaN
Decimal('1228.0000000000')	JPY	Decimal('1228.0000000000')	JPY	Decimal('1211.0000000000')	JPY	Decimal('1225.0000000000')	JPY	Decimal('1271.0000000000')	JPY	443500	2025-01-06 00:00:00	1215.4	NaN
Decimal('1231.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1217.0000000000')	JPY	Decimal('1222.0000000000')	JPY	Decimal('1268.0000000000')	JPY	403500	2025-01-07 00:00:00	1218.8	1205.2
Decimal('1216.0000000000')	JPY	Decimal('1244.0000000000')	JPY	Decimal('1215.0000000000')	JPY	Decimal('1237.0000000000')	JPY	Decimal('1284.0000000000')	JPY	469100	2025-01-08 00:00:00	1224.6	1208.64
Decimal('1232.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1253.0000000000')	JPY	421100	2025-01-09 00:00:00	1222	1210.4
Decimal('1204.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1196.0000000000')	JPY	Decimal('1241.0000000000')	JPY	328400	2025-01-10 00:00:00	1217.4	1209.8
Decimal('1190.0000000000')	JPY	Decimal('1194.0000000000')	JPY	Decimal('1164.0000000000')	JPY	Decimal('1173.0000000000')	JPY	Decimal('1217.0000000000')	JPY	394900	2025-01-14 00:00:00	1207	1207.68
Decimal('1182.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1178.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1253.0000000000')	JPY	362200	2025-01-15 00:00:00	1204	1208.36

5日移動平均線は当該日を含む過去5日分のデータがないと計算できませんので先頭4行はNaNです。25日移動平均線も同様に過去のデータが必要となるため24行がNaNです。

チャートの可視化

PlotlyのCandlestick(ローソク足)を利用すると簡単にチャートに表示できます。ローソク足は四角形(実体)と線(ヒゲ)で表現され、上ヒゲ・下ヒゲ・始値・終値で表示され、始値と終値の上下関係によって赤と緑などで表現され増減やその日の価格の揺れ幅がわかります。この4つの情報と色、横軸の日付データを渡すと簡単に表示できます。

# 略

df_plot = hist_with_ma.with_columns(
    [
        pl.col("open.amount").cast(pl.Float64),
        pl.col("high.amount").cast(pl.Float64),
        pl.col("low.amount").cast(pl.Float64),
        pl.col("close.amount").cast(pl.Float64),
    ]
)
dates = df_plot["date"].to_list()

ma_data = [
    go.Candlestick(
        yaxis="y1",
        x=dates,
        open=df_plot["open.amount"],
        high=df_plot["high.amount"],
        low=df_plot["low.amount"],
        close=df_plot["close.amount"],
        increasing_line_color="red",
        decreasing_line_color="green",
        name=f"{company_name}の株価",
    ),
    # 中略
]

ma_fig = go.Figure(data=ma_data, layout=go.Layout(ma_layout))

# 中略

ma_fig  # ノート上で図表が表示される

ゴールデンクロス・デッドクロス

長期と短期の移動平均線が交わることでトレンドの転換(もしくは売買のシグナル)を示す1つの指標となります。長期の移動平均線を短期の移動平均線が上回った時に「ゴールデンクロス」、長期の移動平均線を短期の移動平均線が下回った時に「デッドクロス」と呼びます。

これらは短期の移動平均線と長期の移動平均線の差を算出し、前日と当日とでその差の値の正負が変化するかで検出できます。

(1) 前日は長期の移動平均より短期の移動平均の価格が安く、当日は短期の移動平均が長期の移動平均を上回ったらゴールデンクロス (2) 前日は長期の移動平均より短期の移動平均の価格が高く、当日は短期の移動平均線が長期の移動平均線を下回ったらデッドクロス

# 前日と当日のSMA5とSMA25の差分を計算
diff = SMA5 - SMA25
prev_diff = diff.shift(1)

# ゴールデンクロス: 前日は SMA5 < SMA25、当日は SMA5 > SMA25
golden_cross = (prev_diff < 0) & (diff > 0)

# デッドクロス: 前日は SMA5 > SMA25、当日は SMA5 < SMA25
dead_cross = (prev_diff > 0) & (diff < 0)

golden_crossがTrueの時にゴールデンクロス、dead_crossがTrueの時にデッドクロスと表示できます。

Plotlyにもゴールデンクロスとデッドクロスを表示できます。

# SMA5とSMA25の差分を計算
df_cross = (
    hist_with_ma.with_columns(
        diff=(pl.col("ma5") - pl.col("ma25")),
    )
    .with_columns(
        prev_diff=pl.col("diff").shift(1),
    )
    .with_columns(
        # ゴールデンクロス: 前日は負(SMA5 < SMA25)、当日は正(SMA5 > SMA25)
        golden_cross=(pl.col("prev_diff") < 0) & (pl.col("diff") > 0),
        # デッドクロス: 前日は正(SMA5 > SMA25)、当日は負(SMA5 < SMA25)
        dead_cross=(pl.col("prev_diff") > 0) & (pl.col("diff") < 0),
    )
)

# ゴールデンクロスの日付を抽出
golden_crosses = df_cross.filter(pl.col("golden_cross")).select(
    pl.col("date"),
    pl.col("close.amount").alias("price"),
    pl.lit("ゴールデンクロス").alias("signal"),
)

# デッドクロスの日付を抽出
dead_crosses = df_cross.filter(pl.col("dead_cross")).select(
    pl.col("date"),
    pl.col("close.amount").alias("price"),
    pl.lit("デッドクロス").alias("signal"),
)

# ゴールデンクロスとデッドクロスを結合してソート
signals = pl.concat([golden_crosses, dead_crosses]).sort("date")

# ゴールデンクロスとデッドクロスのデータを分離
_golden = signals.filter(pl.col("signal") == "ゴールデンクロス")
_dead = signals.filter(pl.col("signal") == "デッドクロス")

signal_data = [
    go.Candlestick(
        yaxis="y1",
        x=_dates,
        open=_df_plot["open.amount"],
        high=_df_plot["high.amount"],
        low=_df_plot["low.amount"],
        close=_df_plot["close.amount"],
        increasing_line_color="red",
        decreasing_line_color="green",
        name=f"{_company_name}の株価",
    ),
    # 中略
    # ゴールデンクロスのマーカー
    go.Scatter(
        yaxis="y1",
        x=_golden["date"].to_list(),
        y=_golden["price"].cast(pl.Float64).to_list(),
        mode="markers",
        name="ゴールデンクロス",
        marker={
            "symbol": "triangle-up",
            "size": 15,
            "color": "lime",
            "line": {"color": "darkgreen", "width": 2},
        },
    ),
    # デッドクロスのマーカー
    go.Scatter(
        yaxis="y1",
        x=_dead["date"].to_list(),
        y=_dead["price"].cast(pl.Float64).to_list(),
        mode="markers",
        name="デッドクロス",
        marker={
            "symbol": "triangle-down",
            "size": 15,
            "color": "red",
            "line": {"color": "darkred", "width": 2},
        },
    ),
]

まとめ

ここまで株価の取得および、ローソク足での表示、移動平均の計算、ゴールデンクロスとデッドクロスの検出までをご紹介しました。Pandasを利用した方が便利な状況が続いていましたが、コードをご覧の通りにPolarsだけでも株価の取得から可視化までPandasを介さずにできます。ゴールデンクロス・デッドクロスだけでは弱いですが基本的で分かりやすい指標をまず作れるようになり、分析の一歩を歩み出すことができたかと思います。今後も、他の指標やより高度な分析をモダンなライブラリを通じて行っていきます。

参考書籍:『Pythonで実践する株式投資分析』(片渕彼富 (著), 山田祥寛 (監修))

comment

コメントを読み込み中...