API가 안 주는 것이 설계를 정한다 — 외부 API 제약과 숨기지 않은 SPOF
API의 기능과 내가 구현한 범위
1편과 2편에서는 규칙 승인과 주문 관문을 다뤘다. 그 경로가 실제로 동작하려면 증권사 API에서 시세와 보유 내역을 읽고 주문을 전달해야 한다. 나는 토스증권 오픈 API를 게이트웨이 뒤에 두고, 진입 엔진과 손절 감시기가 필요한 기능을 조합하도록 만들었다.
먼저 원문의 전제를 정정한다. 2026-09-22 검수 기준 공식 문서에는 WebSocket과 조건주문 API가 있다. 원문은 두 기능이 없어서 로컬 폴링이 불가피하다고 설명했지만, 현재 기능 목록과 맞지 않는다. 최초 발행 당시의 공식 문서 스냅샷은 확보하지 못했으므로 기능이 언제 추가됐는지도 단정하지 않는다. 아래에서는 확인 가능한 프로젝트 구현과 현재 API의 계약을 구분한다. 토스증권 공식 API 개요
어댑터가 맡은 응답 변환
이 프로젝트에서 게이트웨이는 인증과 HTTP 호출을 모으고, 외부 응답을 소비자가 사용하는 형태로 바꾼다. 주요 연결은 다음과 같다.
| 게이트웨이 기능 | 반환하거나 변환하는 정보 | 프로젝트에서 쓰는 곳 |
|---|---|---|
| 토큰 발급 | OAuth 2.0 액세스 토큰 | API 인증 |
| 시세 조회 | 종목별 가격 문자열 | 진입 가격과 손절 조건 평가 |
| 캔들 조회 | 시각과 시가·고가·저가·종가·거래량 | 이동평균·상대강도지수(RSI) 계산 |
| 시장 캘린더 조회 | 개장 여부와 정규장 시작·종료 시각 | 진입 시간창과 세션 실행 범위 |
| 보유 조회 | 종목·수량·평균 매수가 | 손절 평가와 매도 수량 |
| 매수 가능 금액 조회 | 현금 기반 매수 가능액 | 메서드는 있으나 확인한 코어·세션에서는 호출하지 않음 |
| 주문 생성·정정·취소 | 주문 요청과 응답 | 진입·손절은 생성 경로 사용 |
| 주문 조회 | 주문 키에 대응하는 기록 | 타임아웃 복구와 정합성 확인 |
캔들 변환이 한 예다. 외부 응답은 result.candles 안에 있고 종가는 closePrice다. 계산기는 이를 close로 받는다. 아래 코드는 실제 변환에서 시각과 종가만 추린 재구성 예제다. HTTP 호출과 다른 가격 필드는 생략했다.
def candle_closes(payload):
result = payload.get("result") or {}
rows = result.get("candles") or []
return [
{"timestamp": row.get("timestamp"), "close": row.get("closePrice")}
for row in rows
]
이 변환은 가격 문자열을 유지하고 외부 필드명을 한곳에 모은다. 다만 필드명을 바꾸는 것만으로 계산기의 입력 계약이 완성되지는 않는다. 현재 공식 스펙은 캔들을 최신순으로 반환한다. 확인한 어댑터는 그 순서를 유지하지만 계산기는 배열 뒤쪽을 최근 구간으로 사용한다. 공식 캔들 응답 명세
가상 종가가 최신순으로 30, 20, 10일 때 최근 두 봉의 평균은 25다. 현재 코드에 그대로 넣으면 뒤의 두 값을 골라 15를 계산한다. 상대강도지수도 시간 방향의 영향을 받는다. 이 불일치는 별도 재현으로 확인했으며, 이번 글 검수에서 거래 프로그램을 수정하지는 않았다. 어댑터의 테스트에는 필드명뿐 아니라 시간순서와 누락 값 처리까지 포함해야 한다.
진입과 손절의 평가 범위를 나눈다
진입 엔진은 캘린더의 개장 시각부터 한 시간 동안 활성 규칙을 평가한다. 정상 개장이 09:00이면 10:00 전까지이고, 지연 개장이라면 시작 시각도 달라진다. 조건을 충족하면 현재가를 기준으로 지정가 주문 의도를 만들어 케이지에 제출한다. 현재가를 지정가로 썼다고 즉시 체결되는 것은 아니다.
손절 감시기는 진입 시간창을 적용하지 않는다. 세션 루프가 실행되는 동안 보유 종목의 시세를 조회하고 손절 조건을 평가한다. 확인한 구현은 익절을 자동 처리하지 않으며, 지원하는 정수 수량의 보유분 전체를 시장가 매도 의도로 제출한다. 두 경로 모두 실제 전송 여부는 케이지의 검사와 실행 모드에 달려 있다.
원문은 오래된 시세를 차단한다고도 설명했다. 하지만 확인한 get_prices는 응답의 시각을 버리고 가격만 반환하며, 가격 신호 제공자에도 경과 시간 검사가 없다. 가격 누락이나 조회 실패를 처리하는 것과 오래된 가격을 식별하는 것은 다른 검사다. 현재 구현에 신선도 검사가 있다고 설명할 근거는 없다.
미지원 기능과 미구현 기능을 구분한다
현재 공식 API는 WebSocket으로 체결·호가·주문 이벤트를 제공한다. 확인한 프로젝트의 주문 코어는 REST 폴링을 사용한다. 따라서 폴링은 이 구현의 방식이며, API에 실시간 채널이 없다는 증거가 아니다. 요청 한도에 걸리면 게이트웨이는 제한된 횟수로 재시도하고, 소진되면 예외를 전달한다. 상위 루프는 다음 주기에 다시 조회할 수 있으므로 이를 시스템 전체의 영구 중지로 표현해서도 안 된다.
조건주문 역시 별도 API로 제공된다. 국내 주식은 KRX 정규장에서 발동하며, 주문 유형마다 허용되는 호가 유형과 조건이 다르다. 따라서 LIMIT·MARKET만 사용한 프로젝트의 주문 함수를 보고 서버 측 조건 감시 기능도 없다고 판단할 수는 없다. 도입하려면 발동 세션, 수량, 만료와 상태 조회 계약을 함께 검토해야 한다. 공식 조건주문 명세
샌드박스와 뉴스 API도 원문에서는 미제공으로 단정했다. 이번에 확인한 공식 문서만으로는 두 기능의 제공 여부를 확정하지 못했다. 분명한 것은 이 프로젝트가 가짜 전송 객체와 임시 데이터베이스로 주문 경로를 시험할 수 있다는 점이다. 공식 모의 주문 환경을 쓰지 않는다고 테스트가 소액 실거래로만 가능한 것은 아니다. SHADOW·DRY도 실제 체결을 재현하는 증권사 모의투자 환경이 아니며, 케이지에서는 둘 다 검사 결과를 반환하고 주문 전송을 생략한다.
로컬 손절 감시의 단일 장애점
이 구현의 자동 손절은 로컬 프로세스에 의존한다. 노트북이 꺼지거나 프로세스가 종료되면 조건 평가와 주문 제출이 함께 멈춘다. 전원·네트워크·프로세스 중 한 곳의 장애가 감시 경로를 끊을 수 있다는 의미에서 단일 장애점(SPOF, Single Point of Failure)이 남아 있다.
시장가를 선택한 목적은 지정가 제한으로 주문이 미체결되는 상황을 줄이려는 것이다. 그렇다고 원하는 가격이나 즉시 체결이 보장되는 것은 아니다. 유동성이 부족하거나 거래가 정지되면 기대한 대로 실행되지 않을 수 있다. 스톱주문도 조건 충족 후 주문을 내는 방식이며 손실 한도를 보장하지 않는다. 감시 주체를 언제나 거래소로 단정할 수도 없고, 증권사와 주문별 계약을 확인해야 한다. SEC 주문 유형 설명
구현에는 완화 장치가 있다. caffeinate로 세션 중 절전을 억제하고, 시세가 누락되거나 지원하지 않는 수량이라 손절 주문을 만들지 못하면 경고를 남긴다. 하지만 프로세스가 죽으면 그 프로세스의 로그도 멈춘다. 또 세션 캘린더 조회 실패를 장이 닫힌 것으로 처리해 감시 루프가 끝나는 경로가 있다. 새 진입을 막는 처리가 기존 보유분의 감시까지 멈추게 할 수 있다는 뜻이다.
운영 호스트 변경, 프로세스 외부의 생존 감시, 서버 측 조건주문 연동은 각각 검토할 수 있는 보완책이다. 어느 하나만으로 모든 장애가 사라지지는 않는다. 여러 실행기를 두려면 중복 주문 방지도 함께 설계해야 한다. 이번 검수는 이 대안을 구현하거나 실거래 안전성을 입증한 작업은 아니다.
검증 결과와 학습
검수에서는 필요한 소스를 임시 폴더로 복사해 외부 통신을 막고 관련 테스트 129개를 실행했다. 응답 변환, 진입 시간창, 손절 조건, 모드별 주문 차단, 세션과 절전 억제 호출을 가짜 객체로 확인했으며 모두 통과했다. 추가로 본문 예제와 캔들 정렬 불일치, 시세 시각 누락, 캘린더 실패 시 감시 종료, 미감시 경고를 재현했다. 실제 증권사 접수·체결이나 운영체제의 절전 동작을 시험한 결과는 아니다.
이 작업에서 얻은 기준은 세 가지다. API가 제공하는 기능, 내가 연결한 기능, 테스트로 입증한 동작을 나누어 적어야 한다. 응답 변환에는 필드명뿐 아니라 데이터의 시간순서와 유효성도 포함된다. 로컬 감시에 의존한다면 감시기가 멈췄다는 사실을 누가 알아낼지도 설계해야 한다.
외부 API의 제약은 설계에 영향을 주지만, 구현하지 않은 기능까지 API의 부재로 설명할 수는 없다. API 계약을 확인한 날짜와 구현 범위를 함께 남겨야 나중에도 설계 판단의 근거를 다시 검토할 수 있다.