실시간 시세 (WebSocket)

종목 시세가 바뀔 때마다 서버가 밀어주는 WebSocket이에요. 퍼스톡 웹 화면도 이걸로 시세를 받아요. 여러 종목을 짧은 주기로 폴링하는 대신 이걸 쓰면 빠르고, REST 요청 한도도 아낄 수 있어요.

항목값
주소wss://ws.furstock.trade/socket
프로토콜Phoenix Channels (JSON 직렬화 v2)
인증필요 없음(시세는 공개 데이터)
방향수신 전용. 주문은 항상 REST(POST /v1/orders)로
토픽stock:{symbol} 종목마다 하나

서버·봇 프로그램(Node, Bun, Python 등)은 바로 접속돼요. 브라우저에서 접속할 땐 현재 퍼스톡 도메인만 허용돼 있어서, 다른 도메인에서 서비스하는 웹 클라이언트는 시세를 REST로 폴링하세요.

이벤트

이벤트언제payload
snapshot처음 구독했을 때, 끊겼다 다시 붙어 놓친 게 많을 때현재가 스냅샷 전체
price체결로 시세가 바뀔 때마다(종목당 최대 초당 약 10회로 묶여서 옴)현재가 스냅샷 전체
cold서버가 아직 이 종목 시세를 모를 때(최근 거래 없음){ "symbol": "..." }

snapshot과 price의 payload는 현재가 API(GET /v1/market/price/{symbol}) 응답과 같은 모양이에요: symbol, sessionId, seq, lastPrice, openPrice, high, low, bandLow, bandHigh, volume, orderbook, ts.

cold를 받았다면 REST로 현재가를 한 번 조회해 두세요. 그 종목에 체결이 생기면 그때부터 price가 와요.

재접속

구독할 때 마지막으로 받은 seq를 last_seq로 넘기면, 서버가 그 뒤의 변경분만 보내거나(짧은 끊김) snapshot으로 전체를 다시 보내줘요(긴 끊김).

JavaScript / TypeScript (phoenix 패키지)

npm install phoenix
import { Socket } from "phoenix";

const socket = new Socket("wss://ws.furstock.trade/socket");
socket.connect();

let lastSeq = 0;
const channel = socket.channel("stock:FOXX", () => ({ last_seq: lastSeq }));

const onPrice = (snap: { seq: number; lastPrice: number; volume: number }) => {
  lastSeq = snap.seq;
  console.log("FOXX", snap.lastPrice, "vol", snap.volume);
};
channel.on("snapshot", onPrice);
channel.on("price", onPrice);
channel.on("cold", () => console.log("아직 시세 없음 — REST로 조회"));

channel
  .join()
  .receive("ok", () => console.log("구독 시작"))
  .receive("error", (e) => console.error("구독 실패", e));

Node 20 이하에서는 전역 WebSocket이 없어서 new Socket(url, { transport: WebSocket })에 ws 패키지의 WebSocket을 넘겨야 해요. Node 22+, Bun, Deno, 브라우저는 그대로 됩니다.

라이브러리 없이 (원시 프로토콜)

Phoenix v2 직렬화는 메시지 하나가 JSON 배열 [join_ref, ref, topic, event, payload]이에요.

  1. wss://ws.furstock.trade/socket/websocket?vsn=2.0.0에 접속
  2. 구독: ["1", "1", "stock:FOXX", "phx_join", {"last_seq": 0}] 전송 → phx_reply(status ok) 수신
  3. 수신: [join_ref, null, "stock:FOXX", "price", { ...스냅샷 }]
  4. 30초마다 하트비트: [null, "<ref>", "phoenix", "heartbeat", {}]. 안 보내면 60초 뒤 서버가 연결을 끊어요.
  5. 구독 해제: ["1", "<ref>", "stock:FOXX", "phx_leave", {}]

ref는 보낼 때마다 1씩 늘리는 문자열이면 돼요. 한 연결로 여러 종목을 구독할 수 있어요(종목마다 다른 join_ref).

Python (websockets)

pip install websockets
import asyncio, json, itertools
import websockets

URL = "wss://ws.furstock.trade/socket/websocket?vsn=2.0.0"
SYMBOLS = ["FOXX", "WOLF"]

async def main():
    refs = itertools.count(1)
    async with websockets.connect(URL) as ws:
        for i, sym in enumerate(SYMBOLS, start=1):
            await ws.send(json.dumps([str(i), str(next(refs)), f"stock:{sym}", "phx_join", {"last_seq": 0}]))

        async def heartbeat():
            while True:
                await asyncio.sleep(30)
                await ws.send(json.dumps([None, str(next(refs)), "phoenix", "heartbeat", {}]))

        asyncio.create_task(heartbeat())
        async for raw in ws:
            join_ref, ref, topic, event, payload = json.loads(raw)
            if event in ("snapshot", "price"):
                print(topic, payload["lastPrice"], payload["volume"])

asyncio.run(main())

체결 테이프

WebSocket은 현재가 스냅샷만 보내요. 개별 체결 목록이 필요하면 GET /v1/market/trades/{symbol}을 폴링하고 seq로 새 체결만 골라내세요. 내 주문 체결 여부도 REST(GET /v1/orders)로 확인해요.