ここに1本のバーがある。Binance USDⓈ-M無期限契約、BTCUSDT、UTC 13:46:00に始まる1分間だ。klines endpointから、12個の値だけで構成された配列として返ってきた。これは、個人トレード周辺のクオンツ研究で最も一般的な入力単位だ。私たちが生成するほぼすべてのストラテジーが、このような形のデータに触れる。
では、インデックスごとに分解して、バックテストがどれほど多くを間違えているのか見てみよう。
| インデックス | 値 | 名称 |
|---|---|---|
| 0 | 1773495960000 | 始値時刻 (ms) |
| 1 | "84120.50" | 始値 |
| 2 | "84177.90" | 高値 |
| 3 | "84098.10" | 安値 |
| 4 | "84163.40" | 終値 |
| 5 | "38.417" | 基準通貨出来高 (BTC) |
| 6 | 1773496019999 | 終値時刻 (ms) |
| 7 | "3232108.94" | クォート通貨出来高 (USDT) |
| 8 | 1204 | 取引件数 |
| 9 | "21.883" | taker買い基準通貨出来高 |
| 10 | "1841203.55" | taker買いクォート通貨出来高 |
| 11 | "0" | 無視 |
[0]と[6]:この1分間はいつなのか、そして誰の時計なのか
始値時刻は1773495960000、終値時刻は1773496019999だ。後者に注目してほしい。19999で終わっており、次のバーの始値の1ミリ秒前だ。ウィンドウは半開区間であり、取引所はそれを明示的に伝えている。私が追いかけてきたデータバグの半分は、両端点を含むものとして扱い境界の取引を二重計上したり、リサンプルの位置合わせを誤って各5分足が隣のバーから1ミリ秒借りてしまったりしたことから始まっている。
タイムスタンプは取引所のマッチングエンジン時刻だ。あなたの時計でも、ベンダーの取り込み時計でもなく、funding snapshotが使う時計とも同じではない。別のendpointから取得したfunding-rate系列やopen-interest系列とkline系列を結合するとき、ほとんどの場合、両者が一致するのはおおむね1秒以内にすぎない。1分足ストラテジーでは、結合された各行に1.7%のタイミング誤差があることになる。1分未満のものでは致命的だ。
そして、このバーに付いている時刻は始値だ。そこに含まれる情報が知り得るのは13:46:59.999になってからである。インデックスの規約に関する問い — 1つずらすべきか、イベント時刻としてbar-closeとbar-openのどちらを使うべきか — は、結局のところ、衣装を変えた同じlookahead問題だ。
[1] "84120.50":取引できない始値
始値とは、そのウィンドウ内で最初に成立した取引の価格だ。あなた以外の2人の間で完了した取引である。バーがあなたのdataframeの行として存在する頃には、その価格はすでに60秒前のものだ。
前のバーは84109.80で引けたので、隣り合う2本の1分足の間には$10.70の値動きがある。1.3 basis pointsで、チャート上では見えず、taker手数料のおよそ4分の1に相当する。まあ、それ自体はいい。だが、同じ系列を米国CPI発表中のalt perpで見てみると、バー間のギャップは15から40 bpsになる。バーの終値でシグナルを出し、次のバーの始値で約定させるバックテストは、ひそかにそのギャップがゼロだと仮定している。そして、そのギャップがゼロなのは、まさに影響しないと困る場面なのだ。
[2]と[3]:高値"84177.90"、安値"84098.10"
この2つは、ほとんどのfill engineが崩壊する場所なので、ここは長くなる。
高値と安値は、極端な価格に触れた値だ。そこには数量も継続時間も含まれない。この同じ1分間のraw trade tapeを見ると、84100.00以下のすべての取引は、1.4秒の間に9件、合計0.62 BTCだった。つまり、バックテストのストップが84100で「約定」する。そしてポジションが3 BTCなら、下落途中で見えていた板をすべて食い尽くし、残りの数量は84105–84130まで戻る過程のどこかで約定したことになる。バーが示しているのは安値が84098.10だったということだけだ。そこで取引されたnotionalが$52,000にすぎなかったとは、バーは教えてくれない。
逆方向は、あなたに都合よく見える分だけ、さらに悪い。84175にresting limit sellを置いていたとしよう。バーの高値は84177.90なので、素朴なengineなら84175で約定させ、takerコストではなくmakerリベートを計上する。だが、その約定が実際に起きたかは、その価格帯でのqueue position次第であり、それをバーが知ることはできないし、あなたもおそらく記録していない。触れたことと約定したことは違う。
私たちがfill engineで採用したルールはこうだ。resting limitは、バーがその水準に到達しただけでなく、通り抜けて取引された場合にのみ約定する。極値ティックぴったりでの約定は、trade tapeのvolume-at-priceによる証拠がなければ却下する。このルールによって、典型的なmean-reversion bookから取引のおよそ6%が削られ、候補の1つのバックテストSharpeは1.9から1.1に落ちた。その候補は最初から現実のものではなかった。そう言うだけの正直さを、fill ruleが最初に示したのだ。
関連する落とし穴がintrabar pathだ。バーのレンジがストップとtake-profitの両方をまたぐ場合、OHLCVからはどちらが先だったのか分からない。すべてのengineは規約を1つ選ばなければならない。私たちは常にstop-firstを仮定する。悲観的で、ときどき間違っているが、偽の勝ちを生み出すことはない。あなたのengineがtake-profit-firstを仮定しているなら、レンジの広いバーは曖昧さから利益を捏造する。そして、レンジの広いバーこそ、PnL分布を支配するものなのだ。
[4] "84163.40":バーの中で最も頑健性の低い数字
終値とは、そのウィンドウ内で最後に成立した取引だ。それだけである。13:46:59.8にbotがポジションを丸めるために行った、0.002 BTCのodd-lotかもしれない。この構造上恣意的な単一のティックを、ほとんどの研究パイプラインがすべてのシグナル計算、すべてのポジションの評価、すべての決済評価に使っている。
BTC perpではほとんど問題にならないが、UTC 04:00の薄いaltcoin perpでは非常に大きな問題になる。同じ1分間でも、2つのvenueの終値の差が1取引あたりのedge全体を上回ることさえある。ストラテジーのPnLが終値そのものに依存しているとき、私たちは代わりにexchange mark priceで評価して再実行する。これはindex-derivedで、はるかに操作しにくい。結果が乖離するなら、そのストラテジーはartifactを取引していたということだ。
[5]と[7]:"38.417"と"3232108.94"、出来高の単位は何か
base volumeはBTC、quote volumeはUSDTだ。ここでは両方が示されているが、これはすべてのvenueが提供してくれるわけではない親切だ。気にすべき理由は、venueをまたいだ集計にある。coin-margined contractは$100 notionalのcontract単位でクォートされる。株式のfeedにはround lotで報告するものがある。prediction marketのvenueは、1 shareがドル建ての二値claimであるようなshare数を報告する。異なるuniverseの「volume」を単一のnotional単位に正規化せず合計すると、流動性ランキングは完全なナンセンスになる。そして、レビューをすり抜けるほど安定して見えるナンセンスになってしまう。
取り込み時にすべてをquote notionalへ正規化する。raw fieldも保存するが、ストラテジーには決して見せない。
[8] 1204:impact modelを決めるべきフィールド
base volumeを取引件数で割ると、平均約定は0.032 BTC、約$2,700になる。候補ストラテジーが一度に$250,000を投入したいなら、その1分間の典型的な取引の約92倍のサイズを求めていることになる。この数字こそが、汎用的な5-bps slippage定数ではなく、square-root impact termを動かすべきものだ。私たちはbarごとのparticipation rateをfirst-class columnとして計算し、median entryがbar notionalの数パーセントを超えるストラテジーを却下する。そこから先のすべては作り話だからだ。
取引件数は、奇妙な1分間を安価に見つける手がかりにもなる。出来高は通常なのに、取引件数が11まで落ちている?誰かがblockを処理した。出来高は通常なのに、取引件数が9,000?清算カスケードが小さな単位で食い尽くされている。
[9]と[10]:"21.883"、誰もが捨ててしまうフィールド
taker buy base volume。このバーの38.417 BTCのうち21.883が買い主導だったため、signed volume deltaは+5.349 BTC、aggressor splitは買い優勢の57/43になる。取引所はorder flow imbalanceを無料で渡してくれている。多くの人がこのフィールドを読まないのは、pandasが列名を付けてくれなかったからだ。
これ単独でリターンを予測できると言っているわけではない。素朴なdeltaストラテジーは、手数料台帳に寄付する方法としてはかなり確実な部類に入る。だが、これは価格とは本当に異なる測定値であり、すでに行っている同じrequestで取得でき、買い手が買ったことで起きたrallyと、売り手が退いたことで起きたrallyを見分けられる。この2つはOHLCでは同じに見えるが、10分後には異なる動きをする。私たちのresearch agentは、公開されているvenueでtaker splitを無視する仮説を、使える証拠を残したままにするものとして扱う。
[11] "0":ignore — そしてここにないものすべて
インデックス11は非推奨フィールドで、永久にゼロだ。より興味深いのは、このバーに含まれていないものの一覧である。bidなし、askなし、spreadなし、book depthなし、funding rateなし、open interestなし、清算情報なし、mark priceなし、index priceなし。そして決定的に重要なのは、あなたの注文がmakerだったのかtakerだったのかを知る方法がないことだ。このvenueでは、0.045%を支払うか、0.01%を得るかの違いになる。
だから、klineだけで構築された手数料モデルは、数字の服を着た仮定にすぎない。私たちは、すべてのストラテジーに最初にexecution styleを宣言させ、それ以外を証明できないものにはtaker手数料を課すことで解決している。
一度も現れなかったバー
最後の部分であり、主要銘柄以外では最も痛いところだ。取引がゼロの1分間には、kline自体が存在しない。ベンダーやライブラリは、それを一般にforward-fillする。open = high = low = close = 前の終値、volume 0となる。あなたのindicatorは問題なく計算される。ストラテジーは有効な行を見て、世界中の誰もその銘柄を取引していない1分間にシグナルを出せる。
私たちが取り込んだあるmid-cap perpでは、12か月間の1分足の4.1%に取引がなかった。その銘柄のmean-reversion候補は、entryの38%をsynthetic barに出していた。フラットなsynthetic priceは、moving averageからの乖離を測るものなら何でも誘惑するからだ。バックテストは見事だった。データの隙間を取引していたのである。
だから現在、ingest layerはバーごとにsynthetic booleanを保持し、それをすべてのresampleに伝播させている。そしてverification gauntletは、そこで取引が集中するストラテジーを失格にする。安価な列だ。これまで私たちが書いたどのindicatorよりも多くの候補を葬ってきた。
12個の値。そのうち4つは日常的に読み違えられ、2つは日常的に捨てられ、さらに1カテゴリー全体が行に存在しないのにengineによって想像されている。次のバックテストの前に、自分のstoreからraw barを1本取り出し、fill logicが想定していることと照らし合わせながら、すべてのフィールドを声に出して読んでみてほしい。20分の作業だが、これをやって何も見つけなかった人を私は見たことがない。
← 記事一覧