
最近、友人からアービトラージ戦略を作成できないかと尋ねられました。Inventorプラットフォームにはそのような機能がありませんでした。最初は難しくないだろうと思っていましたが、基本的なロジックをなんとか動作させるまでに約1ヶ月かかりました。今振り返ってみると、最初のアイデアから実装に至るまでに遭遇した落とし穴は、想像をはるかに超えるものでした。
この記事では、この裁定取引戦略の開発中に発生した実際的な問題と、開発された解決策について説明します。学習および参照目的のみであり、実用的な投資価値はありません。。
受渡契約とスポット価格の間には価格差があり、通常は一定の平均値を中心に変動します。価格差が過度に乖離すると、理論的には裁定取引の機会が存在します。
最初のアイデアはシンプルでした。
素晴らしいですね。しかし実際には、「ポジションを開く」というステップだけでも、処理すべき細かい作業が無数にあることに気づくでしょう。
当初は価格スプレッドの乖離を直接的な指標として用いていましたが、価格スプレッドが拡大し続け、以前の状態に戻らない場合があることに気づきました。その後、私たちは…すべての価格スプレッドが安定しているわけではありません。。
特に受渡日が近づくにつれて、スプレッドの動きが変化する可能性があります。そのため、スプレッド系列が定常であるかどうかを判断するために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回連続で失敗した場合、取引は禁止されます。これにより、市場が異常な状態にある際に、盲目的にポジションを開くことを防ぐことができます。
先物口座の損益計算は簡単で、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);
これが本当に頭を悩ませる問題です。Inventor Quantitative Platform は10年間運用されてきたにもかかわらず、なぜ先物契約の裁定取引戦略がこれほど少ないのでしょうか?答えは簡単です。受渡契約市場における流動性の不足。
裁定取引は、複数のポジションを同時に開くようなおとぎ話ではありません。現実はこうです。
これは「片足リスク」の典型的な例です。片足はポジションを保有していますが、もう片方は依然としてポジションを保有していません。この時点では、価格変動はもはや裁定取引ではなく、片側だけのポジションとなります。
解決策は参加することだロールバックメカニズム:
if (!deliveryOrder) {
Log('❌ 期货卖单失败,回滚现货');
exchanges[0].CreateOrder(pair.spotSymbol, 'sell', -1, spotAmount);
addCooldown(pair.deliverySymbol, pair.coin, '期货卖单失败,已回滚现货');
return false;
}
いずれかのレッグが失敗した場合は、直ちに成行注文を出して、すでに実行されたレッグをクローズし、そのレッグのリスク露出を減らします。
さらに不可解なことに、成行注文が失敗することもあります。これは為替リスク管理や市場の厚み不足が原因となる場合があり、簡単に言えば、注文が成立しないのです。
それで私はそれをやった。成行注文+指値注文の二重の仕組み:
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 回再試行することができ、各試行では安全ネットとして市場注文が使用されます。
この落とし穴は特に巧妙に隠されており、細心の注意が必要です。先物市場注文の数量は…コインの枚数ただし、スポット市場注文を購入する場合の注文数量は…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 に変換されます。
もう 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 スコアを再計算して、機会がまだ存在すると確信できる場合にのみ注文を実行します。
戦略を再開したり、以前のポジションを決済したりすると、先物口座にポジションが残ることがあります。これらのポジションに対処しないと、新しいポジションが古いポジションと重複し、ポジションサイズが制御不能に陥る可能性があります。
それで追加されました。ポジション開設前の強制清算ロジック:
// 检查期货现有仓位并平仓
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;
}
}
ポジションを開く前に、まずは状況を確認してください。残っているポジションがある場合は、アカウントに問題がないか確認するために、それらを決済してください。
これが戦略策定プロセス全体です最も隠蔽され、最も致命的問題の一つ。
戦略の実行が開始されると、奇妙な現象が観察されました。
代わりに成行注文を使ってみてはどうでしょうか?結果はさらに悪かったです。
一体何が起こったのでしょうか?取引所のライブ取引データを何度も比較した結果、ついに問題が発見されました。
ティッカーデータは最新の実際の取引価格を反映しています。これは問題ないように聞こえますが、受渡契約のような流動性の低い市場では問題が発生します。
时间轴:
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 を使用して裁定取引の機会を計算し、指値注文を出す場合は、次のようになります。
デリバリー契約の流動性はスポット契約の流動性よりもはるかに悪いです。
流動性の低い契約の場合、ティッカー価格と実際の注文簿価格の差は次のようになる場合があります。
裁定取引戦略にとって、この乖離は致命的です。期待される価格差による利益はわずか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. 深度データのさらなる応用 注文帳の不均衡を分析し、価格動向を予測し、裁定取引の成功率を向上させることができます。
裁定取引戦略は魅力的に聞こえますが、実際には、理想と現実の間に無数の落とし穴があることは明らかです。
特に、ティッカーデータの遅れの問題は、戦略策定プロセス全体における問題です。最も見落とされやすいが、最も大きな影響を与える落とし穴。流動性の低いデリバリー契約市場の場合:
基本原則: ティッカーを使用して履歴の連続性を維持し、深度を使用してリアルタイムの機会を捉えます。
この記事には、探索プロセス中に発生した問題と解決策が記録されており、皆様にとって参考になるものになれば幸いです。繰り返しになりますが、この記事は教育および議論のみを目的としています。コードはまだ開発中であり、実際の取引で直接使用しないでください。。
同じような戦略を使っている方は、ぜひお気軽にご相談ください。市場は複雑で、まさにこの複雑さこそがクオンツ取引を非常に困難なものにしているのです。
戦略ソースコード: https://www.fmz.com/strategy/519280