WebSockets — /ws/*
quant-ai는 두 개의 사용자별 WebSocket 채널을 노출합니다.
| 경로 | 용도 | Phase |
|---|---|---|
/ws/live | 봇 시그널·체결·상태 (BTC + 멀티 자산) | P1 (기존) |
/ws/equity/orders | 브로커 주문 이벤트 fan-out (accepted/fill/canceled/...) | P3-04b |
인증
WebSocket은 헤더 대신 ?token=<JWT> query 파라미터로 인증합니다.
const ws = new WebSocket(
`ws://localhost:8000/ws/equity/orders?token=${jwt}`,
);
토큰이 없거나 invalid이면 close code 4001 Missing token / 4001 Invalid token으로 즉시 종료됩니다.
멀티유저 격리
BotRegistry.get_manager(user_id)→ user별 BotManager 인스턴스- 모든 메시지는 토큰의
sub(user_id)로 식별된 사용자 데이터만 broadcast - 다른 user의 이벤트는 절대 노출되지 않음
/ws/live
Hello / Keep-alive
연결 시 즉시 broadcast 가능 상태가 됩니다 (별도 hello 프레임 없음). 클라이언트가 텍스트 메시지를 보내면 서버는 무시하지만 keep-alive로 활용 가능합니다.
서버 → 클라이언트 메시지
Signal
{
"type": "signal",
"symbol": "BTC/USDT",
"asset_class": "crypto",
"direction": "BUY",
"confidence": 0.71,
"stop_loss": 96420.0,
"take_profit": 99850.0,
"ts": "2026-04-26T10:11:53Z"
}
Trade
{
"type": "trade",
"symbol": "BTC/USDT",
"asset_class": "crypto",
"side": "buy",
"amount": 0.012,
"price": 96850.0,
"fee": 1.16,
"ts": "2026-04-26T10:12:01Z"
}
Bot Status
{
"type": "bot_status",
"is_running": true,
"is_halted": false,
"halt_reason": null,
"ts": "2026-04-26T10:11:53Z"
}
Tick (선택)
{
"type": "tick",
"symbol": "BTC/USDT",
"last": 96820.5,
"ts": "2026-04-26T10:12:01.123Z"
}
Reconnect
연결 끊김 시 클라이언트가 backoff (1s → 2s → 4s, max 30s)로 재연결해야 합니다. 미수신 이벤트는 REST (/api/trades, /api/positions)로 backfill.
/ws/equity/orders
Hello 프레임
연결 직후 사용 가능한 자산군 목록을 hello 프레임으로 받습니다.
{
"type": "hello",
"asset_classes": ["us_equity", "kr_equity"],
"user_id": 42
}
자산군 목록이 비어있으면 사용자가 해당 자산군 브로커를 등록하지 않은 것입니다.
서버 → 클라이언트 메시지 (WSOrderEvent)
{
"type": "order_event",
"event_type": "fill",
"asset_class": "us_equity",
"broker": "alpaca",
"broker_order_id": "alp_8e2...",
"client_order_id": "user-uuid-xxx",
"symbol": "AAPL",
"filled_qty": 5.0,
"avg_fill_price": 188.21,
"status": "filled",
"received_at": "2026-04-26T13:35:08Z",
"payload": { /* broker-native dict */ }
}
event_type 종류
| 값 | 설명 |
|---|---|
placed | API에서 placement 직후 audit 이벤트 |
accepted | 브로커 접수 |
open | 호가창 등록 |
partial_fill | 부분 체결 |
fill / filled | 완전 체결 |
canceled | 취소 (만료/사용자/시장) |
rejected | 브로커 거부 |
replaced | 수정 (Alpaca replace) |
동작
- 브로커별
stream_order_events비동기 generator를 user-level fan-out - 모든 이벤트는
broker_order_events테이블에도 영속화 → 재연결 후 REST 백필 가능 - 마지막 client 연결 해제 시 broker stream task 자동 cancel (리소스 회수)
멀티 자산 동시 구독
한 연결로 사용자의 모든 자산군 (us_equity + kr_equity) 이벤트를 동시 수신. payload의 asset_class / broker 필드로 라우팅.
Keep-alive
클라이언트는 30초마다 임의의 텍스트 메시지를 보내 NAT 타임아웃을 방지하세요. 서버는 메시지를 폐기.
Reconnect
let ws;
function connect() {
ws = new WebSocket(`ws://localhost:8000/ws/equity/orders?token=${jwt}`);
ws.onclose = () => setTimeout(connect, 2000); // 2s backoff
ws.onmessage = (e) => handleEvent(JSON.parse(e.data));
}
connect();
재연결 후 누락된 이벤트는 GET /api/equity/orders?status=...&page=...로 backfill.
에러 close code
| Code | 의미 |
|---|---|
4001 | Missing/invalid token |
1000 | 정상 종료 (서버 / 클라이언트) |
1006 | 비정상 종료 (네트워크) |
1011 | 서버 internal error |
TypeScript 클라이언트 예시
import { useEffect, useState } from 'react';
export function useEquityOrderEvents(jwt: string) {
const [events, setEvents] = useState<WSOrderEvent[]>([]);
useEffect(() => {
const ws = new WebSocket(
`${WS_BASE}/ws/equity/orders?token=${jwt}`,
);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'order_event') {
setEvents((prev) => [...prev, msg]);
}
};
ws.onclose = (e) => {
if (e.code !== 1000) {
console.warn('WS closed, will retry', e.code);
}
};
return () => ws.close();
}, [jwt]);
return events;
}
비고
- WebSocket은 user별 동시 연결 1개 권장 (브로커 stream 중복 비용 회피)
- Telegram 알림과 WebSocket은 별개 채널 — Telegram은 critical 이벤트만, WebSocket은 모든 이벤트
- 환경변수
WS_HEARTBEAT_INTERVAL_SEC(기본 30) — 서버측 ping 간격