👅

日経先物分析その4...開けた窓はいつ埋めるのか

2024/01/21に公開

はじめに

ニュースなどを聞いていると
「日経平均株価は窓を開けて続伸し...」
などと言っているのを耳にします。そもそも窓とは何なのでしょうか。
窓とは前日始値から終値の範囲外から(あるいは高安値)翌日の取引が始まった場合の隙間の事を指しているようです。

「開けた窓はいずれ閉める」などの言葉を見る事もありますが、本当に窓を閉めるのでしょうか。
日経平均株価を検証しても使えないので、今回は日経先物miniのデータを使用し、Session別に価格を集計、窓を埋めるまでの時間を計測してみます。

実行環境

pythonの実行環境はGoogle Colaboratoryを想定しています。
https://colab.research.google.com/?hl=ja

Installされていない、またはUp gradeが必要なライブラリは以下の通りです。

# colabでライブラリをup gradeする場合、Installが終わったらセッションを再起動してください。
# upgradeしないと使用できないモジュールがあるので必ずupgradeしてください。
!pip install japanize_matplotlib
!pip install --upgrade polars==20

データ

今回は日経先物miniの1分足データを使用します。

Python

Import

from dataclasses import dataclass
from dataclasses import asdict
import datetime

import japanize_matplotlib
from matplotlib import pyplot as plt
import polars as pl
import rich
import seaborn as sns
from tqdm.notebook import tqdm
pl.Config.set_tbl_rows(30)
sns.set(style='whitegrid')
japanize_matplotlib.japanize()

ファイルの読み込み

fp = 'ファイルのパス'
data = pl.read_parquet(fp)
print(data.head())
print(data.tail())
OUTPUT
┌─────────────────────┬───────┬───────┬───────┬───────┐
│ datetime            ┆ op    ┆ hi    ┆ lw    ┆ cl    │
│ ---------------   │
│ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆ i64   │
╞═════════════════════╪═══════╪═══════╪═══════╪═══════╡
│ 2013-01-04 09:00:0010750107651074510750 │
│ 2013-01-04 09:01:0010750107651073510745 │
│ 2013-01-04 09:02:0010740107501074010745 │
│ 2013-01-04 09:03:0010745107501074010745 │
│ 2013-01-04 09:04:0010740107501074010745 │
└─────────────────────┴───────┴───────┴───────┴───────┘

┌─────────────────────┬───────┬───────┬───────┬───────┐
│ datetime            ┆ op    ┆ hi    ┆ lw    ┆ cl    │
│ ---------------   │
│ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆ i64   │
╞═════════════════════╪═══════╪═══════╪═══════╪═══════╡
│ 2023-12-30 05:56:0033280332803328033280 │
│ 2023-12-30 05:57:0033280332803328033280 │
│ 2023-12-30 05:58:0033280332803328033280 │
│ 2023-12-30 05:59:0033280332803328033280 │
│ 2023-12-30 06:00:0033255332553325533255 │
└─────────────────────┴───────┴───────┴───────┴───────┘

DaySessionとNightSessionの要素を追加

今回窓を判断する基準として、1日の取引で判断するのではなく

  • DaySession(現在は8:45~15:15)
  • NightSession(現在は16:30~6:00)

で判断していきます。

data = (
    data
    # 日時の列から時刻のみ取り出し、NightSessionとDaySessionに分類する
    .with_columns([
        pl.col('datetime').dt.time().alias('time')
    ])
    .with_columns([
        # 時刻の情報を使用してSession名を入力する
        pl.when(
            (datetime.time(8, 0) <= pl.col('time'))
            & (pl.col('time') <= datetime.time(16, 0))
            )
            .then(pl.lit('Day Session'))
            .otherwise(pl.lit('Night Session'))
            .alias('Session')
    ])
    .with_columns([
        # Session毎にUniqueなIDを入力する
        pl.when(pl.col('Session').shift() != pl.col('Session'))
            .then(1)
            .otherwise(0)
            .cum_sum()
            .alias('Session ID')
    ])
)
print(data.head())
OUTPUT
┌─────────────────────┬───────┬───────┬───────┬───────┬──────────┬─────────────┬────────────┐
│ datetime            ┆ op    ┆ hi    ┆ lw    ┆ cl    ┆ time     ┆ Session     ┆ Session ID │
│ ------------------------        │
│ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆ i64   ┆ time     ┆ str         ┆ i32        │
╞═════════════════════╪═══════╪═══════╪═══════╪═══════╪══════════╪═════════════╪════════════╡
│ 2013-01-04 09:00:001075010765107451075009:00:00 ┆ Day Session ┆ 0          │
│ 2013-01-04 09:01:001075010765107351074509:01:00 ┆ Day Session ┆ 0          │
│ 2013-01-04 09:02:001074010750107401074509:02:00 ┆ Day Session ┆ 0          │
│ 2013-01-04 09:03:001074510750107401074509:03:00 ┆ Day Session ┆ 0          │
│ 2013-01-04 09:04:001074010750107401074509:04:00 ┆ Day Session ┆ 0          │
└─────────────────────┴───────┴───────┴───────┴───────┴──────────┴─────────────┴────────────┘

┌─────────────────────┬───────┬───────┬───────┬───────┬──────────┬───────────────┬────────────┐
│ datetime            ┆ op    ┆ hi    ┆ lw    ┆ cl    ┆ time     ┆ Session       ┆ Session ID │
│ ------------------------        │
│ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆ i64   ┆ time     ┆ str           ┆ i32        │
╞═════════════════════╪═══════╪═══════╪═══════╪═══════╪══════════╪═══════════════╪════════════╡
│ 2013-01-04 16:30:001070510710107001070516:30:00 ┆ Night Session ┆ 1          │
│ 2013-01-04 16:31:001070510715107051071516:31:00 ┆ Night Session ┆ 1          │
│ 2013-01-04 16:32:001071510720107101072016:32:00 ┆ Night Session ┆ 1          │
│ 2013-01-04 16:33:001071510725107151072516:33:00 ┆ Night Session ┆ 1          │
│ 2013-01-04 16:34:001072010725107201072516:34:00 ┆ Night Session ┆ 1          │
└─────────────────────┴───────┴───────┴───────┴───────┴──────────┴───────────────┴────────────┘

Session IDごとに集計処理する

# SessionIDごとに集計して、翌日の始値をシフトしておきます。
session_df = (
    data
    .group_by('Session ID')
        .agg([
            pl.col(['Session', 'datetime', 'op']).first(),
            pl.col('hi').max(),
            pl.col('lw').min(),
            pl.col('cl').last()
        ])
    .sort('Session ID')
    .with_columns([
        pl.col('op').shift(-1).alias('next_op')
    ])
)
print(session_df.head())
OUTPUT
┌────────────┬───────────────┬─────────────────────┬───────┬───────┬───────┬───────┬─────────┐
│ Session ID ┆ Session       ┆ datetime            ┆ op    ┆ hi    ┆ lw    ┆ cl    ┆ next_op │
│ ------------------------     │
│ i32        ┆ str           ┆ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆ i64   ┆ i64     │
╞════════════╪═══════════════╪═════════════════════╪═══════╪═══════╪═══════╪═══════╪═════════╡
│ 0          ┆ Day Session   ┆ 2013-01-04 09:00:001075010765106501068510705   │
│ 1          ┆ Night Session ┆ 2013-01-04 16:30:001070510805106701075010740   │
│ 2          ┆ Day Session   ┆ 2013-01-07 09:00:001074010745105851062010615   │
│ 3          ┆ Night Session ┆ 2013-01-07 16:30:001061510625105351057510545   │
│ 4          ┆ Day Session   ┆ 2013-01-08 09:00:001054510600104651048010460   │
└────────────┴───────────────┴─────────────────────┴───────┴───────┴───────┴───────┴─────────┘

窓の幅と窓埋めを判断するターゲット価格を取得

今回は窓を2種類の定義で考えます。

  • 前Session始値から終値の範囲外から翌Sessionの取引価格が始まった
  • 前Session安値から高値の範囲外から翌Sessionの取引価格が始まった

窓を始値と終値から考える場合の処理

select_cols = ['datetime', 'gap', 'trg']
oc_gaps = (
    session_df
    .with_columns([
        # 始値と終値の高い方を取得
        pl.when(pl.col('op') < pl.col('cl'))
            .then(pl.col('cl'))
            .otherwise(pl.col('op'))
            .alias('upper'),
        # 始値と終値の低い方を取得
        pl.when(pl.col('cl') <= pl.col('op'))
            .then(pl.col('cl'))
            .otherwise(pl.col('op'))
            .alias('lower'),
    ])
    .with_columns([
        # 窓の計算
        # 続伸で窓を開けた場合
        pl.when(pl.col('upper') < pl.col('next_op'))
            .then(pl.col('next_op') - pl.col('upper'))
            # 続落で窓を開けた場合
            .when(pl.col('next_op') < pl.col('lower'))
            .then(pl.col('next_op') - pl.col('lower'))
            .otherwise(0)
            .alias('gap')
    ])
    .with_columns([
        # 窓の距離をシフト
        pl.col('gap').shift().fill_null(0),
        # 窓埋めを判断するターゲット価格を取得
        pl.col('cl').shift().alias('trg')
    ])
    # 必要な列のみ抽出
    .select(select_cols)
    # 窓を開けた行のみ抽出
    .filter(pl.col('gap') != 0)
)

print(oc_gaps.head())
OUTPUT
┌─────────────────────┬─────┬───────┐
│ datetime            ┆ gap ┆ trg   │
│ ---------   │
│ datetime[μs]        ┆ i64 ┆ i64   │
╞═════════════════════╪═════╪═══════╡
│ 2013-01-07 16:30:00-510620 │
│ 2013-01-08 09:00:00-3010575 │
│ 2013-01-08 16:30:00-2010480 │
│ 2013-01-09 09:00:00-1010405 │
│ 2013-01-09 16:30:002010565 │
└─────────────────────┴─────┴───────┘

窓を高値と安値から考える場合

hl_gaps = (
    session_df
    .with_columns([
        # 窓の計算
        # 続伸で窓を開けた場合
        pl.when(pl.col('hi') < pl.col('next_op'))
            .then(pl.col('next_op') - pl.col('hi'))
            # 続落で窓を開けた場合
            .when(pl.col('next_op') < pl.col('lw'))
            .then(pl.col('next_op') - pl.col('lw'))
            .otherwise(0)
            .alias('gap'),
    ])
    .with_columns([
        # 窓の距離をシフト
        pl.col('gap').shift().fill_null(0),
        # 窓埋めを判断するTarget価格を取得
        pl.when(pl.col('gap') < 0)
            .then(pl.col('lw').shift())
            .otherwise(pl.col('hi').shift())
            .alias('trg')
    ])
    # 必要な列のみ抽出
    .select(select_cols)
    # 窓を開けた行のみ抽出
    .filter(pl.col('gap') != 0)
)

print(hl_gaps.head())
OUTPUT
┌─────────────────────┬─────┬───────┐
│ datetime            ┆ gap ┆ trg   │
│ ---------   │
│ datetime[μs]        ┆ i64 ┆ i64   │
╞═════════════════════╪═════╪═══════╡
│ 2013-01-08 16:30:00-510600 │
│ 2013-01-10 09:00:00510635 │
│ 2013-01-11 09:00:005510755 │
│ 2013-01-18 09:00:001510830 │
│ 2013-01-23 16:30:00-1010660 │
└─────────────────────┴─────┴───────┘

データを見てみる

all = session_df.shape[0]
oc_count = oc_gaps.shape[0]
hl_count = hl_gaps.shape[0]
fmt = '%Y-%m-%d'
start = data['datetime'].min().date().strftime(fmt)
stop = data['datetime'].max().date().strftime(fmt)
print(f"{start} から {stop} までのSesison数は {all} 回です。")
print(f'始値と終値からの窓: {oc_count} ({round(oc_count / all * 100, 1)}%)')
print(f'安値と高値からの窓: {hl_count} ({round(hl_count / all * 100, 1)}%)')

2013-01-04 から 2023-12-30 までのSesison数は 5404 回です。
始値と終値からの窓: 3020 (55.9%)
安値と高値からの窓: 1092 (20.2%)

オリジナルのデータへ結合

oc_df = (
    data
    .drop('time')
    .join(oc_gaps, on='datetime', how='outer')
)
hl_df = (
    data
    .drop('time')
    .join(hl_gaps, on='datetime', how='outer')
)

print(oc_df.head())
print(oc_df.filter(pl.col('Session ID') == 5).head())
OUTPUT
┌─────────────────────┬───────┬───────┬───────┬───┬────────────┬────────────────┬──────┬──────┐
│ datetime            ┆ op    ┆ hi    ┆ lw    ┆ … ┆ Session ID ┆ datetime_right ┆ gap  ┆ trg  │
│ ------------   ┆   ┆ ------------  │
│ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆   ┆ i32        ┆ datetime[μs]   ┆ i64  ┆ i64  │
╞═════════════════════╪═══════╪═══════╪═══════╪═══╪════════════╪════════════════╪══════╪══════╡
│ 2013-01-04 09:00:00107501076510745 ┆ … ┆ 0          ┆ null           ┆ null ┆ null │
│ 2013-01-04 09:01:00107501076510735 ┆ … ┆ 0          ┆ null           ┆ null ┆ null │
│ 2013-01-04 09:02:00107401075010740 ┆ … ┆ 0          ┆ null           ┆ null ┆ null │
│ 2013-01-04 09:03:00107451075010740 ┆ … ┆ 0          ┆ null           ┆ null ┆ null │
│ 2013-01-04 09:04:00107401075010740 ┆ … ┆ 0          ┆ null           ┆ null ┆ null │
└─────────────────────┴───────┴───────┴───────┴───┴────────────┴────────────────┴──────┴──────┘

┌──────────────┬───────┬───────┬───────┬───┬────────────┬─────────────────────┬──────┬───────┐
│ datetime     ┆ op    ┆ hi    ┆ lw    ┆ … ┆ Session ID ┆ datetime_right      ┆ gap  ┆ trg   │
│ ------------   ┆   ┆ ------------   │
│ datetime[μs] ┆ i64   ┆ i64   ┆ i64   ┆   ┆ i32        ┆ datetime[μs]        ┆ i64  ┆ i64   │
╞══════════════╪═══════╪═══════╪═══════╪═══╪════════════╪═════════════════════╪══════╪═══════╡
│ 2013-01-08104601046510455 ┆ … ┆ 52013-01-08 16:30:00-2010480 │
│ 16:30:00     ┆       ┆       ┆       ┆   ┆            ┆                     ┆      ┆       │
│ 2013-01-08104651046510455 ┆ … ┆ 5          ┆ null                ┆ null ┆ null  │
│ 16:31:00     ┆       ┆       ┆       ┆   ┆            ┆                     ┆      ┆       │
│ 2013-01-08104601046010445 ┆ … ┆ 5          ┆ null                ┆ null ┆ null  │
│ 16:32:00     ┆       ┆       ┆       ┆   ┆            ┆                     ┆      ┆       │
│ 2013-01-08104451045510445 ┆ … ┆ 5          ┆ null                ┆ null ┆ null  │
│ 16:33:00     ┆       ┆       ┆       ┆   ┆            ┆                     ┆      ┆       │
│ 2013-01-08104501045510445 ┆ … ┆ 5          ┆ null                ┆ null ┆ null  │
│ 16:34:00     ┆       ┆       ┆       ┆   ┆            ┆                     ┆      ┆       │
└──────────────┴───────┴───────┴───────┴───┴────────────┴─────────────────────┴──────┴───────┘

窓埋めまでの挙動を記録する

あとで分析しやすい様に

  • SessionID
  • 窓を開けたSession名
  • 窓を開け始めた日時
  • 窓を閉めた日時
  • 窓の幅
  • 窓のタイプ(ギャップアップ・ギャップダウン)
  • 窓を開けていた取引時間(分)
  • 窓を開けていた現実の時間(分)
  • 窓を開けていた期間に逆方向に動いた距離

を記録します。今回はなるべくpolarsだけで計算しましたが、恐らくfilter関数がボトルネックとなっていて計算が遅いです。

記録をとる関数を作成

@dataclass
class GapDetails:
    session_id: int = None
    space: str = None
    start: datetime.datetime = None
    stop: datetime.datetime = None
    gap: int = None
    gap_type: str = None
    trading_time: int = None
    timedelta: int = None
    reverse_move: int = None
    dict = asdict


def gap_report(df, session_id):
    """
    窓埋めまでの挙動を記録する
    ※filterで適当にやってしまいましたが、結構計算には時間が掛かります
    """
    rows = df.filter(session_id <= pl.col('Session ID'))

    # 開始時間を取得
    start = rows['datetime'][0]
    # 窓の距離を取得
    gap = rows['gap'][0]
    # 窓を開ける前の価格を取得
    trg = rows['trg'][0]

    # 窓の方向を取得する
    if gap is None:
        return GapDetails()
    elif gap < 0:
        side = True
        gap_type = 'gap_down'
    else:
        side = False
        gap_type = 'gap_up'

    # 窓埋め行を取得
    if side:
        close_row = rows.filter(trg <= pl.col('hi'))
    else:
        close_row = rows.filter(pl.col('lw') <= trg)

    # 窓の位置を取得
    session = rows['Session'][0]
    if session == 'Day Session':
        space = 'Night to Day'
    else:
        space = 'Day to Night'

    if close_row.shape[0] == 0:
        # 窓を埋める事が出来なかった
        return GapDetails(
            session_id=session_id, space=space,
            start=start, gap=gap)
    else:
        # 窓を埋めた時刻を取得する
        stop = close_row['datetime'][0]

    # 窓を開けていた期間内の行を抽出
    within_df = rows.filter(pl.col('datetime') <= stop)

    # 取引時間を取得(分)
    trading_time = within_df.shape[0]

    # 実際に掛かった現実の時間を取得(分)
    _delta = stop - start
    days = _delta.days * 1440
    minutes = _delta.seconds / 60 + 1
    timedelta = int(days + minutes)

    # 逆方向に動いた値幅を計算する
    op = within_df['op'][0]
    if side:
        reverse_move = op - within_df['lw'].min()
    else:
        reverse_move = within_df['hi'].max() - op
    return GapDetails(
        session_id, space, start, stop, gap,
        gap_type, trading_time, timedelta,
        reverse_move
    )
rich.print(gap_report(oc_df, 5).dict())
{
    'session_id': 5,
    'space': 'Day to Night',
    'start': datetime.datetime(2013, 1, 8, 16, 30),
    'stop': datetime.datetime(2013, 1, 8, 17, 32),
    'gap': -20,
    'gap_type': 'gap_down',
    'trading_time': 63,
    'timedelta': 63,
    'reverse_move': 15
}

始値と終値からの窓を記録する

ちょと時間が掛かります。自分のPCで動作させた時は2分半、colabでは5分半ほど計算に時間が掛かります。

sessions = oc_df['Session ID'].unique()
datasets = []
for i in tqdm(sessions):
    res = gap_report(oc_df, i)
    if not res.gap is None:
        datasets.append(res.dict())

DataFrame化

oc_gap_details = pl.DataFrame(datasets)
print(oc_gap_details.head(3))
OUTPUT
┌───────────┬───────────┬───────────┬───────────┬───┬──────────┬───────────┬───────────┬───────────┐
│ session_i ┆ space     ┆ start     ┆ stop      ┆ … ┆ gap_type ┆ trading_t ┆ timedelta ┆ reverse_m │
│ d         ┆ ---------       ┆   ┆ ---      ┆ ime       ┆ ---       ┆ ove       │
│ ---str       ┆ datetime[ ┆ datetime[ ┆   ┆ str---       ┆ i64       ┆ ---       │
│ i64       ┆           ┆ μs]       ┆ μs]       ┆   ┆          ┆ i64       ┆           ┆ i64       │
╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪══════════╪═══════════╪═══════════╪═══════════╡
│ 3         ┆ Day to    ┆ 2013-01-02013-01-0 ┆ … ┆ gap_down ┆ 110         │
│           ┆ Night     ┆ 77         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 16:30:0016:30:00  ┆   ┆          ┆           ┆           ┆           │
│ 4         ┆ Night to  ┆ 2013-01-02013-01-0 ┆ … ┆ gap_down ┆ 575710        │
│           ┆ Day       ┆ 88         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0009:56:00  ┆   ┆          ┆           ┆           ┆           │
│ 5         ┆ Day to    ┆ 2013-01-02013-01-0 ┆ … ┆ gap_down ┆ 636315        │
│           ┆ Night     ┆ 88         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 16:30:0017:32:00  ┆   ┆          ┆           ┆           ┆           │
└───────────┴───────────┴───────────┴───────────┴───┴──────────┴───────────┴───────────┴───────────┘

安値と高値からの窓を記録する

ちょと時間が掛かります。自分のPCで動作させた時は2分半、colabでは5分半ほど計算に時間が掛かりました。

sessions = hl_df['Session ID'].unique()
datasets = []
for i in tqdm(sessions):
    res = gap_report(hl_df, i)
    if not res.gap is None:
        datasets.append(res.dict())

DataFrame化

hl_gap_details = pl.DataFrame(datasets)
print(hl_gap_details.head(3))
OUTPUT
┌───────────┬───────────┬───────────┬───────────┬───┬──────────┬───────────┬───────────┬───────────┐
│ session_i ┆ space     ┆ start     ┆ stop      ┆ … ┆ gap_type ┆ trading_t ┆ timedelta ┆ reverse_m │
│ d         ┆ ---------       ┆   ┆ ---      ┆ ime       ┆ ---       ┆ ove       │
│ ---str       ┆ datetime[ ┆ datetime[ ┆   ┆ str---       ┆ i64       ┆ ---       │
│ i64       ┆           ┆ μs]       ┆ μs]       ┆   ┆          ┆ i64       ┆           ┆ i64       │
╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪══════════╪═══════════╪═══════════╪═══════════╡
│ 5         ┆ Day to    ┆ 2013-01-02013-01-0 ┆ … ┆ gap_down ┆ 861122095        │
│           ┆ Night     ┆ 89         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 16:30:0012:49:00  ┆   ┆          ┆           ┆           ┆           │
│ 8         ┆ Night to  ┆ 2013-01-12013-01-1 ┆ … ┆ gap_up   ┆ 1115        │
│           ┆ Day       ┆ 00         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0009:00:00  ┆   ┆          ┆           ┆           ┆           │
│ 10        ┆ Night to  ┆ 2013-01-12013-01-1 ┆ … ┆ gap_up   ┆ 22122125        │
│           ┆ Day       ┆ 11         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0012:40:00  ┆   ┆          ┆           ┆           ┆           │
└───────────┴───────────┴───────────┴───────────┴───┴──────────┴───────────┴───────────┴───────────┘

窓埋めまでの取引時間をPlotしてみる

plt.hist(oc_gap_details['trading_time'], bins=30);

どうやら窓埋めまでの取引時間は幅が大きいようで、ヒストグラムで見てみても全くイメージが掴めません。

ビニング処理してパイチャートを見てみる

取引時間の幅が大きくイメージが掴めないので、ビニング処理である程度取引時間を分類し、パイチャートで確認してみましょう。

ビニング処理して集計する関数を作成する

def count(df, bins):
    # 窓埋めまでの時間をbin分割します
    agg_df = (
        df
        .filter(pl.col('trading_time').is_not_null())
        .with_columns([
            pl.col('trading_time')
                .cut(breaks=list(bins.keys())[: -1],
                    labels=list(bins.values()))
                .alias('bins')
        ])
        .group_by(['bins', 'space', 'gap_type'])
            .agg([
                pl.col('stop').count().alias('count'),
                pl.col('trading_time').first().alias('sort')
            ])
        .sort(['space', 'gap_type', 'sort'])
        .drop('sort')
    )
    return agg_df

ビニング処理(始値から終値の場合)

分割幅は適当に設定しています。

bins = {
    # 30分以内に窓を閉めた
    30: '30m',
    # 1時間以内に窓を閉めた
    60: '1h',
    # 2時間以内に窓を閉めた
    120: '2h',
    # 4時間以内に窓を閉めた
    240: '4h',
    # 現在のDaySessionの取引時間と同じ 6.5時間以内に窓を閉めた
    390: '6.5h',
    # 現在のNightSessionの取引時間と同じ 13.5時間以内に窓を閉めた
    810: '13.5h',
    # 現在の1日分の取引時間と同じ 20時間以内に窓を閉めた
    1200: '1d',
    # 現在の2日分の取引時間と同じ 40時間以内に窓を閉めた
    2400: '2d',
    # 現在の1週間分の取引時間と同じ 100時間以内に窓を閉めた
    6000: '1w',
    # 上記よりも窓埋めに時間が掛かった
    -100: 'outlier'
}

oc_gap_count = count(oc_gap_details, bins)
print(oc_gap_count.head(50))
┌─────────┬──────────────┬──────────┬───────┐
│ bins    ┆ space        ┆ gap_type ┆ count │
│ ------------   │
│ cat     ┆ strstr      ┆ u32   │
╞═════════╪══════════════╪══════════╪═══════╡
│ 30m     ┆ Day to Night ┆ gap_down ┆ 277   │
│ 1h      ┆ Day to Night ┆ gap_down ┆ 79    │
│ 2h      ┆ Day to Night ┆ gap_down ┆ 47    │
│ 4h      ┆ Day to Night ┆ gap_down ┆ 34    │
│ 6.5h    ┆ Day to Night ┆ gap_down ┆ 47    │
│ 13.5h   ┆ Day to Night ┆ gap_down ┆ 68    │
│ 1d      ┆ Day to Night ┆ gap_down ┆ 28    │
│ 2d      ┆ Day to Night ┆ gap_down ┆ 20    │
│ 1w      ┆ Day to Night ┆ gap_down ┆ 21    │
│ outlier ┆ Day to Night ┆ gap_down ┆ 25    │
│ 30m     ┆ Day to Night ┆ gap_up   ┆ 333   │
│ 1h      ┆ Day to Night ┆ gap_up   ┆ 90    │
│ 2h      ┆ Day to Night ┆ gap_up   ┆ 58    │
│ 4h      ┆ Day to Night ┆ gap_up   ┆ 28    │
│ 6.5h    ┆ Day to Night ┆ gap_up   ┆ 47    │
│ 13.5h   ┆ Day to Night ┆ gap_up   ┆ 67    │
│ 1d      ┆ Day to Night ┆ gap_up   ┆ 24    │
│ 2d      ┆ Day to Night ┆ gap_up   ┆ 27    │
│ 1w      ┆ Day to Night ┆ gap_up   ┆ 27    │
│ outlier ┆ Day to Night ┆ gap_up   ┆ 51    │
│ 30m     ┆ Night to Day ┆ gap_down ┆ 373   │
│ 1h      ┆ Night to Day ┆ gap_down ┆ 53    │
│ 2h      ┆ Night to Day ┆ gap_down ┆ 58    │
│ 4h      ┆ Night to Day ┆ gap_down ┆ 39    │
│ 6.5h    ┆ Night to Day ┆ gap_down ┆ 20    │
│ 13.5h   ┆ Night to Day ┆ gap_down ┆ 34    │
│ 1d      ┆ Night to Day ┆ gap_down ┆ 35    │
│ 2d      ┆ Night to Day ┆ gap_down ┆ 51    │
│ 1w      ┆ Night to Day ┆ gap_down ┆ 30    │
│ outlier ┆ Night to Day ┆ gap_down ┆ 55    │
│ 30m     ┆ Night to Day ┆ gap_up   ┆ 424   │
│ 1h      ┆ Night to Day ┆ gap_up   ┆ 69    │
│ 2h      ┆ Night to Day ┆ gap_up   ┆ 47    │
│ 4h      ┆ Night to Day ┆ gap_up   ┆ 53    │
│ 6.5h    ┆ Night to Day ┆ gap_up   ┆ 34    │
│ 13.5h   ┆ Night to Day ┆ gap_up   ┆ 30    │
│ 1d      ┆ Night to Day ┆ gap_up   ┆ 29    │
│ 2d      ┆ Night to Day ┆ gap_up   ┆ 53    │
│ 1w      ┆ Night to Day ┆ gap_up   ┆ 41    │
│ outlier ┆ Night to Day ┆ gap_up   ┆ 77    │
└─────────┴──────────────┴──────────┴───────┘

ビニング処理(安値から高値の場合)

hl_gap_count = count(hl_gap_details, bins)
print(hl_gap_count.head(50))
┌─────────┬──────────────┬──────────┬───────┐
│ bins    ┆ space        ┆ gap_type ┆ count │
│ ------------   │
│ cat     ┆ strstr      ┆ u32   │
╞═════════╪══════════════╪══════════╪═══════╡
│ 30m     ┆ Day to Night ┆ gap_down ┆ 5     │
│ 1h      ┆ Day to Night ┆ gap_down ┆ 2     │
│ 4h      ┆ Day to Night ┆ gap_down ┆ 2     │
│ 6.5h    ┆ Day to Night ┆ gap_down ┆ 4     │
│ 13.5h   ┆ Day to Night ┆ gap_down ┆ 23    │
│ 1d      ┆ Day to Night ┆ gap_down ┆ 28    │
│ 2d      ┆ Day to Night ┆ gap_down ┆ 24    │
│ 1w      ┆ Day to Night ┆ gap_down ┆ 31    │
│ outlier ┆ Day to Night ┆ gap_down ┆ 38    │
│ 30m     ┆ Day to Night ┆ gap_up   ┆ 109   │
│ 1h      ┆ Day to Night ┆ gap_up   ┆ 28    │
│ 2h      ┆ Day to Night ┆ gap_up   ┆ 9     │
│ 4h      ┆ Day to Night ┆ gap_up   ┆ 6     │
│ 6.5h    ┆ Day to Night ┆ gap_up   ┆ 10    │
│ 13.5h   ┆ Day to Night ┆ gap_up   ┆ 30    │
│ 1d      ┆ Day to Night ┆ gap_up   ┆ 12    │
│ 2d      ┆ Day to Night ┆ gap_up   ┆ 10    │
│ 1w      ┆ Day to Night ┆ gap_up   ┆ 11    │
│ outlier ┆ Day to Night ┆ gap_up   ┆ 16    │
│ 30m     ┆ Night to Day ┆ gap_down ┆ 19    │
│ 1h      ┆ Night to Day ┆ gap_down ┆ 3     │
│ 2h      ┆ Night to Day ┆ gap_down ┆ 5     │
│ 4h      ┆ Night to Day ┆ gap_down ┆ 9     │
│ 6.5h    ┆ Night to Day ┆ gap_down ┆ 15    │
│ 13.5h   ┆ Night to Day ┆ gap_down ┆ 22    │
│ 1d      ┆ Night to Day ┆ gap_down ┆ 24    │
│ 2d      ┆ Night to Day ┆ gap_down ┆ 40    │
│ 1w      ┆ Night to Day ┆ gap_down ┆ 42    │
│ outlier ┆ Night to Day ┆ gap_down ┆ 81    │
│ 30m     ┆ Night to Day ┆ gap_up   ┆ 208   │
│ 1h      ┆ Night to Day ┆ gap_up   ┆ 26    │
│ 2h      ┆ Night to Day ┆ gap_up   ┆ 22    │
│ 4h      ┆ Night to Day ┆ gap_up   ┆ 21    │
│ 6.5h    ┆ Night to Day ┆ gap_up   ┆ 17    │
│ 13.5h   ┆ Night to Day ┆ gap_up   ┆ 20    │
│ 1d      ┆ Night to Day ┆ gap_up   ┆ 19    │
│ 2d      ┆ Night to Day ┆ gap_up   ┆ 26    │
│ 1w      ┆ Night to Day ┆ gap_up   ┆ 22    │
│ outlier ┆ Night to Day ┆ gap_up   ┆ 44    │
└─────────┴──────────────┴──────────┴───────┘

パイチャートを作成する関数

def pie(df, space, gap_type, ax):
    rows = df.filter(
        (pl.col('space') == space)
        & (pl.col('gap_type') == gap_type)
    )
    colors = sns.color_palette("viridis", n_colors=12)
    _, _, autotexts = (
        ax.pie(
            rows['count'], 
            labels=rows['bins'], 
            colors=colors, 
            autopct="%0.0f%%",
            counterclock=False, 
            startangle=90,
            wedgeprops={'linewidth': .5, 'edgecolor': 'white'},
        )
    )
    for autotext in autotexts:
        autotext.set_color('white')
        autotext.set_fontsize(9)
    ax.set_title(f"Space: {space},  Gap type: {gap_type}")

# 
spaces = [
    'Day to Night',
    'Night to Day'
]

gap_types = [
    'gap_down',
    'gap_up'
]

始値から終値の場合のパイチャート

fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 10))
plt.suptitle("""
    前日始値から前日終値の範囲外から考える
    日経先物のSessionとGapType別の窓埋め時間
""")

for i, space in enumerate(spaces):
    for j, gap_type in enumerate(gap_types):
        pie(oc_gap_count, space, gap_type, ax[i, j])
        
plt.show()

安値から高値の場合のパイチャート

fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 10))
plt.suptitle("""
    前日安値から前日高値の範囲外から考える
    日経先物のSessionとGapType別の窓埋め時間
""")

for i, space in enumerate(spaces):
    for j, gap_type in enumerate(gap_types):
        pie(hl_gap_count, space, gap_type, ax[i, j])
        
plt.show()

パイチャートから

パイチャートを見てみると大体の窓は30~60分で閉める様です。
しかし高安値から考える、DaySessionからNightSessionに掛けて開けたギャップダウンの窓に関しては、むしろ窓を閉めるまでに時間が掛かる事が分かります。安易に窓は閉めるものと考えて逆張りするのは危険そうですね。

始値から終値で定義する窓のNight to Dayをもう少し見てみる

全てのデータを深堀はしませんが、もう少しだけデータの詳細を見ていきます。

slt_oc_gap_details = (
    oc_gap_details
    .filter(pl.col('space') == 'Night to Day')
)

print(slt_oc_gap_details.shape)
print(slt_oc_gap_details.head())
(1615, 9)
┌───────────┬───────────┬───────────┬───────────┬───┬──────────┬───────────┬───────────┬───────────┐
│ session_i ┆ space     ┆ start     ┆ stop      ┆ … ┆ gap_type ┆ trading_t ┆ timedelta ┆ reverse_m │
│ d         ┆ ---------       ┆   ┆ ---      ┆ ime       ┆ ---       ┆ ove       │
│ ---str       ┆ datetime[ ┆ datetime[ ┆   ┆ str---       ┆ i64       ┆ ---       │
│ i64       ┆           ┆ μs]       ┆ μs]       ┆   ┆          ┆ i64       ┆           ┆ i64       │
╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪══════════╪═══════════╪═══════════╪═══════════╡
│ 4         ┆ Night to  ┆ 2013-01-02013-01-0 ┆ … ┆ gap_down ┆ 575710        │
│           ┆ Day       ┆ 88         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0009:56:00  ┆   ┆          ┆           ┆           ┆           │
│ 6         ┆ Night to  ┆ 2013-01-02013-01-0 ┆ … ┆ gap_down ┆ 110         │
│           ┆ Day       ┆ 99         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0009:00:00  ┆   ┆          ┆           ┆           ┆           │
│ 8         ┆ Night to  ┆ 2013-01-12013-01-1 ┆ … ┆ gap_up   ┆ 171720        │
│           ┆ Day       ┆ 00         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0009:16:00  ┆   ┆          ┆           ┆           ┆           │
│ 10        ┆ Night to  ┆ 2013-01-12013-01-1 ┆ … ┆ gap_up   ┆ 22122125        │
│           ┆ Day       ┆ 11         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0012:40:00  ┆   ┆          ┆           ┆           ┆           │
│ 12        ┆ Night to  ┆ 2013-01-12013-01-1 ┆ … ┆ gap_up   ┆ 18218240        │
│           ┆ Day       ┆ 55         ┆   ┆          ┆           ┆           ┆           │
│           ┆           ┆ 09:00:0012:01:00  ┆   ┆          ┆           ┆           ┆           │
└───────────┴───────────┴───────────┴───────────┴───┴──────────┴───────────┴───────────┴───────────┘

窓埋めまでの時間が30分以下か31分以上かで見てみる

slt_oc_gap_details = (
    slt_oc_gap_details
    .with_columns([
        pl.when(pl.col('timedelta') <= 30)
            .then(pl.lit('close <= 30m'))
            .otherwise(pl.lit('30m < close'))
            .alias('period')
    ])
)
print(slt_oc_gap_details.head())

窓埋めまでの期間に埋めるのとは逆方向に動いた価格幅を見てみる

sns.histplot(
    data=slt_oc_gap_details.to_pandas(),
    x="reverse_move",
    hue="period"
);


窓埋めまでかなり時間が掛かった部分で7000円ほど動いた記録がありヒストグラムが見づらいです。
1週間以内に絞り込んでみましょう。

fig, ax = plt.subplots(ncols=2, figsize=(10, 5))

sns.histplot(
    data=(
        slt_oc_gap_details
        .filter(
            (pl.col('trading_time') <= 6000)
            & (pl.col('period') == 'close <= 30m')
        )
        .to_pandas()
    ),
    x="reverse_move",
    hue="period",
    ax=ax[0]
)
sns.histplot(
    data=(
        slt_oc_gap_details
        .filter(
            (pl.col('trading_time') <= 6000)
            & (pl.col('period') == '30m < close')
        )
        .to_pandas()
    ),
    x="reverse_move",
    hue="period",
    ax=ax[1]
);

今回はここまでとします。気になった方はご自分でこの先を見てみてください。

注意点 窓を埋めなかった事はあるのか

窓埋めを期待して安易に逆張りをするのは危険です。
窓を埋めなかった記録を見てみましょう。

oc_ncl = (
    oc_gap_details
    .filter(pl.col('stop').is_null())
)
print(oc_ncl.select(show_cols))
shape: (17, 4)
┌────────────┬──────────────┬─────────────────────┬─────┐
│ session_id ┆ space        ┆ start               ┆ gap │
│ ------------ │
│ i64        ┆ str          ┆ datetime[μs]        ┆ i64 │
╞════════════╪══════════════╪═════════════════════╪═════╡
│ 34         ┆ Night to Day ┆ 2013-01-30 09:00:0035  │
│ 74         ┆ Night to Day ┆ 2013-02-28 09:00:0040  │
│ 119        ┆ Day to Night ┆ 2013-04-02 16:30:0010  │
│ 328        ┆ Night to Day ┆ 2013-09-02 09:00:00125 │
│ 329        ┆ Day to Night ┆ 2013-09-02 16:30:00125 │
│ 1722       ┆ Night to Day ┆ 2016-07-11 09:00:0095  │
│ 1723       ┆ Day to Night ┆ 2016-07-11 16:30:0070  │
│ 3524       ┆ Night to Day ┆ 2020-03-24 08:45:00240 │
│ 3542       ┆ Night to Day ┆ 2020-04-06 08:45:0070  │
│ 3595       ┆ Day to Night ┆ 2020-05-18 16:30:0065  │
│ 3710       ┆ Night to Day ┆ 2020-08-11 08:45:0045  │
│ 3826       ┆ Night to Day ┆ 2020-11-04 08:45:00390 │
│ 3832       ┆ Night to Day ┆ 2020-11-09 08:45:00220 │
│ 5053       ┆ Day to Night ┆ 2023-04-27 16:30:0020  │
│ 5075       ┆ Day to Night ┆ 2023-05-12 16:30:0010  │
│ 5080       ┆ Night to Day ┆ 2023-05-17 08:45:0015  │
│ 5319       ┆ Day to Night ┆ 2023-10-31 16:30:0065  │
└────────────┴──────────────┴─────────────────────┴─────┘
hl_ncl = (
    hl_gap_details
    .filter(pl.col('stop').is_null())
)

print(hl_ncl.select(show_cols))
┌────────────┬──────────────┬─────────────────────┬─────┐
│ session_id ┆ space        ┆ start               ┆ gap │
│ ------------ │
│ i64        ┆ str          ┆ datetime[μs]        ┆ i64 │
╞════════════╪══════════════╪═════════════════════╪═════╡
│ 35         ┆ Day to Night ┆ 2013-01-30 16:30:0040  │
│ 74         ┆ Night to Day ┆ 2013-02-28 09:00:0020  │
│ 328        ┆ Night to Day ┆ 2013-09-02 09:00:0070  │
│ 329        ┆ Day to Night ┆ 2013-09-02 16:30:0075  │
│ 1722       ┆ Night to Day ┆ 2016-07-11 09:00:0085  │
│ 3595       ┆ Day to Night ┆ 2020-05-18 16:30:0065  │
│ 3710       ┆ Night to Day ┆ 2020-08-11 08:45:0045  │
│ 3826       ┆ Night to Day ┆ 2020-11-04 08:45:00365 │
│ 3832       ┆ Night to Day ┆ 2020-11-09 08:45:00115 │
└────────────┴──────────────┴─────────────────────┴─────┘

一年に一度あるかないかという感じですね。しかし発生した年には偏りがあるように見えます。
当然ですが埋めなかった窓は全てギャップアップなので、大きな上昇トレンドの時に発生した窓は埋めない場合があるのでしょう。

fig, ax = plt.subplots(figsize=(12, 5))
plt.title('日経先物 ~埋めなかった窓の位置~')
ax.plot(session_df['datetime'], session_df['cl'], c='#008899')
q = oc_ncl['session_id'].to_list()
rows = session_df.filter(pl.col('Session ID').is_in(q))
ax.scatter(rows['datetime'], rows['cl'], marker='x', c='red', s=50, zorder=2)
plt.show()

おわりに

あまり深堀はしていませんが、まぁ記事も長くなるのでこれくらいで...
気になった方はご自分でデータを集めて実行してみてください。

Discussion