日経先物分析その2 ... SQまでの残存時間の計算
その1では何をしたのか
その1では日経先物のタイムバーに、DaySessionとNightSessionの情報を追加し、SesssionごとにUniqueなIDを割り当てました。
今回その情報は使用しませんが、気になる方は見てみて下さい。
その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:00 ┆ 29040 ┆ 29050 ┆ 29025 ┆ 29045 ┆ 21350 │
│ 2022-01-04 08:46:00 ┆ 29045 ┆ 29065 ┆ 29040 ┆ 29055 ┆ 3215 │
│ 2022-01-04 08:47:00 ┆ 29055 ┆ 29060 ┆ 29040 ┆ 29055 ┆ 1535 │
│ 2022-01-04 08:48:00 ┆ 29055 ┆ 29070 ┆ 29055 ┆ 29065 ┆ 1964 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 2022-12-31 05:57:00 ┆ 25765 ┆ 25765 ┆ 25765 ┆ 25765 ┆ 0 │
│ 2022-12-31 05:58:00 ┆ 25765 ┆ 25765 ┆ 25765 ┆ 25765 ┆ 0 │
│ 2022-12-31 05:59:00 ┆ 25765 ┆ 25765 ┆ 25765 ┆ 25765 ┆ 0 │
│ 2022-12-31 06:00:00 ┆ 25740 ┆ 25740 ┆ 25740 ┆ 25740 ┆ 4373 │
└─────────────────────┴───────┴───────┴───────┴───────┴────────┘
環境
前回と同様に今回もpandasではなくpolarsを使用していきます。それから祝日を判定する為にjpholidayも使用します。まだinstallしていない方は下記を参考にどうぞ
実行
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金曜日を判定する為には年と月の情報があれば計算できます。
sq = SpecialQuotation()
sq._second_friday(2023, 10)
Output
datetime.datetime(2023, 10, 13, 9, 0)
SQカレンダーの作成
開始日時と終了日時を渡してSQカレンダーのDataFrameを作成します。
sq = SpecialQuotation()
print(sq.sq_calendar(2023, 2023))
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に追加します。
sq = SpecialQuotation()
result = sq\
.add_sq_id(data, 'datetime')\
.drop(['op', 'hi', 'lw'])
print(result)
Output
shape: (298_096, 5)
┌─────────────────────┬───────┬────────┬───────┬───────┐
│ datetime ┆ cl ┆ volume ┆ sq ┆ sq_id │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ i64 ┆ i64 ┆ bool ┆ i16 │
╞═════════════════════╪═══════╪════════╪═══════╪═══════╡
│ 2022-01-04 08:45:00 ┆ 29045 ┆ 21350 ┆ false ┆ 0 │
│ 2022-01-04 08:46:00 ┆ 29055 ┆ 3215 ┆ false ┆ 0 │
│ 2022-01-04 08:47:00 ┆ 29055 ┆ 1535 ┆ false ┆ 0 │
│ 2022-01-04 08:48:00 ┆ 29065 ┆ 1964 ┆ false ┆ 0 │
│ … ┆ … ┆ … ┆ … ┆ … │
│ 2022-12-31 05:57:00 ┆ 25765 ┆ 0 ┆ false ┆ 12 │
│ 2022-12-31 05:58:00 ┆ 25765 ┆ 0 ┆ false ┆ 12 │
│ 2022-12-31 05:59:00 ┆ 25765 ┆ 0 ┆ false ┆ 12 │
│ 2022-12-31 06:00:00 ┆ 25740 ┆ 4373 ┆ false ┆ 12 │
└─────────────────────┴───────┴────────┴───────┴───────┘
SQまでの残存時間の計算
taimedeltaに対して数値を渡せばそれを使用してsq_idごとの累積和を計算します。
単純に累積和を計算すると、SQ直前が一番大きな値になってしまうので、一旦datetime列を使用し降順にソートします。その後sq_idごとに累積和を計算し、また昇順に戻す処理をしています。
出力を見ると8:59でSQまでの残存時間が1になり、SQの9:00にはまた次のSQまでの残存時間が表示されているのが確認できます。
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
shape: (6, 5)
┌─────────────────────┬───────┬───────┬───────────┬───────┐
│ datetime ┆ sq ┆ sq_id ┆ timedelta ┆ to_sq │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ bool ┆ i16 ┆ i32 ┆ i32 │
╞═════════════════════╪═══════╪═══════╪═══════════╪═══════╡
│ 2022-02-10 08:57:00 ┆ false ┆ 1 ┆ 1 ┆ 3 │
│ 2022-02-10 08:58:00 ┆ false ┆ 1 ┆ 1 ┆ 2 │
│ 2022-02-10 08:59:00 ┆ false ┆ 1 ┆ 1 ┆ 1 │
│ 2022-02-10 09:00:00 ┆ true ┆ 2 ┆ 1 ┆ 22838 │
│ 2022-02-10 09:01:00 ┆ false ┆ 2 ┆ 1 ┆ 22837 │
│ 2022-02-10 09:02:00 ┆ false ┆ 2 ┆ 1 ┆ 22836 │
└─────────────────────┴───────┴───────┴───────────┴───────┘
秒単位の残存期間計算もしてみます。
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
shape: (6, 5)
┌─────────────────────┬───────────┬───────┬───────┬─────────┐
│ datetime ┆ timedelta ┆ sq ┆ sq_id ┆ to_sq │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ i64 ┆ bool ┆ i16 ┆ i64 │
╞═════════════════════╪═══════════╪═══════╪═══════╪═════════╡
│ 2022-02-10 08:57:00 ┆ 60 ┆ false ┆ 1 ┆ 180 │
│ 2022-02-10 08:58:00 ┆ 60 ┆ false ┆ 1 ┆ 120 │
│ 2022-02-10 08:59:00 ┆ 60 ┆ false ┆ 1 ┆ 60 │
│ 2022-02-10 09:00:00 ┆ 60 ┆ true ┆ 2 ┆ 2505600 │
│ 2022-02-10 09:01:00 ┆ 60 ┆ false ┆ 2 ┆ 2505540 │
│ 2022-02-10 09:02:00 ┆ 60 ┆ false ┆ 2 ┆ 2505480 │
└─────────────────────┴───────────┴───────┴───────┴─────────┘
メジャーSQまでの残存期間計算もしてみます。
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
shape: (6, 5)
┌─────────────────────┬───────────┬───────┬───────┬─────────┐
│ datetime ┆ timedelta ┆ sq ┆ sq_id ┆ to_sq │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ datetime[μs] ┆ i64 ┆ bool ┆ i16 ┆ i64 │
╞═════════════════════╪═══════════╪═══════╪═══════╪═════════╡
│ 2022-02-10 08:57:00 ┆ 60 ┆ false ┆ 2 ┆ 2505780 │
│ 2022-02-10 08:58:00 ┆ 60 ┆ false ┆ 2 ┆ 2505720 │
│ 2022-02-10 08:59:00 ┆ 60 ┆ false ┆ 2 ┆ 2505660 │
│ 2022-02-10 09:00:00 ┆ 60 ┆ false ┆ 2 ┆ 2505600 │
│ 2022-02-10 09:01:00 ┆ 60 ┆ false ┆ 2 ┆ 2505540 │
│ 2022-02-10 09:02:00 ┆ 60 ┆ false ┆ 2 ┆ 2505480 │
└─────────────────────┴───────────┴───────┴───────┴─────────┘
終わりに
今回はSQまでの残存期間を計算しました。
次に書く事はまだ決めていません。あまり変な事を書くと、自分の首を絞める事になるので...
金融と関係ありませんが、GISの記事も書いていこうと思っているので、次はPythonとGISを使った地理的分析でコンビニ出店計画を支援する。的な記事を書こうかと思っています。
Discussion