알고리즘 트레이딩의 주문 유형: 체이싱 리밋부터 가상 주문까지
초보자가 거래소 터미널을 열면 두 개의 버튼이 보인다: "매수"와 "매도". 알고 트레이더가 자신의 코드베이스를 열면 27가지 주문 유형, 3단계의 추상화, 그리고 노트북을 닫고 시장에서 오이를 팔고 싶어지게 만드는 엣지 케이스 더미가 보인다. 하지만 안타깝게도 오이로는 UTC 3:59에 펀딩 레이트 차익거래를 할 수 없다 — 그러니 파고들어 보자.
이 글에서는 기본적인 거래소 주문부터 시스템 내에만 존재하고 호가창에는 절대 나타나지 않는 합성 가상 구조까지 전 과정을 다룬다. TypeScript, Python, 약간의 고통, 그리고 조금의 깨달음을 기대하시라.
1. 표준 거래소 주문: 생략할 수 없는 기초
표준 주문 유형 분류: 시장가부터 아이스버그까지
복잡한 것을 구축하기 전에 기본 구성 요소를 올바르게 이해하고 있는지 확인해야 한다. 스톱 리밋과 스톱 마켓을 혼동한 후 왜 자신의 스톱이 "작동하지 않았는지" 의아해하는 사람이 놀라울 정도로 많다(스포일러: 작동은 했지만, 슬리피지 때문에 지정가 주문이 체결되지 않은 것이다).
시장가 주문 (Market order)
가장 단순하면서도 가장 위험한 유형. 거래소에 "지금 당장, 어떤 가격이든 매수/매도하라"고 말하는 것이다. 거래소는 호가창에서 최우선 가격부터 유동성을 가져간다. 최우선 호가의 수량이 부족하면 — 더 깊은 가격으로 슬리피지가 발생한다.
사용 시점: 긴급 포지션 청산, 가격보다 속도가 중요한 시그널 실행.
함정: 유동성이 낮은 시장에서 100 BTC 시장가 주문은 가격을 몇 퍼센트나 움직일 수 있다. 임팩트를 고려하지 않고 시장가 주문을 모델링하는 백테스트는 환상에 불과하다.
지정가 주문 (Limit order)
특정 가격을 지정한다. 주문이 호가창에 들어가 누군가가 그 가격에 동의할 때까지 대기한다. 지정가 매수 주문의 가격이 현재 시장가보다 높으면 — 즉시 체결된다(시장가와 유사하지만, 최대 가격이 보장됨).
핵심: 지정가 주문은 체결을 보장하지 않는다. 가격이 당신의 레벨에 도달한 후 반전할 수 있으며, 당신은 큐에 남게 된다(이에 대한 자세한 내용은 호가창 큐 포지션 글 참조).
스톱 마켓과 스톱 리밋
여기서부터 혼란이 시작된다. 두 유형 모두 트리거 가격(스톱 가격)에 도달하면 활성화되는 "대기" 주문이다. 그러나:
- 스톱 마켓: 발동 시 시장가 주문으로 전환. 체결은 보장되지만, 가격은 보장되지 않는다.
- 스톱 리밋: 발동 시 지정가 주문으로 전환. 가격은 보장되지만(지정가 이하/이상), 체결은 보장되지 않는다.
변동성이 큰 암호화폐 시장에서 스톱 리밋은 "빗나갈" 수 있다 — 가격이 스톱을 관통하고 지정가 주문이 제출되었지만, 시장은 이미 저 멀리 가버렸다. 미체결 지정가 주문과 늘어나는 손실만 남는다. 이것이 스톱 로스에 스톱 마켓이 더 많이 사용되는 이유다.
트레일링 스톱
설정된 거리에서 가격을 "추적"하는 스톱. 가격이 오르면 — 스톱도 올라간다. 가격이 내리면 — 스톱은 제자리에 머문다. 추세 추종 전략에서 이익을 보호하는 데 유용하다.
거래소 지원: 모든 거래소가 네이티브 트레일링 스톱을 지원하는 것은 아니다. 알고 트레이더는 프로그래밍으로 직접 구현하는 경우가 많다 — 파라미터(콜백 비율, 활성화 가격, 스텝 크기)를 더 세밀하게 제어할 수 있기 때문이다.
아이스버그 주문
호가창에 전체 수량의 일부만 표시되는 주문. 1,000 BTC를 사고 싶지만 호가창에는 10만 표시한다. 처음 10이 체결되면 — 다음 10이 나타난다.
이유: 시장에 진정한 의도를 숨기기 위해. 호가창의 대량 주문은 "큰 손이 매수/매도하려 한다"는 신호를 모두에게 보낸다. 이에 대응하여 HFT 알고리즘이 프런트러닝을 시작하고, 가격이 당신에게 불리하게 움직인다.
주의: 많은 암호화폐 거래소에서 아이스버그 주문은 지원되지 않거나 동일한 수량 패턴으로 쉽게 감지된다. 고급 알고리즘은 표시 부분의 크기를 랜덤화한다.
유효 기간 파라미터: GTC, GTD, IOC, FOK
이들은 별도의 주문 유형이 아니라 유효 기간(time-in-force) 파라미터다 — 주문이 얼마나 오래 유효한지:
| 파라미터 | 정식 명칭 | 동작 |
|---|---|---|
| GTC | Good Till Cancelled | 취소할 때까지 유효. 기본 표준 |
| GTD | Good Till Date | 지정된 날짜/시간까지 유효 |
| IOC | Immediate or Cancel | 즉시 체결(전부 또는 일부), 나머지 취소 |
| FOK | Fill or Kill | 전량 즉시 체결만 가능. 불가능하면 — 전량 취소 |
IOC vs FOK: 차이가 결정적이다. IOC는 부분 체결이 가능하다 — 100 BTC를 사려 했는데 3만 사고 나머지는 취소됐다. FOK는 100이거나 아무것도 아니다.
포스트 온리 (Maker-only)
메이커로서 호가창에 들어가는 것이 보장되며, 테이커로는 절대 체결되지 않는 주문. 주문 시점의 가격에서 즉시 체결될 상황이라면 — 거래소가 거부한다(또는 가격을 조정한다, 거래소에 따라).
이유: 메이커 수수료는 보통 테이커 수수료보다 낮다(Binance에서 VIP 등급 기준 0.02% vs 0.04%). 하루에 수천 개의 주문을 내는 마켓메이커에게 수수료 차이는 수익과 손실의 차이다.
2. TWAP과 VWAP: 기관이 호가창에 코끼리를 숨기는 방법
헤지 펀드가 5,000만 달러 포지션을 구축하려 할 때, 하나의 시장가 주문을 내지 않는다. 실행 알고리즘을 사용한다 — 대규모 주문을 다수의 소규모 주문으로 분할하고 시간에 걸쳐 실행하여 시장 임팩트를 최소화하는 알고리즘이다.
TWAP (시간 가중 평균 가격)
아이디어는 매우 간단하다: 총 수량을 균등하게 나누고 균일한 시간 간격으로 실행한다.
import asyncio
from datetime import datetime, timedelta
class TWAPExecutor:
"""
TWAP 실행기: 대규모 주문을 균등하게 나누어
균일한 시간 간격으로 실행한다.
"""
def __init__(self, exchange, symbol: str, side: str,
total_qty: float, duration_minutes: int, num_slices: int):
self.exchange = exchange
self.symbol = symbol
self.side = side
self.total_qty = total_qty
self.slice_qty = total_qty / num_slices
self.interval = (duration_minutes * 60) / num_slices
self.num_slices = num_slices
self.executed_qty = 0.0
self.fills: list[dict] = []
async def execute(self):
for i in range(self.num_slices):
remaining = self.total_qty - self.executed_qty
qty = min(self.slice_qty, remaining)
if qty <= 0:
break
try:
order = await self.exchange.create_order(
symbol=self.symbol,
type="market",
side=self.side,
amount=qty,
)
self.executed_qty += float(order["filled"])
self.fills.append(order)
print(f"[TWAP] slice {i+1}/{self.num_slices}: "
f"filled {order['filled']} @ {order['average']}")
except Exception as e:
print(f"[TWAP] slice {i+1} failed: {e}")
if i < self.num_slices - 1:
await asyncio.sleep(self.interval)
avg_price = (
sum(f["cost"] for f in self.fills) /
sum(f["filled"] for f in self.fills)
) if self.fills else 0
print(f"[TWAP] done: {self.executed_qty}/{self.total_qty} "
f"avg price: {avg_price:.2f}")
VWAP (거래량 가중 평균 가격)
VWAP은 더 스마트하다: 전형적인 거래량 프로파일을 고려한다. 9:00부터 10:00까지 보통 일일 거래량의 30%가 거래된다면, VWAP은 그 시간대에 주문의 30%를 실행한다. 목표는 평균 체결 가격을 시장 VWAP에 최대한 가깝게 하는 것이다.
class VWAPExecutor:
"""
VWAP 실행기: 과거 거래량 프로파일에
비례하여 수량을 배분한다.
"""
def __init__(self, exchange, symbol: str, side: str,
total_qty: float, volume_profile: list[float]):
self.exchange = exchange
self.symbol = symbol
self.side = side
self.total_qty = total_qty
total_weight = sum(volume_profile)
self.weights = [w / total_weight for w in volume_profile]
async def execute(self, interval_seconds: float = 60.0):
executed = 0.0
for i, weight in enumerate(self.weights):
qty = self.total_qty * weight
remaining = self.total_qty - executed
qty = min(qty, remaining)
if qty <= 0:
break
order = await self.exchange.create_order(
symbol=self.symbol,
type="market",
side=self.side,
amount=qty,
)
executed += float(order["filled"])
print(f"[VWAP] period {i+1}: weight={weight:.2%}, "
f"filled={order['filled']} @ {order['average']}")
await asyncio.sleep(interval_seconds)
TWAP vs VWAP 차이: TWAP은 더 단순하고 예측 가능하다. VWAP은 더 나은 평균 가격을 제공하지만 신뢰할 수 있는 거래량 프로파일이 필요하다. 거래량이 허수 거래(wash trading)일 수 있는 암호화폐 시장에서는 VWAP 프로파일을 신중하게 구축해야 한다.
3. 체이싱 리밋: 주문이 가격을 쫓아갈 수 있을 때
체이싱 리밋: 설정 가능한 적극성으로 움직이는 가격을 추적하는 주문
이제 정말 흥미로운 부분이다. 표준 지정가 주문은 수동적인 존재다: 호가창에 앉아서 기다린다. 가격이 움직이면 — 주문은 미체결 상태로 남는다. 알고 트레이더에게 이것은 종종 받아들일 수 없다: 진입 시그널이 나왔는데 시장이 0.1% 움직였다고 포지션이 구축되지 않는 것이다.
체이싱 지정가 주문(Chasing limit order) 은 지정가 주문을 감싸는 프로그래밍 래퍼로:
- 현재 최우선 가격(또는 약간의 오프셋 포함)으로 지정가 주문을 제출
- WebSocket을 통해 가격 모니터링
- 가격이 주문에서 벗어나면 — 취소하고 현재 가격에 더 가깝게 재제출
- 주문이 체결되거나 허용 편차를 초과할 때까지 반복
주요 파라미터
- chase_interval_ms — 확인 및 재제출 빈도. 100ms — 적극적, 1000ms — 온건.
- max_chase_distance — 초기 가격으로부터의 최대 편차, 이를 초과하면 주문 취소. 폭주하는 시장을 추격하는 것에 대한 보호.
- aggression_level — 시장 가격에 얼마나 가깝게 지정가 주문을 놓을지.
0— 최우선 매수/매도호가에 (수동적),1— 스프레드를 관통 (적극적, 사실상 테이커). - chase_on_partial — 주문이 부분 체결된 경우 계속 추격할지 여부.
TypeScript 구현
interface ChasingOrderParams {
symbol: string;
side: "buy" | "sell";
totalQty: number;
/** 0 = passive (at best bid/ask), 1 = cross spread */
aggression: number;
/** max price deviation from initial price */
maxChaseDistance: number;
/** how often to re-evaluate, ms */
chaseIntervalMs: number;
/** stop chasing after this many ms */
timeoutMs: number;
}
class ChasingLimitOrder {
private currentOrderId: string | null = null;
private filledQty = 0;
private initialPrice: number | null = null;
private startTime = Date.now();
constructor(
private exchange: any, // ccxt exchange instance
private params: ChasingOrderParams
) {}
async execute(): Promise<{ filledQty: number; avgPrice: number }> {
const fills: Array<{ qty: number; price: number }> = [];
while (this.filledQty < this.params.totalQty) {
// 타임아웃
if (Date.now() - this.startTime > this.params.timeoutMs) {
console.log("[CHASE] timeout reached, cancelling");
await this.cancelCurrent();
break;
}
// 현재 호가창 조회
const book = await this.exchange.fetchOrderBook(
this.params.symbol, 5
);
const bestBid = book.bids[0][0];
const bestAsk = book.asks[0][0];
const spread = bestAsk - bestBid;
// 목표 가격 계산
let targetPrice: number;
if (this.params.side === "buy") {
targetPrice = bestBid + spread * this.params.aggression;
} else {
targetPrice = bestAsk - spread * this.params.aggression;
}
// 초기 가격 기록
if (this.initialPrice === null) {
this.initialPrice = targetPrice;
}
// 최대 체이싱 거리 확인
const deviation = Math.abs(targetPrice - this.initialPrice);
if (deviation > this.params.maxChaseDistance) {
console.log(
`[CHASE] max deviation exceeded: ${deviation.toFixed(4)} > ` +
`${this.params.maxChaseDistance}`
);
await this.cancelCurrent();
break;
}
// 현재 주문 확인
if (this.currentOrderId) {
const order = await this.exchange.fetchOrder(
this.currentOrderId, this.params.symbol
);
if (order.status === "closed") {
fills.push({ qty: order.filled, price: order.average });
this.filledQty += order.filled;
this.currentOrderId = null;
continue;
}
// 부분 체결 filledQty 업데이트
if (order.filled > 0) {
const newFilled = order.filled - (
fills.reduce((s, f) => s + f.qty, 0) - this.filledQty
);
// 주문이 유지되고 있다 — 가격 변경이 필요한가?
}
const currentPrice = parseFloat(order.price);
const priceDiff = Math.abs(currentPrice - targetPrice);
const tickSize = spread * 0.1 || 0.01;
if (priceDiff > tickSize) {
// 가격이 움직임 — 가격 변경
console.log(
`[CHASE] repricing: ${currentPrice} -> ` +
`${targetPrice.toFixed(4)}`
);
await this.cancelCurrent();
} else {
// 주문이 올바른 가격에 있음 — 대기
await this.sleep(this.params.chaseIntervalMs);
continue;
}
}
// 새 주문 제출
const remainingQty = this.params.totalQty - this.filledQty;
const order = await this.exchange.createLimitOrder(
this.params.symbol,
this.params.side,
remainingQty,
targetPrice
);
this.currentOrderId = order.id;
console.log(
`[CHASE] placed ${this.params.side} ${remainingQty} ` +
`@ ${targetPrice.toFixed(4)}`
);
await this.sleep(this.params.chaseIntervalMs);
}
const totalCost = fills.reduce((s, f) => s + f.qty * f.price, 0);
const avgPrice = this.filledQty > 0 ? totalCost / this.filledQty : 0;
return { filledQty: this.filledQty, avgPrice };
}
private async cancelCurrent(): Promise<void> {
if (this.currentOrderId) {
try {
await this.exchange.cancelOrder(
this.currentOrderId, this.params.symbol
);
} catch { /* 주문이 이미 체결되었거나 취소됨 */ }
this.currentOrderId = null;
}
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
체이싱이 해로울 때
체이싱은 강력한 도구이지만 손실 제조기로 쉽게 변할 수 있다:
- 취소/재제출 스팸. 매번 취소와 재제출은 API에 부하를 준다. 거래소는 요청 빈도를 제한하며, 적극적인 체이싱은 API 키 정지로 이어질 수 있다.
- 역선택. 가격이 당신에게서 계속 멀어진다면 — 시장이 당신이 모르는 무언가를 알고 있을 수 있다. 이 상황에서 가격을 추격하는 것은 고점에서 매수하는 것이다.
- 메이커에서 테이커로 전환. 높은 적극성에서는 사실상 테이커 수수료를 지불하게 되지만, 지연이 있다(취소 + 새 주문). 때로는 처음부터 시장가 주문을 내는 것이 더 간단하다.
4. 시간 기반 주문: 밀리초 단위의 정밀도
"가격 X에서"가 아니라 "시간 T에" 주문을 실행해야 하는 상황이 있다. 이상하게 들리는가? 실은 전략의 한 분류 전체에 해당한다.
활용 사례
펀딩 레이트 차익거래. 무기한 선물에서 펀딩은 8시간마다 지급된다(Binance에서 UTC 00:00, 08:00, 16:00). 펀딩 레이트 = +0.1%이면 결제 순간에 숏 포지션이어야 한다. 전략: 결제 몇 초 전에 숏 오픈, 펀딩 수취, 포지션 청산. 타이밍이 결정적이다 — 1초의 지연은 펀딩을 놓치는 것을 의미한다.
세션 개장/폐장. 전통 시장과 일부 암호화폐 파생상품에는 고정 세션이 있다. 개장 경매(NYSE, CME)는 유동성이 최대인 순간이다. 경매 100ms 전에 주문을 내는 것은 우위가 된다.
뉴스 기반 실행. 인플레이션 데이터가 예정된 시간에 발표된다. 알고리즘이 뉴스 피드에서 숫자를 파싱하고 50ms 내에 주문을 낸다. 여기서 시간 기반 실행은 이벤트 드리븐 로직과 결합된다.
구현
class TimeBasedOrder {
constructor(
private exchange: any,
private symbol: string,
private side: "buy" | "sell",
private qty: number,
private orderType: "market" | "limit",
private limitPrice?: number
) {}
/**
* 정확한 시간에 실행을 예약한다.
* 최대 정밀도를 위해 바쁜 대기 루프를 사용한다.
*/
async executeAt(targetTime: Date): Promise<any> {
const targetMs = targetTime.getTime();
// 1단계: 대략적인 대기 (sleep)
const coarseWait = targetMs - Date.now() - 500; // 500ms 일찍 깨어남
if (coarseWait > 0) {
console.log(
`[TIME-ORDER] sleeping for ${(coarseWait / 1000).toFixed(1)}s`
);
await new Promise((r) => setTimeout(r, coarseWait));
}
// 2단계: 정밀 대기 (바쁜 대기)
while (Date.now() < targetMs) {
// 스핀 — CPU를 소모하지만 약 1ms 정밀도를 달성
}
// 3단계: 실행
const sendTime = Date.now();
const order = await this.exchange.createOrder(
this.symbol,
this.orderType,
this.side,
this.qty,
this.limitPrice
);
console.log(
`[TIME-ORDER] executed at ${new Date(sendTime).toISOString()}, ` +
`target was ${targetTime.toISOString()}, ` +
`delta: ${sendTime - targetMs}ms`
);
return order;
}
}
// 예시: UTC 00:00:00 정각에 주문 실행 (펀딩 결제)
const executor = new TimeBasedOrder(exchange, "BTC/USDT", "sell", 0.1, "market");
const target = new Date("2026-03-24T00:00:00.000Z");
await executor.executeAt(target);
중요 참고: 시간 기반 주문의 정밀도는 코드가 아니라 거래소까지의 네트워크 지연에 의해 제한된다. API까지의 핑이 50ms라면, 완벽한 바쁜 대기도 50ms의 델타를 보인다. 본격적인 HFT에서는 코로케이션이 사용된다 — 서버가 물리적으로 거래소 매칭 엔진 옆에 위치한다.
5. 가상/합성 주문: 시스템 속의 투명인간
가상 주문: 트리거가 발동할 때까지 봇의 메모리에만 존재하는 주문
이것은 아마도 알고 트레이더의 무기고에서 가장 과소평가된 도구다. 가상 주문(합성 주문이라고도 함)은 오직 당신의 시스템에만 존재하는 주문이다. 트리거 조건(보통 가격이 특정 레벨에 도달)이 충족될 때까지 거래소에 전송되지 않는다.
작동 방식
- 알고리즘이 결정한다: "BTC를 $40,000에 사고 싶다"
- 거래소에 지정가 주문을 보내는 대신 메모리에 가상 주문을 생성한다
- WebSocket 가격 스트림을 구독한다
- 매수/매도 호가가 $40,000에 도달하면 — 거래소에 실제 시장가 또는 지정가 주문을 보낸다
가상 주문이 중요한 이유
정보 누출 없음. 당신의 주문은 호가창에 보이지 않는다. 다른 트레이더, HFT 알고리즘, 심지어 거래소 자체도 — 실행 순간까지 당신의 의도를 모른다. 이것은 힘의 균형을 근본적으로 변화시킨다.
프런트러닝 보호. 암호화폐 거래소, 특히 투명성이 낮은 거래소에서는 대량 지정가 주문의 정보가 프런트러닝에 사용될 수 있다는 합리적 의심이 있다(이에 대한 연구도 있다). 가상 주문은 이 위험을 제거한다.
그리드 봇. 전형적인 그리드 봇은 다양한 가격 레벨에 50-200개의 주문을 배치한다. 모두 거래소에 보내면 — 호가창에 200개의 주문이 들어가며: (a) 모두에게 보이고, (b) 거래소의 주문 한도를 사용하고(보통 계정당 200-300개 미체결 주문), (c) 급격한 가격 변동 시 모두 체결되어 거대한 포지션을 갖게 된다. 가상 주문은 세 가지 문제를 모두 해결한다.
떨어지는 칼 잡기. 전략: 현재 가격의 -5%, -10%, -15% 레벨에 가상 매수 주문을 배치. 시장이 하락하면 — 주문이 단계적으로 트리거된다. 하락하지 않으면 — 아무 위험도 없고 거래소 주문 한도도 사용하지 않는다.
TypeScript 구현
interface VirtualOrder {
id: string;
symbol: string;
side: "buy" | "sell";
triggerPrice: number;
qty: number;
/** 트리거 시 거래소에 보내는 주문 유형 */
executionType: "market" | "limit";
/** 지정가의 경우: 트리거 가격으로부터의 오프셋 */
limitOffset?: number;
status: "pending" | "triggered" | "filled" | "failed";
}
class VirtualOrderManager {
private orders: Map<string, VirtualOrder> = new Map();
private orderCounter = 0;
constructor(private exchange: any) {}
/**
* 가상 주문 생성. 거래소에는 아무것도 전송되지 않는다.
*/
addOrder(params: Omit<VirtualOrder, "id" | "status">): string {
const id = `virt_${++this.orderCounter}`;
this.orders.set(id, { ...params, id, status: "pending" });
console.log(
`[VIRTUAL] created ${params.side} ${params.qty} ` +
`${params.symbol} @ trigger ${params.triggerPrice}`
);
return id;
}
/**
* 매 가격 틱마다 호출됨 (WebSocket에서).
*/
async onPriceUpdate(
symbol: string, bestBid: number, bestAsk: number
): Promise<void> {
for (const [id, order] of this.orders) {
if (order.symbol !== symbol || order.status !== "pending") continue;
const triggered =
(order.side === "buy" && bestAsk <= order.triggerPrice) ||
(order.side === "sell" && bestBid >= order.triggerPrice);
if (!triggered) continue;
order.status = "triggered";
console.log(
`[VIRTUAL] ${id} triggered! bid=${bestBid} ask=${bestAsk}`
);
try {
let realOrder: any;
if (order.executionType === "market") {
realOrder = await this.exchange.createMarketOrder(
order.symbol, order.side, order.qty
);
} else {
const limitPrice = order.side === "buy"
? order.triggerPrice + (order.limitOffset ?? 0)
: order.triggerPrice - (order.limitOffset ?? 0);
realOrder = await this.exchange.createLimitOrder(
order.symbol, order.side, order.qty, limitPrice
);
}
order.status = "filled";
console.log(
`[VIRTUAL] ${id} filled: ${realOrder.filled} ` +
`@ ${realOrder.average ?? realOrder.price}`
);
} catch (err) {
order.status = "failed";
console.error(`[VIRTUAL] ${id} execution failed:`, err);
}
}
}
/**
* 모든 활성 가상 주문을 조회한다.
*/
getPendingOrders(): VirtualOrder[] {
return [...this.orders.values()].filter(
(o) => o.status === "pending"
);
}
cancelOrder(id: string): boolean {
const order = this.orders.get(id);
if (order && order.status === "pending") {
this.orders.delete(id);
return true;
}
return false;
}
}
// --- 예시: 가상 주문을 사용하는 그리드 봇 ---
async function gridBot(exchange: any) {
const manager = new VirtualOrderManager(exchange);
const currentPrice = 42000;
const gridStep = 200; // 그리드 간격
const gridLevels = 20; // 각 방향의 레벨 수
const qtyPerLevel = 0.01; // 레벨당 BTC 수량
// 가상 그리드 생성
for (let i = 1; i <= gridLevels; i++) {
// 현재 가격 아래의 매수 주문
manager.addOrder({
symbol: "BTC/USDT",
side: "buy",
triggerPrice: currentPrice - gridStep * i,
qty: qtyPerLevel,
executionType: "limit",
limitOffset: 1, // limit price = trigger + 1 USDT
});
// 현재 가격 위의 매도 주문
manager.addOrder({
symbol: "BTC/USDT",
side: "sell",
triggerPrice: currentPrice + gridStep * i,
qty: qtyPerLevel,
executionType: "limit",
limitOffset: 1,
});
}
console.log(
`[GRID] created ${gridLevels * 2} virtual orders, ` +
`0 on exchange`
);
// WebSocket 구독 (ccxt.pro 의사코드)
while (true) {
const ticker = await exchange.watchTicker("BTC/USDT");
await manager.onPriceUpdate(
"BTC/USDT", ticker.bid, ticker.ask
);
}
}
가상 주문의 함정
-
지연 격차. 가격을 확인한 순간부터 실제 주문이 거래소에 도달하는 순간까지 시간이 걸린다. 변동성이 큰 시장에서는 그 20-100ms 사이에 가격이 날아갈 수 있다. 해결책: 약간 적극적인 지정가 주문(여유를 두고)을 보낸다.
-
체결 누락. 가격이 한 틱 만에 당신의 레벨을 "관통"(플래시 크래시)하고 되돌아왔다면 — 반응이 늦을 수 있다. 호가창에 걸려 있는 일반 지정가 주문이라면 체결되었겠지만, 가상 주문은 체결되지 않는다.
-
상태 관리. 가상 주문은 메모리에 존재한다. 프로세스가 크래시하면 — 주문이 소실된다. 해결책: 영구 저장소(Redis, SQLite, 파일)와 재시작 시 복구.
6. 조건부/스마트 주문: 주문의 조합론
단일 주문으로 충분하지 않을 때, 트레이더는 조건부 구조로 조합한다. 일부는 거래소에서 네이티브로 지원되고, 나머지는 프로그래밍으로 구현된다.
OCO (One Cancels Other)
두 주문이 연결된다: 하나가 체결되면 — 다른 하나가 자동 취소된다. 전형적인 예: 롱 포지션에서 이익 실현과 손절매를 모두 설정하고 싶다. 어느 것이 먼저 발동하든 — 다른 하나는 취소되어야 한다.
class OCOHandler:
"""
OCO: 한 주문이 체결되면 다른 주문이 취소된다.
"""
def __init__(self, exchange, symbol: str):
self.exchange = exchange
self.symbol = symbol
self.order_a_id: str | None = None
self.order_b_id: str | None = None
async def place(
self,
take_profit_price: float,
stop_loss_price: float,
qty: float,
):
tp = await self.exchange.create_limit_sell_order(
self.symbol, qty, take_profit_price
)
self.order_a_id = tp["id"]
sl = await self.exchange.create_order(
self.symbol, "stop", "sell", qty,
None, {"stopPrice": stop_loss_price}
)
self.order_b_id = sl["id"]
print(f"[OCO] TP @ {take_profit_price}, SL @ {stop_loss_price}")
async def monitor(self):
"""상태를 확인하고 쌍 주문을 취소한다."""
while True:
if self.order_a_id:
a = await self.exchange.fetch_order(
self.order_a_id, self.symbol
)
if a["status"] == "closed":
print("[OCO] take-profit filled, cancelling stop-loss")
await self.exchange.cancel_order(
self.order_b_id, self.symbol
)
break
if self.order_b_id:
b = await self.exchange.fetch_order(
self.order_b_id, self.symbol
)
if b["status"] == "closed":
print("[OCO] stop-loss filled, cancelling take-profit")
await self.exchange.cancel_order(
self.order_a_id, self.symbol
)
break
await asyncio.sleep(0.5)
브라켓 주문
3구성 요소 구조: 메인 진입 주문 + 출구용 OCO(이익 실현 + 손절매). 본질적으로 한 번의 호출로 완전한 거래 사이클:
- 진입: 지정가 매수 주문
- 이익 실현: 지정가 매도 주문 (상위)
- 손절매: 스톱 마켓 매도 주문 (하위)
진입이 체결되면 TP와 SL이 자동 제출된다. 둘 중 하나가 체결되면 — 나머지 하나가 취소된다.
If-Then 로직
가장 유연한 옵션 — 임의의 조건을 가진 주문 체인:
rules = [
{
"condition": {"symbol": "BTC/USDT", "price_above": 50000},
"action": {"type": "market_buy", "symbol": "ETH/USDT", "qty": 10},
"then": [
{
"condition": {"symbol": "ETH/USDT", "price_above": 4000},
"action": {"type": "market_sell", "symbol": "ETH/USDT", "qty": 10},
},
{
"condition": {"symbol": "ETH/USDT", "price_below": 3500},
"action": {"type": "market_sell", "symbol": "ETH/USDT", "qty": 10},
},
]
}
]
이러한 구성은 어떤 거래소에서도 네이티브로 지원되지 않는다 — 프로그래밍 구현만 가능하다. 이것이 알고트레이딩 시스템이 불가피하게 자체 주문 관리 레이어를 발전시키는 이유 중 하나다.
7. 마켓메이커는 어떻게 특수 주문 유형을 사용하는가
마켓메이킹은 그 자체로 하나의 세계이며, 주문 도구도 그에 맞게 구성된다. 마켓메이커의 임무는 지속적으로 매수/매도 호가를 제시하고 스프레드로 수익을 올리면서, 역선택(정보를 가진 트레이더가 당신에게 불리하게 거래하는 상황)을 최소화하는 것이다.
포스트 온리는 필수
마켓메이커에게 포스트 온리는 선택이 아니라 필수다. 주문이 실수로 테이커로 체결되면 — 메이커 리베이트를 받는 대신 테이커 수수료를 지불하게 된다. 하루 수천 건의 주문에서 이것은 재앙이다.
async def quote(exchange, symbol, mid_price, half_spread, qty):
bid_price = mid_price - half_spread
ask_price = mid_price + half_spread
bid = await exchange.create_order(
symbol, "limit", "buy", qty, bid_price,
{"postOnly": True} # 마켓메이커에게 필수
)
ask = await exchange.create_order(
symbol, "limit", "sell", qty, ask_price,
{"postOnly": True}
)
return bid, ask
히든 주문
일부 거래소(Kraken, Bitfinex)에서는 히든(숨겨진) 주문을 사용할 수 있다 — 호가창에 표시되지 않지만 거래소에 존재하며 매칭에 참여한다. 트레이드오프: 메이커로서도 테이커 수수료를 지불하지만, 익명성을 얻는다.
마켓메이커에게 이것은 재고 관리 도구다: 큰 포지션이 축적되면, 시장에 의도를 드러내지 않고 히든 주문으로 포지션을 줄일 수 있다.
페그 주문
최우선 매수/매도 호가에 연동된 주문. Coinbase Advanced Trade에서는, 예를 들어, 자동으로 최우선 매수호가를 추적하며 항상 큐의 맨 앞에 서는 주문을 낼 수 있다. 이것은 거래소 수준의 네이티브 체이싱 주문이다 — 하지만 어디서나 사용할 수 있는 것은 아니다.
대량 주문 관리
프로 마켓메이커는 배치 API를 사용하여 단일 HTTP 요청으로 수십 개의 주문을 동시에 취소하고 제출한다. Binance에서는 batchOrders, Bybit에서는 place-batch-order다. 이를 통해 지연과 레이트 리밋 압력이 줄어든다.
8. 주문 유형 비교표
| 주문 유형 | 체결 보장 | 가격 보장 | 호가창에 표시 | 거래소 네이티브 | 구현 복잡도 |
|---|---|---|---|---|---|
| 시장가 | 예 | 아니오 | 아니오 (즉시) | 예 | 없음 |
| 지정가 | 아니오 | 예 | 예 | 예 | 없음 |
| 스톱 마켓 | 예 (트리거 후) | 아니오 | 아니오 | 예 | 없음 |
| 스톱 리밋 | 아니오 | 예 | 아니오 (트리거 전) | 예 | 없음 |
| 트레일링 스톱 | 예 (트리거 후) | 아니오 | 아니오 | 부분적 | 낮음 |
| 아이스버그 | 아니오 | 예 | 부분적 | 부분적 | 중간 |
| 포스트 온리 | 아니오 | 예 | 예 | 예 | 없음 |
| TWAP | 아니오 (슬라이스 의존) | 아니오 | 부분적 | 아니오 | 중간 |
| VWAP | 아니오 | 아니오 | 부분적 | 아니오 | 높음 |
| 체이싱 리밋 | 지정가보다 높음 | 부분적 | 예 (현재 주문) | 아니오 | 중간 |
| 시간 기반 | 유형에 따름 | 유형에 따름 | 아니오 (시간 T까지) | 아니오 | 낮음 |
| 가상/합성 | 지정가보다 낮음 | 유형에 따름 | 아니오 | 아니오 | 중간 |
| OCO | 예 (둘 중 하나) | 부분적 | 예 (둘 다) | 부분적 | 중간 |
| 브라켓 | 예 | 부분적 | 예 | 드묾 | 높음 |
| 히든 | 아니오 | 예 | 아니오 | 드묾 | 없음 |
| 페그 | 아니오 | 동적 | 예 | 매우 드묾 | 높음 (프로그래밍 시) |
결론: 전략의 구성 요소로서의 주문
주문 유형은 "인터페이스의 버튼"이 아니다. 모든 트레이딩 시스템의 실행 레이어를 구축하는 기본 원시 요소다. "백테스트에서 수익이 나는 전략"과 "프로덕션에서 수익이 나는 전략" 사이의 차이는 종종 바로 여기에 있다 — 거래소에 주문을 어떻게 보내느냐에 달려 있다.
몇 가지 실용적인 시사점:
- 표준 주문부터 시작하라, 뉘앙스를 이해하고 있는지 확인하라(스톱 리밋 vs 스톱 마켓, IOC vs FOK). 대부분의 실수가 여기서 발생한다.
- 가상 주문은 그리드 봇의 필수품이다. 50개 이상의 주문을 내려면 — 전부 거래소에 보내지 마라.
- 체결률이 가격보다 중요할 때 체이싱이 필요하다. 그러나 반드시 max_chase_distance를 설정하라 — 그렇지 않으면 매우 멀리 벗어날 수 있다.
- 시간 기반 실행은 틈새적이지만 펀딩 레이트 차익거래와 이벤트 드리븐 전략에 강력한 도구다.
- 자체 주문 관리 레이어는 진지한 알고트레이딩 시스템에서 불가피하다. 거래소 네이티브 주문 유형만으로는 충분하지 않다.
트레이딩 시스템을 구축하며 더 깊이 알고 싶다면 — 호가창의 큐 포지션, CCXT의 WebSocket 메서드, 펀딩 레이트 차익거래에 관한 글을 참고하시라.
참고 문헌 및 출처
- CCXT Library — 100개 이상의 거래소를 지원하는 통합 암호화폐 거래소 라이브러리
- Binance API Documentation — Binance 주문 유형 문서
- Bybit API v5 — 배치 주문을 포함한 Bybit 문서
- Moallemi, C. & Yuan, K. (2017). The Value of Queue Position in a Limit Order Book. Columbia Business School Research Paper
- Cartea, A., Jaimungal, S., & Penalva, J. (2015). Algorithmic and High-Frequency Trading. Cambridge University Press
- Avellaneda, M. & Stoikov, S. (2008) — High-frequency trading in a limit order book. Quantitative Finance
- Erik Rigtorp — Order Queue Position Estimation — 큐 포지션 추정 관련 자료
- Trading Technologies (TT) — 고급 주문 유형을 갖춘 전문 트레이딩 플랫폼
MarketMaker.cc Team
퀀트 리서치 및 전략