2026-04-23

自動売買への道 (2026-04-23)

楽天証券の口座でデイトレの自動売買に挑戦しようと、Windows / Excel 上で動作する マーケットスピード II RSS を利用した Python アプリ (Kabuto) を開発しています。今月は、来るゴールデン・ウィークに存分に強化学習モデルのバックテストができるように準備を進めています。

今日の日経平均株価

現在値 59,140.23 -445.63 -0.75% 15:45
前日終値 59,585.86 04/22 高値 60,013.98 09:06
始値 59,758.64 09:00 安値 58,621.48 12:40

※ 右の 15 分足チャートは Yahoo! Finance のデータを yfinance で取得して作成しました。

【関連ニュース】

強化学習の沼

月末から始まるゴールデンウィークにゆっくりバックテストができそうなので、それまでにどれだけ強化学習モデルを育てられるかに挑んでいます。

学習環境 TrainingEnv(gymnasium.Env)

学習環境の概略です。

行動空間 Action Space
  1. HOLD : 何もしない
  2. BUY  : 「買建」または「返済」
  3. SELL : 「売建」または「返済」
「返済」を行動空間に加えると学習が進まなかったことを踏まえ「返済」は環境側で制御。ナンピン禁止を行動マスクで制御
PositionType に対する mask [HOLD, BUY, SELL]
  • WARMUP [True, False, False](寄り付き後、売買できない期間を設定)
  • NONE   [True, True,  True ]
  • LONG   [True, False, True ]
  • SHORT  [True, True,  False]
観測空間 Observation Space
株価、インジケータなど [-inf, +inf](おまかせ「標準化」)
  1. MA1 / 始値 : 短周期の移動平均
  2. Profit     : 含み損益
クロス関連 [-inf, +inf](符号が重要なので「標準化」なし)
  1. DiffMA   : MA乖離率 = (MA1 - MA2) / MA2
  2. DiffVWAP : VWAP乖離率 = (MA1 - VWAP) / VWAP
カウンタ関連 [0, +inf](おまかせ「標準化」)
  1. n_trade        : 約定回数
  2. count_negative : 含み損の継続カウンタ
ポジションを One-Hot エンコーディング
  1. SHORT [1. 0. 0.]
  2. NONE  [0. 1. 0.]
  3. LONG  [0. 0. 1.]
報酬 Rewards
  • 建玉なし    : ゴールデン/デッド クロス・シグナル近辺の報酬分布の一部をペナルティとして付与
  • 建玉保持    : 含み損益の一定割合を付与+前ステップの含み益からの増減の一定割合を付与
  • 買建/売建時 : 約定コスト(ペナルティ)+ゴールデン/デッド クロス・シグナル近辺の報酬分布から報酬を付与
  • 返済時      : 直前の含み損益を付与
  • 約定コスト  : 建玉、返済時いずれも固定の約定コストをペナルティとして引く
  • 連続含み損  : 許容回数を超えたらペナルティを急激に増大
終了条件 Episode End
  1. terminated
    • "目的を達成した/失敗した" など、エージェント側の原因で終了
    • 終端として扱う(価値は 0)
    1. なし(「約定回数の上限で終了」を評価予定)
  2. truncated
    • 時間制限・ステップ制限・データ終端など “外的理由” で終了
    • 終端ではない(価値を bootstrap)
    1. ティックデータが最終行に達した時
      • 終了時、建玉があれば強制返済。報酬条件、約定コストは同じ
      • 約定回数に応じて報酬付与。現在は 25 回が極大になる式を適用(ただし約定回数は偶数)。

今回は、観測空間の特徴量を絞りました。とは言え、単周期移動平均 MA1 と、現在は報酬対象にしていない VWAP乖離率 を残しました。思い切りが悪いですね。

複数のティックデータで学習

過去 20 日分のティックデータに対してそれぞれ 100 エピソードの学習を実施しました。

なお、1 日遅れなので学習データのスコープは 2026-03-25 - 2026-04-21 の 20 日分になります。

過去 20 日分の学習における報酬 (episord_reward)、損益 (pnl)、約定回数 (transactions) トレンド by TensorBoard

モデルの学習における現在の着目点は「学習全体を通じてモデルの報酬をプラス圏で維持できるか」です。

過去 20 日分の学習における報酬 (episord_reward) トレンド(拡大表示) by TensorBoard

いままで、学習を重ねるうちに報酬がマイナス側に収束してしまう傾向が見られたので、報酬設計に調整して学習全体で報酬が概ねプラス圏になることに注力しました。

モデルは報酬の最大化を追求しているだけなので、報酬がプラスになる方策を探索できずに 0 近辺で収束してしまうことを避けたかったのでした。

今回は学習全体を通じてエピソードの報酬が概ねプラス側に寄ったので、報酬パラメータの設定はまずまずだったと評価できます。約定回数が多いので局所的な高頻度売買が発生していることが予想できますが、高頻度売買の抑制の対策はあらためて検討することにして、まずは良しとしたいです。

生成 AI に成果を確認してみた

今回は、エピソード報酬の符号にこだわりましたが、生成 AI に確認すると、エピソード報酬の符号より大小関係の方が重要だということでした。下記のように慰められてしまいました。報酬設計の前提を補足すると、意見が翻りましたが…。

結論として:今回の調整は “方向として正しかった” と見ていいです。

ただし、今後の指針としては:

  • 目的:エピソード報酬をプラスにすること

→ ではなく

  • 目的:報酬の構造を整理して、PnL との相関を高めること

と捉え直すと、迷いが減ると思います。

実務的には:

  • 見るべきもの:
    • PnL と episode_reward の相関(ざっくりでも)
    • 大損エピソードの頻度が減っているか
    • 取引回数が変な方向に暴れていないか
  • あまり気にしなくていいもの:
    • エピソード報酬の絶対値がプラスかマイナスか
    • 「0 をまたいでいるかどうか」そのもの

整理してくれてありがとう。

ただね、報酬設計において、モデルが報酬を最大化する方策を選べば、エピソード報酬はかならずプラスになるようにしているんだよね。だからマイナス側に収束しているということは期待する方向に学習が進んでいないように見えるんだ。

なるほど、その前提があるなら話は一段クリアになりますね。


🎯 あなたの設計では「最適方策ならエピソード報酬は必ずプラスになる」

つまり、

  • 報酬設計そのものが “プラス圏=望ましい行動” を保証する構造になっている
  • にもかかわらず 学習後のエピソード報酬がマイナス圏に沈む
  • これは 「方策が期待した方向に向かっていない」サインに見える

という感覚は完全に正しいです。

あなたの環境では、

“プラス圏に収束する=正しい方向に学習が進んでいる”

という意味を持つように報酬を設計しているわけです。

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

🧩 まとめ

  • 一般論:RL は報酬の絶対値や符号に不変
  • あなたの環境:報酬構造が「プラス=望ましい行動」を意味するように設計されている
  • だから プラス圏に収束しないのは “望ましい行動を取れていない” サインで正しい解釈
  • プラス圏に寄せる調整は 無駄ではなく、むしろ本質的な改善だった

インプットした情報が不十分だったので自分が悪いのですが、このように態度が変わると、ときどきムカつきます。

初期学習を再検討

生成 AI と議論を進めた結果、「過去 20 日分のティックデータに対してそれぞれ 100 エピソードの学習を実施」は悪手であることが判りました。

「1 日あたり 100 エピソード」は、

“初期学習としてはやりすぎ” で、

むしろ 追加学習フェーズでゆっくり積み上げる方が理にかなっています。

 

現在の収集方法になってから、55 日分のティックデータが蓄積されたので、次のように初期学習をやり直すことにしました。

初期学習計画

  • モデル:今の MaskablePPO の設定そのまま
  • データ:55 日分
  • エピソード数:275(=55 日 × 5)
  • エピソードの順番:55 日を 5 回ずつ並べて、全体をランダムシャッフル

ファイルリストをシャッフルするコードを生成 AI に示してもらったので、これを流用します。

import random

# 55 日分のファイルリスト(例)
file_list = [
    "20240201.csv", "20240202.csv", "20240203.csv",
    # ...
    "20240326.csv"  # ← 55 日分あると仮定
]

# 1 日あたり 5 エピソード
episodes_per_day = 5

# 55 日 × 5 = 275 エピソード分のリストを作る
episode_list = file_list * episodes_per_day

# 1 本の長いリストをシャッフル
random.shuffle(episode_list)

# episode_list が「学習順番」になる
for i, f in enumerate(episode_list[:20]):
    print(i, f)

むむ、以下のサイトを読んでいたのに、使うべきときになっても思い出せませんでした。

学習用のエージェントは、環境のインスタンスに Stable-Baselines3 の環境ラッパーを適用していますが、ティックデータを 1 エピソード毎に学習させる場合、すこし調整が必要になりました。今回の結果が良ければ、学習部分のコードを詳しくまとめる予定です。

graph LR
    subgraph make_env
        A[TrainingEnv] --> B[[Monitor]]
    end
    B --> C[[DummyVecEnv]]
    C --> D[[VecNormalize]]

学習結果

今回は学習する総エピソードが少なかったので、数時間で学習が終わりました。

過去 55 日分× 5 回(シャッフル)の学習における報酬 (episord_reward)、損益 (pnl)、約定回数 (transactions) トレンド by TensorBoard

惜しい結果でした。報酬トレンドはプラス圏に向かうものの定着しませんでした。

興味があるので、一日あたり 5 エピソードではなく 20 エピソードに増やして、あらためて結果を確認します。

参考サイト

  1. マーケットスピード II RSS | 楽天証券のトレーディングツール
  2. マーケットスピード II RSS 関数マニュアル
  3. 注文 | マーケットスピード II RSS オンラインヘルプ | 楽天証券のトレーディングツール
  4. PythonでGUIを設計 | Qtの公式Pythonバインディング
  5. Python in Excel alternative: Open. Self-hosted. No limits.
  6. Book - xlwings Documentation
にほんブログ村 株ブログ 株日記へ
PVアクセスランキング にほんブログ村

0 件のコメント:

コメントを投稿