2
フォロー
439
フォロワー

スポットデリバリー裁定戦略の実践的探究:理想から現実への落とし穴

作成日:: 2025-11-28 14:22:49, 更新日:: 2025-12-16 11:31:06
comments   0
hits   345

スポットデリバリー裁定戦略の実践的探究:理想から現実への落とし穴

序文

最近、友人からアービトラージ戦略を作成できないかと尋ねられました。Inventorプラットフォームにはそのような機能がありませんでした。最初は難しくないだろうと思っていましたが、基本的なロジックをなんとか動作させるまでに約1ヶ月かかりました。今振り返ってみると、最初のアイデアから実装に至るまでに遭遇した落とし穴は、想像をはるかに超えるものでした。

この記事では、この裁定取引戦略の開発中に発生した実際的な問題と、開発された解決策について説明します。学習および参照目的のみであり、実用的な投資価値はありません。

戦略の基本論理

受渡契約とスポット価格の間には価格差があり、通常は一定の平均値を中心に変動します。価格差が過度に乖離すると、理論的には裁定取引の機会が存在します。

最初のアイデアはシンプルでした。

  • スポット契約とデリバリー契約間の価格差を監視する
  • 価格差が標準偏差の 2 倍を超えたらポジションを開きます。
  • 価格差が元の値に戻ったときにポジションをクローズして利益を得ます。

素晴らしいですね。しかし実際には、「ポジションを開く」というステップだけでも、処理すべき細かい作業が無数にあることに気づくでしょう。

最初の落とし穴:価格スプレッドの安定性

当初は価格スプレッドの乖離を直接的な指標として用いていましたが、価格スプレッドが拡大し続け、以前の状態に戻らない場合があることに気づきました。その後、私たちは…すべての価格スプレッドが安定しているわけではありません。

特に受渡日が近づくにつれて、スプレッドの動きが変化する可能性があります。そのため、スプレッド系列が定常であるかどうかを判断するためにADFテストが追加されました。

function adfTest(series, maxLag=null){
  const n = series.length;
  if(n<10) throw new Error('series too short');
  if(maxLag===null) maxLag = Math.floor(12*Math.pow(n/100, 1/4));
  
  // ... ADF检验的核心计算逻辑
  
  const res = ols(rows, Ys);
  const tstat = tStat(res.beta, res.cov, 1);
  return { tStat: tstat, pValue: pval, usedLag: p };
}

当初は分散比検定、半減期検定、KS検定など、多くの検定を追加していましたが、検定が多すぎるとポジションをオープンする機会が大幅に減少することがわかりました。最終的には、ADF検定のみに簡素化し、p値の閾値を0.1に設定しました。

さらに重要なことは、さらに 1 つ追加されたことです。連続失敗カウンター

if (!adfPass) {
  stationarityFailCount[deliverySymbol] = (stationarityFailCount[deliverySymbol] || 0) + 1;
} else {
  stationarityFailCount[deliverySymbol] = 0;
}

let consecutiveFails = stationarityFailCount[deliverySymbol];
let canTrade = consecutiveFails < CONFIG.consecutiveFailThreshold;

テストが3回連続で失敗した場合、取引は禁止されます。これにより、市場が異常な状態にある際に、盲目的にポジションを開くことを防ぐことができます。

2つ目の落とし穴:リアルタイム損益統計をめぐる混乱

先物口座の損益計算は簡単で、USDTの変動を見るだけです。しかし、スポット口座の場合は違います。USDTと暗号通貨の両方が含まれています。どのように計算するのでしょうか?

見落とされやすい詳細は次のとおりです。スポット商品の保有コストは取引に応じて変化します。例えば、100 USDTでコインを1枚購入し、90 USDTで売却し、その後85 USDTで買い戻した場合、原価は当初の100 USDTではなくなります。ポジションをオープンした時点での固定価格をそのままコインの価値計算に用いるだけでは、真の損益状況を反映しません。

正しいアプローチは注文オブジェクトから平均実取引価格を抽出します。

let openSpotPrice = (openSpotOrder && openSpotOrder.AvgPrice) ? 
  openSpotOrder.AvgPrice : record.openSpotPrice;
let closeSpotPrice = closeSpotOrder.AvgPrice || currentPair.spotPrice;

次に、実際の取引価格に基づいて収益率が計算されます。

// 正套:买现货+卖期货
if (record.direction === 'positive') {
  spotReturnRate = (closeSpotPrice - openSpotPrice) / openSpotPrice;
  deliveryReturnRate = (openDeliveryPrice - closeDeliveryPrice) / openDeliveryPrice;
} else {
  // 反套:卖现货+买期货
  spotReturnRate = (openSpotPrice - closeSpotPrice) / openSpotPrice;
  deliveryReturnRate = (closeDeliveryPrice - openDeliveryPrice) / openDeliveryPrice;
}

let totalReturnRate = spotReturnRate + deliveryReturnRate;
let requiredUSD = openSpotPrice * record.spotAmount;
let actualTotalPnl = totalReturnRate * requiredUSD;

ここでの損益計算ロジックに注意してください。

  • レギュラーセット当時は、スポット市場がロング、先物市場がショートであったため、スポット市場の損益は(終値 - 始値)/ 始値、先物市場の損益は(始値 - 終値)/ 始値でした。
  • 逆行する当時、スポット取引ではショートが、先物取引ではロングが、それぞれ反対方向に行われていました。

最後に、各取引の実際の利益または損失が合計され、総利益または総損失が算出されます。

accumulatedProfit += actualTotalPnl;
_G('accumulatedProfit', accumulatedProfit);

3つ目の大きな落とし穴:流動性の罠

これが本当に頭を悩ませる問題です。Inventor Quantitative Platform は10年間運用されてきたにもかかわらず、なぜ先物契約の裁定取引戦略がこれほど少ないのでしょうか?答えは簡単です。受渡契約市場における流動性の不足

質問1: シングルレッグ取引

裁定取引は、複数のポジションを同時に開くようなおとぎ話ではありません。現実はこうです。

  • スポット注文は執行されましたが、先物注文はまだ保留中です。
  • あるいは、先物注文は執行されたが、現物注文はキャンセルされた。

これは「片足リスク」の典型的な例です。片足はポジションを保有していますが、もう片方は依然としてポジションを保有していません。この時点では、価格変動はもはや裁定取引ではなく、片側だけのポジションとなります。

解決策は参加することだロールバックメカニズム

if (!deliveryOrder) {
  Log('❌ 期货卖单失败,回滚现货');
  exchanges[0].CreateOrder(pair.spotSymbol, 'sell', -1, spotAmount);
  addCooldown(pair.deliverySymbol, pair.coin, '期货卖单失败,已回滚现货');
  return false;
}

いずれかのレッグが失敗した場合は、直ちに成行注文を出して、すでに実行されたレッグをクローズし、そのレッグのリスク露出を減らします。

質問 2: 成行注文も拒否される可能性があります。

さらに不可解なことに、成行注文が失敗することもあります。これは為替リスク管理や市場の厚み不足が原因となる場合があり、簡単に言えば、注文が成立しないのです。

それで私はそれをやった。成行注文+指値注文の二重の仕組み

function createOrderWithFallback(exchange, symbol, direction, amount, limitPrice, orderType, maxRetry = 3) {
  let useMarketOrder = (limitPrice === -1);
  
  // 先尝试限价单
  if (!useMarketOrder) {
    orderId = exchange.CreateOrder(symbol, direction, limitPrice, amount);
    if (!orderId) {
      Log(`❌ 限价单提交失败,改用市价单`);
      useMarketOrder = true;
    }
  }
  
  // 限价单失败则用市价单
  if (useMarketOrder && !orderId) {
    orderId = exchange.CreateOrder(symbol, direction, -1, marketAmount);
  }
  
  // ... 检查订单状态,失败则重试
}

システムは最大 3 回再試行することができ、各試行では安全ネットとして市場注文が使用されます。

質問3:スポット買い注文における金額トラップ

この落とし穴は特に巧妙に隠されており、細心の注意が必要です。先物市場注文の数量は…コインの枚数ただし、スポット市場注文を購入する場合の注文数量は…USDT金額

ここでは、変換ロジックが具体的に追加されています。

function getActualAmount(useMarket) {
  if (isSpotBuy && useMarket) {
    // 现货市价买单:需要用USDT金额
    let currentPrice = getDepthMidPrice(exchange, symbol);
    let usdtAmount = amount * currentPrice;
    Log(`  💡 现货买单转换: ${amount.toFixed(6)} 币 → ${usdtAmount.toFixed(4)} USDT`);
    return usdtAmount;
  }
  return amount;
}

指値注文で使用される暗号通貨の金額は、成行注文用に自動的に USDT に変換されます。

4番目の落とし穴:裁定取引の機会の幻想

もう 1 つのイライラする問題は、裁定取引シグナルが検出され、ポジションを開く準備ができているにもかかわらず、注文を出すまでに機会がすでに失われていることです。

価格はリアルタイムで変動するため、シグナル検出から実際のポジション執行までの間に市場状況が変化する可能性があります。価格スプレッドが縮小し、裁定取引の機会が消滅している可能性もあります。この時点でまだ愚かにもポジションを保有すれば、手数料を無駄にしていることになります。

それで追加されました。二次確認メカニズム

// 开仓前重新获取实时价格并验证套利机会
Log('🔄 重新获取实时Depth盘口价格并验证套利机会...');

let realtimeSpotPrice = getDepthMidPrice(exchanges[0], pair.spotSymbol, true);
let realtimeDeliveryPrice = getDepthMidPrice(exchanges[1], pair.deliverySymbol, true);

let realtimeSpread = realtimeDeliveryPrice - realtimeSpotPrice;
let realtimeSpreadRate = realtimeSpread / realtimeSpotPrice;

// 重新计算实时Z-Score
let realtimeZScore = (realtimeSpreadRate - mu) / (sigma || 1e-6);

// 验证:实时Z-Score是否仍然满足开仓条件
if (absRealtimeZ < CONFIG.zScoreEntry) {
  Log('❌ 套利机会已消失!');
  Log('  取消开仓,避免亏损');
  return false;
}

実際に注文する前に、リアルタイムの価格を再度取得し、Z スコアを再計算して、機会がまだ存在すると確信できる場合にのみ注文を実行します。

5 番目の落とし穴: 先物ポジションに関連するレガシー問題。

戦略を再開したり、以前のポジションを決済したりすると、先物口座にポジションが残ることがあります。これらのポジションに対処しないと、新しいポジションが古いポジションと重複し、ポジションサイズが制御不能に陥る可能性があります。

それで追加されました。ポジション開設前の強制清算ロジック:

// 检查期货现有仓位并平仓
let existingPosition = getPositionBySymbol(pair.deliverySymbol);

if (existingPosition && Math.abs(existingPosition.Amount) > 0) {
  Log('⚠️ 检测到该合约的现有仓位,执行平仓操作...');
  
  let closeDirection = existingPosition.Type === PD_LONG ? 'closebuy' : 'closesell';
  let closeAmount = Math.abs(existingPosition.Amount);
  
  let closeOrder = createOrderWithFallback(
    exchanges[1],
    pair.deliverySymbol,
    closeDirection,
    closeAmount,
    -1,
    '期货'
  );
  
  if (!closeOrder) {
    Log('❌ 平仓现有持仓失败,终止开仓');
    addCooldown(pair.deliverySymbol, pair.coin, '平仓现有持仓失败');
    return false;
  }
}

ポジションを開く前に、まずは状況を確認してください。残っているポジションがある場合は、アカウントに問題がないか確認するために、それらを決済してください。

6番目の大きな落とし穴:ティッカーデータのラグトラップ

これが戦略策定プロセス全体です最も隠蔽され、最も致命的問題の一つ。

戦略の実行が開始されると、奇妙な現象が観察されました。

  • 裁定取引シグナルが検出されたので、ポジションを開くタイミングです。
  • 最適な指値注文価格は、チケット価格に価格差を加算および減算することによって計算されます。
  • その結果、注文は履行を待つ保留中のままになりました。
  • 取引が完了せずに長時間待った後、注文はキャンセルされました。

代わりに成行注文を使ってみてはどうでしょうか?結果はさらに悪かったです。

  • 成行注文は正常に実行されました。
  • しかし、取引価格は予想された裁定スプレッドとは全く異なっていました。
  • 計画されていた裁定取引の機会は、取引が完了した後に利益を生まないことが判明しました。

一体何が起こったのでしょうか?取引所のライブ取引データを何度も比較した結果、ついに問題が発見されました。

ティッカーとデプス:データの根本的な違い

ティッカーデータは最新の実際の取引価格を反映しています。これは問題ないように聞こえますが、受渡契約のような流動性の低い市場では問題が発生します。

时间轴:
10:00:00 - 有人以50000成交了1张合约 → Ticker价格更新为50000
10:00:05 - 盘口挂单:买49800 / 卖50200(但没有成交)
10:00:10 - 盘口挂单:买49850 / 卖50150(但没有成交)
...
10:05:00 - Ticker价格仍然是50000(因为5分钟内没有新的成交)

問題に気づきましたか?ティッカー価格 50,000 は 5 分前にはすでに過去最高値でした。ただし、現在の実勢価格(注文板価格)は49850/50150となっている可能性があります。

ティッカー価格 50,000 を使用して裁定取引の機会を計算し、指値注文を出す場合は、次のようになります。

  1. 判断ミス裁定取引シグナルは古い価格に基づいて計算されます。
  2. 注文に失敗しました指値注文の価格が実際の市場価格からかけ離れているため、取引を完了することができません。
  3. 市場損失強制的に取引を行うために成行注文を使用しましたが、実際の価格は予想とはまったく異なっていました。

配送契約のティッカーが特に信頼できないのはなぜですか?

デリバリー契約の流動性はスポット契約の流動性よりもはるかに悪いです。

  • スポット市場毎秒大量の取引が発生し、ティッカー価格は遅延がほとんどなくほぼリアルタイムで更新されます。
  • 配送契約取引が行われるまで数分またはそれ以上かかる場合があり、ティッカー価格は大幅に遅れます。

流動性の低い契約の場合、ティッカー価格と実際の注文簿価格の差は次のようになる場合があります。

  • 正常範囲: 0.1% - 0.5%
  • 変動期間: 1% - 3% 以上

裁定取引戦略にとって、この乖離は致命的です。期待される価格差による利益はわずか0.5%かもしれませんが、実際の取引価格は全く異なるものになることがあります。

解決策: ティッカーを深度ディスクデータに置き換える

ティッカーは信頼できないので、使用してみましょう…深度(注文板の深度)データ

function getDepthMidPrice(exchange, symbol, logDetail = false) {
    let depth = exchange.GetDepth(symbol);
    if (!depth || !depth.Bids || depth.Bids.length === 0 || 
        !depth.Asks || depth.Asks.length === 0) {
        Log(`❌ 获取${symbol}盘口失败`);
        return null;
    }
    
    let bestBid = depth.Bids[0].Price;  // 最优买价
    let bestAsk = depth.Asks[0].Price;  // 最优卖价
    let midPrice = (bestBid + bestAsk) / 2;  // 中间价
    
    if (logDetail) {
        let spread = bestAsk - bestBid;
        let spreadRate = spread / midPrice * 100;
        Log(`📊 ${symbol} 盘口: Bid=${bestBid.toFixed(2)}, Ask=${bestAsk.toFixed(2)}, Mid=${midPrice.toFixed(2)}, Spread=${spread.toFixed(2)} (${spreadRate.toFixed(3)}%)`);
    }
    
    return midPrice;
}

深度データの利点:

  • リアルタイム遅延なく、注文簿の現在の実際のステータスを反映します。
  • 正確さお客様のご注文はこれらの注文帳と照合され、これが実際の市場価格となります。
  • 操作性注文簿の価格に基づいて計算された指値注文は、実行される可能性が高くなります。

戦略の二重価格システム

最終的に採用されたティッカー + デプスのハイブリッドソリューション

1. ティッカーを使用して履歴データ シーケンスを維持します。

// 用Ticker更新历史价差序列(保持连续性)
let spotTicker = exchanges[0].GetTicker(pair.spotSymbol);
let deliveryTicker = exchanges[1].GetTicker(pair.deliverySymbol);

pair.spotPrice = spotTicker.Last;
pair.deliveryPrice = deliveryTicker.Last;
pair.spread = pair.deliveryPrice - pair.spotPrice;

// 历史序列用于ADF检验、Z-Score计算
priceHistory[pair.deliverySymbol].push({
    time: Date.now(),
    spreadRate: pair.spread / pair.spotPrice,
    spread: pair.spread,
    spotPrice: pair.spotPrice,
    deliveryPrice: pair.deliveryPrice
});

なぜティッカーは履歴データにまだ使用されているのでしょうか? それは必要だからです。データの継続性履歴データも Depth を使用して表すと、注文簿価格の変動により履歴シーケンスに不連続が生じ、統計分析の精度に影響します。

2. リアルタイムの判断とポジションオープンの検証にはDepthを使用します。

// 开仓前用Depth重新验证套利机会
let realtimeSpotPrice = getDepthMidPrice(exchanges[0], pair.spotSymbol, true);
let realtimeDeliveryPrice = getDepthMidPrice(exchanges[1], pair.deliverySymbol, true);

// 基于Depth价格重新计算Z-Score
let realtimeSpread = realtimeDeliveryPrice - realtimeSpotPrice;
let realtimeSpreadRate = realtimeSpread / realtimeSpotPrice;
let realtimeZScore = (realtimeSpreadRate - mu) / (sigma || 1e-6);

// 二次验证:套利机会是否仍然存在
if (Math.abs(realtimeZScore) < CONFIG.zScoreEntry) {
    Log('❌ 套利机会已消失(基于Depth实时价格)');
    return false;
}

3. Depth を使用して指値注文価格を計算します。

// 基于Depth价格和平均价差计算限价单价格
let spreadDeviation = realtimeSpread - avgSpread;
let adjustmentRatio = Math.min(
    Math.abs(spreadDeviation) * CONFIG.limitOrderSpreadRatio,
    spreadStd * 0.5
);

if (direction === 'positive') {
    spotLimitPrice = realtimeSpotPrice + adjustmentRatio;
    deliveryLimitPrice = realtimeDeliveryPrice - adjustmentRatio;
} else {
    spotLimitPrice = realtimeSpotPrice - adjustmentRatio;
    deliveryLimitPrice = realtimeDeliveryPrice + adjustmentRatio;
}

このようにして計算された指値注文価格は実際の注文簿に基づいているため、約定確率が大幅に高まります。

4. Depthを使用してリアルタイムの損益を計算します。

function calculateUnrealizedPnL(record, currentPair) {
    // 优先用Depth价格计算实时盈亏
    let currentSpotPrice = getDepthMidPrice(exchanges[0], currentPair.spotSymbol);
    let currentDeliveryPrice = getDepthMidPrice(exchanges[1], currentPair.deliverySymbol);
    
    // Depth获取失败才回退到Ticker
    if (!currentSpotPrice || !currentDeliveryPrice) {
        currentSpotPrice = currentPair.spotPrice;
        currentDeliveryPrice = currentPair.deliveryPrice;
    }
    
    // 计算盈亏...
}

実際の戦闘効果の比較

ティッカーの使用に関する問題:

检测到套利信号(基于Ticker)
→ 计算限价单价格
→ 下单等待
→ 长时间不成交(价格已经不对了)
→ 改用市价单
→ 成交价格和预期差很多
→ 套利失败或微利

Depth 使用後の改善点:

检测到套利信号(基于Ticker历史)
→ 用Depth重新验证(机会仍在)
→ 基于Depth计算限价单价格
→ 下单,价格贴近盘口
→ 较快成交
→ 成交价格符合预期
→ 套利成功

価格指値注文の価格設定の最適化

指値注文を使用する場合、価格はどのように設定すればよいでしょうか? あまりに積極的に設定すると取引が成立せず、逆にあまりに保守的に設定すると適切な価格が得られません。

深度価格に基づくと、ここでのアプローチは次のようになります。現在の価格スプレッドと平均価格スプレッドの偏差に基づいて動的に調整します。

let spreadDeviation = realtimeSpread - avgSpread;
let adjustmentRatio = Math.min(
  Math.abs(spreadDeviation) * CONFIG.limitOrderSpreadRatio,
  spreadStd * 0.5
);

// 限制调整幅度在合理区间
let minAdjustment = realtimeSpotPrice * 0.0005;
let maxAdjustment = realtimeSpotPrice * 0.005;
adjustmentRatio = Math.max(minAdjustment, Math.min(maxAdjustment, adjustmentRatio));

フルセットの場合(価格差が大きい場合)

  • スポット買付価格 = デプス中間点 + 調整範囲
  • 先物売り価格 = デプス中間点 - 調整範囲

これにより、注文簿から離れすぎて取引が見逃されることを避けながら、有利な価格差で取引を完了できるようになります。

冷却期メカニズム

ポジション開設の試みが失敗した場合、市場に何らかの問題があることを示しています。これは、流動性の不足や過剰なボラティリティが原因である可能性があります。そのような場合は、すぐに再試行するのではなく、冷静さを保つことが重要です。

したがって、失敗した取引ペアごとにペナルティが追加されました。10分間のクールダウン

function addCooldown(deliverySymbol, coin, reason) {
  pairCooldowns[deliverySymbol] = Date.now() + CONFIG.cooldownDuration;
  Log(`⏸️ ${deliverySymbol} 进入10分钟冷却期`);
  Log(`   原因: ${reason}`);
  _G('pairCooldowns', pairCooldowns);
}

クーリングオフ期間中は、失敗の繰り返しや取引手数料の無駄を避けるため、この取引ペアのポジションは開かれません。

現在の欠点と改善の余地

この戦略はまだ進行中の作業であり、最適化できる領域は数多くあります。

1. 遅延の問題 現在、価格はポーリング方式で取得されており、大きな遅延が発生しています。WebSocket に切り替えてリアルタイムの価格更新を実現すれば、応答速度が大幅に向上します。

2. リスク管理の最適化 現在のストップロスの方法は比較的シンプルでわかりやすく、次のような方法を検討できます。

  • ダイナミックストップロス(ボラティリティに基づいて調整)
  • 時間ベースのストップロス(保有時間が長すぎると強制的に清算される)
  • 最大撤回制御

3. スリッページ管理 指値注文の価格設定戦略は、注文簿の深さや最近の取引量などの要素に基づいて動的に調整するなど、よりインテリジェントに行うことができます。

4. 深度データのさらなる応用 注文帳の不均衡を分析し、価格動向を予測し、裁定取引の成功率を向上させることができます。

要約する

裁定取引戦略は魅力的に聞こえますが、実際には、理想と現実の間に無数の落とし穴があることは明らかです。

  • 定常性検定の落とし穴
  • 損益統計の落とし穴
  • 流動性不足の落とし穴
  • シングルレッグ取引の落とし穴
  • 成行注文の失敗の落とし穴
  • スポット購入額の落とし穴
  • 裁定取引機会の消滅の落とし穴
  • 残余ポジションの落とし穴
  • 最も致命的な欠陥は、ティッカー メカニズムの遅延です。

特に、ティッカーデータの遅れの問題は、戦略策定プロセス全体における問題です。最も見落とされやすいが、最も大きな影響を与える落とし穴。流動性の低いデリバリー契約市場の場合:

基本原則: ティッカーを使用して履歴の連続性を維持し、深度を使用してリアルタイムの機会を捉えます。

  • ティッカーは履歴データ分析(ADF テスト、Z スコア計算)に適しています。
  • 深度は、リアルタイムの判断と取引実行(ポジション開設の検証、指値注文の価格設定、損益計算)に適しています。

この記事には、探索プロセス中に発生した問題と解決策が記録されており、皆様にとって参考になるものになれば幸いです。繰り返しになりますが、この記事は教育および議論のみを目的としています。コードはまだ開発中であり、実際の取引で直接使用しないでください。

同じような戦略を使っている方は、ぜひお気軽にご相談ください。市場は複雑で、まさにこの複雑さこそがクオンツ取引を非常に困難なものにしているのです。

戦略ソースコード: https://www.fmz.com/strategy/519280