価格が動く前の圧力
価格が勝手に動くことはまれで、注文が到着して流動性を取り除くから動きます。約定だけを見ると話の大半を見逃します。動きの大部分は板の最上位での注文の発注と取消だからです。注文フロー不均衡(OFI)は、最良気配の更新をすべて符号付きの1つの数にまとめます。買い手が増やすか売り手が引くと正、逆なら負です。短い期間では、次の価格変化を驚くほどよく予測します。
def book_imbalance(bid_size, ask_size):
total = bid_size + ask_size
if total == 0:
return 0.0
return (bid_size - ask_size) / total
print(book_imbalance(450, 200)) # 0.3846 more size on the bid: upward pressure
print(book_imbalance(100, 400)) # -0.6 sellers dominate
print(book_imbalance(0, 0)) # 0.0 an empty top does not divide by zero1回の更新、1つの数
連続する2つの気配を比べます。ビッド側では、価格が上がるか変わらなければ新しい数量を足し、下がるか変わらなければ古い数量を引きます(価格が同じなら両方が適用され、数量の変化分が効きます)。アスク側は符号が逆で、価格が下がるか同じなら売り圧力が加わり(- 新しいアスク数量)、上がるか同じなら古い数量が取り除かれます(+ 古いアスク数量)。4つの項は (b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as + (a >= pa) * pas で、各比較は1または0です。
# The order-flow imbalance of one quote update, tried on four changes of the top of the book
def ofi_event(prev, cur):
pb, pbs, pa, pas_ = prev
b, bs, a, as_ = cur
return (b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as_ + (a >= pa) * pas_
q0 = (99.5, 300, 100.0, 200)
print(ofi_event(q0, (99.5, 450, 100.0, 200))) # 150 more size on the same bid
print(ofi_event(q0, (99.6, 100, 100.0, 200))) # 100 a higher bid: the whole new size counts
print(ofi_event(q0, (99.4, 300, 100.0, 200))) # -300 the bid price fell: the old size is gone
print(ofi_event(q0, (99.5, 300, 100.1, 200))) # 200 the ask retreated: less selling pressure系列とそれが説明するもの
すべての連続する気配のペアにルールを適用するとOFIの系列が得られます。1秒のような短い窓でイベントを合計して仲値の変化と比べると、関係はほぼ線形で d_mid = slope * OFI です。傾きはフロー1単位が価格をどれだけ動かすかを表し、板が厚いと小さくなります。原点を通る最小二乗直線として sum(ofi * dmid) / sum(ofi ** 2) で求めます。
# A series of events from consecutive quotes, set beside the change of the mid price
import numpy as np
def ofi_event(prev, cur):
pb, pbs, pa, pas_ = prev
b, bs, a, as_ = cur
return (b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as_ + (a >= pa) * pas_
quotes = [(99.5, 300, 100.0, 200), (99.5, 450, 100.0, 200), (99.6, 100, 100.0, 150), (99.6, 100, 100.1, 400)]
events = np.array([ofi_event(prev, cur) for prev, cur in zip(quotes, quotes[1:])], dtype=float)
print(events) # [150. 150. 150.]
mid = np.array([(q[0] + q[2]) / 2 for q in quotes])
dmid = np.diff(mid)
print(np.round(dmid, 3)) # the mid moved in the direction of the flow# Recover the price impact of the flow with a slope through the origin
import numpy as np
def impact_slope(ofi, dmid):
return float(np.sum(ofi * dmid) / np.sum(ofi ** 2))
rng = np.random.default_rng(1)
flow = rng.normal(0, 200, size=500) # order-flow imbalance per second
price_change = 0.00005 * flow + rng.normal(0, 0.004, size=500) # the mid moves with the flow, plus noise
print(round(impact_slope(flow, price_change), 6)) # 5e-05, the true impact per unit of flow予測力はあるが、楽に儲かるわけではないOFIが予測するのはスプレッドより小さな値動きで、高速な参加者はコスト控除前にすでにそれを取引しています。多くの人にとっての価値は執行にあります。板が自分に有利に傾いているので買い注文を急がず待てるのか、価格が走りそうなので急ぐべきなのかを教えてくれます。また市場にも依存し、傾きは銘柄や時間帯で異なります。
- 板の不均衡
(bid - ask) / (bid + ask)は-1から+1の間。空の場合に備える - 1回の更新のOFI:
(b >= pb) * bs - (b <= pb) * pbs - (a <= pa) * as + (a >= pa) * pas。系列は連続する各ペアにこれを適用したもの - 仲値の変化はOFIにほぼ線形:
slope = sum(ofi * dmid) / sum(ofi**2)。短期のシグナルで、特に執行に役立つ
次はasyncioによる非同期コードです。多数のデータストリームを、それぞれを待たずに扱います。
ラボ:採点される内容
book_imbalanceで両サイドを比べる25点ofi_eventで1回の更新を評価する25点ofi_seriesで圧力の推移を追う25点impact_slopeでフローの価格への効き方を測る25点