
Kürzlich fragte mich ein Freund, ob ich eine Arbitrage-Strategie entwickeln könnte, die auf der Inventor-Plattform fehlte. Ich dachte, es wäre nicht schwer, aber es dauerte fast einen Monat, bis die grundlegende Logik überhaupt funktionierte. Rückblickend waren die Hürden, auf die ich von der ersten Idee bis zur Umsetzung stieß, weitaus zahlreicher als erwartet.
Dieser Artikel dokumentiert die praktischen Probleme, die bei der Entwicklung dieser Arbitragestrategie auftraten, und die entwickelten Lösungen.Nur zu Lern- und Referenzzwecken; es hat keinen praktischen Investitionswert.。
Zwischen dem Liefervertrag und dem Spotpreis besteht eine Preisdifferenz, die üblicherweise um einen bestimmten Durchschnittswert schwankt. Bei zu großen Preisabweichungen ergeben sich theoretisch Arbitragemöglichkeiten.
Die Ausgangsidee war einfach:
Klingt toll, oder? In der Praxis werden Sie jedoch feststellen, dass allein der Schritt des “Eröffnens einer Position” unzählige Details beinhaltet.
Anfangs nutzten wir die Abweichung des Preisspreads als direkten Indikator, stellten aber fest, dass sich der Preisspread manchmal weiter ausdehnte und nicht zu seinem vorherigen Zustand zurückkehrte. Später erkannten wir…Nicht alle Preisspannen sind stabil.。
Insbesondere mit Annäherung des Liefertermins kann sich das Verhalten des Spreads ändern. Daher wurde der ADF-Test hinzugefügt, um festzustellen, ob die Spread-Reihe stationär ist.
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 };
}
Anfangs führten wir zahlreiche Tests durch, darunter den Varianzverhältnistest, den Halbwertszeittest und den KS-Test. Wir stellten jedoch fest, dass zu viele Tests die Anzahl der Möglichkeiten zur Positionsöffnung deutlich reduzierten. Schließlich beschränkten wir uns auf den ADF-Test und legten den p-Wert-Schwellenwert auf 0,1 fest.
Noch wichtiger ist jedoch, dass ein zusätzliches Exemplar hinzugefügt wurde.Kontinuierlicher Ausfallzähler:
if (!adfPass) {
stationarityFailCount[deliverySymbol] = (stationarityFailCount[deliverySymbol] || 0) + 1;
} else {
stationarityFailCount[deliverySymbol] = 0;
}
let consecutiveFails = stationarityFailCount[deliverySymbol];
let canTrade = consecutiveFails < CONFIG.consecutiveFailThreshold;
Der Handel ist untersagt, wenn der Test dreimal hintereinander fehlschlägt. Dies soll verhindern, dass in einem anormalen Marktzustand unüberlegt Positionen eröffnet werden.
Die Berechnung von Gewinnen und Verlusten bei einem Futures-Konto ist einfach; man muss nur die Veränderungen des USDT-Kurses betrachten. Bei Spot-Konten ist das anders; sie enthalten sowohl USDT als auch Kryptowährung. Wie berechnet man das?
Hier ist ein Detail, das leicht übersehen wird:Die Kosten für die Lagerung von Spotwaren ändern sich mit dem Handel.Wenn Sie beispielsweise eine Kryptowährung für 100 USDT kaufen, sie für 90 USDT verkaufen und sie dann für 85 USDT zurückkaufen, beträgt Ihr Einstandspreis nicht mehr die ursprünglichen 100 USDT. Die Berechnung des Wertes der Kryptowährung anhand des zum Zeitpunkt der Positionseröffnung festgelegten Preises spiegelt nicht die tatsächliche Gewinn- und Verlustsituation wider.
Die richtige Herangehensweise istErmitteln Sie den durchschnittlichen tatsächlichen Transaktionspreis aus dem Auftragsobjekt.:
let openSpotPrice = (openSpotOrder && openSpotOrder.AvgPrice) ?
openSpotOrder.AvgPrice : record.openSpotPrice;
let closeSpotPrice = closeSpotOrder.AvgPrice || currentPair.spotPrice;
Anschließend wird die Rendite auf Basis des tatsächlichen Transaktionspreises berechnet:
// 正套:买现货+卖期货
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;
Beachten Sie hier die Logik der Gewinn- und Verlustrechnung:
Schließlich werden die tatsächlichen Gewinne oder Verluste jeder einzelnen Transaktion zum Gesamtgewinn oder -verlust addiert:
accumulatedProfit += actualTotalPnl;
_G('accumulatedProfit', accumulatedProfit);
Das ist das eigentliche Problem. Warum bietet die Inventor Quantitative Platform nach 10 Jahren Betrieb so wenige Arbitragestrategien für Futures-Kontrakte? Die Antwort ist einfach:Unzureichende Liquidität auf dem Markt für Lieferverträge。
Arbitrage ist kein Märchen, in dem man gleichzeitig Positionen eröffnet. Die Realität sieht anders aus:
Dies ist ein klassisches Beispiel für ein einseitiges Risiko. Ein Teil des Risikos ist investiert, der andere noch nicht. In diesem Fall stellt jede Preisschwankung keine Arbitragemöglichkeit mehr dar, sondern eine einseitige Position.
Die Lösung besteht darin, sich anzuschließenRückrollmechanismus:
if (!deliveryOrder) {
Log('❌ 期货卖单失败,回滚现货');
exchanges[0].CreateOrder(pair.spotSymbol, 'sell', -1, spotAmount);
addCooldown(pair.deliverySymbol, pair.coin, '期货卖单失败,已回滚现货');
return false;
}
Falls ein Teil der Transaktion fehlschlägt, platzieren Sie sofort eine Marktorder, um den bereits ausgeführten Teil zu schließen und so das Risiko dieses einzelnen Teils zu reduzieren.
Noch absurder ist, dass Marktorders manchmal auch fehlschlagen. Dies kann an Börsenrisikokontrollen oder unzureichender Markttiefe liegen; kurz gesagt, die Order wird einfach nicht ausgeführt.
Also habe ich es getan.Doppelmechanismus aus Marktorder und Limitorder:
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);
}
// ... 检查订单状态,失败则重试
}
Das System kann maximal 3 Wiederholungsversuche unternehmen, wobei bei jedem Versuch eine Marktorder als Sicherheitsnetz verwendet wird.
Diese Falle ist besonders gut versteckt und erfordert daher besondere Vorsicht. Die Menge für eine Terminmarktorder beträgt…Anzahl der MünzenDie beim Kauf einer Spotmarktorder bestellte Menge beträgt jedoch…USDT-Betrag!
Hier wurde speziell eine Konvertierungslogik hinzugefügt:
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;
}
Der bei einer Limit-Order verwendete Kryptowährungsbetrag wird bei einer Market-Order automatisch in USDT umgerechnet.
Ein weiteres ärgerliches Problem ist, dass ein Arbitragesignal erkannt wird und man bereit ist, eine Position zu eröffnen, die Gelegenheit aber bereits vorbei ist, wenn man den Auftrag erteilt.
Die Preise schwanken in Echtzeit, und die Marktbedingungen können sich zwischen der Signalerkennung und der tatsächlichen Ausführung einer Position geändert haben. Preisspannen können sich verringert haben, und Arbitragemöglichkeiten können verschwunden sein. Wenn Sie zu diesem Zeitpunkt dennoch unklugerweise eine Position eröffnen, werfen Sie nur unnötige Gebühren zum Fenster hinaus.
Also wurde es hinzugefügt.Sekundärer Bestätigungsmechanismus:
// 开仓前重新获取实时价格并验证套利机会
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;
}
Bevor Sie einen Auftrag erteilen, rufen Sie den Echtzeitpreis erneut ab, berechnen Sie den Z-Score neu und führen Sie den Auftrag nur dann aus, wenn Sie sicher sind, dass die Gelegenheit noch besteht.
Manchmal können nach dem Neustart einer Strategie oder der Schließung einer vorherigen Position Restpositionen im Futures-Konto verbleiben. Werden diese nicht berücksichtigt, überschneiden sich neue und alte Positionen, was zu einer unkontrollierbaren Positionsgröße führt.
Also wurde es hinzugefügt.Zwangsliquidation vor Eröffnung einer PositionDie Logik:
// 检查期货现有仓位并平仓
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;
}
}
Prüfen Sie vor dem Eröffnen einer Position diese zunächst. Sollten noch Positionen vorhanden sein, schließen Sie diese, um sicherzustellen, dass Ihr Konto bereinigt ist.
Dies ist der gesamte Strategieentwicklungsprozess.Am besten versteckt und am tödlichstenEines der Probleme.
Nachdem die Strategie in Gang gesetzt wurde, wurde ein seltsames Phänomen beobachtet:
Versuchen Sie es stattdessen mit Market-Orders? Das Ergebnis war noch schlechter:
Was genau war passiert? Nach wiederholtem Vergleich der Live-Handelsdaten der Börse wurde das Problem schließlich entdeckt:
Die Tickerdaten spiegeln den aktuellsten tatsächlichen Transaktionspreis wider.Das klingt gut, aber in Märkten mit geringer Liquidität, wie beispielsweise Lieferverträgen, entstehen Probleme:
时间轴:
10:00:00 - 有人以50000成交了1张合约 → Ticker价格更新为50000
10:00:05 - 盘口挂单:买49800 / 卖50200(但没有成交)
10:00:10 - 盘口挂单:买49850 / 卖50150(但没有成交)
...
10:05:00 - Ticker价格仍然是50000(因为5分钟内没有新的成交)
Ist Ihnen das Problem aufgefallen?Der Kurs von 50.000 war vor 5 Minuten bereits Geschichte.Der aktuelle Marktpreis (Orderbuchpreis) könnte jedoch bei 49850⁄50150 liegen.
Wenn Sie den Tickerpreis von 50.000 verwenden, um Arbitragemöglichkeiten zu berechnen und Limit-Orders zu platzieren, dann gilt Folgendes:
Die Liquidität von Lieferverträgen ist deutlich schlechter als die von Spotverträgen:
Bei Verträgen mit geringer Liquidität kann die Abweichung zwischen dem Börsenkurs und dem tatsächlichen Orderbuchpreis Folgendes betragen:
Für Arbitragestrategien ist diese Abweichung fatal. Der erwartete Preisvorteil mag nur 0,5 % betragen, doch der tatsächliche Transaktionspreis fällt völlig anders aus.
Da Ticker unzuverlässig ist, verwenden wir…Daten zur Orderbuchtiefe:
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;
}
Vorteile von Tiefendaten:
Letztendlich übernommenHybridlösung aus Ticker und Tiefe:
1. Verwenden Sie Ticker, um historische Datensequenzen zu verwalten.
// 用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
});
Warum wird Ticker immer noch für historische Daten verwendet? Weil es notwendig ist.DatenkontinuitätWerden historische Daten ebenfalls mithilfe der Tiefenanalyse dargestellt, führen Schwankungen der Orderbuchpreise zu Diskontinuitäten in der historischen Sequenz, was die Genauigkeit der statistischen Analyse beeinträchtigt.
2. Nutzen Sie die Tiefenanalyse für Echtzeit-Beurteilungen und zur Überprüfung der Positionseröffnung.
// 开仓前用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. Berechnen Sie den Preis der Limit-Order unter Verwendung der Ordertiefe.
// 基于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;
}
Der so berechnete Limit-Order-Preis basiert auf dem tatsächlichen Orderbuch, was die Ausführungswahrscheinlichkeit erheblich erhöht.
4. Gewinn und Verlust in Echtzeit unter Verwendung der Tiefe berechnen.
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;
}
// 计算盈亏...
}
Probleme bei der Verwendung von Ticker:
检测到套利信号(基于Ticker)
→ 计算限价单价格
→ 下单等待
→ 长时间不成交(价格已经不对了)
→ 改用市价单
→ 成交价格和预期差很多
→ 套利失败或微利
Verbesserungen nach Anwendung der Tiefenfunktion:
检测到套利信号(基于Ticker历史)
→ 用Depth重新验证(机会仍在)
→ 基于Depth计算限价单价格
→ 下单,价格贴近盘口
→ 较快成交
→ 成交价格符合预期
→ 套利成功
Wenn wir Limit-Orders verwenden, wie legen wir den Preis fest? Setzen wir ihn zu hoch an, wird die Transaktion nicht ausgeführt; setzen wir ihn zu niedrig an, erzielen wir keinen guten Preis.
Ausgehend vom Tiefenpreis ist die Vorgehensweise hier folgende:Die Anpassung erfolgt dynamisch auf Basis der Abweichung zwischen der aktuellen Preisspanne und der durchschnittlichen Preisspanne.。
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));
Wenn es sich um ein komplettes Set handelt (mit einem großen Preisunterschied):
Dadurch können Transaktionen zu einem günstigen Preisunterschied abgeschlossen werden, ohne dass man zu weit vom Orderbuch entfernt ist und somit eine Transaktion verpasst.
Jeder fehlgeschlagene Versuch, eine Position zu eröffnen, deutet auf ein Problem im Markt hin, das auf unzureichende Liquidität oder übermäßige Volatilität zurückzuführen sein kann. In solchen Fällen sollte man es nicht sofort erneut versuchen, sondern Ruhe bewahren.
Deshalb wurde für jedes fehlgeschlagene Handelspaar eine Strafe eingeführt.10-minütige Abkühlphase:
function addCooldown(deliverySymbol, coin, reason) {
pairCooldowns[deliverySymbol] = Date.now() + CONFIG.cooldownDuration;
Log(`⏸️ ${deliverySymbol} 进入10分钟冷却期`);
Log(` 原因: ${reason}`);
_G('pairCooldowns', pairCooldowns);
}
Während der Abkühlungsphase werden für dieses Handelspaar keine Positionen eröffnet, um wiederholte Fehlschläge und unnötige Transaktionsgebühren zu vermeiden.
Diese Strategie befindet sich noch in der Entwicklung und es gibt viele Bereiche, die optimiert werden können:
1. Verzögerungsproblem Aktuell werden die Preise per Polling abgerufen, was zu erheblichen Verzögerungen führt. Die Umstellung auf WebSocket für Echtzeit-Preisaktualisierungen würde die Reaktionszeit deutlich verbessern.
2. Optimierung des Risikomanagements Die derzeitige Stop-Loss-Methode ist relativ einfach und unkompliziert, und Sie können Folgendes in Betracht ziehen:
3. Slippage-Management Die Preisstrategie für Limit-Orders kann intelligenter gestaltet werden, beispielsweise durch dynamische Anpassung auf Basis von Faktoren wie der Orderbuchtiefe und dem jüngsten Transaktionsvolumen.
4. Weitere Anwendungen von Tiefendaten Es kann das Ungleichgewicht im Orderbuch analysieren, Preistrends vorhersagen und die Erfolgsquote von Arbitragegeschäften verbessern.
Arbitragestrategien klingen verlockend, doch in der Praxis zeigt sich, dass zwischen Ideal und Realität unzählige Fallstricke lauern.
Insbesondere die Problematik der verzögerten Tickerdaten stellt ein Problem im gesamten Strategieentwicklungsprozess dar.Am leichtesten zu übersehen, aber mit der größten WirkungDie Fallstricke. Für Liefervertragsmärkte mit geringer Liquidität:
Grundprinzip: Ticker nutzen, um die historische Kontinuität zu wahren, und Depth nutzen, um Chancen in Echtzeit zu ergreifen.
Dieser Artikel dokumentiert die aufgetretenen Probleme und deren Lösungen während des Erkundungsprozesses und soll hoffentlich als Orientierungshilfe für alle dienen.Zur Klarstellung: Dieser Artikel dient ausschließlich Bildungs- und Diskussionszwecken. Der Code befindet sich noch in der Entwicklung und sollte nicht direkt im Live-Handel eingesetzt werden.。
Wenn Sie eine ähnliche Strategie verfolgen, sprechen Sie mich gerne darauf an. Der Markt ist komplex, und genau diese Komplexität macht den quantitativen Handel so anspruchsvoll.
Quellcode der Strategie: https://www.fmz.com/strategy/519280