2025-10-21

自動売買への道 (2025-10-21)

楽天証券の口座でデイトレの自動売買に挑戦しようと Windows / Excel 上で利用できる マーケットスピード II RSS を活用して Python であれこれ取り組んでいます。この「自動売買への道」のトピックでは、プログラミングの話題にも踏み込んで、日々の活動をまとめていきます。

デイトレ用自作アプリ

以下は株価に関連する情報の流れを示しています。

株価データの流れ(Windows 11)

楽天証券では、Python からネットワーク越しに直接取引できるような API が提供されていないので、マーケットスピード II RSS を介して取引をする構成を取っています。

今回のブログ記事は、Python / PyTorch を利用した強化学習に特化した内容になります。

PyTorch で仕切り直す出発点

過去のティックデータを使って「取引用モデル」を強化学習するにシステムを構築するにあたって、前回のブログ記事(下記)にまとめたように、利用する Python パッケージを StableBaselines3 から PyTorch に戻すことにしました。

今のところ PyTorch の PPO の学習処理について簡単なサンプルを作って機能を確認している段階です。この段階を超えれば、GUI に組み込むための調整に入ります。そうすると、スレッド化する処理をはじめ、関連するモジュールが飛躍的に多くなってコードを紹介するのが難しくなります。

そこで、備忘録も兼ねて PyTorch を使った最初の強化学習の関連コードをブログ記事に残しておくことにしました。なお、PyTorch については初心者に毛が生えた程度の知識・経験しかないので、生成 AI 頼みでコーディングしていることをご了承下さい。

過去のティックデータ

ティックデータは、自作アプリを使って、マーケットスピード II RSS の Excel シートから、複数銘柄の株価と出来高を 1 秒毎に読み取っています。取得したデータは銘柄コードをシート名にした Excel ファイルにして保存しています(例 : ticks_20250819.xlsx)。

ティックデータの例
Time	           Price    Volume
1755561677.68986   4036     1826100
1755561678.69288   4036     1826100
1755561679.67502   4031     1879100
1755561680.67299   4031     1879100
1755561681.68757   4030     1895200
...
(以下省略)
...

時間帯は 9:00:00(実際は寄った時刻)からクロージング・オークション前の 15:24:50 までを収集しています。最大データ数は 5 ✕ 60 ✕ 60 + 24 ✕ 60 + 50 = 19,490 になります(ランチタイムはデータがありません)。

Time 列はタイムスタンプに変換しています。また Volume(出来高)はティックデータの場合は累積になります。

複数銘柄のティックデータを収集していますが、評価では銘柄を「三菱重工業 (7011)」に固定しています。

環境クラス

環境クラスの報酬は、まだ行動マスクを前提とした設計になっていません。また、観測空間の特徴量は、テクニカル指標などを加えずに最低限しか揃えていません。

なお、報酬と取引履歴を管理する機能を TransactionManager クラスに分離しています。

env_mask.py
import datetime
from enum import Enum

import gymnasium as gym
import numpy as np
import pandas as pd


class ActionType(Enum):
    HOLD = 0
    BUY = 1
    SELL = 2
    REPAY = 3


class PositionType(Enum):
    NONE = 0
    LONG = 1
    SHORT = 2


class TransactionManager:
    # ナンピンをしない(建玉を1単位しか持たない)売買管理クラス
    def __init__(self):
        self.reward_sell_buy = 0.1  # 約定ボーナスまたはペナルティ(買建、売建)
        self.penalty_repay = -0.05  # 約定ボーナスまたはペナルティ(返済)
        self.reward_pnl_scale = 0.3  # 含み損益のスケール(含み損益✕係数)
        self.reward_hold = 0.001  # 建玉を保持する報酬
        self.penalty_none = -0.001  # 建玉を持たないペナルティ
        self.penalty_rule = -1.0  # 売買ルール違反
        # 売買ルール違反カウンター
        self.penalty_count = 0  # 売買ルール違反ペナルティを繰り返すとカウントを加算

        self.action_pre = ActionType.HOLD
        self.position = PositionType.NONE

        self.price_entry = 0.0
        self.pnl_total = 0.0

        self.dict_transaction = self._init_transaction()
        self.code: str = '7011'

        self.unit: int = 1

    @staticmethod
    def _init_transaction() -> dict:
        return {
            "注文日時": [],
            "銘柄コード": [],
            "売買": [],
            "約定単価": [],
            "約定数量": [],
            "損益": [],
        }

    def _add_transaction(self, t: float, transaction: str, price: float, profit: float = np.nan):
        self.dict_transaction["注文日時"].append(self._get_datetime(t))
        self.dict_transaction["銘柄コード"].append(self.code)
        self.dict_transaction["売買"].append(transaction)
        self.dict_transaction["約定単価"].append(price)
        self.dict_transaction["約定数量"].append(self.unit)
        self.dict_transaction["損益"].append(profit)

    @staticmethod
    def _get_datetime(t: float) -> str:
        return str(datetime.datetime.fromtimestamp(int(t)))

    def clearAll(self):
        """
        初期状態に設定
        :return:
        """
        self.resetPosition()
        self.action_pre = ActionType.HOLD
        self.pnl_total = 0.0
        self.dict_transaction = self._init_transaction()

    def resetPosition(self):
        """
        ポジション(建玉)をリセット
        :return:
        """
        self.position = PositionType.NONE
        self.price_entry = 0.0

    def setAction(self, action: ActionType, t: float, price: float) -> float:
        reward = 0.0
        if action == ActionType.HOLD:
            # ■■■ HOLD: 何もしない
            # 建玉があれば含み損益から報酬を付与、無ければ少しばかりの保持ボーナス
            reward += self._calc_reward_pnl(price)
            # 売買ルールを遵守した処理だったのでペナルティカウントをリセット
            self.penalty_count = 0
        elif action == ActionType.BUY:
            # ■■■ BUY: 信用買い
            if self.position == PositionType.NONE:
                # === 建玉がない場合 ===
                # 買建 (LONG)
                self.position = PositionType.LONG
                self.price_entry = price
                # print(get_datetime(t), "買建", price)
                self._add_transaction(t, "買建", price)
                # 約定ボーナス付与(買建)
                reward += self.reward_sell_buy
                # 売買ルールを遵守した処理だったのでペナルティカウントをリセット
                self.penalty_count = 0
            else:
                # ○○○ 建玉がある場合 ○○○
                # 建玉があるので、含み損益から報酬を付与
                reward += self._calc_reward_pnl(price)
                # ただし、建玉があるのに更に買建 (BUY) しようとしたので売買ルール違反ペナルティも付与
                self.penalty_count += 1
                reward += self.penalty_rule * self.penalty_count
        elif action == ActionType.SELL:
            # ■■■ SELL: 信用空売り
            if self.position == PositionType.NONE:
                # === 建玉がない場合 ===
                # 売建 (SHORT)
                self.position = PositionType.SHORT
                self.price_entry = price
                self._add_transaction(t, "売建", price)
                # 約定ボーナス付与(売建)
                reward += self.reward_sell_buy
                # 売買ルールを遵守した処理だったのでペナルティカウントをリセット
                self.penalty_count = 0
            else:
                # ○○○ 建玉がある場合 ○○○
                # 建玉があるので、含み損益から報酬を付与
                reward += self._calc_reward_pnl(price)
                # ただし、建玉があるのに更に売建しようとしたので売買ルール違反ペナルティも付与
                self.penalty_count += 1
                reward += self.penalty_rule * self.penalty_count
        elif action == ActionType.REPAY:
            # ■■■ REPAY: 建玉返済
            if self.position != PositionType.NONE:
                # ○○○ 建玉がある場合 ○○○
                if self.position == PositionType.LONG:
                    # 実現損益(売埋)
                    profit = price - self.price_entry
                    self._add_transaction(t, "売埋", price, profit)
                else:
                    # 実現損益(買埋)
                    profit = self.price_entry - price
                    self._add_transaction(t, "買埋", price, profit)
                # ポジション状態をリセット
                self.resetPosition()
                # 総収益を更新
                self.pnl_total += profit
                # 報酬に収益を追加
                reward += profit
                # 売買ルールを遵守した処理だったのでペナルティカウントをリセット
                self.penalty_count = 0
            else:
                # === 建玉がない場合 ===
                # 建玉がないのに建玉を返済しようとしたので売買ルール違反、ペナルティを付与
                self.penalty_count += 1
                reward += self.penalty_rule * self.penalty_count
        else:
            raise ValueError(f"{action} is not defined!")
        self.action_pre = action
        return reward

    def _calc_reward_pnl(self, price: float) -> float:
        """
        含み損益に self.reward_pnl_scale を乗じた報酬を算出
        ポジションが無い場合は微小なペナルティを付与
        :param price:
        :return:
        """
        if self.position == PositionType.NONE:
            # PositionType.NONE に対して僅かなペナルティ
            return self.penalty_none
        else:
            reward = 0.0
            if self.position == PositionType.LONG:
                # 含み損益(買建)× 少数スケール
                reward += (price - self.price_entry) * self.reward_pnl_scale
            elif self.position == PositionType.SHORT:
                # 含み損益(売建)× 少数スケール
                reward += (self.price_entry - price) * self.reward_pnl_scale
            reward += self.reward_hold
            return reward


class TradingEnv(gym.Env):
    # 環境クラス
    def __init__(self, df: pd.DataFrame):
        super().__init__()
        self.df = df.reset_index(drop=True)  # Time, Price, Volume のみ
        # ウォームアップ期間
        self.period = 60
        # 特徴量の列名のリストが返る
        self.cols_features = self._add_features()
        # 現在の行位置
        self.current_step = 0
        # 売買管理クラス
        self.transman = TransactionManager()
        # obs: len(self.cols_features) + one-hot(3)
        n_features = len(self.cols_features) + 3
        self.observation_space = gym.spaces.Box(
            low=-np.inf,
            high=np.inf,
            shape=(n_features,),
            dtype=np.float32
        )
        self.action_space = gym.spaces.Discrete(len(ActionType))

    def _add_features(self) -> list:
        # 特徴量の追加
        list_features = list()
        # 調整用係数
        factor_ticker = 10  # 調整因子(銘柄別)
        unit = 100  # 最小取引単位
        # 最初の株価(株価比率の算出用)
        price_start = self.df["Price"].iloc[0]
        # 1. 株価比率
        colname = "PriceRatio"
        self.df[colname] = self.df["Price"] / price_start
        list_features.append(colname)
        # 2. 累計出来高差分 / 最小取引単位
        colname = "dVol"
        self.df[colname] = np.log1p(self.df["Volume"].diff() / unit) / factor_ticker
        list_features.append(colname)
        return list_features

    def _get_action_mask(self) -> np.ndarray:
        # 行動マスク
        if self.current_step < self.period:
            # ウォーミングアップ期間
            return np.array([1, 0, 0, 0], dtype=np.int8)  # 強制 HOLD
        elif self.transman.position == PositionType.NONE:
            # 建玉なし
            return np.array([1, 1, 1, 0], dtype=np.int8)  # HOLD, BUY, SELL
        else:
            # 建玉あり
            return np.array([1, 0, 0, 1], dtype=np.int8)  # HOLD, REPAY

    def _get_observation(self):
        if self.current_step >= self.period:
            features = self.df.iloc[self.current_step][self.cols_features]
        else:
            features = [0] * len(self.cols_features)
        obs = np.array(features, dtype=np.float32)
        # PositionType → one-hot
        pos_onehot = np.eye(3)[self.transman.position.value].astype(np.float32)
        obs = np.concatenate([obs, pos_onehot])
        return obs

    def reset(self, seed=None, options=None):
        self.current_step = 0
        self.transman.clearAll()
        obs = self._get_observation()
        return obs, {"action_mask": self._get_action_mask()}

    def step(self, n_action: int):
        # --- ウォームアップ期間 (self.period) は強制 HOLD ---
        if self.current_step < self.period:
            action = ActionType.HOLD
        else:
            action = ActionType(n_action)
        reward = 0.0
        done = False
        t = self.df.at[self.current_step, "Time"]
        price = self.df.at[self.current_step, "Price"]
        reward += self.transman.setAction(action, t, price)
        obs = self._get_observation()
        if self.current_step >= len(self.df) - 1:
            done = True
        self.current_step += 1
        info = {"pnl_total": self.transman.pnl_total, "action_mask": self._get_action_mask()}
        return obs, reward, done, False, info

PPO エージェント

PPO エージェントと呼んではいますが、PPO, Proximal Policy Optimization(近接方策最適化)のアルゴリズムに必要な下記の処理を集めただけです。Microsoft Copilot に提示してもらったサンプルをほとんどそのまま利用しています。

✅ 構成概要
モジュール 役割 マスク対応
PolicyNetwork 方策ネットワーク forward(obs, mask) で無効行動を除外
ValueNetwork 状態価値関数 通常通り
select_action() 推論・行動選択 Categorical(logits=masked_logits)
compute_loss() PPO損失計算 logits にマスク適用
ReplayBuffer 軌跡保存 action_mask も保存(任意)
agent_mask.py
import torch
import torch.nn as nn
from torch.distributions import Categorical


def compute_ppo_loss(
        policy_net,
        value_net,
        obs, actions,
        old_log_probs,
        returns,
        advantages,
        action_masks
):
    # 📦 PPO損失関数(Clip付き)
    logits = policy_net(obs, action_masks)
    dist = Categorical(logits=logits)
    new_log_probs = dist.log_prob(actions)

    ratio = torch.exp(new_log_probs - old_log_probs)
    clipped_ratio = torch.clamp(ratio, 1 - 0.2, 1 + 0.2)
    policy_loss = -torch.min(ratio * advantages, clipped_ratio * advantages).mean()

    values = value_net(obs).squeeze()
    value_loss = nn.functional.mse_loss(values, returns)

    return policy_loss + 0.5 * value_loss


def select_action(policy_net, obs, action_mask):
    # 🧮 行動選択関数
    logits = policy_net(obs, action_mask)
    dist = Categorical(logits=logits)
    action = dist.sample()
    log_prob = dist.log_prob(action)
    return action.item(), log_prob


class PolicyNetwork(nn.Module):
    # 🎯 方策ネットワーク(マスク対応)
    def __init__(self, obs_dim: int, act_dim: int):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_dim, 128),
            nn.ReLU(),
            nn.Linear(128, act_dim)
        )

    def forward(self, obs, action_mask=None):
        logits = self.net(obs)
        if action_mask is not None:
            logits = logits.masked_fill(action_mask == 0, float('-inf'))
        return logits


class ValueNetwork(nn.Module):
    # 🧠 ValueNetwork の基本構造
    def __init__(self, obs_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 1)  # 出力はスカラー(状態価値)
        )

    def forward(self, obs):
        return self.net(obs)  # shape: [batch_size, 1]

学習用サンプル

学習用サンプルは Microsoft Copilot に提示してもらったサンプルベースにして、ティックファイルの読み込み処理と、学習したモデルを保存する処理を加えました。

sample_training.py
import os

import torch
from torch import optim

from funcs.ios import get_excel_sheet
from funcs.models import get_trained_ppo_model_path
from modules.agent_mask import PolicyNetwork, ValueNetwork, compute_ppo_loss, select_action
from modules.env_mask import TradingEnv
from structs.res import AppRes

if __name__ == "__main__":
    res = AppRes()
    file_excel = "ticks_20250819.xlsx"
    code = "7011"
    path_excel = os.path.join(res.dir_collection, file_excel)
    df = get_excel_sheet(path_excel, code)
    env = TradingEnv(df)

    obs_dim = env.observation_space.shape[0]
    act_dim = env.action_space.n

    """
    ネットワークとオプティマイザの初期化
    """
    # 行動分布を出力する方策ネットワーク
    policy_net = PolicyNetwork(obs_dim, act_dim)
    # 状態価値を推定するネットワーク
    # ValueNetwork は 学習時のAdvantage計算専用
    value_net = ValueNetwork(obs_dim)
    # 両ネットワークのパラメータを同時に更新
    optimizer = optim.Adam(list(policy_net.parameters()) + list(value_net.parameters()), lr=3e-4)

    num_epochs = 3
    gamma = 0.99

    # 学習ループ(PPO)
    for epoch in range(num_epochs):
        obs_list, action_list, logprob_list, reward_list, mask_list = [], [], [], [], []

        # 初期状態とマスク取得
        obs, info = env.reset()
        done = False

        while not done:
            # 環境から得られた観測データ obs を PyTorch のテンソルに変換
            obs_tensor = torch.tensor(obs, dtype=torch.float32)
            # 行動マスク(action_mask)をテンソルに変換
            mask_tensor = torch.tensor(info["action_mask"], dtype=torch.float32)
            # マスク付きで行動分布を生成し、サンプリング、log_prob はPPOの損失計算に必要
            action, log_prob = select_action(policy_net, obs_tensor, mask_tensor)

            """
            1エピソード分の履歴を後でバッチ化して、PPOの損失関数に渡す
            このようにするとエピソード全体を1つのテンソルバッチとして扱えるようになります。
            """
            # 現在の観測(状態)を保存
            # 後で torch.stack(obs_list) によってバッチ化され、方策・価値ネットワークの入力になる
            obs_list.append(obs_tensor)
            # 選択した行動を保存
            # torch.tensor(action) によってテンソル化し、後で損失計算に使用
            action_list.append(torch.tensor(action))
            # 選択した行動の対数確率を保存
            # log_prob は Categorical(...).log_prob(action) で得られた値
            logprob_list.append(log_prob)
            # 行動マスクを保存
            # PPOの損失計算で、マスクされた行動を除外するために使用
            mask_list.append(mask_tensor)

            # 状態遷移と報酬取得
            obs, reward, done, _, info = env.step(action)
            reward_list.append(torch.tensor(reward, dtype=torch.float32))

        """
        PPO(Proximal Policy Optimization)における「割引報酬(Return)」の計算処理
        エピソード全体の報酬履歴から、各時点での累積報酬(Return)を逆順で計算。
        
        PPOでは、状態の価値(value)と実際のReturnとの差分(Advantage)を使って学習
        𝐴_𝑡 = 𝐺_𝑡 − 𝑉(𝑠_𝑡)
        そのため、各ステップのReturn 𝐺_𝑡 を正確に計算しておく必要がある
        """
        # 強化学習では、ある時点 𝑡 における「Return」𝐺_𝑡 は、
        # その時点から将来にわたって得られる報酬の合計
        returns = []
        G = 0
        # 報酬リストを後ろから前へ処理
        for r in reversed(reward_list):
            # 現在の報酬 𝑟 に、次のステップの累積報酬 𝐺 を割引して加える
            # これにより、未来の報酬を考慮した累積値が得られる
            G = r + gamma * G
            # returns の先頭に 𝐺 を挿入することで、元の時間順に戻す
            returns.insert(0, G)

        """
        PPOの損失計算に向けた「バッチ化と前処理」
        """
        # 方策ネットワーク・価値ネットワークの入力
        obs_batch = torch.stack(obs_list)
        # 各ステップで選択した行動(整数)をテンソル化
        # PPOの損失関数では、行動ごとの確率や価値を比較するために必要
        action_batch = torch.stack(action_list)
        # 各行動の対数確率(log_prob)をまとめる
        # PPOでは「新しい方策と古い方策の確率比」を使って損失を計算するため、
        # 過去の確率を記録しておく必要がある
        logprob_batch = torch.stack(logprob_list)
        # 各ステップの割引報酬(Return)をまとめる
        # これは「実際に得られた報酬の累積」であり、
        # 価値ネットワークとの比較に使う
        return_batch = torch.stack(returns)
        # 状態ベクトル obs_batch を価値ネットワークに通して、各ステップの状態価値 𝑉(𝑠_𝑡) を取得
        # .squeeze() によって [T, 1] → [T] に変形(損失計算のため)
        # value_batch = value_net(obs_batch).squeeze()
        value_batch = value_net(obs_batch).squeeze(-1)  # 最後の次元だけを潰す
        # Advantage(利得)の計算
        # 𝐴_𝑡 = 𝐺_𝑡 − 𝑉(𝑠_𝑡)
        # detach() によって、価値ネットワークの勾配計算を切り離す。
        # (方策ネットワークだけを更新するため)
        adv_batch = return_batch - value_batch.detach()
        # 各ステップの行動マスクをまとめる
        # PPOの損失計算で、選択不可能な行動を除外するために使う
        mask_batch = torch.stack(mask_list)
        """
        PPOの損失関数を計算(方策・価値・エントロピー項を含む)
        """
        loss = compute_ppo_loss(
            policy_net,
            value_net,
            obs_batch,
            action_batch,
            logprob_batch,
            return_batch,
            adv_batch,
            mask_batch
        )
        # 勾配をゼロに初期化
        # 前回の勾配情報を消去して、今回の学習ステップに備えるための必須操作
        optimizer.zero_grad()
        # 損失関数から勾配を計算
        loss.backward()
        # 勾配に基づいてパラメータを更新
        optimizer.step()

        print(f"Epoch {epoch + 1}: Loss = {loss.item():.4f}")

    # 学習モデルの保存
    # https://docs.pytorch.org/docs/stable/generated/torch.save.html
    model_path = get_trained_ppo_model_path(res, code)
    os.makedirs(os.path.dirname(model_path), exist_ok=True)
    obj = {
        "policy_state_dict": policy_net.state_dict(),
        "value_state_dict": value_net.state_dict()
    }
    torch.save(obj, model_path)
    print(f"✅ モデルを保存しました: {model_path}")
Epoch 1: Loss = 5.6165
Epoch 2: Loss = 7.1147
Epoch 3: Loss = 5.9576
✅ モデルを保存しました: models/trained/ppo_7011.pth

推論用サンプル

推論用サンプルも Microsoft Copilot に提示してもらったサンプルベースにしています。このサンプルの目的は、設定した方策マスクが確かに効いているかを確認することだったのですが、それだけでは味気が無いので、取引履歴を環境から参照して、1 株あたりの損益も示すようにしました。

sample_inferring.py
import os

import pandas as pd
import torch
from funcs.ios import get_excel_sheet
from funcs.models import get_trained_ppo_model_path
from modules.agent_mask import PolicyNetwork, select_action
from modules.env_mask import TradingEnv
from structs.res import AppRes

if __name__ == "__main__":
    res = AppRes()
    file_excel = "ticks_20250819.xlsx"
    code = "7011"
    path_excel = os.path.join(res.dir_collection, file_excel)
    df = get_excel_sheet(path_excel, code)
    env = TradingEnv(df)

    obs_dim = env.observation_space.shape[0]
    act_dim = env.action_space.n

    # モデルの読み込み
    model_path = get_trained_ppo_model_path(res, code)
    # モデルの状態を読み込み
    checkpoint = torch.load(model_path)
    policy_net = PolicyNetwork(obs_dim, act_dim)
    # ネットワークにパラメータを復元
    policy_net.load_state_dict(checkpoint["policy_state_dict"])
    # .eval() によって推論モードに切り替え(DropoutやBatchNormが無効化)
    policy_net.eval()

    # 推論ループ
    obs, info = env.reset()
    done = False
    while not done:
        # 状態とマスクをテンソル化(PyTorchネットワークに渡すため)
        obs_tensor = torch.tensor(obs, dtype=torch.float32)
        mask_tensor = torch.tensor(info["action_mask"], dtype=torch.float32)
        # マスク付きで行動分布を生成し、サンプリング
        # log_prob は推論では使わないが、ログや分析に活用可能
        action, log_prob = select_action(policy_net, obs_tensor, mask_tensor)

        """
        選択された行動がマスクで禁止されていないかを確認
        mask_tensor[action] == 0 の場合は違反行動(設計ミスやバグの検出に有効)
        select_action() 内部でマスクが正しく適用されていれば、通常は違反は起きない
        """
        if mask_tensor[action] == 0:
            print(f"⚠️ 違反行動: {action}, Mask: {mask_tensor.tolist()}")
        else:
            print(f"✅ 行動: {action}, Mask: {mask_tensor.tolist()}")

        obs, reward, done, _, info = env.step(action)

    df_transaction = pd.DataFrame(env.transman.dict_transaction)
    print(df_transaction)
    print(f"一株当りの損益 : {df_transaction['損益'].sum()} 円")
✅ 行動: 0, Mask: [1.0, 0.0, 0.0, 0.0]
✅ 行動: 0, Mask: [1.0, 0.0, 0.0, 0.0]
✅ 行動: 0, Mask: [1.0, 0.0, 0.0, 0.0]
...
(途中省略)
...
✅ 行動: 2, Mask: [1.0, 1.0, 1.0, 0.0]
✅ 行動: 0, Mask: [1.0, 0.0, 0.0, 1.0]
✅ 行動: 3, Mask: [1.0, 0.0, 0.0, 1.0]
                      注文日時 銘柄コード  売買  約定単価  約定数量   損益
0      2025-08-19 09:02:19  7011  売建  4016     1  NaN
1      2025-08-19 09:02:20  7011  買埋  4016     1  0.0
2      2025-08-19 09:02:22  7011  買建  4016     1  NaN
3      2025-08-19 09:02:23  7011  売埋  4020     1  4.0
4      2025-08-19 09:02:24  7011  買建  4020     1  NaN
...                    ...   ...  ..   ...   ...  ...
10975  2025-08-19 15:24:52  7011  売埋  3915     1  0.0
10976  2025-08-19 15:24:53  7011  買建  3914     1  NaN
10977  2025-08-19 15:24:56  7011  売埋  3913     1 -1.0
10978  2025-08-19 15:24:57  7011  売建  3915     1  NaN
10979  2025-08-19 15:24:59  7011  買埋  3915     1  0.0

[10980 rows x 6 columns]
一株当りの損益 : 38.0 円

損益はプラスになっているものの、こんな高頻度の取引は危なっかしくてできません。あらためて報酬設計をやり直すことになりますが、おそらくは取引回数の上限を設定するアプローチを取ることになりそうです。

次のステップ

次は、学習と推論の処理を PPOAgent と名付けたクラスに組み込んで、GUI から利用しやすいようにします。アルゴリズムのチューニングを含めた細かい調整をした上で、GUI (PySide6) のアプリの別スレッドで非同期に扱えるようにします。

PyTorch に戻って最初に作ったサンプルが、幸運にもエラーに悩まされること無く、あっさり動作確認が出来てしまいました。今後 GUI 化していく過程で初期のサンプルは削除されてしまうので、たとえレポジトリを掘り起こせば見つけ出すことが可能であっても、出発点としてブログに残しておきたくなりました。全くの自己満足のためです。🙇🏻

次回こそは、そこそこ使えそうな推論結果が出せることを目指します。

ちなみに、一連の強化学習のコーディングでは、どうやら Google Gemini より Microsoft Copilot との方が、自分との相性が良さそうです。いや(無料プランで利用する限りでは)コードの質が高いように感じます。ただ、Copilot の方が無駄なおべっかが多くてうんざりしていたのですが、最近は平気でスルーできるようになりました。

参考サイト

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

0 件のコメント:

コメントを投稿