
Baru-baru ini, seorang teman bertanya apakah saya bisa membuat strategi arbitrase, yang kebetulan tidak ada di platform Inventor. Saya pikir itu tidak akan sulit, tetapi butuh sekitar satu bulan untuk sekadar membuat logika dasarnya berfungsi. Jika dilihat kembali sekarang, kendala yang saya temui dari ide awal hingga implementasinya jauh lebih banyak daripada yang saya bayangkan.
Artikel ini mendokumentasikan masalah praktis yang dihadapi dan solusi yang dikembangkan selama pengembangan strategi arbitrase ini.Hanya untuk tujuan pembelajaran dan referensi; tidak memiliki nilai investasi praktis.。
Terdapat perbedaan harga antara kontrak pengiriman dan harga spot, yang biasanya berfluktuasi di sekitar rata-rata tertentu. Ketika perbedaan harga tersebut menyimpang terlalu jauh, secara teori peluang arbitrase dapat tercipta.
Ide awalnya sederhana:
Terdengar bagus, bukan? Tetapi dalam praktiknya, Anda akan menemukan bahwa ada banyak sekali detail yang perlu ditangani hanya pada langkah “membuka posisi”.
Awalnya, kami menggunakan deviasi spread harga sebagai indikator langsung, tetapi kami menemukan bahwa terkadang spread harga akan terus melebar dan tidak pernah kembali ke keadaan semula. Kemudian, kami menyadari…Tidak semua selisih harga bersifat stabil.。
Terutama saat tanggal pengiriman semakin dekat, perilaku spread dapat berubah. Oleh karena itu, uji ADF ditambahkan untuk menentukan apakah rangkaian spread bersifat stasioner.
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 };
}
Awalnya, kami menambahkan sejumlah pengujian, termasuk uji rasio varians, uji waktu paruh, dan uji KS, tetapi kami menemukan bahwa terlalu banyak pengujian secara signifikan mengurangi jumlah peluang untuk membuka posisi. Pada akhirnya, kami menyederhanakannya hanya menjadi uji ADF dan menetapkan ambang batas nilai p menjadi 0,1.
Yang lebih penting lagi, satu tambahan telah ditambahkan.Penghitung kegagalan berkelanjutan:
if (!adfPass) {
stationarityFailCount[deliverySymbol] = (stationarityFailCount[deliverySymbol] || 0) + 1;
} else {
stationarityFailCount[deliverySymbol] = 0;
}
let consecutiveFails = stationarityFailCount[deliverySymbol];
let canTrade = consecutiveFails < CONFIG.consecutiveFailThreshold;
Perdagangan dilarang jika pengujian gagal tiga kali berturut-turut. Hal ini dapat mencegah pembukaan posisi secara membabi buta ketika pasar berada dalam kondisi abnormal.
Menghitung keuntungan dan kerugian untuk akun berjangka itu sederhana; cukup lihat perubahan USDT. Tetapi akun spot berbeda; akun ini berisi USDT dan mata uang kripto. Bagaimana cara menghitungnya?
Berikut detail yang mudah terlewatkan:Biaya penyimpanan barang spot akan berubah seiring dengan aktivitas perdagangan.Sebagai contoh, jika Anda membeli satu koin seharga 100 USDT, menjualnya seharga 90 USDT, dan kemudian membelinya kembali seharga 85 USDT, maka harga pokok Anda bukan lagi 100 USDT awal. Hanya menggunakan harga yang dibekukan pada saat membuka posisi untuk menghitung nilai koin tidak akan mencerminkan situasi laba rugi yang sebenarnya.
Pendekatan yang benar adalahEkstrak harga transaksi rata-rata aktual dari objek pesanan.:
let openSpotPrice = (openSpotOrder && openSpotOrder.AvgPrice) ?
openSpotOrder.AvgPrice : record.openSpotPrice;
let closeSpotPrice = closeSpotOrder.AvgPrice || currentPair.spotPrice;
Kemudian, tingkat pengembalian dihitung berdasarkan harga transaksi sebenarnya:
// 正套:买现货+卖期货
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;
Perhatikan logika perhitungan laba rugi di sini:
Terakhir, laba atau rugi aktual dari setiap transaksi dijumlahkan untuk mendapatkan total laba atau rugi:
accumulatedProfit += actualTotalPnl;
_G('accumulatedProfit', accumulatedProfit);
Inilah masalah sebenarnya. Mengapa, setelah 10 tahun beroperasi, Inventor Quantitative Platform hanya memiliki sedikit strategi arbitrase untuk kontrak berjangka? Jawabannya sederhana:Likuiditas yang tidak mencukupi di pasar kontrak pengiriman.。
Arbitrase bukanlah dongeng di mana Anda membuka posisi secara bersamaan. Realitanya adalah:
Ini adalah contoh klasik dari “risiko satu kaki.” Satu kaki sudah masuk, sementara kaki lainnya masih di luar. Pada titik ini, fluktuasi harga apa pun bukan lagi arbitrase tetapi posisi satu sisi.
Solusinya adalah bergabung.Mekanisme pengembalian:
if (!deliveryOrder) {
Log('❌ 期货卖单失败,回滚现货');
exchanges[0].CreateOrder(pair.spotSymbol, 'sell', -1, spotAmount);
addCooldown(pair.deliverySymbol, pair.coin, '期货卖单失败,已回滚现货');
return false;
}
Jika salah satu posisi gagal, segera tempatkan order pasar untuk menutup posisi yang sudah dieksekusi, sehingga mengurangi risiko pada posisi tersebut.
Yang lebih absurd lagi, terkadang order pasar juga gagal. Hal ini bisa disebabkan oleh kontrol risiko bursa atau kedalaman pasar yang tidak mencukupi; singkatnya, order tersebut tidak akan diproses.
Jadi, saya melakukannya.Mekanisme ganda order pasar + order batas:
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);
}
// ... 检查订单状态,失败则重试
}
Sistem dapat mencoba ulang maksimal 3 kali, dengan setiap percobaan menggunakan order pasar sebagai jaring pengaman.
Jebakan ini sangat tersembunyi dan membutuhkan kehati-hatian ekstra. Kuantitas untuk pesanan pasar berjangka adalah…Jumlah koinNamun, kuantitas yang dipesan saat membeli pesanan pasar spot adalah…Jumlah USDT!
Di sini, logika konversi telah ditambahkan secara khusus:
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;
}
Jumlah mata uang kripto yang digunakan dalam order limit secara otomatis dikonversi ke USDT untuk order market.
Masalah menjengkelkan lainnya adalah sinyal arbitrase terdeteksi, dan Anda siap membuka posisi, tetapi peluang tersebut sudah hilang pada saat Anda menempatkan pesanan.
Harga berfluktuasi secara real-time, dan kondisi pasar mungkin telah berubah antara deteksi sinyal dan eksekusi posisi yang sebenarnya. Spread harga mungkin telah menyempit, dan peluang arbitrase mungkin telah hilang. Jika Anda masih dengan bodohnya membuka posisi pada saat ini, Anda hanya membuang-buang biaya komisi.
Jadi, itu ditambahkan.Mekanisme konfirmasi sekunder:
// 开仓前重新获取实时价格并验证套利机会
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;
}
Sebelum benar-benar melakukan pemesanan, ambil kembali harga real-time, hitung ulang Z-Score, dan hanya eksekusi pesanan jika Anda yakin peluang tersebut masih ada.
Terkadang, ketika suatu strategi dimulai kembali atau posisi sebelumnya ditutup, mungkin ada posisi residual di akun berjangka. Jika hal ini tidak ditangani, posisi baru akan tumpang tindih dengan posisi lama, yang menyebabkan ukuran posisi menjadi tidak terkendali.
Jadi, itu ditambahkan.Likuidasi paksa sebelum membuka posisiLogikanya:
// 检查期货现有仓位并平仓
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;
}
}
Sebelum membuka posisi, periksa terlebih dahulu. Jika masih ada posisi yang terbuka, tutup posisi tersebut untuk memastikan akun Anda bersih.
Inilah keseluruhan proses pengembangan strategi.Paling tersembunyi dan paling mematikanSalah satu masalahnya.
Setelah strategi tersebut mulai dijalankan, sebuah fenomena aneh diamati:
Coba gunakan order pasar sebagai gantinya? Hasilnya bahkan lebih buruk:
Apa sebenarnya yang terjadi? Setelah berulang kali membandingkan data perdagangan langsung bursa, masalah tersebut akhirnya ditemukan:
Data ticker mencerminkan harga transaksi aktual terbaru.Ini terdengar baik-baik saja, tetapi untuk pasar dengan likuiditas rendah seperti kontrak pengiriman, masalah muncul:
时间轴:
10:00:00 - 有人以50000成交了1张合约 → Ticker价格更新为50000
10:00:05 - 盘口挂单:买49800 / 卖50200(但没有成交)
10:00:10 - 盘口挂单:买49850 / 卖50150(但没有成交)
...
10:05:00 - Ticker价格仍然是50000(因为5分钟内没有新的成交)
Apakah Anda menyadari masalahnya?Harga saham di angka 50.000 sudah menjadi sejarah 5 menit yang lalu.Namun, harga pasar aktual saat ini (harga buku pesanan) mungkin telah menjadi 49850⁄50150.
Jika Anda menggunakan harga Ticker 50.000 untuk menghitung peluang arbitrase dan menempatkan order limit, maka:
Likuiditas kontrak pengiriman jauh lebih buruk daripada kontrak spot:
Untuk kontrak dengan likuiditas rendah, penyimpangan antara harga ticker dan harga order book sebenarnya dapat mencapai:
Untuk strategi arbitrase, penyimpangan ini berakibat fatal. Keuntungan selisih harga yang diharapkan mungkin hanya 0,5%, tetapi harga transaksi aktual ternyata sangat berbeda.
Karena Ticker tidak dapat diandalkan, mari kita gunakan…Data kedalaman (kedalaman buku pesanan):
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;
}
Keunggulan data kedalaman:
Akhirnya diadopsiSolusi hibrida antara Ticker + Kedalaman:
1. Gunakan Ticker untuk memelihara urutan data historis.
// 用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
});
Mengapa Ticker masih digunakan untuk data historis? Karena itu perlu.Kontinuitas dataJika data historis juga direpresentasikan menggunakan Kedalaman, fluktuasi harga buku pesanan akan menyebabkan diskontinuitas dalam urutan historis, yang memengaruhi keakuratan analisis statistik.
2. Gunakan Kedalaman untuk penilaian waktu nyata dan verifikasi pembukaan posisi.
// 开仓前用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. Hitung harga order limit menggunakan 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;
}
Harga order limit yang dihitung dengan cara ini didasarkan pada order book aktual, yang sangat meningkatkan probabilitas eksekusi.
4. Hitung laba dan rugi secara real-time menggunakan Kedalaman.
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;
}
// 计算盈亏...
}
Masalah dalam menggunakan Ticker:
检测到套利信号(基于Ticker)
→ 计算限价单价格
→ 下单等待
→ 长时间不成交(价格已经不对了)
→ 改用市价单
→ 成交价格和预期差很多
→ 套利失败或微利
Peningkatan setelah menggunakan Depth:
检测到套利信号(基于Ticker历史)
→ 用Depth重新验证(机会仍在)
→ 基于Depth计算限价单价格
→ 下单,价格贴近盘口
→ 较快成交
→ 成交价格符合预期
→ 套利成功
Jika kita akan menggunakan order limit, bagaimana cara kita menetapkan harganya? Jika kita menetapkannya terlalu agresif, transaksi tidak akan berhasil; jika kita menetapkannya terlalu konservatif, kita tidak akan mendapatkan harga yang bagus.
Berdasarkan harga kedalaman, pendekatannya adalah:Sesuaikan secara dinamis berdasarkan penyimpangan antara selisih harga saat ini dan selisih harga rata-rata.。
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));
Jika itu adalah satu set lengkap (dengan perbedaan harga yang besar):
Hal ini memungkinkan transaksi diselesaikan dengan selisih harga yang menguntungkan, sekaligus menghindari jarak yang terlalu jauh dari buku pesanan sehingga tidak mengakibatkan transaksi yang terlewatkan.
Setiap upaya yang gagal untuk membuka posisi menunjukkan adanya masalah di pasar, yang mungkin disebabkan oleh likuiditas yang tidak mencukupi atau volatilitas yang berlebihan. Dalam kasus seperti itu, seseorang tidak boleh langsung mencoba lagi, tetapi sebaiknya tetap tenang.
Oleh karena itu, penalti ditambahkan ke setiap pasangan perdagangan yang gagal.Pendinginan 10 menit:
function addCooldown(deliverySymbol, coin, reason) {
pairCooldowns[deliverySymbol] = Date.now() + CONFIG.cooldownDuration;
Log(`⏸️ ${deliverySymbol} 进入10分钟冷却期`);
Log(` 原因: ${reason}`);
_G('pairCooldowns', pairCooldowns);
}
Selama masa pendinginan, tidak ada posisi yang akan dibuka untuk pasangan perdagangan ini untuk menghindari kegagalan berulang dan biaya transaksi yang terbuang sia-sia.
Strategi ini masih dalam tahap pengembangan, dan terdapat banyak area yang dapat dioptimalkan:
1. Masalah penundaan Saat ini, harga diambil menggunakan metode polling, yang mengakibatkan latensi yang signifikan. Beralih ke WebSocket untuk pembaruan harga secara real-time akan secara signifikan meningkatkan kecepatan respons.
2. Optimalisasi pengendalian risiko Metode stop-loss saat ini relatif sederhana dan mudah dipahami, dan Anda dapat mempertimbangkan:
3. Manajemen Selip Strategi penetapan harga untuk order limit dapat dibuat lebih cerdas, misalnya dengan menyesuaikan secara dinamis berdasarkan faktor-faktor seperti kedalaman order book dan volume transaksi terkini.
4. Aplikasi lebih lanjut dari data kedalaman Sistem ini dapat menganalisis ketidakseimbangan buku pesanan, memprediksi tren harga, dan meningkatkan tingkat keberhasilan arbitrase.
Strategi arbitrase terdengar menarik, tetapi dalam praktiknya, jelas bahwa terdapat banyak sekali jebakan yang memisahkan antara ideal dan realitas.
Secara khusus, masalah keterlambatan data Ticker merupakan kendala dalam keseluruhan proses pengembangan strategi.Paling mudah diabaikan tetapi memiliki dampak terbesar.Jebakan-jebakannya. Untuk pasar kontrak pengiriman dengan likuiditas rendah:
Prinsip inti: Gunakan Ticker untuk mempertahankan kesinambungan historis, dan gunakan Depth untuk memanfaatkan peluang secara real-time.
Artikel ini mencatat masalah-masalah yang dihadapi dan solusi-solusi selama proses eksplorasi, dan semoga dapat memberikan referensi bagi semua orang.Sekali lagi, artikel ini hanya untuk tujuan pendidikan dan diskusi. Kode tersebut masih dalam pengembangan dan tidak boleh digunakan langsung dalam perdagangan nyata.。
Jika Anda menggunakan strategi serupa, jangan ragu untuk mendiskusikannya dengan saya. Pasar itu kompleks, dan justru kompleksitas inilah yang membuat perdagangan kuantitatif sangat menantang.
Kode sumber strategi: https://www.fmz.com/strategy/519280