AlgoLab Blog · 토스증권 · API 실무 · 2026

토스증권 수급 API 5종 — 반영 시점이 다 다르다

토스증권 · Open API 2026-09-11 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 토스증권 Open API의 수급 데이터 5종은 파라미터가 count·until로 똑같아서 한 번에 받아 같은 날짜로 붙이기 쉬운데, 확정 시점이 전부 다릅니다. program-trades장중 갱신, short-selling·securities-lending당일 저녁, credit-tradesT+1 새벽이라 장중에 보이는 최신 기록이 전 영업일입니다. investor-trading은 당일 잠정치가 오지만 개인·기관 세부·기타법인·외국인 보유·CFD가 전부 null입니다. 집계 범위도 갈립니다 — 투자자별은 KRX+NXT 통합, 프로그램매매는 KRX 전용입니다.

이 글은 토스증권 오픈API 자동매매 가이드(발급·주문·호출한도)에서 수급 데이터 한 갈래만 떼어 낸 글입니다. 발급 절차·X-Tossinvest-Account 헤더·주문 필드는 그쪽에 있습니다. 아래 스펙은 2026-09-11에 공식 openapi.json(버전 1.2.15)과 overview.md 원문을 직접 받아 정리한 것입니다.

이 글에서 확인할 것

  1. 엔드포인트 5종과 공통 파라미터
  2. 반영 시점 대조표 — 이 글의 핵심
  3. updatedAt이 시점을 증명한다
  4. 집계 범위가 갈린다 — KRX+NXT vs KRX
  5. 신용대주 ≠ 대차거래 (이중 계상 사고)
  6. 페이지네이션 nextUntil의 inclusive 함정
  7. 봇에 붙일 때의 수집 스케줄

1. 엔드포인트 5종과 공통 파라미터

전부 Stock Info 그룹 · 국내(KR) 종목 전용이고, 해외 티커를 넣으면 400 unsupported-market으로 거절됩니다.

엔드포인트무엇
/api/v1/stocks/{symbol}/investor-trading투자자별 매매동향개인·외국인·기관·기타법인 수량
/api/v1/stocks/{symbol}/program-trades프로그램매매 동향차익(arbitrage)·비차익 수량
/api/v1/stocks/{symbol}/short-selling공매도 동향수량·금액·비중
/api/v1/stocks/{symbol}/credit-trades신용거래 동향융자(marginLoan)·대주(stockLoan) 잔고
/api/v1/stocks/{symbol}/securities-lending대차거래 동향체결·상환·잔고 수량/금액

다섯 개가 파라미터까지 똑같습니다count(최대 100) · until(YYYY-MM-DD, inclusive). 응답도 result.records 배열 + result.nextUntil 커서로 동일하고, 전부 STOCK_TRADING_TREND 한도 그룹(초당 10회)입니다. 이 균질함이 함정의 출발점입니다 — 코드가 하나로 통일되니 데이터도 통일돼 있다고 착각하게 됩니다.

import requests

BASE = "https://openapi.tossinvest.com"
H = {"Authorization": f"Bearer {access_token}"}          # 계좌 헤더 불필요

def trend(symbol, kind, count=100, until=None):
    p = {"count": count}
    if until: p["until"] = until
    r = requests.get(f"{BASE}/api/v1/stocks/{symbol}/{kind}",
                     params=p, headers=H, timeout=5)
    r.raise_for_status()
    return r.json()["result"]                             # {"records": [...], "nextUntil": "..."}

for kind in ["investor-trading", "program-trades", "short-selling",
             "credit-trades", "securities-lending"]:
    res = trend("005930", kind, count=5)
    print(kind, res["records"][0]["date"], res["records"][0]["updatedAt"])

위 반복문의 출력에서 date가 서로 다르게 나오는 것이 정상입니다. 같은 시각에 다섯 번 호출했는데 credit-trades만 하루 전 날짜를 돌려주는 식입니다. 이걸 모르고 date로 조인하면 가장 최근 행이 조용히 비어 버립니다 — 예외도 에러 코드도 없이 NaN만 남습니다.

2. 반영 시점 대조표 — 이 글의 핵심

엔드포인트당일 기록확정 시점장중 최신 date
program-trades제공장 종료 전까지 계속 갱신당일
investor-trading잠정치(일부 null)당일 저녁 + 외국인 보유 T+1 오전 재확정 + CFD T+1 새벽당일(불완전)
short-selling당일 저녁전 영업일
securities-lending당일 저녁전 영업일
credit-tradesT+1 새벽전 영업일

investor-trading의 잠정치는 "일부만 채워진다"가 아니라 어느 필드가 비는지가 명세에 못 박혀 있습니다. 공식 문구 그대로 — 잠정치가 제공되지 않는 individual(개인) · institution.breakdown(기관 세부) · otherCorporation(기타법인) · foreignerHolding(외국인 보유) · cfd(CFD 잔고)는 null입니다. 즉 장중에 확실히 쓸 수 있는 건 외국인·기관 합계 두 개뿐입니다.

// GET /api/v1/stocks/005930/investor-trading  — 공식 명세의 응답 예시(발췌)
{ "result": { "nextUntil": "2026-07-15", "records": [
  { "date": "2026-07-17", "updatedAt": "2026-07-17T14:35:08+09:00",
    "individual": null,                                  // ← 장중 잠정치엔 없음
    "foreigner":   { "buyVolume": "2105300", "sellVolume": "1985400", "netBuyVolume": "119900" },
    "institution": { "buyVolume": "910200",  "sellVolume": "1023400",
                     "netBuyVolume": "-113200", "breakdown": null },   // ← 세부도 없음
    "otherCorporation": null, "foreignerHolding": null, "cfd": null },
  { "date": "2026-07-16", "updatedAt": "2026-07-17T09:12:43+09:00",    // ← 다음날 오전 확정
    "individual": { "buyVolume": "8412300", "sellVolume": "8120450", "netBuyVolume": "291850" },
    "institution": { "breakdown": { "financialInvestment": {...}, "insurance": {...},
                     "trust": {...}, "privateEquityFund": {...}, "bank": {...},
                     "otherFinancialInstitution": {...}, "pensionFund": {...} } },
    "foreignerHolding": { "holdingQuantity": "3012456789",
                          "limitQuantity": "5919637922", "holdingRate": "0.5089" },
    "cfd": { "buyBalanceQuantity": "1250000", "sellBalanceQuantity": "890000" } } ] } }

"개인 순매수가 0이면 매수" 같은 규칙을 장중에 돌리면 큰일 납니다 — individual은 0이 아니라 null이고, 파이썬에서 None을 그대로 int()에 넣으면 TypeError, pandas로 읽으면 조용히 NaN이 됩니다. 잠정치와 확정치를 구분하는 플래그를 직접 만들어 붙이십시오.

3. updatedAt이 시점을 증명한다

다섯 엔드포인트 모두 레코드마다 updatedAt을 돌려줍니다. 공식 예시에 박혀 있는 시각만 모아 봐도 위 표가 그대로 재현됩니다.

엔드포인트레코드 dateupdatedAt읽는 법
short-selling2026-07-162026-07-16T17:25:43같은 날 저녁
credit-trades2026-07-162026-07-17T02:35:00다음날 새벽 2시대
investor-trading2026-07-172026-07-17T14:35:08장중 잠정
investor-trading2026-07-162026-07-17T09:12:43다음날 오전 확정

그래서 수집기는 "한 번 받은 날짜는 끝"이라고 가정하면 안 됩니다. 같은 date를 나중에 다시 받으면 값이 바뀌어 있을 수 있습니다. updatedAt을 함께 저장하고, 그 값이 달라졌을 때만 갱신하는 방식이 안전합니다 — 기록·로그 설계와 같은 원칙입니다.

# 잠정치·확정치를 구분해 저장 — updatedAt이 바뀌면 덮어쓴다
rows = {}                                   # (date) -> record
for rec in trend("005930", "investor-trading", count=100)["records"]:
    key, prev = rec["date"], rows.get(rec["date"])
    rec["_provisional"] = rec["individual"] is None      # ← 잠정 플래그
    if prev is None or rec["updatedAt"] > prev["updatedAt"]:
        rows[key] = rec

4. 집계 범위가 갈린다 — KRX+NXT vs KRX

같은 종목·같은 날짜인데 분모가 다릅니다. 명세에 명시된 차이만 모으면 이렇습니다.

항목기준주의
investor-trading 거래량KRX + NXT 통합두 데이터로 비율을 계산해 비교하면 어긋난다
program-trades 거래량KRX만 (NXT 미포함)
공매도 비중 분모장전·장후·시간외단일가 포함 누적정규장만으로 재계산하면 값이 다르다
foreigner (종목 단위)등록외국인이름이 같아도 다른 숫자
시장지표 투자자별 매매대금등록 + 미등록 합계
수량 단위전부 주식 수(주) 정수investor-trading·program-trades금액 축이 없다

NXT 차이는 실전에서 티가 납니다. 대체거래소로 주문이 갈라지는 구조는 NXT·KRX 주문 라우팅에서 다뤘는데, 수급 데이터에서도 같은 균열이 그대로 나타납니다 — 프로그램매매 비중을 "프로그램 순매수 ÷ 투자자별 총거래량"으로 계산하면 분자와 분모의 시장이 다릅니다.

5. 신용대주 ≠ 대차거래

이름이 비슷해 가장 자주 합쳐지는 두 데이터입니다. 명세가 양쪽 모두에 "다른 데이터"라고 못 박아 두었습니다.

구분신용대주 stockLoan대차거래 securities-lending
주체개인이 증권사에서 차입기관 간 대여·차입
위치credit-trades 응답 안의 객체독립 엔드포인트
반영T+1 새벽당일 저녁
필드newQuantity·returnQuantity·balanceQuantity·balanceRate·tradingRate체결·상환·잔고 수량 + 잔고 금액

둘을 더하면 공매도 대기 물량을 이중으로 셉니다. 게다가 credit-trades는 융자·대주 중 한쪽 데이터만 있으면 없는 쪽 객체가 통째로 null입니다 — rec["stockLoan"]["balanceQuantity"]처럼 바로 파고들면 TypeError가 납니다. 수급 신호를 안전장치 조건으로 쓸 계획이라면 이 null 처리를 먼저 넣으십시오.

6. nextUntil의 inclusive 함정

untilinclusive입니다 — "이 날짜까지"이지 "이 날짜 전"이 아닙니다. 그런데 다음 페이지 커서인 nextUntil을 그대로 넘기라고 되어 있으니, 커서 날짜의 레코드가 두 페이지에 걸쳐 나옵니다. (캔들 조회의 before·nextBefore도 같은 구조입니다.)

def trend_all(symbol, kind, pages=5):
    rows, until, seen = [], None, set()
    for _ in range(pages):
        res = trend(symbol, kind, count=100, until=until)
        for rec in res["records"]:
            if rec["date"] in seen:      # ← inclusive 커서가 만드는 경계 중복 제거
                continue
            seen.add(rec["date"]); rows.append(rec)
        until = res.get("nextUntil")
        if not until:                    # null = 마지막 페이지
            break
        time.sleep(0.1)                  # STOCK_TRADING_TREND = 초당 10회
    return rows

time.sleep(0.1)초당 10회 한도에 맞춘 값입니다. 한도는 사전 공지 없이 조정될 수 있으므로 상수로 박지 말고 응답 헤더 X-RateLimit-Limit·X-RateLimit-Remaining을 읽어 조절하십시오. 429가 오면 Retry-After 초만큼 쉬었다가 지수 백오프로 재시도합니다 — 설계 원칙은 호출 제한 설계에 정리해 두었고, 에러 봉투 구조는 토스증권 오픈API 에러코드에 있습니다.

7. 봇에 붙일 때의 수집 스케줄

반영 시점이 다르니 한 번에 다 받는 스케줄은 성립하지 않습니다. 최소 세 번으로 나뉩니다.

시점받을 것용도
장중 (필요 시)program-trades · investor-trading(잠정)장중 참고 — 확정 아님
장 마감 후 저녁short-selling · securities-lending · investor-trading당일 확정치
다음 영업일 오전credit-trades · investor-trading 재수집T+1 확정 · 외국인 보유 재확정

스케줄러를 짤 때 휴장일 처리가 같이 필요합니다. 토스증권은 /api/v1/market-calendar/KR로 영업일과 세션별 운영시간을 주고, 거래소 공통 처리는 휴장일 확인과 봇 스케줄링에 정리해 두었습니다.

다른 증권사와 함께 쓸 계획이라면 기준을 먼저 맞추십시오. 한국투자증권 KIS API의 투자자별 순매수 조회는 금액 축까지 주고 필드명·집계 기준도 다릅니다. 같은 "외국인 순매수"라도 증권사마다 정의가 달라, 두 소스를 합치기 전에 같은 날짜·같은 종목으로 값을 대조해 차이를 먼저 재 보는 편이 안전합니다. 증권사별로 무엇을 열어 주는지는 국내 증권사 API 비교에 정리돼 있습니다.

한계 — 이 글이 확정하지 않은 것

자주 묻는 질문

토스증권 Open API로 받을 수 있는 수급 데이터는 무엇인가요?

국내 종목 전용으로 다섯 가지입니다 — investor-trading(투자자별) · program-trades(프로그램매매) · short-selling(공매도) · credit-trades(신용거래) · securities-lending(대차거래). 전부 count(최대 100)·until만 받고, 커서는 nextUntil, 한도 그룹은 STOCK_TRADING_TREND(초당 10회)입니다.

당일 데이터를 장중에 쓸 수 있나요?

엔드포인트마다 다릅니다. program-trades는 당일 기록이 오고 장 종료 전까지 갱신됩니다. investor-trading은 잠정치가 오지만 개인·기관 세부·기타법인·외국인 보유·CFD가 null입니다. short-selling·securities-lending은 당일 저녁, credit-tradesT+1 새벽이라 장중 최신 기록이 전 영업일입니다.

신용대주와 대차거래는 무엇이 다른가요?

주체가 다릅니다. 신용대주(credit-trades 안의 stockLoan)는 개인이 증권사에서 빌려 매도하는 신용거래이고, 대차거래(securities-lending)는 기관 간 대여·차입입니다. 합치면 공매도 대기 물량을 이중으로 세게 됩니다. 반영 시점도 T+1 새벽 vs 당일 저녁으로 다릅니다.

거래량 집계 범위는 어떻게 되나요?

investor-tradingKRX+NXT 통합, program-tradesKRX만입니다. 외국인 기준도 갈려서, 종목 단위 foreigner등록외국인이고 시장지표 쪽 투자자별 매매대금은 등록+미등록 합계입니다. 섞어 쓰면 안 됩니다.

이 스펙을 그대로 믿어도 되나요?

경로·파라미터·필드명·적시성 문구는 2026-09-11에 openapi.tossinvest.com의 공식 openapi.json(버전 1.2.15)과 overview.md 원문을 직접 받아 확인했습니다. 다만 증권사 API 명세와 한도는 사전 공지 없이 바뀝니다 — 작업 시점에 같은 URL에서 다시 받아 대조하고, 현재 허용 한도는 응답 헤더 X-RateLimit-Limit으로 확인하십시오.

수급 데이터를 쓰는 봇이 필요하다면

수집 스케줄·잠정치 처리·재수집까지 설계해 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기