# 실시간 시세 (WebSocket)

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

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

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

## 이벤트

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

`snapshot`과 `price`의 payload는 [현재가 API](/developers/market)(`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 패키지)

```bash
npm install phoenix
```

```ts
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)

```bash
pip install websockets
```

```python
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`)로 확인해요.
