いろいろな株の売買の方法が提案されています。

例えば、以下はある本で紹介されていたものを単純化したものです。

買いのタイミング
株価の75日移動平均線が上を向いていて、株価が75日移動平均線を下から上に抜けるとき。

売りのタイミング
株価が75日移動平均線を上から下に抜けるとき。

 
このブログは、上の方法を東証COREの30銘柄のデータすべてに試してみたときの覚書になります。

コードは、Pythonで、Jupyter notebook で順番に実行していくという想定です。

移動平均を求める関数を作る

75日移動平均線とは、今から過去75日までの株価の平均を、「今」の時間でプロットして作られる線です。

以下は、日数を引数として移動平均線を求める関数を作り、ChatGPTにリファクタリングしてもらってできたコードです。作る過程はこちらのブログに詳細を書きました。

def moving_average(price_series, n):
    """
    移動平均を求める関数。

    Args:
        price_series (List[float]): 価格時系列データ。
        n (int): 平均化する区間の幅。

    Returns:
        List[float]: 移動平均。

    Examples:
        >>> dat = [0, 1, 2, 3, 4, 5]
        >>> ma = moving_average(dat, n=3)
        >>> print(ma)
        [None, None, 1.0, 2.0, 3.0, 4.0]
    """
    moving_avg = [None] * (n - 1)
    for i in range(n - 1, len(price_series)):
        avg = sum(price_series[i - n + 1:i + 1]) / n
        moving_avg.append(avg)
    return moving_avg

株価の人工データを生成する関数を作る

はじめから実データを使うより、小さい人工データを作ってプログラムを確かめながら進めた方が効率的ですので、人工データ生成関数を作りました。

import matplotlib.pyplot as plt
import numpy as np

def generate_price_series(initial_price: float, num_days: int, std_days: float, seed: int = 0) -> list:
    """
    指定された初期価格、日数、1日あたりの変動の標準偏差に基づいて、株価データを生成する関数。

    Parameters
    ----------
    initial_price: float
        株価の初期値。
    num_days: int
        株価データを生成する日数。
    std_days: float
        株価の1日あたりの変動の標準偏差。
    seed: int, optional (default=0)
        乱数生成器のシード値。

    Returns
    -------
    price_series: list
        生成された株価データのリスト。

    """
    # 乱数生成器のシード値を設定
    np.random.seed(seed)
    
    # 日毎の価格変動を表す乱数を生成
    price_series = [initial_price]
    for i in range(1, num_days):
        # 標準正規分布に従う乱数を生成し、1日あたりの変動の標準偏差を掛けて株価を決定
        price_today = price_series[-1] * (1 + np.random.randn() * std_days)
        price_series.append(price_today)

    return price_series

# テスト -----
# データの生成
initial_price = 1000  # 初期価格
num_days = 250  # 日数
std_days = 0.1  # 1日の変動の標準偏差
seed = 0  # 乱数生成器のシード値
price_series = generate_price_series(initial_price, num_days, std_days, seed)

# グラフの描画
plt.plot(price_series)
plt.title("株価の変動", fontname="MS Gothic")
plt.xlabel("日数", fontname="MS Gothic")
plt.ylabel("価格", fontname="MS Gothic")
plt.show()

ランダムウォークを仮定して作っています。いい感じですね。

株価売買アルゴリズムを作る

では、株価売買アルゴリズムを実装していきます。

小さい人工データを作る

まず、人工データを準備します。

# 株価の人工データを作る -----
initial_price = 1000  # 初期価格
num_days = 20  # 日数
std_days = 0.1  # 1日の変動の標準偏差
seed = 5  # 乱数生成器のシード値
v = generate_price_series(initial_price, num_days, std_days, seed)
v = np.array(v)
ts = np.array(range(len(v)))

# 確認
plt.plot(ts, v, '.-');
plt.show()

2日間の移動平均を求めて、先ほどのグラフに重ねる

実際には75日間の移動平均を使いますが、プログラムが正しいことを確かめやすいように、まず2日間の移動平均でアルゴリズムを作ります。

# n日移動平均を作る ----
num_average = 2
ml = moving_average(v, n=num_average)

# 確認
plt.plot(ts, v, '.-', label='data')
plt.plot(ts, ml, '.-', label=f'average {num_average}')
plt.legend();

今の株価と一つ前の日の株価の平均が、正しくオレンジのグラフで表されていることがわかります。

メインの売買シミュレーションを作る

では、アルゴリズムの実装です。ルールを再掲します。

買いのタイミング
株価の75日移動平均線が上を向いていて、株価が75日移動平均線を下から上に抜けるとき。
売りのタイミング
株価が75日移動平均線を上から下に抜けるとき。

# 売買シミュレーション -----
# 状態を表す変数
stock = 0  # 0:株を保持していない、1)保持している
# 記録用変数
wallets = [0]  # 資産
t_wallets = [0]  # 資産が変化したときの日(表示用)
t_sells = []   # 株を買った日(表示用)
t_buys = []  # 株を売った日(表示用)

# シミュレーション
for t in range(1, len(ml)):
    if ml[t-1] is None:
        continue
    if stock == 0:
        # 株を持っていない時
        if ml[t-1] < ml[t] and v[t-1] < ml[t-1] and ml[t] < v[t]:
            # mlが上昇中でかつ、 vがmlを下から上に抜けた時に買い
            t_buys.append(t)
            debt = v[t]
            stock = 1
    else:
        # 株を持っている時
        if v[t] < ml[t]:
            # vがmlを下回ったら 売り
            t_sells.append(t)
            wallets.append(wallets[-1]+v[t]-debt)
            t_wallets.append(t)
            debt = 0
            stock = 0

# 出力のまとめ
out = {
    "wallets": wallets,
    "t_wallets": t_wallets,
    "t_sells": t_sells,
    "t_buys": t_buys,
}

out

以下のように出力されます。

{'wallets': [0, -31.63707397727876, -208.8477441428047, -202.82313182644646],
 't_wallets': [0, 4, 15, 19],
 't_sells': [4, 15, 19],
 't_buys': [3, 14, 17]}

結果をグラフ表示します。

# グラフ表示
plt.figure(figsize=(10, 6))
plt.subplots_adjust(hspace=0.6)
# 株価の変化
ax = plt.subplot(2,1,1)
plt.plot(ts, v, marker=".")
plt.plot(ts, ml, marker=".")
for t0, t1 in zip(t_buys, t_sells):
    plt.plot([ts[t0], ts[t1]], [v[t0], v[t1]], '-y', linewidth=5)
plt.plot(ts[t_buys], v[t_buys], 'or')
plt.plot(ts[t_sells], v[t_sells], 'sb')
plt.grid()
xrange = ax.get_xlim()

# 資産の変化
plt.subplot(2,1,2)
plt.step(ts[t_wallets], wallets, '.-', where="post")
plt.xlim(xrange)
plt.grid()
plt.title("wallets")
plt.show()

株価(青)が平均線(オレンジ)を越えた時に、株を買い(赤丸)、株価が平均線よりも下がったら株を売ります(青)。
株を売った時に、買った時との差額でお財布(Wallets)の中身が変わります。

赤と青を結んだ黄色の線が上向きだと儲けが出て、下向きだと損をしていることになります。この例ですと、3回の取引をしていて、初めの2回で損をして、3回目に少し得をするという結果になっています。

関数化する

ここまでの処理を関数にして、複数のデータを一度に処理する準備をします。

def sim_trade(v, num_average, f_show=True):
    v = np.array(v)
    ts = np.array(range(len(v)))

    # 75日移動平均を作る ----
    ml = moving_average(v, n=num_average)

    # 売買シミュレーション -----
    # 状態を表す変数
    stock = 0  # 0:株を保持していない、1)保持している
    # 記録用変数
    wallets = [0]  # 資産
    t_wallets = [0]  # 資産が変化したときの日(表示用)
    t_sells = []   # 株を買った日(表示用)
    t_buys = []  # 株を売った日(表示用)

    # シミュレーション
    for t in range(1, len(ml)):
        if ml[t-1] is None:
            continue
        if stock == 0:
            # 株を持っていない時
            if ml[t-1] < ml[t] and v[t-1] < ml[t-1] and ml[t] < v[t]:
                # mlが上昇中でかつ、 vがmlを下から上に抜けた時に買い
                t_buys.append(t)
                debt = v[t]
                stock = 1
        else:
            # 株を持っている時
            if v[t] < ml[t]:
                # vがmlを下回ったら 売り
                t_sells.append(t)
                wallets.append(wallets[-1]+v[t]-debt)
                t_wallets.append(t)
                debt = 0
                stock = 0

    # 出力のまとめ
    out = {
        "wallets": wallets,
        "t_wallets": t_wallets,
        "t_sells": t_sells,
        "t_buys": t_buys,
    }

    # グラフ表示
    if f_show:
        # グラフ表示
        plt.subplots_adjust(hspace=0.6)
        # 株価の変化
        ax = plt.subplot(2,1,1)
        plt.plot(ts, v, marker=".")
        plt.plot(ts, ml, marker=".")
        for t0, t1 in zip(t_buys, t_sells):
            plt.plot([ts[t0], ts[t1]], [v[t0], v[t1]], '-y', linewidth=5)
        plt.plot(ts[t_buys], v[t_buys], 'or')
        plt.plot(ts[t_sells], v[t_sells], 'sb')
        plt.grid()
        plt.title("株価の変動", fontname="MS Gothic")
        xrange = ax.get_xlim()

        # 資産の変化
        plt.subplot(2,1,2)
        plt.step(ts[t_wallets], wallets, '.-', where="post")
        plt.xlim(xrange)
        plt.grid()
        plt.title("お財布", fontname="MS Gothic")        

    return out

# 株価の人工データを作る -----
initial_price = 1000  # 初期価格
num_days = 20  # 日数
std_days = 0.1  # 1日の変動の標準偏差
seed = 5  # 乱数生成器のシード値
v = generate_price_series(initial_price, num_days, std_days, seed)
# テスト ----
num_average = 2
out  =sim_trade(v, num_average, f_show=True)

実際の株価のデータでテストする

前のブログで作った東証Coreの30銘柄データをまとめたall_close.xls を使います。

株価データの読み込み

銘柄の一つ"c5"(リクルートホールディングス)に対して、売買のアルゴリズムを試してみます。

c5 (リクルートホールディングス)のデータに、実際の手順で指示されている75日移動平均を使いました。売買のチャンスはめったにこないですね。この場合ですと、200日の間に、買って売れたのは1回だけ、そこで損をしています。

30銘柄で試す

では、30銘柄のデータすべてに同じアルゴリズムで売買してみて、どれくらい稼げるのかを見てみます。

上のグラフの各プロットが、各30銘柄の結果になります。横軸が稼いだ金額、縦軸が売買をした回数です。

儲けが出ている銘柄もありますが、多くは損をしています。平均が-39.22、中央値が-20.15でした。

うーん、やはり簡単にはいきませんね。

しかし、株は、売買のアルゴリズムを過去の実データで簡単に検証できるののがとても良いですね。