【資産運用×自動化】株アルゴリズムをPythonで自作して長期運用する完全ロードマップ決定版

自動化・運用

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

株式市場における「大口注文」は、相場に大きな影響を与えます。機関投資家は、数百万株、数十億円といった膨大な注文を市場に流す際、その影響を最小化するための特殊なアルゴリズムを使用しています。TWAP(Time Weighted Average Price)、VWAP(Volume Weighted Average Price)、POV(Percentage of Volume)といったアルゴリズムです。

個人投資家の視点から見ると、これらのアルゴリズムは「相手を知ること」として重要な学習対象になります。なぜなら、機関投資家のこうした発注行動を理解することで、市場で起こっている現象を読み解き、より賢い投資判断ができるようになるからです。

本記事では、TWAP・VWAP・POVの仕組みを初心者向けに徹底解説し、Pythonを使ってこれらのアルゴリズムがどのように市場に影響を与えるかを分析します。

機関投資家の「大口注文問題」とアルゴリズムの誕生

なぜアルゴリズムが必要なのか

想像してください。あなたが突然、「1日で100万株を買いたい」と思ったとします。市場全体の1日の出来高が500万株だとすれば、あなたの注文は全体出来高の20%に相当します。

もし、この100万株を一度に注文したら、何が起こるでしょう?

【悪いシナリオ】
1. 巨大な買い注文が板に表示される
2. 他のトレーダーが「大きな買いが来ている」と気付く
3. 先回り買い(フロントランニング)で価格が上昇
4. あなたの買値が平均で+5%高くなる
5. 100万株 × 平均株価2,500円 × 5% = 1,250万円の余分なコスト

機関投資家にとって、このような「市場への悪影響(インパクト)」は致命的です。そこで生まれたのが、大口注文を小分けにして执行し、市場への影響を最小化するアルゴリズムです。

TWAP・VWAP・POVの基本概念

TWAP(Time Weighted Average Price):時間分割

仕組み:
大口注文を等間隔の時間で分割し、定期的に市場に出します。

:
100万株を5等分して、1日を5時間に分割し、1時間ごとに20万株ずつ買う。

時間帯別の発注パターン:

09:00-09:30   09:30-10:00   10:00-10:30   ...
────────────────────────────────────────
  20万株       20万株       20万株

メリット:

  • 計算が単純(時間で等分するだけ)
  • 市場の価格変動に左右されない

デメリット:

  • 出来高が少ない時間帯に注文が集中すると、相場を動かしてしまう
  • 価格が急上昇する時間帯があっても、機械的に買い続けるため、高値掴みのリスク

VWAP(Volume Weighted Average Price):出来高分割

仕組み:
過去の出来高パターンに基づいて、大口注文を出来高に比例させて分割します。

:
100万株を、市場全体の出来高分布に合わせて発注。

出来高別の発注パターン:

時間帯    出来高シェア   発注量
────────────────────────────
09:00-10:00    20%      20万株
10:00-11:00    30%      30万股
11:00-12:00    15%      15万株
...etc

メリット:

  • 市場の実際の流動性に合わせるため、相場への影響が少ない
  • 高出来高の時間帯に大量発注するため、個別の注文の痕跡が薄れる

デメリット:

  • 過去の出来高パターンが将来も続くという仮定が破綻することがある
  • 計算が複雑

POV(Percentage of Volume):相対的出来高

仕組み:
リアルタイムで市場全体の出来高を監視し、その一定比率(例:10%)の注文を逐次発注します。

:
市場全体で毎分100万株が取引されている場合、POV 10%なら毎分10万株を発注。

リアルタイム出来高監視:

実時間    市場出来高    POV 10%発注量
──────────────────────────────
09:30     100万株      10万株
09:31     120万株      12万株
09:32     80万株       8万株

メリット:

  • 市場の流動性にリアルタイムで適応する
  • 相場が活発な時間帯に多く買い、閑散時は抑制される
  • 執行が「自然」に見える

デメリット:

  • 市場流動性が枯渇すると、執行が遅延する
  • 相場が急騰する場面で追加発注が困難になる可能性

三者の比較表

特性TWAPVWAPPOV
分割基準時間過去出来高パターンリアルタイム出来高
計算複雑度
相場適応性
市場への影響大(特に出来高が少ない時間帯)
機関投資家の使用頻度
実装難度容易中程度困難

【コピペOK】Pythonでアルゴリズム注文をシミュレートする

では、これら3つのアルゴリズムを Python で実装し、その振る舞いを分析します。

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt

# ==============================
# 設定エリア
# ==============================
SYMBOL = "7203.T"                    # トヨタ自動車
ORDER_SIZE = 100000                  # 発注総量:100万株(仮想)
ANALYSIS_PERIOD = "1mo"              # 分析期間:1ヶ月
TARGET_EXECUTION_TIME = 1            # 執行時間:1営業日

# ==============================
# ステップ1:市場データの取得と分析
# ==============================
def fetch_market_data(symbol, period=ANALYSIS_PERIOD):
    """
    市場データを取得

    Args:
        symbol (str): 銘柄コード
        period (str): データ期間

    Returns:
        pd.DataFrame: 市場データ
    """
    print(f"[{datetime.now()}] {symbol} のデータ取得中...")

    try:
        ticker = yf.Ticker(symbol)
        df = ticker.history(period=period, interval='1d')

        if df.empty:
            print("データ取得失敗")
            return None

        print(f"取得完了: {len(df)}営業日分")
        return df

    except Exception as e:
        print(f"エラー: {e}")
        return None

def analyze_volume_pattern(df):
    """
    出来高パターンを分析(営業時間内の分布を推定)

    Args:
        df (pd.DataFrame): 日次データ

    Returns:
        dict: 時間帯別出来高分布
    """
    print("\n【出来高パターン分析】")

    # 営業時間(09:00-15:00)を5つに分割
    time_periods = {
        '09:00-10:00': 0.15,    # 寄り付き直後(出来高少ない)
        '10:00-11:00': 0.25,    # 前場後半(活発)
        '11:00-12:00': 0.20,    # 昼間
        '12:00-14:00': 0.20,    # 昼休以後
        '14:00-15:00': 0.20,    # 大引け前(活発)
    }

    print("推定時間帯別出来高分布:")
    for period, share in time_periods.items():
        print(f"  {period}: {share*100:.0f}%")

    return time_periods

# ==============================
# ステップ2:TWAP アルゴリズムの実装
# ==============================
class TWAPExecutor:
    """TWAP(Time Weighted Average Price)注文を実行"""

    def __init__(self, order_size, num_splits):
        """
        Args:
            order_size (int): 総注文数量
            num_splits (int): 分割数
        """
        self.order_size = order_size
        self.num_splits = num_splits
        self.split_size = order_size / num_splits

    def generate_schedule(self, start_price):
        """
        執行スケジュールを生成

        Args:
            start_price (float): 開始時の価格

        Returns:
            pd.DataFrame: 執行スケジュール
        """
        schedule = []

        # 営業時間を等間隔に分割
        time_slots = [
            '09:00', '09:48', '10:36', '11:24', '12:12'
        ]

        for i, time_slot in enumerate(time_slots[:self.num_splits]):
            # 各時間帯の価格を仮想的に生成(ランダムノイズを追加)
            noise = np.random.normal(0, start_price * 0.01)  # ±1%のノイズ
            price = start_price + noise

            schedule.append({
                'time': time_slot,
                'quantity': int(self.split_size),
                'price': price,
                'order_type': 'TWAP',
            })

        return pd.DataFrame(schedule)

    def calculate_execution_price(self, prices):
        """
        平均執行価格を計算

        Args:
            prices (list): 各分割の実行価格

        Returns:
            float: 加重平均価格
        """
        return np.mean(prices)

# ==============================
# ステップ3:VWAP アルゴリズムの実装
# ==============================
class VWAPExecutor:
    """VWAP(Volume Weighted Average Price)注文を実行"""

    def __init__(self, order_size, volume_distribution):
        """
        Args:
            order_size (int): 総注文数量
            volume_distribution (dict): 時間帯別出来高分布
        """
        self.order_size = order_size
        self.volume_distribution = volume_distribution

    def generate_schedule(self, start_price):
        """
        出来高分布に基づいて執行スケジュールを生成

        Args:
            start_price (float): 開始時の価格

        Returns:
            pd.DataFrame: 執行スケジュール
        """
        schedule = []

        time_periods = [
            ('09:00-10:00', '09:30'),
            ('10:00-11:00', '10:30'),
            ('11:00-12:00', '11:30'),
            ('12:00-14:00', '13:00'),
            ('14:00-15:00', '14:30'),
        ]

        for period, time_slot in time_periods:
            quantity = int(self.order_size * self.volume_distribution[period])

            # 時間帯による価格変動をシミュレート
            period_effect = np.random.normal(0, start_price * 0.01)
            price = start_price + period_effect

            schedule.append({
                'period': period,
                'time': time_slot,
                'quantity': quantity,
                'price': price,
                'order_type': 'VWAP',
            })

        return pd.DataFrame(schedule)

    def calculate_execution_price(self, prices, quantities):
        """
        出来高加重平均価格を計算

        Args:
            prices (list): 各分割の実行価格
            quantities (list): 各分割の数量

        Returns:
            float: 加重平均価格
        """
        return np.average(prices, weights=quantities)

# ==============================
# ステップ4:POV アルゴリズムの実装
# ==============================
class POVExecutor:
    """POV(Percentage of Volume)注文を実行"""

    def __init__(self, order_size, pov_ratio, market_volumes):
        """
        Args:
            order_size (int): 総注文数量
            pov_ratio (float): 市場出来高に対する比率(例:0.1 = 10%)
            market_volumes (dict): 時間帯別市場出来高
        """
        self.order_size = order_size
        self.pov_ratio = pov_ratio
        self.market_volumes = market_volumes

    def generate_schedule(self, start_price, market_volumes_intraday):
        """
        リアルタイム出来高に基づいて執行スケジュールを生成

        Args:
            start_price (float): 開始時の価格
            market_volumes_intraday (dict): 時間帯別市場出来高

        Returns:
            pd.DataFrame: 執行スケジュール
        """
        schedule = []

        time_periods = [
            ('09:30', 1000000),     # 寄り付き直後:100万株
            ('10:30', 1200000),     # 活発時:120万株
            ('11:30', 900000),      # 昼間:90万株
            ('13:00', 950000),      # 昼休後:95万株
            ('14:30', 1100000),     # 大引け前:110万株
        ]

        total_scheduled = 0

        for time_slot, market_volume in time_periods:
            # POV比率で注文量を決定
            pov_quantity = int(market_volume * self.pov_ratio)

            # 総注文量を超えないよう調整
            if total_scheduled + pov_quantity > self.order_size:
                pov_quantity = self.order_size - total_scheduled

            price = start_price + np.random.normal(0, start_price * 0.01)

            schedule.append({
                'time': time_slot,
                'market_volume': market_volume,
                'quantity': pov_quantity,
                'price': price,
                'order_type': 'POV',
            })

            total_scheduled += pov_quantity

        return pd.DataFrame(schedule)

    def calculate_execution_price(self, prices, quantities):
        """
        加重平均価格を計算

        Args:
            prices (list): 各分割の実行価格
            quantities (list): 各分割の数量

        Returns:
            float: 加重平均価格
        """
        return np.average(prices, weights=quantities)

# ==============================
# ステップ5:シミュレーション結果の比較
# ==============================
class AlgorithmComparison:
    """3つのアルゴリズムを比較"""

    @staticmethod
    def run_comparison(symbol, order_size):
        """
        比較を実行

        Args:
            symbol (str): 銘柄コード
            order_size (int): 注文数量
        """
        print("="*70)
        print("アルゴリズム注文シミュレーション")
        print("="*70)

        # データ取得
        df = fetch_market_data(symbol)
        if df is None:
            return

        start_price = df['Close'].iloc[0]
        volume_dist = analyze_volume_pattern(df)

        # TWAP シミュレーション
        print("\n【TWAP シミュレーション】")
        twap = TWAPExecutor(order_size, num_splits=5)
        twap_schedule = twap.generate_schedule(start_price)
        twap_avg_price = twap.calculate_execution_price(twap_schedule['price'].tolist())
        twap_cost = order_size * twap_avg_price

        print(f"分割数: 5")
        print(f"各分割量: {int(order_size/5):,}株")
        print(f"平均執行価格: ¥{twap_avg_price:,.0f}")
        print(f"総取引額: ¥{twap_cost:,.0f}")
        print(f"\n【TWAP スケジュール】")
        print(twap_schedule)

        # VWAP シミュレーション
        print("\n【VWAP シミュレーション】")
        vwap = VWAPExecutor(order_size, volume_dist)
        vwap_schedule = vwap.generate_schedule(start_price)
        vwap_avg_price = vwap.calculate_execution_price(
            vwap_schedule['price'].tolist(),
            vwap_schedule['quantity'].tolist()
        )
        vwap_cost = order_size * vwap_avg_price

        print(f"平均執行価格: ¥{vwap_avg_price:,.0f}")
        print(f"総取引額: ¥{vwap_cost:,.0f}")
        print(f"\n【VWAP スケジュール】")
        print(vwap_schedule)

        # POV シミュレーション
        print("\n【POV シミュレーション】")
        pov = POVExecutor(order_size, pov_ratio=0.1, market_volumes={})
        pov_schedule = pov.generate_schedule(start_price, {})
        pov_avg_price = pov.calculate_execution_price(
            pov_schedule['price'].tolist(),
            pov_schedule['quantity'].tolist()
        )
        pov_cost = order_size * pov_avg_price

        print(f"POV比率: 10%")
        print(f"平均執行価格: ¥{pov_avg_price:,.0f}")
        print(f"総取引額: ¥{pov_cost:,.0f}")
        print(f"\n【POV スケジュール】")
        print(pov_schedule)

        # 比較サマリー
        print("\n" + "="*70)
        print("比較サマリー")
        print("="*70)

        print(f"\n現物買付(仮想)価格: ¥{start_price:,.0f}")
        print(f"\n【平均執行価格の比較】")
        print(f"TWAP: ¥{twap_avg_price:,.0f} (差分: ¥{twap_avg_price - start_price:+,.0f})")
        print(f"VWAP: ¥{vwap_avg_price:,.0f} (差分: ¥{vwap_avg_price - start_price:+,.0f})")
        print(f"POV : ¥{pov_avg_price:,.0f}  (差分: ¥{pov_avg_price - start_price:+,.0f})")

        # 最も効率的なアルゴリズム
        algorithms = {
            'TWAP': twap_avg_price,
            'VWAP': vwap_avg_price,
            'POV': pov_avg_price,
        }

        best_algo = min(algorithms, key=algorithms.get)
        print(f"\n【最適アルゴリズム】")
        print(f"{best_algo} が最も低い平均実行価格で執行できました")

        worst_price_difference = max(algorithms.values()) - min(algorithms.values())
        print(f"最高と最低の差: ¥{worst_price_difference:,.0f}")
        print(f"全注文額に対する影響: {worst_price_difference/start_price*100:.2f}%")

# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    AlgorithmComparison.run_comparison(SYMBOL, ORDER_SIZE)

このコードの処理フロー:

  • TWAPExecutor: 時間で等分割
  • VWAPExecutor: 過去出来高パターンで分割
  • POVExecutor: リアルタイム出来高で対応的に分割
  • 3つのアルゴリズムを同じ条件で比較

個人投資家が「アルゴリズム注文」を読み解く方法

パターン認識:板の動きから注文意図を推測

実際の市場では、機関投資家の大口注文が「痕跡」を残します。

【観察パターン】

TWAP型:
時間帯ごとに一定量の買いが入る
→ 時間が経つごとに「買い板」が更新される

VWAP型:
出来高が多い時間帯に大量の買いが入る
→ 「活発な時間帯」に買い圧力が集中

POV型:
市場出来高に比例して買いが入る
→ 相場が活発になると「それに応じて」買い圧力も増す

対抗戦略:大口注文の前に買う

もし、あなたが「機関投資家の大口買い注文が来ている」ことに気付いたら?

【先回り戦略】

1. TWAP型 → 時間分割が予測できる
   → 各時間帯の直前に小口で買い、
     大口が来た後に売却

2. VWAP型 → 出来高パターンが決まっている
   → 出来高が多くなる時間帯の直前に買い
     → 大口注文と一緒に売却

3. POV型 → 相場に適応的に動く
   → 予測が困難だが、相場が急上昇している時は
     POVが多く発注される可能性があり、
     その後の「反動売り」を狙う

よくあるエラーと対処法

「市場出来高データが取得できません」

原因: yfinanceが日中の細かい出来高データを提供していない

対処法:

日中出来高は、より高度なデータソース(ポーランドテクノロジー、Polygon.ioなど)が必要です。本記事は教育用で、推定値を使用しています。

「シミュレーション結果がランダムに変動します」

原因: コード内で np.random.normal() を使用しているため、実行するたびに異なる結果が出ます。

対処法:

乱数シードを固定:

np.random.seed(42)  # コードの先頭に追加

まとめ

本記事では、機関投資家が使う3つのアルゴリズム注文(TWAP・VWAP・POV)の仕組みを徹底解説しました。

要点を整理します。

  • TWAP: 時間で等分割。シンプルだが、出来高が少ない時間帯で相場を動かすリスクがある
  • VWAP: 過去出来高パターンで分割。市場への影響が少なく、機関投資家が最も使う方法
  • POV: リアルタイム出来高に対応的に分割。最も自然に見えるが、実装が複雑
  • 個人投資家は、板の動きからこれらの注文パターンを「読む」ことで、市場先導機関の意図を推測できる
  • 「先回り買い」は高リスク・高リターンの戦略。慎重に検討が必要

本シリーズを通じて、個人投資家がアルゴリズム取引システムを構築し、さらには市場で起こっている現象を読み解く力を身に付けることができました。

次のステップは、本記事で学んだ「相手を知る」という視点を、あなた自身のアルゴリズムに組み込むことです。単に「自分の取引ルール」だけでなく、「市場全体で起こっていることへの理解」を深めることが、長期的な投資成功の鍵になります。

タイトルとURLをコピーしました