2025-11-07

【備忘録】VWAP の算出

VWAP(Volume Weighted Average Price, 売買高加重平均価格)とは、当日の東京証券取引所のオークション市場で成立した価格を価格毎の売買高で加重平均した価格をいいます。VWAP は、より取引実態に近い平均的な約定値段として、主に機関投資家の執行価格の目標値として用いられています。

日本取引所グループの用語集 [1] より引用

東証の取引時間中、特定銘柄について 1 秒間隔で株価と出来高(累計)を取得して分析していますが、生成 AI (Microsoft Copilot) に VWAP の算出と活用方法についてアドバイスをもらったので、備忘録にまとめました。

VWAP の計算式

英語版の Wikipedia [2] に VWAP の計算式が示されていたので、それを引用して株価にあてはめました。

\[ P_{VWAP} = \frac{\sum_j {P_j \times Q_j}}{\sum_j Q_j} \]
  • \(P_{VWAP}\)  : VWAP(売買高加重平均価格)
  • \(P_j\) : 取引\(j\) の株価
  • \(Q_j\) : 取引\(j\) の出来高
  • \(j\) : 定義された期間内に発生する個々の取引

 

Microsoft Copilot にお願いをして、VWAP の活用方法、長所・短所についてまとめてもらいました。

VWAP の活用例

  1. エントリー・エグジットの判断基準
    • VWAP より安く買えれば「割安」、高く売れれば「割高」と判断できる
    • 機関投資家は VWAP 付近で約定することを目指すことが多い
  2. トレンドの中心線として利用
    • VWAP は滑らかに推移するため、価格の上下動を相対的に評価できる
    • GUI 上では基準線として表示されることが多い
  3. アルゴリズム取引の基準価格
    • VWAP は多くのアルゴリズム取引で「目標価格」として使われる
    • 特に大量注文を分割して執行する際に、VWAP を基準にすることで市場への影響を抑える

VWAP の長所

長所 内容
市場の平均的な約定価格を示す 実際の取引に基づいた価格なので信頼性が高い
滑らかでノイズに強い 短期的なスパイクに左右されにくく、トレンド把握に適している
逐次更新が可能 リアルタイムでVWAPを更新できるため、ティックデータにも対応しやすい
機関投資家の行動と親和性が高い 実需に基づいた価格判断が可能

VWAP の短所・注意点

短所 内容
過去のデータに依存する 累積的な指標のため、直近の急変には反応が遅れる
時間帯によって意味が変わる 前場と後場で市場の性質が異なるため、VWAP の解釈も変わることがある
出来高が極端に偏ると歪む 特定の時間帯に出来高が集中すると、VWAP がその価格に引っ張られる
単独では売買シグナルにならない 他の指標(RSI、MACDなど)と組み合わせることで真価を発揮する

まとめ

  • VWAP は、市場の平均的な取引価格を把握するための強力なツールです。
  • 特に短期売買やアルゴリズム取引においては、VWAP を基準にすることで、合理的なエントリー・エグジット判断が可能になります。
  • ただし、VWAP はあくまで「過去の加重平均」であり、未来の価格を予測するものではないことに注意が必要です。
  • 他のテクニカル指標と組み合わせて使うことで、より精度の高いトレード判断ができるでしょう。

JupyterLab でプロット

Excel ファイルの保存したティックデータ(シート名は銘柄コード)を読み込んで、VMAP と、株価との乖離率を算出してプロットした例を示します。

必要なパッケージのインポート

import datetime
import os

import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib import dates as mdates

Matplotlib の設定(省略可)

FONT_PATH = "../fonts/RictyDiminished-Regular.ttf"
fm.fontManager.addfont(FONT_PATH)

# FontPropertiesオブジェクト生成(名前の取得のため)
font_prop = fm.FontProperties(fname=FONT_PATH)
font_prop.get_name()

plt.rcParams["font.family"] = font_prop.get_name()
plt.rcParams["font.size"] = 12

ティックデータの読込

Excel ファイルはデータを取得した時刻を UNIX 形式のタイムスタンプに変換して保持していますが、このままででは扱いにくいので、Pandas の日付時刻形式に変換してインデックスにしています。

file = "ticks_20251006.xlsx"
body = os.path.splitext(os.path.basename(file))[0]
path_excel = os.path.join("..", "collection", file)
code = "7011"
df = pd.read_excel(path_excel, sheet_name=code)
df.index = pd.to_datetime([datetime.datetime.fromtimestamp(ts) for ts in df["Time"]])
df

 

VWAP と 株価との乖離率 の算出

常に過去データを扱うのであれば、Pandas でスマートに VMAP を算出できるのですが、リアルタイムでデータを逐次取得することを想定しているので、転用しやすいように敢えてループ処理をしています。

cum_pv = 0.0
cum_vol = 0.0
volume_prev = None
vwap_list = []
vwap_deviation_list = []


# データを逐次取得することを想定して、ループで処理
for price, volume in zip(df["Price"], df["Volume"]):
    if volume_prev is None:
        vol_diff = 0.0
    else:
        # ティックデータの出来高は累積出来高であるため
        vol_diff = volume - volume_prev

    cum_pv += price * vol_diff
    cum_vol += vol_diff
    volume_prev = volume

    # VWAP の算出
    vwap = cum_pv / cum_vol if cum_vol > 0 else price
    vwap_list.append(vwap)

    # 株価と VWAP の乖離率
    vwap_deviation = (price - vwap) / vwap
    vwap_deviation_list.append(vwap_deviation)

df["VWAP"] = vwap_list
df["VWAPdev"] = vwap_deviation_list
df

 

株価と VWAP のプロット

fig, ax = plt.subplots(figsize=(6, 3))

ax.plot(df["Price"], linewidth=1, label="株価")
ax.plot(df["VWAP"], linewidth=1, label="VWAP")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
ax.set_ylabel("VWAP")
ax.grid()
ax.legend(fontsize=7)
ax.set_title(f"{code} in {file}")

plt.tight_layout()
# plt.savefig(f"{body}_{code}_vwap.png")
plt.show()

 

株価と VWAP の乖離率

fig, ax = plt.subplots(figsize=(6, 3))

ax.plot(df["VWAPdev"], linewidth=1, color="C2", label="VWAP 乖離率")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
ax.set_ylabel("VWAP 乖離率")
ax.grid()
ax.legend(fontsize=7)
ax.set_title(f"{code} in {file}")

plt.tight_layout()
# plt.savefig(f"{body}_{code}_vwap_dev.png")
plt.show()

 

参考サイト

  1. 用語集 | 日本取引所グループ VWAP
  2. Volume-weighted average price - Wikipedia
  3. pandasでExcelファイル(xlsx, xls)の読み込み(read_excel) | note.nkmk.me

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2025-11-01

【備忘録】ウォルラス演算子と正規表現 ~ Python

Python 3.8 から導入された := は、代入と評価を同時に行える構文で、条件式やループ内での一時変数の代入に特に便利です [1]。恥ずかしながら、生成 AI とのコーディングのやりとりで話題になるまで、知らなかった演算子でした😳。

生成 AI (Microsoft Copilot) が次のようにまとめてくれました。

🦭 ウォルラス演算子( := )とは?

  • 正式名称:Assignment Expression(代入式)
  • 通称:ウォルラス演算子( := の形がセイウチ, walrus の目と牙に似ていることから)
  • 導入バージョン:Python 3.8 (PEP 572 [2])

この演算子は、式の中で変数に値を代入しつつ、その値を評価に使うことができます。従来の = は文としてしか使えず、式の中では使えませんでした。

特に便利だと思う、正規表現を扱う時の例を備忘録としてまとめました。

ウォルラス演算子の利用例
import re

pattern = re.compile(
    r"^[a-zA-Z0-9_+-]+(\.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$"
)


addr = "user@example.com"

if match := pattern.match(addr):
    print(f"メールアドレスは '{match.group(0)}' です。")
else:
    print(f"'{addr}' は無効なメールアドレスです。")
メールアドレスは user@example.com です。

電子メールアドレスを判別する正規表現は、参考サイト [3] を引用しました。

参考サイト

  1. What's New In Python 3.8 — Python ドキュメント
  2. PEP 572 – Assignment Expressions | peps.python.org
  3. メールアドレスの正規表現 | 正規表現入門

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2025-10-08

【備忘録】NumPy の警告を例外として扱う

NumPy は、プログラミング言語 Python において数値計算を効率的に行うための拡張モジュールです。効率的な数値計算を行うための型付きの多次元配列(例えばベクトルや行列などを表現できる)のサポートを Python に加えるとともに、それらを操作するための大規模な高水準の数学関数ライブラリを提供しています。

Wikipedia より引用、編集

Numpy の関数計算で無効な引数を指定した場合、RuntimeWarning という警告が出るものの np.float64(nan) を返すだけの場合があります。例を以下に示しました。

import numpy as np

v = -1
np.log(v)
/tmp/ipykernel_10573/3390615159.py:4: RuntimeWarning: invalid value encountered in log
  np.log(v)
np.float64(nan)

np.float64(nan) で処理されて都合が良い時もありますが、例外処理をしたい場合もあります。

警告を例外として扱う方法を Microsoft Copilot に尋ねてみたところ、三つのやり方を教えてくれました。

その中で、最もしっくりきた方法を備忘録にしました。

import warnings

import numpy as np

# NumPyの警告を例外として扱う設定
warnings.filterwarnings("error")

try:
    v = -1
    result = np.log(v)
except Warning as e:
    print(f"警告が例外としてキャッチされました: {e}")
警告が例外としてキャッチされました: invalid value encountered in log

使用した OS 環境は以下のとおりです。

Fedora Linux Workstation 42 x86_64
Python 3.13.7
jupyterlab 4.4.9
numpy 2.2.6

参考サイト

  1. Pythonのwarningsで警告(Warning)を非表示、例外化 | note.nkmk.me

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2025-10-03

倒立振子問題 (CartPole)

Python で強化学習をしようと、いろいろなサイトを調べると CartPole の例をよく見かけます。強化学習をしっかり学んでいる方なら、よく知られた問題なのであらためて調べる必要はないのでしょうが、自分のような初学者にはいまひとつピンときません。

そこで、Gemini に CartPole について説明をしてもらった内容を以下にまとめ直しました。

なお、OpenAI Gym [1] の後継 Gymnasium [2] の利用を前提としているので OpenAI Gym/Gymnasium と記述された部分を Gymnasium だけに直しています。

Gemini がまとめてくれた倒立振子問題 (CartPole)

CartPole(カートポール)とは、台車(カート)の上に取り付けられた棒(ポール)を、台車を左右に動かすことで倒れないように制御する、強化学習の分野における古典的な問題であり、またその問題をシミュレーションする Gymnasium の環境名です。

状態(カートの位置・速度、ポールの角度・角速度)を観測し、台車を左右に動かすという 2 つの行動の中から、できるだけ長くポールを立たせておくことを目的とします。

問題の概要

  • 台車 (Cart): 左右に動かすことができる台車です。
  • (Pole): 台車の上部に垂直に立てられた棒で、この棒が倒れないように制御します。
  • 目的: 棒が倒れないように、台車を左右に動かしてバランスを取ることです。

学習環境

  • CartPole は、Gymnasium が提供する強化学習の学習環境に含まれるタスクです。
  • AI エージェントが学習を進めるための、定番かつシンプルな環境として広く利用されています。

状態 (Observation)

  • 現在の状況は以下の4つの数値で表現されます。
    • 台車の位置
    • 台車の速度
    • ポールの角度
    • ポールの角速度

行動 (Action)

  • AIエージェントは、以下の2つの行動の中から選択します。
    • 台車を左に押す
    • 台車を右に押す

報酬 (Reward)

  • 各ステップで棒が倒れていなければ、+1 の報酬が与えられます。
  • 棒が倒れる、または特定の条件を超えるとエピソードが終了し、報酬が終了します。

終了条件

  • 以下のいずれかの条件を満たすと、1つのエピソード(制御タスクの開始から終了まで)が終了します。
    • ポールの角度が ±12° より大きくなった場合
    • 台車の位置が一定の範囲を超えた場合
    • 制御ステップ数が設定された最大値(v0 では 200 ステップ、v1 では 500 ステップ)を超えた場合

Stable-Baselines3 の PPO モデルで学習

Gymnasiom のドキュメント [3] の Basic Usage(以下)のページでは、Your First RL Program に Hello World 的な最初のコードが示されています。

このコードは、倒立振子問題 (CartPole) を扱った出来合いの環境 CartPole-v1 を用いて、ランダムでアクションを生成して、「環境」の動作を確認できるサンプルです。Gymnasium は強化学習エージェントを訓練するための環境を提供するライブラリなので、この程度しかできないのは仕方がないのですが、どうせならエージェントを用意して強化学習をさせてみたくなります。

そこで、同じ OpenAI 由来の Stable-Baselines3 [3] パッケージの PPO を利用して学習させてみました。

下記の OS 環境で動作確認をしています。

通常は Fedora Linux の環境を示しますが、今回はクリーンな状態から Python venv の仮想環境を作って必要なパッケージをインストールして確認したかったので、OS をインストールしたままの状態で運用している AlmaLinux 10 を使用しました。

AlmaLinux 10 x86_64_v2
python3 3.12.9-2
python3-devel
python3-tkinter
swig 4.3.0-3

Python は本体 (python3) だけでなく、パッケージのビルドに使用する python3-devel と、Matplotlib をインタラクティブモードで使用するために python3-tkinter のパッケージもインストールします。また、swig はパッケージのビルドで必要になります。

開発環境を用意

Python パッケージをインストールする際、一部 C/C++ でコンパイルするものがあるので、次のように開発環境を整えておきます。

$ sudo dnf group install "Development Tools"

Python venv の仮想環境の作成とパッケージのインストール

下記のように例えば rl_cartpole というディレクトリを作成して、その中で venv の仮想環境を作成します。多くの場合、pip のバージョンが古くなっているので、気になる場合はあらかじめアップデートします。

$ mkdir rl_cartpole
$ cd rl_cartpole
$ python -m venv venv
$ source venv/bin/activate
(venv) $ pip install --upgrade pip

以下のように gymnasium と stable-baseline3 パッケージをインストールします。必要な関連パッケージもインストールされます。

(venv) $ pip install gymnasium[box2d] stable-baselines3[extra]

強化学習用サンプル

Gymnasium には出来合いの環境がいくつも用意されています。一覧は下記のとおりです。

import gymnasium as gym
gym.pprint_registry()
===== classic_control =====
Acrobot-v1             CartPole-v0            CartPole-v1
MountainCar-v0         MountainCarContinuous-v0 Pendulum-v1
===== phys2d =====
phys2d/CartPole-v0     phys2d/CartPole-v1     phys2d/Pendulum-v0
===== box2d =====
BipedalWalker-v3       BipedalWalkerHardcore-v3 CarRacing-v3
LunarLander-v3         LunarLanderContinuous-v3
===== toy_text =====
Blackjack-v1           CliffWalking-v1        CliffWalkingSlippery-v1
FrozenLake-v1          FrozenLake8x8-v1       Taxi-v3
===== tabular =====
tabular/Blackjack-v0   tabular/CliffWalking-v0
===== None =====
Ant-v2                 Ant-v3                 GymV21Environment-v0
GymV26Environment-v0   HalfCheetah-v2         HalfCheetah-v3
Hopper-v2              Hopper-v3              Humanoid-v2
Humanoid-v3            HumanoidStandup-v2     InvertedDoublePendulum-v2
InvertedPendulum-v2    Pusher-v2              Reacher-v2
Swimmer-v2             Swimmer-v3             Walker2d-v2
Walker2d-v3
===== mujoco =====
Ant-v4                 Ant-v5                 HalfCheetah-v4
HalfCheetah-v5         Hopper-v4              Hopper-v5
Humanoid-v4            Humanoid-v5            HumanoidStandup-v4
HumanoidStandup-v5     InvertedDoublePendulum-v4 InvertedDoublePendulum-v5
InvertedPendulum-v4    InvertedPendulum-v5    Pusher-v4
Pusher-v5              Reacher-v4             Reacher-v5
Swimmer-v4             Swimmer-v5             Walker2d-v4
Walker2d-v5

今回は CartPole-v1 の学習環境を試します。gym.make で以下のように CartPole-v1 を指定することで Gymnasium の仕様に準拠した CartPole-v1 の学習環境 env を生成できます。

env = gym.make("CartPole-v1")

先に触れた Gymnasiom のドキュメント [3] の Basic Usage のページ、Your First RL Programで示されているコードをベースにして、Stable-Baselines3 パッケージの PPO を利用して学習できるようにしたのが下記のコードです。

PPO では、多層パーセプトロン (MLP) ベースの方策と価値関数を使う MlpPolicy を指定しています。

このコードを適当なファイル名(例: cartpole_rl.py)で保存して学習を実行します。

cartpole_rl.py
import os

import gymnasium as gym
from stable_baselines3 import PPO
from stable_baselines3.common.monitor import Monitor

if __name__ == "__main__":
    # ログフォルダの準備
    dir_log = "./logs/"
    os.makedirs(dir_log, exist_ok=True)

    # 学習環境の準備
    env = gym.make("CartPole-v1", render_mode="human")
    env = Monitor(env, dir_log)  # Monitorの利用

    # モデルの準備
    model = PPO("MlpPolicy", env, verbose=True)

    # 学習の実行
    model.learn(total_timesteps=50000)

    # 推論の実行
    obs, info = env.reset()
    print(f"Starting observation: {obs}")

    episode_over = False
    total_reward = 0

    while not episode_over:
        action, _ = model.predict(obs, deterministic=True)

        obs, reward, terminated, truncated, info = env.step(action)

        total_reward += reward
        episode_over = terminated or truncated

    print(f"Episode finished! Total reward: {total_reward}")
    env.close()
(venv) $ python cartpole_rl.py

CartPole の棒が揺れながらバランスを取る様子がウィンドウに表示され、学習後に推論が実行されます。

env = gym.make("CartPole-v1", render_mode="human") で学習中にアニメーション表示
出力例
Using cpu device
Wrapping the env in a DummyVecEnv.
/home/bitwalk/MyProjects/rl_cartpole/venv/lib64/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
---------------------------------
| rollout/           |          |
|    ep_len_mean     | 22.1     |
|    ep_rew_mean     | 22.1     |
| time/              |          |
|    fps             | 47       |
|    iterations      | 1        |
|    time_elapsed    | 43       |
|    total_timesteps | 2048     |
---------------------------------
-----------------------------------------
| rollout/                |             |
|    ep_len_mean          | 29          |
|    ep_rew_mean          | 29          |
| time/                   |             |
|    fps                  | 46          |
|    iterations           | 2           |
|    time_elapsed         | 87          |
|    total_timesteps      | 4096        |
| train/                  |             |
|    approx_kl            | 0.008222323 |
|    clip_fraction        | 0.0791      |
|    clip_range           | 0.2         |
|    entropy_loss         | -0.687      |
|    explained_variance   | -0.00142    |
|    learning_rate        | 0.0003      |
|    loss                 | 6.95        |
|    n_updates            | 10          |
|    policy_gradient_loss | -0.0101     |
|    value_loss           | 54.3        |
-----------------------------------------
...
(途中省略)
...
-----------------------------------------
| rollout/                |             |
|    ep_len_mean          | 398         |
|    ep_rew_mean          | 398         |
| time/                   |             |
|    fps                  | 47          |
|    iterations           | 25          |
|    time_elapsed         | 1079        |
|    total_timesteps      | 51200       |
| train/                  |             |
|    approx_kl            | 0.004092879 |
|    clip_fraction        | 0.043       |
|    clip_range           | 0.2         |
|    entropy_loss         | -0.489      |
|    explained_variance   | 0.0157      |
|    learning_rate        | 0.0003      |
|    loss                 | 0.0149      |
|    n_updates            | 240         |
|    policy_gradient_loss | -0.00138    |
|    value_loss           | 0.021       |
-----------------------------------------
Starting observation: [ 0.0244522   0.03066805  0.03330757 -0.04372405]
Episode finished! Total reward: 500.0

出力される学習に関連する情報について、Gemini に教えてもらった内容を以下にまとめました。

  • rollout
  • データ収集・エピソード結果
  • このセクションは、エージェントが環境と相互作用してデータを収集した結果を示します。
    • ep_len_mean
      • 平均エピソード長 (Episode Length Mean)
      • 直近のエピソードのステップ数の平均値です。デイトレ環境の場合、1日のティック数、またはエピソードが終了するまでの平均時間ステップを示します。
    • ep_rew_mean
      • 平均エピソード報酬 (Episode Reward Mean)
      • 直近のエピソードでエージェントが得た累積報酬(総利益)の平均値です。この値の増加が、学習が順調に進んでいるかどうかの最も重要な指標となります。
  • time
  • 時間・実行速度に関する指標
  • このセクションは、学習プロセス自体の効率と進捗を示します。
    • fps
      • フレーム毎秒 (Frames Per Second)
      • 1 秒あたりに処理されたタイムステップの数。学習の処理速度を示し、高いほど効率的です。
    • iterations
      • イテレーション数
      • PPO の更新サイクルが何回実行されたかを示す回数です。通常、total_timesteps が n_steps * n_envs(データバッファサイズ)に達するごとに 1 回インクリメントされます。
    • time_elapsed
      • 経過時間 (秒)
      • 学習開始からの合計経過時間です。
    • total_timesteps
      • 総タイムステップ数
      • 学習開始から現在までに環境と相互作用した合計のステップ数です。
  • train
  • 学習の質に関する指標
  • このセクションの指標は、モデル(ポリシーと価値関数)がどれだけうまくデータに適合し、ポリシーの更新が適切に行われているかを示します。
    • approx_kl
      • 近似 KL ダイバージェンス (Approximate KL Divergence)
      • 更新後の新しいポリシーと、更新前の古いポリシーとの間の距離を示します。PPO は、ポリシーが急激に変化するのを防ぐため、この値が大きくなりすぎないように制御します。
    • clip_fraction
      • クリップされた割合 (Clipped Fraction)
      • 勾配更新時に、PPO のクリッピング機構によって損失が制限されたサンプルデータの割合です。この値が高すぎると、クリッピングが強すぎて学習が進まない可能性があります。
    • clip_range
      • クリップ範囲
      • PPO のハイパーパラメータであり、ポリシー更新時の比率(rt(θ))を制限する範囲(例:[1−ϵ, 1+ϵ] の ϵ)。ここでは ϵ=0.2 と設定されています。
    • entropy_loss
      • エントロピー損失
      • エージェントの行動のランダム性(探索の度合い)に関する損失項。値が負で、絶対値が大きいほど、ポリシーの行動がランダムで多様であることを示します。徐々に 0 に近づくことが期待されます。
    • explained_variance
      • 説明された分散
      • 価値関数 (Value Function) が、実際の累積報酬(リターン)をどれだけうまく予測できているかを示す指標。1 に近いほど、予測精度が高いことを示します。負の値は、予測が平均より悪いことを示し、モデルの不安定さを示唆します。
    • learning_rate
      • 学習率
      • ニューラルネットワークの重みを更新する際のステップサイズです。PPO では、学習の進行とともに線形に減衰(スケジューリング)されることが一般的です。
    • loss
      • 全体の損失関数(ポリシー損失 + 価値関数損失 + エントロピー損失など)の合計。
      • この値が小さくなるほど学習は進んでいると見なされますが、個別の損失に比べて解釈は難しいです。
    • n_updates
      • これまでの総更新回数。
      • Iteration 数と n_epochs (1つの Iteration 内でモデルを更新する回数) の積です。
    • policy_gradient_loss
      • ポリシー勾配損失
      • PPO の主要な損失関数で、ポリシー(行動戦略)の更新に使用されます。負の値を示し、負の絶対値が大きいほど、ポリシーが更新によって報酬を増加させる方向に強く引っ張られていることを意味します。
    • value_loss
      • 価値損失
      • 価値関数(状態の価値を予測するネットワーク)の更新に使用される損失です。この値が低いほど、価値関数が正確に状態の価値を予測できていることを示します。

学習曲線の確認

PPO を利用して学習をできるようにしたコード(cartpole_rl.py)では、gym.make で生成した環境 envMonitor に通しています。この Monitor は、Stable-Baselines3 パッケージが提供する Gymnasium 環境向けのモニターラッパーで、エピソード報酬、長さ、時間、その他のデータを把握するために使用します。

このサンプルでは、dir_log (= "./logs/") 内にログを保存するように設定しています。

    # 学習環境の準備
    env = gym.make("CartPole-v1", render_mode="human")
    env = Monitor(env, dir_log)  # Monitorの利用

学習を実行した後、dir_log (= "./logs/") 内を確認すると monitor.csv というファイルがあり、最初の行がコメント行になっていて、以下 r(エピソード報酬)、l(エピソードの長さ)、t(時間)が記録されています。

(venv) $ ls logs
monitor.csv
(venv) $ cat logs/monitor.csv
#{"t_start": 1759387094.2588797, "env_id": "CartPole-v1"}
r,l,t
12.0,12,8.555453
18.0,18,8.94126
18.0,18,9.327632
...
(以下省略)

報酬トレンド

CartPole-v1 では 500 ステップが上限なので、最大ステップまで棒 (Pole) が倒れなければ報酬は +500 になり、これが最大です。すなわち、報酬を最大化することが、できるだけ長くポールを立たせておくという目的と一致しています。

報酬がどのように増えるかをエピソード順にプロットすることで、学習曲線を確認できます。

plot_monitor_reward.py
import os
import pandas as pd
import matplotlib.pyplot as plt

if __name__ == "__main__":
    # monitor.csv の読み込み
    dir_log = "./logs"
    name_log = "monitor.csv"
    # 最初の行の読み込みを除外
    df = pd.read_csv(os.path.join(dir_log, name_log), skiprows=[0])

    # 報酬のプロット
    plt.plot(df["r"])
    plt.xlabel("episode")
    plt.ylabel("reward")
    plt.grid()
    plt.tight_layout()
    plt.show()
(venv) $ python plot_monitor_reward.py
monitor.csv のエピソード順の r(報酬)トレンド

この例では、エピソードが 350 回に近づくと報酬が 500 になっています。350 回程度のエピソード(学習回数)で、モデルは報酬を最大化できた、すなわち、棒を倒さないコツを学習できたと言えるでしょう。

なお、エピソードの回数とエピソードの長さの積の累積が、cartpole_rl.py における下記の total_timesteps を超えない範囲で学習が行われますので、もう少し確認したければ数値を大きくする必要があります。

    # 学習の実行
    model.learn(total_timesteps=50000)

まとめ

強化学習を利用したい、というニーズあって、そのために Gymnasium であれこれ環境を作っています。最近、少し行き詰まってきたと感じたので、かき集めた知識を整理するためにも一旦立ち止まって、CartPole-v1 のような出来合いの環境を試してみました。

書いてまとめることによって自分の理解の整理ができて、行き詰まり感も解消できたことは良かったのですが、興味がある部分を中心にまとめてしまったきらいがあります。おいおい足りない部分を書き足します。

 

参考サイト

  1. openai/gym: A toolkit for developing and comparing reinforcement learning algorithms.
  2. Farama-Foundation/Gymnasium (formerly Gym)
  3. Gymnasium Documentation
  4. DLR-RM/stable-baselines3: PyTorch version of Stable Baselines
  5. Stable-Baselines3 Docs - Reliable Reinforcement Learning Implementations
  6. Monitor Wrapper — Stable Baselines3 documentation
  7. Stable Baselines 3 入門 (1) - 強化学習アルゴリズム実装セット|npaka
  8. Stable Baselines 3 入門 (2) - Monitor|npaka

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2025-09-30

日経平均株価の構成銘柄を取得

日経平均株価は、日本経済新聞社が算出・公表している日本の株式市場の代表的な株価指数の一つです。東京証券取引所プライム市場に上場している約 2,000 銘柄の株式のうち、取引が活発で流動性が高い 225 銘柄を、日本経済新聞社が選定し算出します。構成銘柄は 4 月と 10 月の年 2 回、上限各 3 銘柄の定期入れ替えが実施されています。

Wikipedia より引用、編集

日経平均株価の構成銘柄を Python で取得しようと Jupyter Lab 上で、参考サイト [1] に従って試したところ、以下のようにエラーが出て取得できませんでした。

dfs = pd.read_html("https://indexes.nikkei.co.jp/nkave/index/component")
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)

...
(途中省略)
...

    612 def http_error_default(self, req, fp, code, msg, hdrs):
--> 613     raise HTTPError(req.full_url, code, msg, hdrs, fp)

HTTPError: HTTP Error 403: Forbidden

ところが、端末エミュレータ上で同じ URL を wget コマンドで同じサイトを取得してみたところ、難なく出来てしまいました。

上記 Pandas で読み込むやり方でも、何か工夫をすれば読み込めるのかもしれませんが、とりあえず Python から wget コマンドを実行して、読み込みたいサイトを一旦ローカルファイルに保存してから、そのファイルを Pandas で読み込むことで、やりたかったことをできるようになったので、備忘録にしました。

下記の OS 環境で動作確認をしています。

Fedora Linux Workstation 42 x86_64
Python 3.13.7
beautifulsoup4 4.14.2
html5lib 1.1
JupyterLab 4.4.9
lxml 6.0.2
pandas 2.3.3
wget2 2.2.0-5 (rpm)

サンプル

以下、Jupyter Lab 上で実行することを想定しています。

最初に利用するライブラリをまとめてインポートします。

import os
import subprocess

import pandas as pd

外部コマンドを実行する関数です。ピッタリな用途の関数が公開されていたので、そのまま流用させていただきました。

# Reference:
# https://brightdata.jp/blog/各種ご利用方法/wget-with-python
def execute_command(command):
    """
    Execute a CLI command and return the output and error messages.

    Parameters:
        - command (str): The CLI command to execute.

    Returns:
        - output (str): The output generated by the command.
        - error (str): The error message generated by the command, if any.
    """
    try:
        # execute the command and capture the output and error messages
        process = subprocess.Popen(
            command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        output, error = process.communicate()
        output = output.decode("utf-8")
        error = error.decode("utf-8")
        # return the output and error messages
        return output, error
    except Exception as e:
        # if an exception occurs, return the exception message as an error
        return None, str(e)

日経平均株価の構成銘柄をデータフレームで取得する処理です。

# wget コマンドで対象サイトをダウンロード
file = "component"
# ファイルが存在していれば削除 
if os.path.exists(file):
    os.remove(file)
output, error = execute_command(f"wget https://indexes.nikkei.co.jp/nkave/index/{file}")

# 保存された HTML ファイルを Pandas で読み込む
dfs = pd.read_html(file)
# 複数のデータフレームを結合して、コード列でソート
df = pd.concat(dfs).sort_values("コード", ignore_index=True)
df

 

参考サイト

  1. Python, pandasでwebページの表(htmlのtable)をスクレイピング | note.nkmk.me
  2. 構成銘柄一覧 - 日経平均プロフィル

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2025-09-16

Ta-Lib を Python で利用する

TA-Lib, Technical Analysis Library は 2001 年にリリースされ、20 年以上経った今でも広く利用されている著名なアルゴリズムを提供しています。コードは安定しており、長年にわたる検証を経ています。200 以上のテクニカル指標をサポートしており、API は C/C++ で記述されており Python ラッパー (wrapper) も提供されています。TA-Lib は BSD License (BSD-2-Clause license) の元で配布されているオープンソースのライブラリです。

以前は Linux 上で TA-Lib の Python 用パッケージを pip でインストールしてもビルドが必要で、しかもエラーでビルドできませんでした。自力でエラーを解決できなかったので TA-Lib の利用を避けていました。しかし最近の TA-Lib のバージョンの Python 用パッケージでは難なくインストールできることが判りました。

そこで今更ですが TA-Lib の使い方をおぼえようと、Jupyter Lab 上でテクニカル指標のいくつかをプロットしてみたので、備忘録的に内容をまとめました。

下記の OS 環境で動作確認をしています。

Fedora Linux Workstation 42 x86_64
Python 3.13.7
JupyterLab 4.4.7
matplotlib 3.10.6
mplfinance 0.12.10b0
numpy 2.3.3
pandas 2.3.2
ta-lib 0.6.7
yfinance 0.2.65

サンプル

ライブラリをインポート

最初に利用するライブラリをまとめてインポートします。

import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
import mplfinance as mpf
import numpy as np
import pandas as pd
import yfinance as yf
from talib import BBANDS, MACD, MFI, MOM, OBV, RSI, SAR, STOCH

yfinance で日経平均株価指数の過去データを取得

サンプルとして、今年の 1 月から半年間の日足データを取得します。

symbol = "^N225"
ticker = yf.Ticker(symbol)
df = ticker.history(start="2025-01-01", end="2025-07-01", interval="1d")

サンプル期間より少し古いデータからも取得しておきます。これでテクニカル指標を算出して、サンプルの期間の最初から指標がプロットされるようにします。

df2 = ticker.history(start="2024-10-01", end="2025-07-01", interval="1d")

mplfinance でチャートを作成

サンプル期間の日足データをローソク足チャートと出来高の棒グラフを並べてプロットしました。

fig = plt.figure(figsize=(8, 4))
ax = dict()
n = 2
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[3 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

mpf.plot(
    df,
    type="candle",
    style="default",
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
    volume=ax[1],
)
ax[0].set_title(f"{ticker.info['longName']} ({symbol})")

plt.tight_layout()
# plt.savefig("screenshots/n225_default.png")
plt.show()

Bollinger bands

過去 20 日間の移動平均、移動標準偏差 +3σ, +2σ, +1σ, mean, -1σ, -2σ, -3σ でボリンジャーバンドを作成しました。

fig, ax = plt.subplots(figsize=(8, 3))

# BBANDS - Bollinger Bands
# upperband, middleband, lowerband = BBANDS(real, timeperiod=5, nbdevup=2, nbdevdn=2, matype=0)
period = 20
mv_upper_1, mv_mean, mv_lower_1 = BBANDS(df2["Close"], period, 1, 1)
mv_upper_2, _, mv_lower_2 = BBANDS(df2["Close"], period, 2, 2)
mv_upper_3, _, mv_lower_3 = BBANDS(df2["Close"], period, 3, 3)

apds = [
    mpf.make_addplot(
        mv_upper_3[df.index],
        width=1,
        color="C0",
        linestyle="dotted",
        label="+3σ",
        ax=ax,
    ),
    mpf.make_addplot(
        mv_upper_2[df.index],
        width=0.9,
        color="C1",
        linestyle="dashdot",
        label="+2σ",
        ax=ax,
    ),
    mpf.make_addplot(
        mv_upper_1[df.index],
        width=0.75,
        color="C2",
        linestyle="dashed",
        label="+1σ",
        ax=ax,
    ),
    mpf.make_addplot(
        mv_mean[df.index],
        width=1,
        color="C3",
        linestyle="solid",
        label="Mean",
        ax=ax,
    ),
    mpf.make_addplot(
        mv_lower_1[df.index],
        width=0.75,
        color="C4",
        linestyle="dashed",
        label="-1σ",
        ax=ax,
    ),
    mpf.make_addplot(
        mv_lower_2[df.index],
        width=0.9,
        color="C5",
        linestyle="dashdot",
        label="-2σ",
        ax=ax,
    ),
    mpf.make_addplot(
        mv_lower_3[df.index],
        width=1,
        color="C6",
        linestyle="dotted",
        label="-3σ",
        ax=ax,
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    update_width_config=dict(candle_linewidth=0.75),
    ax=ax,
)
ax.grid()
ax.legend(fontsize=7)
ax.set_title(
    f"{ticker.info['longName']} ({symbol})\nwith Bollinger Bands (period={period}days)"
)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_bbands.png")
plt.show()

Parabolic SAR

AF step=0.02, max=0.2 で Parabolic SAR をプロットしました。上昇下降トレンドの情報が無いので、灰色の丸点でプロットしました。

fig, ax = plt.subplots(figsize=(8, 3))

# SAR - Parabolic SAR
# real = SAR(high, low, acceleration=0, maximum=0)
af_step = 0.02
af_max = 0.2
sar = SAR(df2["High"], df2["Low"], af_step, af_max)

apds = [
    mpf.make_addplot(
        sar[df.index],
        type="scatter",
        marker='o',
        markersize=3,
        color="darkgray",
        ax=ax,
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    update_width_config=dict(candle_linewidth=0.75),
    ax=ax,
)
ax.grid()
ax.set_title(
    f"{ticker.info['longName']} ({symbol})\nwith Parabolic SAR (AF step={af_step}, max={af_max})"
)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_sar.png")
plt.show()

Momentum

過去 10 日間のデータでモメンタムを算出しました。

fig = plt.figure(figsize=(8, 4))
ax = dict()
n = 2
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[2 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

# MOM - Momentum
# real = MOM(real, timeperiod=10)
period = 10
mom = MOM(df2["Close"], period)
apds = [
    mpf.make_addplot(
        mom[df.index],
        width=1,
        color="C0",
        linestyle="solid",
        ax=ax[1],
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
)
ax[1].set_ylabel("Momentum")
ax[0].set_title(
    f"{ticker.info['longName']} ({symbol})\nwith Momentum (period={period}days)"
)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_mom.png")
plt.show()

RSI, Relative Strength Index

過去 14 日間のデータで RSI を算出しました。

fig = plt.figure(figsize=(8, 4))
ax = dict()
n = 2
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[2 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

# RSI - Relative Strength Index
# real = RSI(real, timeperiod=14)
period = 14
rsi = RSI(df2["Close"], period)
apds = [
    mpf.make_addplot(
        rsi[df.index],
        width=1,
        color="C0",
        linestyle="solid",
        ax=ax[1],
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
)
ax[0].set_title(f"{ticker.info['longName']} ({symbol})\nwith RSI (period={period}days)")
ax[1].set_ylabel("RSI")
ax[1].set_ylim(0, 100)
ax[1].axhline(30, color="black", linewidth=0.5)
ax[1].axhline(70, color="black", linewidth=0.5)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_rsi.png")
plt.show()

Stochastic oscillator

スローストキャスティクスをデフォルトのパラメータのままでプロットしています。

fig = plt.figure(figsize=(8, 4))
ax = dict()
n = 2
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[2 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

# STOCH - Stochastic
# slowk, slowd = STOCH(high, low, close, fastk_period=5, slowk_period=3, slowk_matype=0, slowd_period=3, slowd_matype=0)
slowk, slowd = STOCH(df2["High"], df2["Low"], df2["Close"])

apds = [
    mpf.make_addplot(
        slowk[df.index],
        width=1,
        color="C0",
        linestyle="solid",
        label="Slow%K",
        ax=ax[1],
    ),
    mpf.make_addplot(
        slowd[df.index],
        width=1,
        color="C1",
        linestyle="solid",
        label="Slow%D",
        ax=ax[1],
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
)
ax[0].set_title(f"{ticker.info['longName']} ({symbol})\nwith Stochastic oscillator")
ax[1].set_ylabel("Stochastic")
ax[1].set_ylim(0, 100)
ax[1].axhline(20, color="black", linewidth=0.5)
ax[1].axhline(80, color="black", linewidth=0.5)
ax[1].legend(fontsize=7)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_stoch.png")
plt.show()

MACD, Moving Average Convergence Divergence

MACD もデフォルトのパラメータでプロットしています。MACD のヒストグラムは正負で色を変えたかったのですが、すぐに出来なかったので単色にしてしまいました。

fig = plt.figure(figsize=(8, 4))
ax = dict()
n = 2
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[2 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

# MACD - Moving Average Convergence/Divergence
# macd, macdsignal, macdhist = MACD(real, fastperiod=12, slowperiod=26, signalperiod=9)
period_fast = 12
period_slow = 26
period_signal = 9
macd, signal, macdhist = MACD(df2["Close"], period_fast, period_slow, period_signal)

apds = [
    mpf.make_addplot(
        macd[df.index],
        width=1,
        color="C0",
        linestyle="solid",
        label="MACD",
        ax=ax[1],
    ),
    mpf.make_addplot(
        signal[df.index],
        width=1,
        color="C1",
        linestyle="solid",
        label="Signal",
        ax=ax[1],
    ),
    mpf.make_addplot(
        macdhist[df.index],
        type="bar",
        color="C2",
        ax=ax[1],
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
)
ax[0].set_title(
    f"{ticker.info['longName']} ({symbol})\nwith MACD [{period_fast}, {period_slow}, {period_signal}]"
)
ax[1].set_ylabel("MACD")
ax[1].legend(fontsize=7)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_macd.png")
plt.show()

OBV, On Balance Volume

OBV は終値と出来高から算出する指標です。

fig = plt.figure(figsize=(8, 5))
ax = dict()
n = 3
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[2 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

# OBV - On Balance Volume
# real = OBV(close, volume)
obv = OBV(df2["Close"], df2["Volume"])

apds = [
    mpf.make_addplot(
        obv[df.index],
        width=1,
        color="C0",
        linestyle="solid",
        ax=ax[2],
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
    volume=ax[1],
)
ax[0].set_title(f"{ticker.info['longName']} ({symbol})\nwith OBV")
ax[2].set_ylabel("OBV")

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_obv.png")
plt.show()

MFI, Money Flow Index

MFI も、株価と出来高から算出する指標です。

fig = plt.figure(figsize=(8, 5))
ax = dict()
n = 3
gs = fig.add_gridspec(
    n, 1, wspace=0.0, hspace=0.0, height_ratios=[2 if i == 0 else 1 for i in range(n)]
)
for i, axis in enumerate(gs.subplots(sharex="col")):
    ax[i] = axis
    ax[i].grid()

# MFI - Money Flow Index
# NOTE: The MFI function has an unstable period.
# real = MFI(high, low, close, volume, timeperiod=14)
period = 14
mfi = MFI(df2["High"], df2["Low"], df2["Close"], df2["Volume"], period)

apds = [
    mpf.make_addplot(
        mfi[df.index],
        width=1,
        color="C0",
        linestyle="solid",
        ax=ax[2],
    ),
]
mpf.plot(
    df,
    type="candle",
    style="default",
    addplot=apds,
    datetime_format="%m/%d",
    xrotation=0,
    ax=ax[0],
    volume=ax[1],
)
ax[0].set_title(f"{ticker.info['longName']} ({symbol})\nwith MFI (period={period}days)")
ax[2].axhline(20, color="black", linewidth=0.5)
ax[2].axhline(80, color="black", linewidth=0.5)
ax[2].set_ylabel("MFI")
ax[2].set_ylim(0, 100)

plt.tight_layout()
# plt.savefig("screenshots/n225_talib_mfi.png")
plt.show()

参考サイト

  1. TA-Lib - Technical Analysis Library
  2. TA-Lib/ta-lib: TA-Lib (Core C Library)
  3. TA-Lib/ta-lib-python: Python wrapper for TA-Lib
  4. TA-Lib · PyPI

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加