본문으로 건너뛰기

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 종류

설명
placedAPI에서 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의미
4001Missing/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 간격