
최근 친구가 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 };
}
처음에는 분산 비율 검정, 반감기 검정, KS 검정 등 여러 검정을 추가했지만, 검정이 너무 많으면 채용 기회가 크게 줄어든다는 것을 알게 되었습니다. 결국 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;
테스트에서 3회 연속 실패할 경우 거래가 금지됩니다. 이는 시장 상황이 비정상적일 때 묻지마 매수 포지션을 개설하는 것을 방지하기 위한 것입니다.
선물 계좌의 손익 계산은 간단합니다. 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년 동안 운영되어 온 인벤터 정량 플랫폼이 왜 선물 계약에 대한 차익 거래 전략이 이렇게 적을까요? 답은 간단합니다.인도계약 시장의 유동성 부족。
차익거래는 여러 포지션을 동시에 개설하는 동화 같은 이야기가 아닙니다. 현실은 다음과 같습니다.
이는 전형적인 “한쪽 다리 위험”의 예입니다. 한쪽 다리는 투자했지만 다른 쪽 다리는 투자하지 않은 상태입니다. 이 경우 가격 변동은 더 이상 차익거래가 아니라 한쪽으로 치우친 포지션이 됩니다.
해결책은 가입하는 것입니다.롤백 메커니즘:
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로 변환됩니다.
또 다른 골치 아픈 문제는 차익거래 신호가 감지되어 포지션을 개설할 준비가 되었지만, 주문을 넣는 순간 기회가 이미 사라져 버린다는 것입니다.
가격은 실시간으로 변동하며, 신호 감지 시점과 실제 포지션 실행 시점 사이에 시장 상황이 변할 수 있습니다. 가격 스프레드가 좁아지거나 차익 거래 기회가 사라졌을 수도 있습니다. 이러한 상황에서도 어리석게 포지션을 개설한다면, 수수료만 낭비하는 꼴이 됩니다.
그래서 추가되었다.2차 확인 메커니즘:
// 开仓前重新获取实时价格并验证套利机会
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分钟内没有新的成交)
문제점을 발견하셨나요?해당 종목의 가격 5만 달러는 이미 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;
}
심도 데이터의 장점:
최종적으로 채택됨티커와 심도 정보를 결합한 하이브리드 솔루션:
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가 여전히 과거 데이터에 사용되는 이유는 무엇일까요? 필수적이기 때문입니다.데이터 연속성만약 과거 데이터도 심도(Depth)를 사용하여 표현한다면, 주문장 가격의 변동은 과거 데이터 시퀀스에 불연속성을 초래하여 통계 분석의 정확도에 영향을 미칠 수 있습니다.
2. 실시간 판단 및 포지션 개시 검증을 위해 심도 분석 기능을 활용하십시오.
// 开仓前用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 사용 시 문제점:
检测到套利信号(基于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. 지연 문제 현재 가격 정보는 폴링 방식을 통해 가져오므로 상당한 지연 시간이 발생합니다. 실시간 가격 업데이트를 위해 웹소켓으로 전환하면 응답 속도가 크게 향상될 것입니다.
2. 위험 관리 최적화 현재 손절매 방법은 비교적 간단하고 직관적이며, 다음과 같은 사항을 고려할 수 있습니다.
3. 슬리피지 관리 지정가 주문의 가격 책정 전략을 주문장 깊이 및 최근 거래량과 같은 요소를 기반으로 동적으로 조정하는 등 더욱 지능적으로 만들 수 있습니다.
4. 심도 데이터의 추가 활용 주문장 불균형을 분석하고, 가격 추세를 예측하며, 차익거래 성공률을 높일 수 있습니다.
차익거래 전략은 매력적으로 들리지만, 실제로는 이상적인 상황과 현실 사이에 수많은 함정이 도사리고 있다는 것이 분명합니다.
특히, 종목 코드 데이터의 지연 문제는 전체 전략 개발 과정에서 문제가 됩니다.가장 쉽게 간과되지만 가장 큰 영향을 미치는 요소함정. 유동성이 낮은 인도 계약 시장의 경우:
핵심 원칙: Ticker를 사용하여 과거 추세의 연속성을 유지하고, Depth를 사용하여 실시간 기회를 포착하십시오.
이 글은 탐사 과정에서 겪었던 문제점과 해결책을 기록한 것으로, 모든 분들께 참고 자료가 되기를 바랍니다.다시 한번 말씀드리지만, 이 글은 교육 및 토론 목적으로만 작성되었습니다. 코드는 아직 개발 중이며 실제 거래에 직접 사용해서는 안 됩니다.。
만약 비슷한 전략을 사용하고 계시다면, 언제든지 저와 상의해 주세요. 시장은 복잡하고, 바로 이러한 복잡성 때문에 양적 거래가 매우 어려운 것입니다.
전략 소스 코드: https://www.fmz.com/strategy/519280