👅

日経先物分析その2 ... SQまでの残存時間の計算

2023/09/05に公開

その1では何をしたのか

その1では日経先物のタイムバーに、DaySessionとNightSessionの情報を追加し、SesssionごとにUniqueなIDを割り当てました。
今回その情報は使用しませんが、気になる方は見てみて下さい。
https://zenn.dev/daidai_daitai/articles/92e8fbbfa79e42

その2では何をするのか

日経先物のつなぎ足データから次のSQまでの残存時間を計算します。今回はタイムバーを使用しますが、その他の方法でサンプリングされたデータでも、コードを少し変えれば使用できると思います。

SQとは

以下は生成AIに聞いた回答のコピペです。

SQの概要

SQとは、日経225先物などの株価指数先物取引、または株価指数のオプション取引などを、最終的な決済期日前で決済するための「特別な価格」のことを指します¹。算出された価格は特別清算指数、最終清算指数、あるいはただ単にSQ値などと呼ばれます¹。SQ値は、指数算出日における各指数構成銘柄の始値に基づいて算出され、大引け後大阪取引所より発表されます¹。SQ算出日は各限月の第2金曜日となり、祝日にあたる場合は前倒しされます¹。

¹: SBIネオトレード証券

(1) SQ値(速報値・確定値を掲載)|SBIネオトレード証券. https://www.sbineotrade.jp/225/outline/sq.htm.
(2) SQとは|株式投資をする上で知っておきたい基礎知識 | 投資の教科書. https://toushi-kyokasho.com/what-is-sq/.
(3) メジャーSQ通過後の日経平均株価の動きについて | 三井住友DS .... https://www.smd-am.co.jp/market/ichikawa/2021/09/irepo210910/.
(4) 最終清算数値・最終決済価格 | 日本取引所グループ. https://www.jpx.co.jp/markets/derivatives/special-quotation/.

SQ値が決まる日時

SQ値は、指数算出日における各指数構成銘柄の始値に基づいて算出されます。SQ日の225銘柄の始値から算出されるため、SQ値は毎回異なります¹。SQ算出日は各限月の第2金曜日となり、祝日にあたる場合は前倒しされます¹。詳細については、SBIネオトレード証券を参照してください。

(1) SQとは?SQ前後の株価・チャートの値動きを利用した有効 .... https://openeducation.co.jp/media/sq/.
(2) SQとは?SQ前後の株価・チャートの値動きを利用した有効 .... https://bing.com/search?q=SQ値の決め方.
(3) SQ決済時の計算方法を教えてください。 - auカブコム証券. https://faq.kabu.com/s/article/k000539.
(4) SQ(オプションSQ・メジャーSQ・幻のSQ・米国のSQ)の解説. https://www.kabusoba.jp/kabushiki851sq.html.
(5) SQ値(速報値・確定値を掲載)|SBIネオトレード証券. https://www.sbineotrade.jp/225/outline/sq.htm.

メジャーSQとマイナーSQの違い

メジャーSQとマイナーSQの違いは、以下の通りです¹:

  • メジャーSQ:3月・6月・9月・12月の第2金曜日
  • マイナーSQ:それ以外の月の第2金曜日

先物取引はメジャーSQのみ、オプション取引は毎月SQ日があるため、メジャーSQに該当する場合もあればマイナーSQに該当する場合もあります¹。

¹: SBIネオトレード証券

(1) メジャーSQ、マイナーSQとは | ファイナンシャルプランナー講座 .... https://www.foresight.jp/blog/fp/archives/7392.
(2) SQとは? わかりやすく教えてください。 | いま聞きたいQ&A .... https://manabow.com/qa/sq.html.
(3) メジャーSQ前は相場が荒れる・SQで相場が転換すると言われる理由. https://foxorz.com/major-sq-condition/.
(4) マーケット|SBI証券. https://www.sbisec.co.jp/ETGate/?OutSide=on&_ControlID=WPLETmgR001Control&_PageID=WPLETmgR001Mdtl20&_DataStoreID=DSWPLETmgR001Control&_ActionID=DefaultAID&getFlg=on&burl=search_market&cat1=market&cat2=report&dir=report&file=market_report_ew_150601.html.
(5) メジャーSQとマイナーSQの日程 | 日経平均株価に連動する .... https://nikkeiheikinnkabusyoukenn.com/newpage47.html.

こういった事は調べて書くのが面倒ですが、生成AIは便利ですね。

準備

使用するデータ

DataFrame
shape: (298_096, 6)
┌─────────────────────┬───────┬───────┬───────┬───────┬────────┐
│ datetime            ┆ op    ┆ hi    ┆ lw    ┆ cl    ┆ volume │
│ ------------------    │
│ datetime[μs]        ┆ i64   ┆ i64   ┆ i64   ┆ i64   ┆ i64    │
╞═════════════════════╪═══════╪═══════╪═══════╪═══════╪════════╡
│ 2022-01-04 08:45:002904029050290252904521350  │
│ 2022-01-04 08:46:00290452906529040290553215   │
│ 2022-01-04 08:47:00290552906029040290551535   │
│ 2022-01-04 08:48:00290552907029055290651964   │
│ …                   ┆ …     ┆ …     ┆ …     ┆ …     ┆ …      │
│ 2022-12-31 05:57:00257652576525765257650      │
│ 2022-12-31 05:58:00257652576525765257650      │
│ 2022-12-31 05:59:00257652576525765257650      │
│ 2022-12-31 06:00:00257402574025740257404373   │
└─────────────────────┴───────┴───────┴───────┴───────┴────────┘

環境

前回と同様に今回もpandasではなくpolarsを使用していきます。それから祝日を判定する為にjpholidayも使用します。まだinstallしていない方は下記を参考にどうぞ
https://pypi.org/project/polars/
https://pypi.org/project/jpholiday/

実行

Import

import calendar
import datetime

import jpholiday
import polars

データの読み込み

データはparquetファイルで保存しています。csvと比べて容量も小さく、読み書きも高速なのでオススメです。

fp = r'../datasets/NK225F.parquet'
data = pl.read_parquet(fp)

最終結果

面倒なので先に結果を書いておきます。


class SpecialQuotation(object):
    def __init__(self, df: pl.DataFrame):
        self.df = df
        self.__reset()

    def __reset(self):
        dels = ['sq', 'sq_id', 'timedelta', 'to_sq']
        del_cols = [col for col in self.df.columns if col in dels]
        self.df = self.df.drop(del_cols)


    def _second_friday(
        self, 
        year: int, 
        month: int
    ) -> datetime.datetime:
        ''' 
        指定した月の第2金曜日を取得します。
        '''
        first_day = datetime.datetime(year, month, 1, 9, 0, 0)
        day_of_week = first_day.weekday()
        days_until_friday  = (calendar.FRIDAY - day_of_week) % 7
        second_friday = first_day + datetime.timedelta(days_until_friday + 7)
        return second_friday


    def _convert_from_holiday(
        self, 
        dt_obj: datetime.datetime
    ) -> datetime.datetime:
        '''
        引数として渡された日付が祝日ならば、前日の日付を返します
        '''
        while True:
            if jpholiday.is_holiday(dt_obj):
                dt_obj = dt_obj - datetime.timedelta(days=1)
            else:
                return dt_obj


    def _last_trading_datetime(
        self, latest_dt: 
        datetime.datetime
    ) -> datetime.datetime:
        second_friday = self._second_friday(latest_dt.year, latest_dt.month)
        last_trade = self._convert_from_holiday(second_friday)
        while True:
            if jpholiday.is_holiday(last_trade):
                last_trade = self._convert_from_holiday(last_trade)
            else:
                return last_trade
    

    def sq_calendar(
        self, 
        start_year: int, 
        end_year: int, 
        major: bool=False
    ) -> pl.DataFrame:
        '''
        SQカレンダーを作成する。
        '''
        sqs = []
        for year in range(start_year, end_year + 2):
            for month in range(1, 13):
                dt = datetime.datetime(year, month, 1)
                sq = self._last_trading_datetime(dt)
                sqs.append(sq)
        # SQごとにIDを割り当てたDataFrameを作成する
        df = pl\
            .DataFrame({'datetime': sqs})\
            .with_columns([
                pl.lit(True).alias('sq'),
                pl.Series('sq_id', np.arange(0, len(sqs)), dtype=pl.Int16)])
        
        if major:
            # メジャーSQだけに絞り込む
            df = self.__select_major_sq(df)

        return df


    def __select_major_sq(self, df) -> pl.DataFrame:
        '''
        3, 6, 9, 12のメジャーSQだけに絞り込む
        '''
        df = df\
            .with_columns([
                pl.when(pl.col('datetime').dt.month() % 3 == 0)
                    .then(True)
                    .otherwise(False)
                    .alias('query') ])\
            .filter(pl.col('query') == True)\
            .drop('query')
        ids = pl.Series('sq_id', list(range(df.shape[0])))
        df = df.with_columns([ids])
        return df


    def add_sq_id(
        self, 
        datetime_col: str, 
        major: bool=False
    ) -> pl.DataFrame:
        '''
        引数として渡したDataFrameにSQ idを追加します。
        '''
        # SQ idの取得
        oldest = self.df[datetime_col].min().year
        latest = self.df[datetime_col].max().year
        sq_ids = self\
            .sq_calendar(oldest, latest, major)\
            .rename({'datetime': datetime_col})
        # SQ idを引数として渡したDataFrameに結合する
        df = self.df\
            .join(
                sq_ids, 
                on=datetime_col,
                how='outer')\
            .sort('datetime')\
            .with_columns([
                pl.col('sq_id').fill_null(strategy='backward'),
                pl.col('sq').fill_null(False),
                pl.when((pl.col('session') == None) &
                        (pl.col('session').shift() == None))
                    .then(None)
                    .otherwise(0)
                    .alias('drops')
                ])\
            .drop_nulls('drops')\
            .drop('drops')
        return df


def to_sq_timedelta(
    df: pl.DataFrame, 
    datetime_col: str, 
    timedelta_col: str=None, 
    major: bool=False,
    to_timedelta: datetime.timedelta=datetime.timedelta(minutes=1),
    drop: bool=True,
) -> pl.DataFrame:
    '''
    SQまでの残存時間を計算します。
    '''
    sq = SpecialQuotation(df)
    result = sq.add_sq_id(datetime_col, major)
    latest = result['datetime'][-2]
    next_sq = result['datetime'][-1]
    calen = sq.to_next_sq_calendar(latest, next_sq, to_timedelta)
    result = result\
        .join(calen, on='datetime', how='outer')\
        .sort('datetime')\
        .with_columns([
            pl.col('sq_id').fill_null(strategy='backward'),
            pl.col('sq').fill_null(False)
            ])


    if timedelta_col is None:
        # 時間の情報が渡されない場合
        timedelta_col = 'timedelta'
        result = result\
            .with_columns([
                pl.lit(1).alias(timedelta_col) ])
    
    # SQ idごとにtimedeltaの累積和を計算する。
    result = result\
        .sort(datetime_col, descending=True)\
        .with_columns([
            pl.col('sq_id').shift()])\
        .with_columns([
            pl.col(timedelta_col).cumsum().over('sq_id').alias('to_sq') ])\
        .sort(datetime_col, descending=False)
    if drop:    
        return result.drop_nulls('session')
    else:
        return result.drop_nulls('sq_id')
    

第2金曜日の取得

当月の第2金曜日を判定する為には年と月の情報があれば計算できます。

Execution
sq = SpecialQuotation()
sq._second_friday(2023, 10)
Output
Output
datetime.datetime(2023, 10, 13, 9, 0)

SQカレンダーの作成

開始日時と終了日時を渡してSQカレンダーのDataFrameを作成します。

Execution
sq = SpecialQuotation()
print(sq.sq_calendar(2023, 2023))
Output
Output
shape: (24, 3)
┌─────────────────────┬──────┬───────┐
│ datetime            ┆ sq   ┆ sq_id │
│ ---------   │
│ datetime[μs]bool ┆ i16   │
╞═════════════════════╪══════╪═══════╡
│ 2023-01-13 09:00:00 ┆ true ┆ 0     │
│ 2023-02-10 09:00:00 ┆ true ┆ 1     │
│ 2023-03-10 09:00:00 ┆ true ┆ 2     │
│ 2023-04-14 09:00:00 ┆ true ┆ 3     │
│ …                   ┆ …    ┆ …     │
│ 2024-09-13 09:00:00 ┆ true ┆ 20    │
│ 2024-10-11 09:00:00 ┆ true ┆ 21    │
│ 2024-11-08 09:00:00 ┆ true ┆ 22    │
│ 2024-12-13 09:00:00 ┆ true ┆ 23    │
└─────────────────────┴──────┴───────┘

SQ IDの追加

SQごとにUniqueなIDを作成し、引数として渡したDataFrameに追加します。

Execution
sq = SpecialQuotation()
result = sq\
    .add_sq_id(data, 'datetime')\
    .drop(['op', 'hi', 'lw'])
print(result)
Output
Output
shape: (298_096, 5)
┌─────────────────────┬───────┬────────┬───────┬───────┐
│ datetime            ┆ cl    ┆ volume ┆ sq    ┆ sq_id │
│ ---------------   │
│ datetime[μs]        ┆ i64   ┆ i64    ┆ bool  ┆ i16   │
╞═════════════════════╪═══════╪════════╪═══════╪═══════╡
│ 2022-01-04 08:45:002904521350  ┆ false ┆ 0     │
│ 2022-01-04 08:46:00290553215   ┆ false ┆ 0     │
│ 2022-01-04 08:47:00290551535   ┆ false ┆ 0     │
│ 2022-01-04 08:48:00290651964   ┆ false ┆ 0     │
│ …                   ┆ …     ┆ …      ┆ …     ┆ …     │
│ 2022-12-31 05:57:00257650      ┆ false ┆ 12    │
│ 2022-12-31 05:58:00257650      ┆ false ┆ 12    │
│ 2022-12-31 05:59:00257650      ┆ false ┆ 12    │
│ 2022-12-31 06:00:00257404373   ┆ false ┆ 12    │
└─────────────────────┴───────┴────────┴───────┴───────┘

SQまでの残存時間の計算

taimedeltaに対して数値を渡せばそれを使用してsq_idごとの累積和を計算します。
単純に累積和を計算すると、SQ直前が一番大きな値になってしまうので、一旦datetime列を使用し降順にソートします。その後sq_idごとに累積和を計算し、また昇順に戻す処理をしています。
出力を見ると8:59でSQまでの残存時間が1になり、SQの9:00にはまた次のSQまでの残存時間が表示されているのが確認できます。

Execution
result1 = to_sq_timedelta(data, 'datetime')\
    .filter(pl.col('datetime') < datetime.datetime(2022, 2, 10, 9, 3, 0))\
    .drop(['op', 'hi', 'lw', 'cl', 'volume'])
print(result1.tail(6))
Output
Output
shape: (6, 5)
┌─────────────────────┬───────┬───────┬───────────┬───────┐
│ datetime            ┆ sq    ┆ sq_id ┆ timedelta ┆ to_sq │
│ ---------------   │
│ datetime[μs]bool  ┆ i16   ┆ i32       ┆ i32   │
╞═════════════════════╪═══════╪═══════╪═══════════╪═══════╡
│ 2022-02-10 08:57:00 ┆ false ┆ 113     │
│ 2022-02-10 08:58:00 ┆ false ┆ 112     │
│ 2022-02-10 08:59:00 ┆ false ┆ 111     │
│ 2022-02-10 09:00:00 ┆ true  ┆ 2122838 │
│ 2022-02-10 09:01:00 ┆ false ┆ 2122837 │
│ 2022-02-10 09:02:00 ┆ false ┆ 2122836 │
└─────────────────────┴───────┴───────┴───────────┴───────┘

秒単位の残存期間計算もしてみます。

Execution
data2 = data\
    .with_columns([
        pl.col('datetime')\
            .diff()\
            .shift(-1)\
            .dt\
            .seconds()\
            .alias('timedelta')
    ])
    

result2 = to_sq_timedelta(data2, 'datetime', 'timedelta')\
    .filter(pl.col('datetime') < datetime.datetime(2022, 2, 10, 9, 3, 0))\
    .drop(['op', 'hi', 'lw', 'cl', 'volume'])
print(result2.tail(6))
Output
Output
shape: (6, 5)
┌─────────────────────┬───────────┬───────┬───────┬─────────┐
│ datetime            ┆ timedelta ┆ sq    ┆ sq_id ┆ to_sq   │
│ ---------------     │
│ datetime[μs]        ┆ i64       ┆ bool  ┆ i16   ┆ i64     │
╞═════════════════════╪═══════════╪═══════╪═══════╪═════════╡
│ 2022-02-10 08:57:0060        ┆ false ┆ 1180     │
│ 2022-02-10 08:58:0060        ┆ false ┆ 1120     │
│ 2022-02-10 08:59:0060        ┆ false ┆ 160      │
│ 2022-02-10 09:00:0060        ┆ true  ┆ 22505600 │
│ 2022-02-10 09:01:0060        ┆ false ┆ 22505540 │
│ 2022-02-10 09:02:0060        ┆ false ┆ 22505480 │
└─────────────────────┴───────────┴───────┴───────┴─────────┘

メジャーSQまでの残存期間計算もしてみます。

Execution
result3 = to_sq_timedelta(data2, 'datetime', 'timedelta')\
    .filter(pl.col('datetime') < datetime.datetime(2022, 2, 10, 9, 3, 0))\
    .drop(['op', 'hi', 'lw', 'cl', 'volume'])
print(result3.tail(6))
Output
Output
shape: (6, 5)
┌─────────────────────┬───────────┬───────┬───────┬─────────┐
│ datetime            ┆ timedelta ┆ sq    ┆ sq_id ┆ to_sq   │
│ ---------------     │
│ datetime[μs]        ┆ i64       ┆ bool  ┆ i16   ┆ i64     │
╞═════════════════════╪═══════════╪═══════╪═══════╪═════════╡
│ 2022-02-10 08:57:0060        ┆ false ┆ 22505780 │
│ 2022-02-10 08:58:0060        ┆ false ┆ 22505720 │
│ 2022-02-10 08:59:0060        ┆ false ┆ 22505660 │
│ 2022-02-10 09:00:0060        ┆ false ┆ 22505600 │
│ 2022-02-10 09:01:0060        ┆ false ┆ 22505540 │
│ 2022-02-10 09:02:0060        ┆ false ┆ 22505480 │
└─────────────────────┴───────────┴───────┴───────┴─────────┘

終わりに

今回はSQまでの残存期間を計算しました。
次に書く事はまだ決めていません。あまり変な事を書くと、自分の首を絞める事になるので...
金融と関係ありませんが、GISの記事も書いていこうと思っているので、次はPythonとGISを使った地理的分析でコンビニ出店計画を支援する。的な記事を書こうかと思っています。

Discussion