J-Quants のティックには売り買いの方向が入っていません。値段の動きから推定するのが定番ですが、実際にやってみたら約定の 54.0% は前と同じ値段で、判定できませんでした。そしてそもそも方向が定義できない約定が、出来高の 13.7% ありました。
- 前の約定と比べて上 23.0% / 下 23.0% / 同値 54.0%
- 同値の扱いを変えると、買い比率が最大 3.18 ポイント動きます
- よくある書き方は系統的に低く出ます(48.36% 対 50.1%)
- 板寄せが出来高の 13.74%。大引けだけで 10.13%、ある銘柄は 28.30%
- 答え合わせはできません。気配が配られていないので正解が無い
そもそも、なぜ推定が要るのか
J-Quants のティックの列はこの7つです。
Date, Code, Time, SessionDistinction, Price, TradingVolume, TransactionId「この約定は買い手が仕掛けたのか、売り手が仕掛けたのか」は入っていません。板の情報(最良買い気配・売り気配)も配られていないので、約定した値段と出来高だけから推定することになります。
これができると、板を見ずに「今どちらが押しているか」が数字になります。だから多くの人が試みます。では、どのくらいできるのか。
定番の方法:ティックルール
いちばん素直なやり方です。前の約定より高ければ買い、安ければ売り。
import numpy as np
# ティックルール:前の約定より高ければ買い主導、安ければ売り主導
d = d.sort_values(["Date", "Tid"]) # ★時刻ではなく TransactionId 順
pr = d["Price10"].to_numpy(np.int64)
diff = np.concatenate([[0], np.diff(pr)])
newday = d["Date"].to_numpy() != np.concatenate([[0], d["Date"].to_numpy()[:-1]])
diff[newday] = 0 # 日をまたぐところは比較しない
side = np.sign(diff) # +1 買い / -1 売り / 0 決められない
print(f"決められない割合 {(side == 0).mean() * 100:.1f}%")並べ替えは時刻ではなく TransactionId です。同じマイクロ秒に複数の約定が並ぶので、時刻で並べると順番が狂い、推定そのものが壊れます。
★ 半分以上は、前と同じ値段だった
売買代金の大きい12銘柄について、14営業日ぶんのティックで数えました。
| 銘柄 | 約定数 | 上がった | 下がった | 同値 |
|---|---|---|---|---|
| A(最大手) | 1,433,594 | 21.6% | 21.7% | 56.7% |
| B | 1,823,994 | 25.6% | 25.5% | 48.9% |
| C | 413,398 | 17.6% | 17.6% | 64.9% |
| D | 1,629,697 | 31.1% | 31.3% | 37.7% |
| E | 187,807 | 26.3% | 26.7% | 47.0% |
| F | 498,816 | 24.1% | 24.1% | 51.8% |
| G | 338,129 | 16.1% | 16.0% | 67.9% |
| H | 1,093,087 | 21.4% | 21.5% | 57.1% |
| I | 226,349 | 26.0% | 26.0% | 48.0% |
| J | 958,507 | 26.1% | 26.3% | 47.6% |
| K | 477,084 | 24.0% | 24.0% | 51.9% |
| L | 204,302 | 15.9% | 16.0% | 68.1% |
| 平均 | 773,730 | 23.0% | 23.0% | 54.0% |
上がった 23.0% と下がった 23.0% がきれいに対称なのは、計算が正しいことの確認になります。値段は行って戻るからです。
問題は残りです。54.0%、半分以上が「前と同じ値段」でした。ティックルールはここで何も言えません。銘柄によっては68.1%、つまり7割近くが判定不能です。
同値が多いのは、呼値の単位(値段の刻み)が粗い銘柄です。刻みが粗いほど同じ値段が続きます。「流動性が高いほど推定しやすい」とは限りません。上の表で同値が最も少ないのは D(37.7%)で、最大手の A(56.7%)より判定しやすい。
同値をどう扱うかで、答えが動く
よく使われる3通りで、14営業日ぶんの買い比率(出来高ベース)を出しました。
- A:直前の判定を引き継ぐ(教科書的なティックルール)
- B:同値は捨てる(分母からも外す)
- C:同値は半分ずつ(方向不明として 0.5 を配る)
# 同値のところに、直前の判定を引き継ぐ
carry, last = side.copy(), 0
for i in range(len(carry)):
if newday[i]:
last = 0 # 日の最初は引き継ぎ元が無い
if carry[i] == 0:
carry[i] = last
else:
last = carry[i]
# ★ここで carry == 0 が残る(その日まだ一度も値が動いていない区間)
buy_ratio = vol[carry > 0].sum() / vol.sum() # ← 分母に未確定が入ったまま| 銘柄 | A 引き継ぎ | B 捨てる | C 半分 | 最大差 |
|---|---|---|---|---|
| B | 51.99% | 54.73% | 52.78% | 2.74 |
| D | 48.89% | 49.65% | 49.77% | 0.89 |
| H | 45.54% | 46.34% | 48.22% | 2.67 |
| J | 49.84% | 52.58% | 51.54% | 2.74 |
| F | 49.82% | 50.98% | 50.54% | 1.17 |
| L | 45.38% | 47.26% | 48.56% | 3.18 |
| G | 47.35% | 48.53% | 49.39% | 2.04 |
| C | 48.05% | 50.59% | 50.27% | 2.54 |
| 平均 | 48.36% | 50.08% | 50.13% | 2.25 |
14営業日を丸ごと合計すれば、どのルールでもおよそ半々に落ち着きます。当たり前のようですが、これも確認になります。1日の中で買いと売りは同じ数だけ約定するので、大きく 50% を離れる結果が出たら、それは実態ではなく処理の間違いです。
★ Aだけ 1.7 ポイント低い
表をよく見てください。Aの平均が 48.36%、BとCは 50.1%。Aだけ系統的に低く出ています。
理由は引き継いでも決まらない約定が残るからです。その日まだ一度も値段が動いていない区間には、引き継ぐ元がありません。上のコードの vol[carry > 0].sum() / vol.sum() は、この未確定ぶんを分母に入れたまま分子に入れていません。
| 出来高に占める割合 | |
|---|---|
| 引き継いでも決まらなかった | 平均 3.54%(最大 6.39%) |
3.54% が丸ごと「買いではない」側に数えられるので、買い比率が下がります。差の 1.7 ポイントは、ほぼこれで説明がつきます。
これは推定の精度の話ではなく、割り算の間違いです。
「買い比率が 48% だった、売り優勢だ」と読んでしまうと、存在しない偏りを見ていることになります。
# 未確定を分母からも外す
m = carry != 0
buy_ratio = vol[m & (carry > 0)].sum() / vol[m].sum()
# そして「板寄せ」は最初から除く(方向という概念がない)
is_auction = first_of_day | last_of_session
m &= ~is_auction★ そもそも方向が無い約定が、出来高の 13.7%
ここがいちばん大事な点です。日本株の寄付・前引け・大引けは板寄せで、売り注文と買い注文を突き合わせて一本の値段を付けます。
板寄せの約定に「買いが仕掛けた/売りが仕掛けた」という区別はありません。全部が同時に、同じ値段で約定します。ここをティックルールで分類するのは、意味の無い作業です。
| 銘柄 | 寄付 | 前引け | 大引け | 合計 |
|---|---|---|---|---|
| B | 3.01% | 0.07% | 5.79% | 8.88% |
| D | 1.14% | 0.04% | 2.18% | 3.36% |
| H | 3.07% | 0.06% | 4.82% | 7.94% |
| J | 2.99% | 0.07% | 9.32% | 12.38% |
| F | 1.93% | 0.05% | 5.01% | 6.99% |
| L | 6.39% | 0.17% | 28.30% | 34.85% |
| G | 6.18% | 0.11% | 15.59% | 21.89% |
| C | 3.56% | 0.07% | 10.00% | 13.63% |
| 平均 | 3.53% | 0.08% | 10.13% | 13.74% |
大引けだけで出来高の 10.13%。ある銘柄では 28.30%、つまり1日の出来高の4分の1以上がたった1本の約定で付いています。指数に連動する資金が大引けに集中するためです。
一方、約定の「本数」で見れば板寄せは 0.005% しかありません。本数で数えるか出来高で数えるかで、見え方がまったく変わります。
前引け(11:30)は 0.08% しかありません。板寄せだから大きい、のではなく、大引けにだけ資金が集まっているのが実態です。
答え合わせができない、という限界
ここまで「推定のばらつき」は測れました。では、推定は当たっているのか。
確かめられません。正解が手元に無いからです。判定の正解を知るには、約定した瞬間の最良買い気配・売り気配が要りますが、私の契約(Light + 分足・ティックアドオン)で配られるデータに気配は含まれていません。
だから、この手の記事でよく見る「ティックルールの的中率は◯%」という数字は、このデータだけでは出せません。出ているとしたら、別のデータ源で答え合わせをしているはずです。出典を確かめてください。
使うなら、こうする
- 並べ替えは
TransactionIdで。時刻では順番が狂います - 板寄せは最初に除く。寄付・前引け・大引けの約定。出来高の13.7%です
- 未確定は分母からも外す。入れたままだと系統的に低く出ます
- 同値の扱いを明記する。A・B・Cのどれかで2ポイント動きます
- 合計が 50% 近辺になることを毎回確かめる。大きく外れたら処理の間違いです
- 「的中率」を名乗らない。このデータでは検証できません
まとめ
方向の無いティックから買いと売りを分ける作業は、半分以上が判定不能という前提から始まります。そこに、扱い方で2ポイント動く裁量と、そもそも方向が無い13.7%の出来高が乗ります。
できないと言っているのではありません。何をどう決めたかを書いておけば、使える数字になります。書いていない数字は、あとから自分でも再現できません。
そして的中率は、このデータでは測れません。測れないものを測れたことにしないのが、検証の最低条件だと思っています。
この記事の出どころ
- データ:J-Quants API V2 の bulk ティック(equities/trades)。Light プラン+分足・ティックアドオン
- 期間:2026-09-01 〜 2026-09-18 の14営業日
- 対象:最終日の売買代金 上位12銘柄。約定数 合計 9,284,764 本
- 板寄せの判定:各セッションの最初と最後の約定を板寄せとみなした(Session=1 は 09:00〜11:30、Session=2 は 12:30〜15:30 を実測で確認)
- 環境:Windows 11 / Python 3.12 / pandas 3.0 / numpy 2.5 / pyarrow 21
- 測定日:2026年9月21日
- 注意:掲載したのは集計結果です。銘柄名は伏せ、取得したデータそのものは配布していません