
Недавно друг попросил меня разработать арбитражную стратегию, которой, как оказалось, не было на платформе Inventor. Я думал, это несложно, но потребовался примерно месяц, чтобы едва-едва наладить базовую логику. Оглядываясь назад, я понимаю, что на пути от первоначальной идеи до её реализации я столкнулся с гораздо большим количеством трудностей, чем предполагал.
В данной статье описаны практические проблемы, возникшие в ходе разработки этой арбитражной стратегии, и найденные решения.Предназначено исключительно для учебных и справочных целей; не имеет практической инвестиционной ценности.。
Между ценой поставки по контракту и спотовой ценой существует разница, которая обычно колеблется вокруг определенного среднего значения. Когда разница в цене становится слишком большой, теоретически возникают арбитражные возможности.
Первоначальная идея была проста:
Звучит здорово, правда? Но на практике вы обнаружите, что существует бесчисленное множество деталей, связанных только с этапом «открытия позиции».
Первоначально мы использовали отклонение ценового спреда в качестве прямого индикатора, но обнаружили, что иногда ценовой спред продолжал увеличиваться и никогда не возвращался к прежнему состоянию. Позже мы поняли…Не все ценовые спреды стабильны.。
Особенно по мере приближения даты поставки поведение спреда может меняться. Поэтому был добавлен тест 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 };
}
Первоначально мы добавили ряд тестов, включая тест отношения дисперсий, тест периода полураспада и тест Колмогорова-Смирнова, но обнаружили, что слишком большое количество тестов значительно сокращает число возможностей для открытия позиций. В итоге мы упростили тест, оставив только тест ADF, и установили пороговое значение p-значения равным 0,1.
Что еще более важно, был добавлен еще один.Постоянный счетчик отказов:
if (!adfPass) {
stationarityFailCount[deliverySymbol] = (stationarityFailCount[deliverySymbol] || 0) + 1;
} else {
stationarityFailCount[deliverySymbol] = 0;
}
let consecutiveFails = stationarityFailCount[deliverySymbol];
let canTrade = consecutiveFails < CONFIG.consecutiveFailThreshold;
Торговля запрещена, если тест проваливается три раза подряд. Это может предотвратить открытие позиций вслепую, когда рынок находится в ненормальном состоянии.
Рассчитать прибыль и убытки по фьючерсному счету просто: достаточно посмотреть на изменения курса USDT. Но спотовые счета отличаются; они содержат как USDT, так и криптовалюту. Как это рассчитать?
Вот деталь, которую легко упустить из виду:Стоимость хранения товаров на спотовом рынке будет меняться в зависимости от торговых операций.Например, если вы покупаете одну монету за 100 USDT, продаете ее за 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);
Вот в чем настоящая головная боль. Почему после 10 лет работы платформа Inventor Quantitative Platform предлагает так мало арбитражных стратегий для фьючерсных контрактов? Ответ прост:Недостаточная ликвидность на рынке договоров поставки.。
Арбитраж — это не сказка, где вы открываете позиции одновременно. Реальность такова:
Это классический пример «одностороннего риска». Одна сторона находится внутри, а другая — снаружи. В этот момент любое колебание цены перестает быть арбитражем и превращается в одностороннюю позицию.
Решение заключается в том, чтобы присоединиться.механизм отката:
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 для рыночного ордера.
Ещё одна неприятная проблема заключается в том, что обнаруживается арбитражный сигнал, и вы готовы открыть позицию, но возможность уже исчезла к моменту размещения ордера.
Цены колеблются в режиме реального времени, и рыночные условия могли измениться между обнаружением сигнала и фактическим исполнением позиции. Спреды цен могли сузиться, а арбитражные возможности могли исчезнуть. Если вы все же по глупости откроете позицию на этом этапе, вы просто выбросите комиссионные на ветер.
Поэтому это было добавлено.Вторичный механизм подтверждения:
// 开仓前重新获取实时价格并验证套利机会
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%, но фактическая цена сделки оказывается совершенно иной.
Поскольку Ticker — ненадежный сервис, давайте воспользуемся…Данные о глубине (глубине книги заказов):
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;
}
Преимущества данных о глубине:
В конечном итоге был принятГибридное решение, сочетающее в себе функции Ticker и Depth.:
1. Используйте Ticker для поддержания последовательности исторических данных.
// 用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
});
Почему Ticker до сих пор используется для исторических данных? Потому что это необходимо.непрерывность данныхЕсли исторические данные также представлены с использованием показателя глубины, колебания цен в портфеле заявок вызовут разрывы в исторической последовательности, что повлияет на точность статистического анализа.
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价格和平均价差计算限价单价格
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. Рассчитайте прибыль и убытки в режиме реального времени, используя глубину.
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:
检测到套利信号(基于Ticker)
→ 计算限价单价格
→ 下单等待
→ 长时间不成交(价格已经不对了)
→ 改用市价单
→ 成交价格和预期差很多
→ 套利失败或微利
Улучшения после использования параметра «Глубина»:
检测到套利信号(基于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