farmings-core 의 단일 WebSocket 엔드포인트 메시지 카탈로그.
GET /ws?token={jwt} → WebSocket upgradetoken 의 Supabase JWT. 유효하지 않으면 연결 종료.{ type, ref?, payload? }. ref 는 클라이언트가 요청·응답 매칭용으로 임의 지정.subscribe.* / chart.* / price.* / candle.*),
종목(ticker.*), 농장(farm.*), 대결(contest.*), 랭킹(rank.*).KOSPI 종합지수: 별도 message 없이 ticker_code='KOSPI' 로 같은 메시지 사용
(subscribe.price / subscribe.candle / chart.query). 단 ticker.* 응답에는
포함되지 않음 (종목 마스터 미등록). 분 단위 수집이라 1m candle 의 OHLC 가 모두
같은 값(분 정각 close).
운영 환경
로컬 개발
단일 WebSocket 채널.
클라이언트 → 서버 요청
Available only on servers:
Accepts one of the following messages:
현재가 구독 — 즉시 스냅샷 + 실시간 push 활성
성공 응답: price.snapshot (즉시, 전 종목 현재가).
이후 가격 변경 시 price.update broadcast 활성.
{
"type": "subscribe.price",
"ref": "string"
}
{
"type": "unsubscribe.price",
"ref": "string"
}
종목별 1분봉 실시간 구독
성공 응답: candle.snapshot (즉시, 현재 진행 중 1분봉. 없으면 null payload).
이후 1분봉 변경 시 candle.update broadcast 활성.
{
"type": "subscribe.candle",
"ref": "string",
"payload": {
"tickerCode": "string"
}
}
{
"type": "unsubscribe.candle",
"ref": "string",
"payload": {
"tickerCode": "string"
}
}
단위별 캔들 차트 조회 (1m / 1h / 1d / 1w / 1mo, 최근 limit개)
성공 응답: chart.data (캔들 배열, bucket 오름차순).
unit 미지정 시 1m (기본). tickerCode='KOSPI' 로 KOSPI 종합지수도 조회 가능.
{
"type": "chart.query",
"ref": "string",
"payload": {
"tickerCode": "string",
"unit": "1m",
"from": "string",
"limit": 100
}
}
{
"type": "ticker.list",
"ref": "string"
}
{
"type": "ticker.search",
"ref": "string",
"payload": {
"query": "string",
"limit": 50
}
}
{
"type": "ticker.ranking",
"ref": "string",
"payload": {
"by": "trade_amount",
"limit": 100
}
}
종목 상세 (현재가 + 펀더멘털). 호출 시 최근 검색어에 자동 기록.
성공 응답: ticker.detail. 알 수 없는 종목이면 error (code=UNKNOWN_TICKER).
{
"type": "ticker.detail",
"ref": "string",
"payload": {
"ticker_code": "string"
}
}
{
"type": "ticker.recent_searches",
"ref": "string",
"payload": {
"limit": 5
}
}
{
"type": "farm.info",
"ref": "string"
}
{
"type": "farm.portfolio",
"ref": "string"
}
기간별 수익률 — `[start_date 자정, end_date+1 자정)` 윈도우, end_date 매매 포함
성공 응답: farm.portfolio.period.
거절: error (INVALID_PAYLOAD — start > end / end > today / 잘못된 날짜).
종목별 cash flow 식: 분자 = end_value - start_value - 순매수, 분모 = start_value > 0 ? start_value : 첫 매수금.
농장 전체 = 종목별 합산. trades + stock_candles_1d 영구 보존이라 retention 무관.
{
"type": "farm.portfolio.period",
"ref": "string",
"payload": {
"start_date": "2019-08-24",
"end_date": "2019-08-24"
}
}
1회 무료 청산. cash_locked > 0 또는 reset_used = true 면 거절.
성공 응답: farm.reset.ok. 거절: error (RESET_ALREADY_USED / RESET_HAS_LOCKED_CASH).
{
"type": "farm.reset",
"ref": "string"
}
농장 매수 (정규 거래 시간 09:00–15:30 KST 한정, 시장가)
성공 응답: farm.traded. 거절: error (INSUFFICIENT_CASH / OUT_OF_TRADING_HOURS / INVALID_QUANTITY 등).
수수료 0.1% (amount × 10 / 10000, 정수 floor). cash 에서 amount + fee 차감,
수수료는 어디로도 입금되지 않고 소각. 평단가 식은 수수료 미포함 amount 기준 (체결가 × 수량).
{
"type": "farm.buy",
"ref": "string",
"payload": {
"ticker_code": "string",
"quantity": 1,
"order_type": "MARKET"
}
}
농장 매도 (정규 거래 시간 09:00–15:30 KST 한정, 시장가)
성공 응답: farm.traded (매도 시 payload.realized_pnl 채워짐). 거절: error (INSUFFICIENT_QUANTITY / OUT_OF_TRADING_HOURS 등).
수수료 0.2% (amount × 20 / 10000, 정수 floor). cash 에 amount - fee 가산, 수수료는 소각.
realized_pnl = (체결가 - 평단가) × 수량 - fee (수수료 차감 후 net PnL).
{
"type": "farm.buy",
"ref": "string",
"payload": {
"ticker_code": "string",
"quantity": 1,
"order_type": "MARKET"
}
}
RANDOM 자동 매칭 — 같은 duration_type 의 WAITING 방에 join, 없으면 신규 생성
성공 응답: contest.match.ok (대결 메타). 거절: error (INSUFFICIENT_CASH 등).
{
"type": "contest.match",
"ref": "string",
"payload": {
"duration_type": "ONE_DAY"
}
}
친구 초대 방 생성 — 8자리 invite_code 발급, 본인 자동 join
성공 응답: contest.create_invite.ok (대결 메타, invite_code 포함). 거절: error (INSUFFICIENT_CASH / INVITE_CODE_GENERATION_FAILED / INVALID_PAYLOAD 등).
{
"type": "contest.create_invite",
"ref": "string",
"payload": {
"duration_type": "ONE_DAY",
"max_participants": 2,
"title": "string"
}
}
성공 응답: contest.join_invite.ok. 거절: error (INVITE_CODE_NOT_FOUND / CONTEST_FULL / ALREADY_PARTICIPATING / INSUFFICIENT_CASH).
{
"type": "contest.join_invite",
"ref": "string",
"payload": {
"invite_code": "stringst"
}
}
매칭 대기 중 취소 (WAITING 한정, 시작된 대결은 거절)
성공 응답: contest.cancel.ok (CANCELLED 또는 활성 인원 0이면 contest 자체 cancel). 거절: error (CONTEST_NOT_FOUND / CANNOT_CANCEL_AFTER_START / NOT_PARTICIPATING).
{
"type": "contest.cancel",
"ref": "string",
"payload": {
"contest_id": 0
}
}
{
"type": "contest.list_waiting",
"ref": "string",
"payload": {
"duration_type": "ONE_DAY"
}
}
{
"type": "contest.my_active",
"ref": "string"
}
{
"type": "contest.history",
"ref": "string",
"payload": {
"limit": 50
}
}
대결 격리 자산 매수
성공 응답: contest.traded (payload.contest_id 포함). 거절: error (CONTEST_NOT_PLAYING / INSUFFICIENT_CASH / OUT_OF_TRADING_HOURS 등).
수수료 0.1% (농장 매수와 동일). 대결 격리 cash 에서 amount + fee 차감, 수수료는 소각.
{
"type": "contest.buy",
"ref": "string",
"payload": {
"contest_id": 0,
"ticker_code": "string",
"quantity": 1,
"order_type": "MARKET"
}
}
대결 격리 자산 매도
성공 응답: contest.traded (매도 시 payload.realized_pnl 채워짐). 거절: error (INSUFFICIENT_QUANTITY 등).
수수료 0.2% (농장 매도와 동일). 대결 격리 cash 에 amount - fee 가산, 수수료는 소각.
realized_pnl 도 net (수수료 차감 후).
{
"type": "contest.buy",
"ref": "string",
"payload": {
"contest_id": 0,
"ticker_code": "string",
"quantity": 1,
"order_type": "MARKET"
}
}
농장 주간(직전 7일) 수익률 TOP 10
성공 응답: rank.weekly (요청과 같은 type, payload.rankings 에 TOP 10).
{
"type": "rank.weekly",
"ref": "string",
"payload": {
"ticker_codes": [
"string"
],
"min_position": 0
}
}
농장 월간(직전 30일) 수익률 TOP 10
성공 응답: rank.monthly (요청과 같은 type, payload.rankings 에 TOP 10).
{
"type": "rank.weekly",
"ref": "string",
"payload": {
"ticker_codes": [
"string"
],
"min_position": 0
}
}
윈도우 내 종료된 대결 수익률 TOP 10 (한 사용자가 N개 대결 시 N회 등장)
성공 응답: rank.contest (payload.rankings 에 TOP 10).
{
"type": "rank.contest",
"ref": "string",
"payload": {
"window": "WEEKLY",
"ticker_codes": [
"string"
],
"min_position": 0
}
}
단일 WebSocket 채널.
서버 → 클라이언트 응답·broadcast
Available only on servers:
Accepts one of the following messages:
subscribe.price 응답 — 전 종목 현재가 (Redis HSET)
{
"type": "price.snapshot",
"ref": "string",
"payload": {
"property1": "string",
"property2": "string"
}
}
현재가 변경 broadcast (subscribe.price 활성 세션에 push)
{
"type": "price.update",
"payload": {
"property1": "string",
"property2": "string"
}
}
{
"type": "unsubscribe.price.ok",
"ref": "string"
}
subscribe.candle 응답 — 현재 진행 중 1분봉 (없으면 null)
{
"type": "candle.snapshot",
"ref": "string",
"payload": {}
}
1분봉 변경 broadcast (해당 종목 구독자)
{
"type": "candle.update",
"payload": {
"tickerCode": "string",
"bucket": "string",
"open": "string",
"high": "string",
"low": "string",
"close": "string",
"volume": 0
}
}
{
"type": "unsubscribe.candle.ok",
"ref": "string"
}
chart.query 응답 — 단위별 캔들 배열 (bucket 오름차순)
bucket 은 ISO-8601 string. unit=1m/1h 면 datetime, 1d/1w/1mo 면 date (YYYY-MM-DD).
{
"type": "chart.data",
"ref": "string",
"payload": [
{
"tickerCode": "string",
"bucket": "string",
"open": "string",
"high": "string",
"low": "string",
"close": "string",
"volume": 0
}
]
}
{
"type": "ticker.list",
"ref": "string",
"payload": [
{
"code": "string",
"name": "string"
}
]
}
{
"type": "ticker.search",
"ref": "string",
"payload": {
"results": [
{
"code": "string",
"name": "string",
"current_price": 0,
"change_rate": 0
}
]
}
}
{
"type": "ticker.ranking",
"ref": "string",
"payload": {
"by": "trade_amount",
"results": [
{
"code": "string",
"name": "string",
"current_price": 0,
"change_rate": 0,
"volume": 0,
"trade_amount": 0
}
]
}
}
{
"type": "ticker.detail",
"ref": "string",
"payload": {
"code": "string",
"name": "string",
"current_price": 0,
"previous_close": 0,
"change_rate": 0,
"open": 0,
"high": 0,
"low": 0,
"volume": 0,
"trade_amount": 0,
"market_cap": 0,
"per": 0,
"dividend_yield": 0,
"fundamentals_updated_at": "2019-08-24T14:15:22Z"
}
}
{
"type": "ticker.recent_searches",
"ref": "string",
"payload": {
"results": [
{
"code": "string",
"name": "string"
}
]
}
}
{
"type": "farm.info",
"ref": "string",
"payload": {
"name": "string",
"cash_available": 0,
"cash_locked": 0,
"onboarded_at": "2019-08-24T14:15:22Z",
"reset_used": true
}
}
{
"type": "farm.portfolio",
"ref": "string",
"payload": {
"total_value": 0,
"total_return_rate": 0,
"cash_available": 0,
"cash_locked": 0,
"stock_value": 0,
"positions": [
{
"ticker_code": "string",
"name": "string",
"quantity": 0,
"avg_price": 0,
"current_price": 0,
"value": 0,
"return_rate": 0
}
]
}
}
{
"type": "farm.portfolio.period",
"ref": "string",
"payload": {
"start_date": "2019-08-24",
"end_date": "2019-08-24",
"start_value": 0,
"end_value": 0,
"profit": 0,
"return_rate": 0,
"positions": [
{
"ticker_code": "string",
"name": "string",
"start_value": 0,
"end_value": 0,
"net_buy": 0,
"profit": 0,
"return_rate": 0
}
]
}
}
{
"type": "farm.reset.ok",
"ref": "string",
"payload": {
"cash_available": 0,
"cash_locked": 0,
"reset_used": true
}
}
농장 매매 체결 응답
{
"type": "farm.traded",
"ref": "string",
"payload": {
"side": "BUY",
"ticker_code": "string",
"quantity": 0,
"executed_price": 0,
"executed_amount": 0,
"realized_pnl": 0,
"order_type": "MARKET"
}
}
{
"type": "contest.match.ok",
"ref": "string",
"payload": {
"id": 0,
"host_user_id": "string",
"contest_type": "RANDOM",
"duration_type": "ONE_DAY",
"seed_amount": 0,
"max_participants": 0,
"status": "WAITING",
"invite_code": "string",
"title": "string",
"started_at": "2019-08-24T14:15:22Z",
"finish_at": "2019-08-24T14:15:22Z",
"last_join_at": "2019-08-24T14:15:22Z"
}
}
{
"type": "contest.create_invite.ok",
"ref": "string",
"payload": {
"id": 0,
"host_user_id": "string",
"contest_type": "RANDOM",
"duration_type": "ONE_DAY",
"seed_amount": 0,
"max_participants": 0,
"status": "WAITING",
"invite_code": "string",
"title": "string",
"started_at": "2019-08-24T14:15:22Z",
"finish_at": "2019-08-24T14:15:22Z",
"last_join_at": "2019-08-24T14:15:22Z"
}
}
{
"type": "contest.join_invite.ok",
"ref": "string",
"payload": {
"id": 0,
"host_user_id": "string",
"contest_type": "RANDOM",
"duration_type": "ONE_DAY",
"seed_amount": 0,
"max_participants": 0,
"status": "WAITING",
"invite_code": "string",
"title": "string",
"started_at": "2019-08-24T14:15:22Z",
"finish_at": "2019-08-24T14:15:22Z",
"last_join_at": "2019-08-24T14:15:22Z"
}
}
{
"type": "contest.cancel.ok",
"ref": "string",
"payload": {
"id": 0,
"host_user_id": "string",
"contest_type": "RANDOM",
"duration_type": "ONE_DAY",
"seed_amount": 0,
"max_participants": 0,
"status": "WAITING",
"invite_code": "string",
"title": "string",
"started_at": "2019-08-24T14:15:22Z",
"finish_at": "2019-08-24T14:15:22Z",
"last_join_at": "2019-08-24T14:15:22Z"
}
}
{
"type": "contest.list_waiting",
"ref": "string",
"payload": {
"contests": [
{
"id": 0,
"contest_type": "RANDOM",
"duration_type": "ONE_DAY",
"seed_amount": 0,
"max_participants": 0,
"current_participants": 0,
"host_user_id": "string",
"invite_code": "string",
"title": "string",
"last_join_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z"
}
]
}
}
{
"type": "contest.my_active",
"ref": "string",
"payload": {
"contests": [
{
"contest_id": 0,
"contest_type": "RANDOM",
"duration_type": "ONE_DAY",
"status": "WAITING",
"seed_amount": 0,
"max_participants": 0,
"current_participants": 0,
"title": "string",
"started_at": "2019-08-24T14:15:22Z",
"finish_at": "2019-08-24T14:15:22Z",
"last_join_at": "2019-08-24T14:15:22Z",
"joined_at": "2019-08-24T14:15:22Z"
}
]
}
}
{
"type": "contest.history",
"ref": "string",
"payload": {
"results": [
{
"contest_id": 0,
"place": 1,
"total_participants": 0,
"return_rate": 0,
"final_asset_value": 0,
"verdict": "WIN",
"started_at": "2019-08-24T14:15:22Z",
"finished_at": "2019-08-24T14:15:22Z"
}
]
}
}
대결 매매 체결 응답
{
"type": "contest.traded",
"ref": "string",
"payload": {
"side": "BUY",
"ticker_code": "string",
"quantity": 0,
"executed_price": 0,
"executed_amount": 0,
"realized_pnl": 0,
"order_type": "MARKET",
"contest_id": 0
}
}
농장 랭킹 응답 (메시지 type 은 요청과 동일)
{
"type": "rank.weekly",
"ref": "string",
"payload": {
"rankings": [
{
"user_id": "string",
"display_name": "string",
"profit": 0,
"return_rate": 0,
"end_value": 0
}
]
}
}
{
"type": "rank.contest",
"ref": "string",
"payload": {
"rankings": [
{
"contest_id": 0,
"user_id": "string",
"display_name": "string",
"duration_type": "ONE_DAY",
"max_participants": 0,
"started_at": "2019-08-24T14:15:22Z",
"finished_at": "2019-08-24T14:15:22Z",
"profit": 0,
"return_rate": 0
}
]
}
}
공통 에러. payload.code 로 분류, message 는 사람용.
{
"type": "error",
"ref": "string",
"payload": {
"code": "string",
"message": "string"
}
}
현재가 구독 — 즉시 스냅샷 + 실시간 push 활성
성공 응답: price.snapshot (즉시, 전 종목 현재가).
이후 가격 변경 시 price.update broadcast 활성.
종목별 1분봉 실시간 구독
성공 응답: candle.snapshot (즉시, 현재 진행 중 1분봉. 없으면 null payload).
이후 1분봉 변경 시 candle.update broadcast 활성.
성공 응답: unsubscribe.candle.ok. 해당 종목 broadcast 중지.
단위별 캔들 차트 조회 (1m / 1h / 1d / 1w / 1mo, 최근 limit개)
성공 응답: chart.data (캔들 배열, bucket 오름차순).
unit 미지정 시 1m (기본). tickerCode='KOSPI' 로 KOSPI 종합지수도 조회 가능.
모든 종목 코드+이름 (코드 오름차순)
성공 응답: ticker.list (요청과 같은 type, payload 만 응답 형식).
종목 상세 (현재가 + 펀더멘털). 호출 시 최근 검색어에 자동 기록.
성공 응답: ticker.detail. 알 수 없는 종목이면 error (code=UNKNOWN_TICKER).
사용자별 최근 검색 종목 (LRU)
성공 응답: ticker.recent_searches.
기간별 수익률 — `[start_date 자정, end_date+1 자정)` 윈도우, end_date 매매 포함
성공 응답: farm.portfolio.period.
거절: error (INVALID_PAYLOAD — start > end / end > today / 잘못된 날짜).
종목별 cash flow 식: 분자 = end_value - start_value - 순매수, 분모 = start_value > 0 ? start_value : 첫 매수금.
농장 전체 = 종목별 합산. trades + stock_candles_1d 영구 보존이라 retention 무관.
1회 무료 청산. cash_locked > 0 또는 reset_used = true 면 거절.
성공 응답: farm.reset.ok. 거절: error (RESET_ALREADY_USED / RESET_HAS_LOCKED_CASH).
농장 매수 (정규 거래 시간 09:00–15:30 KST 한정, 시장가)
성공 응답: farm.traded. 거절: error (INSUFFICIENT_CASH / OUT_OF_TRADING_HOURS / INVALID_QUANTITY 등).
수수료 0.1% (amount × 10 / 10000, 정수 floor). cash 에서 amount + fee 차감,
수수료는 어디로도 입금되지 않고 소각. 평단가 식은 수수료 미포함 amount 기준 (체결가 × 수량).
농장 매도 (정규 거래 시간 09:00–15:30 KST 한정, 시장가)
성공 응답: farm.traded (매도 시 payload.realized_pnl 채워짐). 거절: error (INSUFFICIENT_QUANTITY / OUT_OF_TRADING_HOURS 등).
수수료 0.2% (amount × 20 / 10000, 정수 floor). cash 에 amount - fee 가산, 수수료는 소각.
realized_pnl = (체결가 - 평단가) × 수량 - fee (수수료 차감 후 net PnL).
RANDOM 자동 매칭 — 같은 duration_type 의 WAITING 방에 join, 없으면 신규 생성
성공 응답: contest.match.ok (대결 메타). 거절: error (INSUFFICIENT_CASH 등).
친구 초대 방 생성 — 8자리 invite_code 발급, 본인 자동 join
성공 응답: contest.create_invite.ok (대결 메타, invite_code 포함). 거절: error (INSUFFICIENT_CASH / INVITE_CODE_GENERATION_FAILED / INVALID_PAYLOAD 등).
성공 응답: contest.join_invite.ok. 거절: error (INVITE_CODE_NOT_FOUND / CONTEST_FULL / ALREADY_PARTICIPATING / INSUFFICIENT_CASH).
매칭 대기 중 취소 (WAITING 한정, 시작된 대결은 거절)
성공 응답: contest.cancel.ok (CANCELLED 또는 활성 인원 0이면 contest 자체 cancel). 거절: error (CONTEST_NOT_FOUND / CANNOT_CANCEL_AFTER_START / NOT_PARTICIPATING).
참여 가능한 WAITING contest 목록
성공 응답: contest.list_waiting.
내가 진행/대기 중인 대결 (PLAYING + WAITING)
성공 응답: contest.my_active.
종료된 내 대결 전적 (최신순)
성공 응답: contest.history (verdict 포함).
대결 격리 자산 매수
성공 응답: contest.traded (payload.contest_id 포함). 거절: error (CONTEST_NOT_PLAYING / INSUFFICIENT_CASH / OUT_OF_TRADING_HOURS 등).
수수료 0.1% (농장 매수와 동일). 대결 격리 cash 에서 amount + fee 차감, 수수료는 소각.
대결 격리 자산 매도
성공 응답: contest.traded (매도 시 payload.realized_pnl 채워짐). 거절: error (INSUFFICIENT_QUANTITY 등).
수수료 0.2% (농장 매도와 동일). 대결 격리 cash 에 amount - fee 가산, 수수료는 소각.
realized_pnl 도 net (수수료 차감 후).
농장 주간(직전 7일) 수익률 TOP 10
성공 응답: rank.weekly (요청과 같은 type, payload.rankings 에 TOP 10).
농장 월간(직전 30일) 수익률 TOP 10
성공 응답: rank.monthly (요청과 같은 type, payload.rankings 에 TOP 10).
윈도우 내 종료된 대결 수익률 TOP 10 (한 사용자가 N개 대결 시 N회 등장)
성공 응답: rank.contest (payload.rankings 에 TOP 10).
subscribe.price 응답 — 전 종목 현재가 (Redis HSET)
현재가 변경 broadcast (subscribe.price 활성 세션에 push)
subscribe.candle 응답 — 현재 진행 중 1분봉 (없으면 null)
1분봉 변경 broadcast (해당 종목 구독자)
chart.query 응답 — 단위별 캔들 배열 (bucket 오름차순)
bucket 은 ISO-8601 string. unit=1m/1h 면 datetime, 1d/1w/1mo 면 date (YYYY-MM-DD).
농장 매매 체결 응답
대결 매매 체결 응답
농장 랭킹 응답 (메시지 type 은 요청과 동일)
공통 에러. payload.code 로 분류, message 는 사람용.
기간별 수익률 — 종목별 cash flow 결과.
대결 메타. contest.match.ok / contest.create_invite.ok / contest.join_invite.ok / contest.cancel.ok 공통.
손실 제외 + 양수 풀 percentile 5 컷 (모집단 < 15 면 컷 skip) + 보유금액 필터 후 수익률 desc TOP 10.