본문으로 건너뛰기이 문서는 이 데이터를 받아 쓰는 사람을 위한 명세다.
이 문서에 측정값을 적지 않는다. 행 수와 기간과 종목 수는 화면이 1절에 그때 읽어 채우고, 그 밖의 수를 직접 세는 명령은 8절에 있다. 왜 적지 않는지는 docs/structure.md 의 부패할 것을 만들지 않는다 에 있다.
1. 한눈에
1. 한눈에| 원천 | Yahoo Finance (yfinance 파이썬 패키지) |
|---|
| 인증 | 없다 |
|---|
| 자산군 | 주식, ETF, 지수, 국채 수익률, 환율, 원자재 선물 |
|---|
| 관측 단위 | 일봉 하나 (시가, 고가, 저가, 종가, 수정종가, 거래량) |
|---|
| 갱신 | 매일 07:30 KST 시작, 08:15 마감 |
|---|
| 보관처 | PostgreSQL 데이터베이스 a42_yfinance (서버 시간대 Asia/Seoul) |
|---|
| 주 테이블 | yf_daily_bars |
|---|
| 딸린 테이블 | yf_symbols, yf_corporate_actions, yf_restatements |
|---|
| 실행 기록 | sync_runs, sync_run_symbols |
|---|
| 화면 | /yfinance |
|---|
표에 있어도 받지 않는 종목이 있다. 무엇이 왜 그런지는 7절이다.
데이터셋 목록을 가져오지 못했습니다
데이터 서버에 닿지 못했습니다.
데이터 서버가 꺼져 있을 수 있습니다. 터미널에서 uv run a42-data-view up 으로 켠 뒤 새로고침하세요.
2. 무엇이 들어 있나
대상을 어떻게 고르나
정본은 DB 의 yf_symbols 다. 그 표를 채우는 길이 둘이고 source 칸이 그것을 가른다.
대상을 어떻게 고르나source | 무엇 |
|---|
screener | 원천 스크리너에 "미국 정규 거래소의 주식" 을 물어 받은 목록 |
declared | 손으로 적은 목록 (apps/a42_collector/config/yfinance_symbols.toml) |
스크리너 질의는 하나다. 지역이 미국이고 거래소가 NMS(나스닥) 또는 NYQ(뉴욕)인 주식이다. OTC/핑크시트는 넣지 않았다 (2026-08-30 사람 결정).
지수, 국채 수익률, 환율, 원자재 선물은 스크리너로 열거되지 않는다. 그래서 손 선언이 남아 있다.
손 선언이 스크리너를 이긴다. 같은 심볼이 양쪽에 있으면 손 선언이 남는다. 그렇지 않으면 스크리너 갱신이 그 심볼을 조용히 수집에서 빼낸다.
출처별 종목 수를 세는 질의는 8절에 있다.
자산군
asset_class 는 장식이 아니다. 조정가 계산이 걸리는지를 가르는 칸이다. equity, etf, fund 에만 걸린다. 지수와 수익률과 환율과 선물에는 배당과 분할 개념이 없다.
주식이 종목 수와 행수의 대부분이다. 나머지 다섯을 합쳐도 주식의 일부에 못 미친다. 자산군별 종목 수와 행수와 기간을 세는 질의는 8절에 있다.
통화와 거래소
close 칸에 단위가 다른 값이 함께 들어 있다. 달러 가격, 원 환율, 지수 포인트, 수익률 퍼센트가 같은 칸이다. currency 가 그것을 가른다.
통화와 거래소 (currency, 뜻 칸이 있는 표)
USD | 값이 미국 달러다 |
| (없음) | 값이 통화 금액이 아니다. 지수 포인트 또는 수익률 퍼센트 |
KRW | 값이 원이다 (KRW=X) |
JPY | 값이 엔이다 (JPY=X) |
CNY | 값이 위안이다 (CNY=X) |
currency 가 비어 있으면 그 값을 통화 금액으로 읽지 마라.
통화와 거래소 (exchange, 무엇 칸이 있는 표)exchange | 무엇 |
|---|
NYQ | 뉴욕 |
NMS | 나스닥 |
US | 손 선언 ETF |
INDEX | 지수, 국채 수익률 |
FX | 환율 |
COMEX, NYMEX, CME | 원자재 선물 |
US, INDEX, FX 는 손 선언에서 온 굵은 값이다. NMS 와 NYQ 는 원천이 준 값이다. 그 밖에 원천이 주는 값이 낱개로 섞여 있다. 통화별과 거래소별 종목 수를 세는 질의는 8절에 있다.
3. 어떻게 받아오나
3. 어떻게 받아오나| 원천 함수 | yfinance.download (일봉), yfinance.screen (대상 목록) |
|---|
| 요청 단위 | 심볼 40개 묶음 |
|---|
| 묶음 사이 대기 | 1초 |
|---|
| 간격 | 1d |
|---|
| 전량 재적재 요청 | period=max |
|---|
| 이어받기 요청 | start = 실행일 - 7일 |
|---|
| 전량 재적재 회전 | 30일에 한 바퀴. 하루 몫은 대상 수 / 30 을 올림한 값 |
|---|
| 회전 커서 | yf_symbols.last_full_refresh_at. 오래된 것이 먼저다 |
|---|
| 재시도 | 묶음마다 최대 3회 시도. 사이 대기 2초, 4초 |
|---|
| 목록 열거 | 한 페이지 250개, 페이지 사이 0.5초 대기 |
|---|
왜 회전하나
전 기간 재적재의 목적은 배당과 분할로 소급 재계산된 과거를 되받는 것이다. 그 사건은 한 종목에 한 해 몇 번이다. 유니버스 전체를 매일 다시 받는 것은 그 몇 번을 위해 매일 전량을 받는 것이고, 마감까지의 여유를 다 쓴다.
그래서 하루에 1/30 만 전 기간을 다시 받고 나머지는 최근 7일만 받는다. 30일 안에 반드시 한 바퀴가 돌아오므로 손상이 30일 넘게 남지 않는다.
재작성이 감지된 종목은 회전 순서를 기다리지 않는다. 7일 창 안에서 값이 바뀌었으면 창 밖도 바뀌었을 수 있다. 그 종목은 다음 실행에서 전 기간을 받고, 하루 몫을 소비하지 않는다. 그 결과가 7절의 "회전이 무너지는 날" 이다.
auto_adjust=False 로 받는 이유
원천은 기본값으로 조정가를 Close 칸에 넣어 준다. 그러면 원가격이 오지 않는다. False 를 주면 Close 는 원가격이고 조정가는 Adj Close 로 따로 온다. 둘 다 받아 둔다.
우리가 쓰는 조정가는 원천의 Adj Close 가 아니다. 6절을 본다.
배당과 분할을 같은 요청에서 받는다
actions=True 를 주면 같은 요청의 응답 컬럼이 Dividends, Stock Splits, Capital Gains 로 늘어난다. 추가 요청이 0회다. 종목마다 따로 묻지 않는다.
이벤트가 없는 날은 원천이 그 칸을 0 으로 채워 보낸다. 0 이 아닌 것만 yf_corporate_actions 에 남긴다.
빈 결과와 실패를 가른다
원천은 없는 심볼에 오류를 내지 않고 전부 빈 값인 응답을 준다. 속도 제한도 오류가 아니라 빈 응답으로 온다. 그 둘을 구분하지 않으면 차단당한 날에 멀쩡한 종목이 사라진 것으로 기록된다.
그래서 묶음 40개가 통째로 비면 원천에 한 번 더 물어본다. 이번에는 이유를 오류로 달라고 요청한다 (Ticker.history 의 raise_errors, 기간 5d).
빈 결과와 실패를 가른다| 되물어본 결과 | 어떻게 기록하나 |
|---|
| 속도 제한이라고 답한다 | 우리 쪽 실패. 백오프로 다시 하고, 안 되면 실패로 남긴다 |
| 다른 이유를 답한다 (상장폐지 등) | 빈 결과. 원천의 답이다 |
| 값이 정상으로 온다 | 묶음 요청 쪽의 일시 문제. 다시 한다 |
묶음의 일부만 비는 것은 정말 데이터가 없는 것으로 본다. 미국 상장 40개가 한꺼번에 다 비는 일은 정상 경로에 없다.
4. 어떻게 저장하나
TIMESTAMPTZ 칸은 절대 시각을 담고 조회할 때 세션 시간대로 보인다. 서버 시간대는 Asia/Seoul 이다.
아래 표의 널 은 스키마가 허용하는지다. 실제로 비어 있는 행이 있는지는 적지 않는다. 그것은 오늘 센 값이다.
yf_daily_bars - 일봉
yf_daily_bars - 일봉| 칸 | 뜻 | 타입 | 단위 | 시간대 | 널 |
|---|
trade_date | 거래일 | DATE | 날짜 | 없다. 그 상품 거래소의 현지 날짜다 | 아니다 |
symbol | 티커 | TEXT | | | 아니다 |
open | 시가 | NUMERIC(20,6) | yf_symbols.currency 가 정한다 | | 허용 |
high | 고가 | NUMERIC(20,6) | 같다 | | 허용 |
low | 저가 | NUMERIC(20,6) | 같다 | | 허용 |
close | 종가 | NUMERIC(20,6) | 같다 | | 허용 |
adj_close | 원천이 준 수정종가 | NUMERIC(20,6) | 같다 | | 허용 |
volume | 거래량 | BIGINT | 주 또는 계약 수 | | 허용 |
inserted_at | 이 행이 처음 적재된 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
updated_at | 이 값이 마지막으로 바뀐 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
- 키는
(trade_date, symbol) 이다. 같은 종목의 같은 날은 한 행뿐이다 yf_symbols 에 있어도 이 표에 없는 종목이 있다. 원천에 이력이 아예 없어 한 번도 값을 받지 못한 것들이고 대개 신주인수권이다 (7절)- 다시 받은 값이 저장된 값과 같으면 행을 쓰지 않는다. 그래서
updated_at 이 "마지막으로 바뀐 때" 라는 뜻을 유지한다. 매일 전 기간을 다시 받는데 이 걸음이 없으면 모든 행의 updated_at 이 매일 오늘로 밀린다 - 같다고 볼 기준은 가격의 상대오차 1e-5 이내다. 거래량은 정수라 그대로 비교한다
- 종가가 없는 날은 적재하지 않는다. 휴장일이거나 상장 전이다
adj_close 는 대조용이다. 우리가 쓰는 조정가가 아니다. 6절을 본다.
yf_symbols - 종목 목록과 상태
yf_symbols - 종목 목록과 상태| 칸 | 뜻 | 타입 | 단위 | 시간대 | 널 |
|---|
symbol | 티커. 키 | TEXT | | | 아니다 |
asset_class | 자산군 | TEXT | equity etf fund index yield fx futures | | 아니다 |
currency | 값의 통화 | TEXT | ISO 코드 | | 허용. 비어 있으면 통화 금액이 아니다 |
exchange | 거래소 | TEXT | | | 허용 |
region | 지역 | TEXT | | | 허용 |
symbol_group | 어느 묶음에서 왔나 | TEXT | 스크리너 질의 이름 또는 선언 그룹 | | 허용 |
source | 출처 | TEXT | screener 또는 declared | | 아니다 |
collect_enabled | 매일 받을 대상인가 | BOOLEAN | | | 아니다 |
first_seen_at | 처음 목록에 나타난 때. 덮지 않는다 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
last_seen_at | 마지막으로 목록에 있던 때 | TIMESTAMPTZ | | 절대 시각 | 허용 |
delisted_at | 목록에서 사라진 때 | TIMESTAMPTZ | | 절대 시각 | 허용 |
last_full_refresh_at | 전 기간을 마지막으로 다시 받은 때. 회전 커서 | TIMESTAMPTZ | | 절대 시각 | 허용 |
needs_full_refresh | 재작성이 감지되어 전 기간을 기다린다 | BOOLEAN | | | 아니다 |
empty_streak | 연속 빈 결과 일수 | INTEGER | 일 | | 아니다 |
last_empty_at | 마지막으로 빈 결과가 온 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
updated_at | 내용이 바뀐 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
- 상장폐지된 종목의 행도, 그 종목의 일봉도 지우지 않는다. 지우면 살아남은 종목만 보이고 백테스트가 생존편향을 그대로 안는다
- 매일 받는 대상은
collect_enabled 이고 delisted_at IS NULL 인 행이다 - 스크리너로 다시 나타난 종목은
delisted_at 이 지워진다. 손 선언은 그렇지 않다. 손 선언의 이 칸은 사람이 넣고 사람이 지운다
yf_corporate_actions - 조정 근거
원천이 준 값을 그대로 쌓는다. 조정가는 이 표와 일봉으로 우리가 계산한다.
yf_corporate_actions - 조정 근거| 칸 | 뜻 | 타입 | 단위 | 시간대 | 널 |
|---|
symbol | 티커 | TEXT | | | 아니다 |
ex_date | 배당락일 또는 분할일 | DATE | 날짜 | 없다. 거래소 현지 날짜 | 아니다 |
action_type | 종류 | TEXT | dividend split capital_gain | | 아니다 |
amount | 종류에 따라 뜻이 다르다 | NUMERIC(20,8) | 배당/자본이득은 주당 현금, 분할은 비율 (2.0 이면 1주가 2주) | | 아니다 |
first_seen_at | 이 이벤트를 처음 본 때. 덮지 않는다 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
updated_at | 금액이 정정된 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
- 키는
(symbol, ex_date, action_type) 이다. 날짜 하나에 한 줄이 아니다. 같은 날 배당과 자본이득을 함께 분배하는 ETF 가 있다 - 금액이 그대로면
updated_at 도 움직이지 않는다. 그래야 정정된 이벤트만 보인다 first_seen_at 을 덮지 않는 이유: 원천이 배당을 정정하거나 누락분을 채우면 금액이 바뀐다. 그때 "언제 처음 본 값인가" 가 남아 있어야 과거 시점을 재현할 수 있다
capital_gain 은 계산에 들어가 있지만 원천이 그 칸을 채우지 않는다. 컬럼은 오는데 값이 전부 0 이라 이 표에 행이 생기지 않는다. 종류별 건수를 세는 질의는 8절에 있다.
yf_restatements - 원천이 과거를 다시 쓴 사건
yf_restatements - 원천이 과거를 다시 쓴 사건| 칸 | 뜻 | 타입 | 단위 | 시간대 | 널 |
|---|
id | 순번. 키 | BIGSERIAL | | | 아니다 |
run_id | 어느 실행에서 발견했나 | BIGINT | sync_runs.id | | 아니다 |
symbol | 티커 | TEXT | | | 아니다 |
field | 다시 쓰인 칸 | TEXT | open high low close adj_close volume | | 아니다 |
rows_changed | 몇 행이 바뀌었나 | INTEGER | 행 | | 아니다 |
first_trade_date | 바뀐 구간의 시작 | DATE | 날짜 | 없다 | 아니다 |
last_trade_date | 바뀐 구간의 끝 | DATE | 날짜 | 없다 | 아니다 |
sample_old | 가장 최근 한 건의 옛 값 | TEXT | 문자열로 담는다 | | 허용 |
sample_new | 같은 건의 새 값 | TEXT | | | 허용 |
inserted_at | 기록한 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
- 키는
(run_id, symbol, field) 다. 바뀐 행마다 남기지 않는다. 배당 한 번에 그 종목의 전 기간이 재계산되므로, 행마다 남기면 한 사건에 수천 줄이 된다 - 표본은 두 값 한 쌍뿐이다. 전체 구간의 옛 값을 보관하는 표가 아니다
sync_runs - 실행 한 건
sync_runs - 실행 한 건 (칸, 뜻, 타입, 단위, 시간대, 널 칸이 있는 표)| 칸 | 뜻 | 타입 | 단위 | 시간대 | 널 |
|---|
id | 순번. 키 | BIGSERIAL | | | 아니다 |
trigger_type | 무엇이 실행시켰나 | TEXT | scheduled 또는 manual | | 아니다 |
command_name | 무슨 명령이었나 | TEXT | sync 또는 sync-symbol | | 아니다 |
status | 결과 | TEXT | 아래 표 | | 아니다 |
started_at | 시작 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
finished_at | 끝 | TIMESTAMPTZ | | 절대 시각 | 허용. 비어 있으면 끝나지 않았다 |
stop_reason | 왜 멈췄나 | TEXT | 아래 표 | | 허용 |
error_message | 오류 내용 | TEXT | | | 허용 |
inserted_at | 행을 만든 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
updated_at | 행을 마지막으로 고친 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
sync_runs - 실행 한 건 (status, 뜻 칸이 있는 표)status | 뜻 |
|---|
running | 아직 돌고 있다 |
success | 끝났다. 실패 종목이 경보 선 아래다 |
completed_with_errors | 끝났다. 실패 종목이 경보 선을 넘었다 |
sync_runs - 실행 한 건 (stop_reason, 뜻 칸이 있는 표)stop_reason | 뜻 |
|---|
complete | 대상을 다 돌았다 |
budget_exhausted | 시간 상한에 걸려 멈췄다. 실패가 아니다. 다음 실행이 이어받는다 |
수집기가 쓰는 값은 이 둘뿐이다. 실행 기록을 보면 killed 도 나오는데, 그것은 밖에서 죽은 실행을 사람이 닫아둔 것이다 (2026-08-30 에 사람이 손 실행을 그렇게 닫았다).
yf_daily_bars 는 종목 단위로 커밋된다. 실행이 중간에 죽어도 그때까지 받은 종목의 일봉은 남는다.
sync_run_symbols - 그 실행의 종목별 결과
sync_run_symbols - 그 실행의 종목별 결과| 칸 | 뜻 | 타입 | 단위 | 시간대 | 널 |
|---|
id | 순번. 키 | BIGSERIAL | | | 아니다 |
run_id | 어느 실행인가 | BIGINT | sync_runs.id | | 아니다 |
symbol | 티커 | TEXT | | | 아니다 |
status | success 또는 failed | TEXT | | | 아니다 |
rows_written | 실제로 쓴 행 수 | INTEGER | 행 | | 아니다 |
latest_trade_date | 받아온 것 중 가장 최근 거래일 | DATE | 날짜 | 없다 | 허용 |
error_message | 실패 이유 | TEXT | | | 허용 |
inserted_at | 기록한 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
updated_at | 고친 때 | TIMESTAMPTZ | | 절대 시각 | 아니다 |
- 키는
(run_id, symbol) 이다 rows_written 은 받은 행 수가 아니라 값이 달라서 실제로 쓴 행 수다. 전 기간을 다시 받아도 바뀐 것이 없으면 0 이다- 행이 없는 것이 실패가 아니다. 실행이 시간 상한에 걸려 멈추면 차례가 오지 않은 종목의 행이 아예 없다. 그 실행의
stop_reason 을 함께 봐야 구분된다
5. 언제 갱신되나
5. 언제 갱신되나| 스케쥴 | 매일 07:30 KST (30 7 * * *) |
|---|
| 마감 | 08:15 KST. 그때까지 안 끝나면 별도 확인이 돈다 |
|---|
| 배치 시간 상한 | 1,800초. 넘으면 스스로 멈추고 다음 실행이 이어받는다 |
|---|
| flow 시간 상한 | 7,200초. 마지막 안전장치다 |
|---|
| 재시도 | 수집 작업 1회. 120초 후 |
|---|
소요는 무엇에 붙나
그 실행이 전 기간을 다시 받는 종목 수에 붙는다. 이어받기만 하는 날은 짧고, 회전이 무너져 유니버스 거의 전체를 전 기간 다시 받는 날은 몇 배가 된다 (7절).
실행별 소요와 성공/실패 종목 수와 실제로 쓴 행 수를 보는 질의는 8절에 있다.
실패했을 때 무엇이 남나
실패했을 때 무엇이 남나| 상황 | 남는 것 |
|---|
| 종목 하나가 빈 결과 | 그 종목만 sync_run_symbols 에 failed. 나머지는 정상 적재 |
| 실패가 경보 선을 넘음 | 실행이 completed_with_errors. 종료 코드 1 이라 스케쥴러가 실패로 표시 |
| 시간 상한에 걸림 | 실행은 success, stop_reason 이 budget_exhausted. 실패가 아니다 |
| 실행이 밖에서 죽음 | status 가 running 에 finished_at 이 빈 채로 남는다. 그때까지 받은 일봉은 남아 있다 |
경보 선은 max(10건, 대상의 5%) 다.
하나만 실패해도 경보로 두지 않는 이유: 이 규모의 미국 유니버스에서는 상장폐지와 티커 변경과 거래정지가 상시로 섞인다. 매일 울리는 알림은 아무도 보지 않는다. 개별 종목의 실패는 그와 별개로 sync_run_symbols 에 전부 남는다.
미국 장이 쉰 날은 새 행이 없다. 그것도 실패가 아니다.
6. 값을 믿을 수 있는 근거
수정주가를 우리가 계산한다
원천의 Adj Close 를 그대로 쓰지 않는다. 조정 근거(배당, 분할, 자본이득)를 원천 그대로 yf_corporate_actions 에 보관하고, 저장된 원가격과 그 이벤트로 조정 시계열을 만든다.
수정주가를 우리가 계산한다배당락일 d 의 계수 | 1 - (그날 분배액 / 전 거래일 종가) |
|---|
어떤 날 t 의 조정가 | t 의 종가 x (t 보다 뒤에 있는 모든 계수의 곱) |
|---|
| 분배액 | 배당 + 자본이득. 자본이득을 빼면 맞지 않는다 |
|---|
| 분할 | 계수에 넣지 않는다. 원천의 Close 가 이미 분할을 소급 반영한 값이다 |
|---|
| 걸리는 자산군 | equity, etf, fund. 그 밖은 조정가가 종가와 같다 |
|---|
남의 계산 결과에만 의존하는 구조를 끝낸 것이 요점이다. 원천이 조정 규칙을 바꾸거나 이벤트를 정정하면 그 결과만 조용히 달라진다. 근거를 들고 있으면 무엇 때문에 달라졌는지 되짚을 수 있다.
이 구조가 푼 실제 문제가 하나 있다. 이어받기로 받은 데이터에서 adj_close 가 어긋난다. adj_close 는 배당이 날 때마다 과거 전체가 소급 재계산되는 값인데, 최근 며칠만 받으면 그 창 안의 행만 새 조정값으로 갱신되고 창 밖의 과거는 옛 값으로 남는다. 조정 시계열에 가짜 점프가 생긴다. 이어받기 창을 7일로 두는 지금도 그 위험은 그대로 있고, 우리가 계산하는 조정가는 창과 무관하게 전 구간이 한 규칙으로 나온다.
대조가 어긋남을 드러낸다
adj_close 칸은 지우지 않고 대조용으로 계속 받아 적는다. 우리 계산과 어긋나면 그것이 곧 원천이 바뀌었다는 신호다.
허용오차는 상대오차 0.1% 다. 그 선을 넘는 심볼이 있으면 대조 명령이 그것을 심볼과 날짜와 함께 찍는다. 명령은 8절에 있고 DB 만 읽는다.
원천이 과거를 다시 쓴 것을 기록한다
전 기간을 다시 받으면 원천의 재작성이 조용히 덮인다. 그래서 덮기 전에 비교해서 yf_restatements 에 남긴다.
adj_close 는 건수가 적고 바뀐 행이 압도적으로 많다. 배당 한 번이 그 종목의 전 기간을 움직이기 때문이다. 실제로 남은 기록 하나가 그 사슬을 그대로 보여준다.
원천이 과거를 다시 쓴 것을 기록한다LMT 배당락일 | 2026-09-01, 주당 3.45 |
|---|
| 그 이벤트를 처음 본 때 | 2026-09-03 07:38 (실행 34) |
|---|
같은 실행의 adj_close 재작성 | 16,272 행, 1962-01-02 ~ 2026-08-31 |
|---|
| 가장 최근 한 건 | 561.229980 -> 557.779968 |
|---|
배당 한 건이 64년치를 다시 쓴 것이고, 그 사실이 기록으로 남아 있다. 구간이 배당락일 하루 전에서 끝나는 것은 그다음부터가 새 행이기 때문이다. 새 행은 재작성이 아니므로 세지 않는다.
허용오차가 이 기록의 값을 결정한다. 원천은 같은 날의 같은 값을 다시 받아도 소수점 다섯째 자리쯤이 흔들린다. 그것을 재작성으로 세면 매일 수천 줄이 쌓여 기록이 소음이 된다. 그래서 가격은 상대오차 1e-5 안이면 같다고 본다. 실제 배당 조정은 0.1% 이상 움직이므로 100배 떨어져 있다.
칸별 재작성 건수를 세는 질의는 8절에 있다.
원천이 이력을 줄여도 우리 데이터는 온전하다
TWO 가 그 실물이다. 원천은 period=max 에도 최근 몇 주만 주는데, 우리 DB 에는 2009년부터의 일봉이 남아 있다. 매일 받아 쌓고 지우지 않기 때문이다. 원천을 그때그때 조회해 쓰는 구조라면 이 종목의 십수 년치가 오늘 사라졌다.
원천에 직접 물어 우리 것과 견주는 명령은 8절에 있다.
뒤처짐의 기준일이 자산군마다 따로다
"며칠 뒤처졌나" 를 유니버스 전체의 최신 거래일로 재면 안 된다. 이유가 둘이다.
뒤처짐의 기준일이 자산군마다 따로다| 이유 | 무엇이 일어나나 |
|---|
| 거래 달력이 다르다 | 환율은 일요일에도 봉이 생긴다. 그것이 최댓값이 되면 일요일에 미국 주식 전체가 3일 뒤처진 것으로 보인다 |
| 회전 배치가 하루에 일부만 받는다 | 방금 받은 몇 개만 하루 앞선 봉을 갖는다. 그것을 기준으로 삼으면 나머지가 모두 뒤처져 보인다 |
그래서 기준일은 자산군마다 따로 내고, 그 자산군에서 봉이 가장 많이 찍힌 날의 절반 이상이 찍힌 날 가운데 가장 최근이다. 그 자산군이 한 장으로 열린 마지막 날이다. 창은 15일이고 상장폐지 종목은 세지 않는다.
주말과 연휴에 갈라진다. 직전 거래일이 정규 개장일이면 여섯 자산군의 기준일이 같다. 지금 값은 목록 API 의 peer_trade_date 가 준다 (8절).
7. 알려진 한계
원천이 period=max 를 거부하는 상품이 있다
전 기간 요청을 아예 받지 않는 상품이 있다. 원천이 이렇게 답한다.
Period 'max' is invalid, must be one of: 1d, 5d
긴 이력이 존재하지 않는 것이다. 신주인수권(-WT, -RW), 신주인수권증서(-RI), 일부 우선주가 그렇다.
이런 종목은 매일 실패한다. 전 기간을 한 번도 받지 못했으므로 회전에서 항상 맨 앞에 오고, 맨 앞에 오면 전 기간 요청을 받고, 그 요청은 거부된다.
이름 규칙으로 확실한 것만 미리 걸러 발견만 하고 수집하지 않는다. 판정은 packages/a42_yfinance/src/a42_yfinance/screener.py 의 is_collectible 이 한다.
원천이 period=max 를 거부하는 상품이 있다| 심볼 모양 | 무엇 | 걸렀나 |
|---|
-WT -RW | 신주인수권 | 그렇다 |
-RI | 신주인수권증서 | 그렇다 |
NTEST | 나스닥 시험용 심볼 | 그렇다 |
-P<글자> | 우선주 | 아니다. 대부분이 정상 수집된다 |
5글자 끝 W | 관례상 신주인수권 | 아니다. 대부분이 정상 수집된다 |
이름은 관례이고 규칙이 아니다. 우선주를 이름으로 걸렀으면 멀쩡한 우선주가 함께 빠졌다. 걸러지지 않은 것은 아래의 연속 빈 결과 규칙이 판정한다. 그래서 새 신주인수권이 들어오면 닷새 동안은 실패로 세어진다.
원천이 멀쩡한 심볼에 데이터를 안 주는 일이 있다
SPLG 는 SPDR Portfolio S&P 500 ETF 다. 실재하고 거래도 활발한데, 원천이 possibly delisted; no price data found 로 거부하는 일이 있었다.
우리가 고칠 것이 없다. 수집 대상에서 빼지 않는다. 원천이 언제 고칠지 모르고, 빼면 고쳐졌을 때 아무도 모른다. 매일 실패로 남아서 눈에 띄는 쪽이 맞다. 지금 어떤 상태인지는 8절의 실패 목록이 보여준다.
원천이 이력을 줄여버리는 일이 있다
TWO 는 6절에 적은 그 종목이다. 우리 DB 에는 2009년부터의 일봉이 있는데 원천은 최근 몇 주만 준다.
이것이 실패로 나타난다. 이어받기 창은 최근 7일인데 원천이 주는 마지막 봉이 그 창보다 오래되면 창 안에 아무것도 없다. 그래서 매일 빈 결과다.
5일이 지나면 수집 대상에서 내려간다. 스크리너가 발견한 종목이기 때문이다. 쌓인 일봉은 그대로 남는다.
연속 5일 빈 결과면 대상에서 내린다
연속 5일 빈 결과면 대상에서 내린다| 조건 | 값 |
|---|
| 상한 | 5 |
| 세는 단위 | 일. "다섯 번" 이 아니라 "닷새" 다 |
| 걸리는 대상 | source 가 screener 인 종목만 |
| 무엇이 내려가나 | collect_enabled 가 거짓이 되고 delisted_at 이 찍힌다 |
| 일봉 | 지우지 않는다 |
| 초기화 | 값이 한 번이라도 오면 0 으로 돌아간다 |
하루에 한 번만 오른다. 실행 횟수로 세면 손으로 여러 번 돌린 날에 그만큼 올라 멀쩡한 종목이 내려간다. 실제로 2026-08-30 에 여섯 번 돌려 카운트가 4까지 올라갔고, 그것을 보고 날짜 기준으로 고쳤다.
손 선언에는 자동 판정이 걸리지 않는다. 사람이 "이것은 있다" 고 주장한 것을 원천의 침묵으로 뒤집지 않는다. SPLG 가 그 경우다. 카운트는 오르지만 내려가지 않고, 내리는 것은 사람이 선언에서 한다.
걸리는 것은 위 표에서 안 걸러진 모양들이다. 우선주와 5글자 끝이 W 나 R 인 것이 그렇게 내려간다. 지금 무엇이 몇 일째인지는 8절의 empty_streak 질의가 보여준다.
되살리는 방법이다. 카운트도 함께 0 으로 돌려야 한다. 안 그러면 다음 실행에서 바로 다시 내려간다.
update yf_symbols
set collect_enabled = true, delisted_at = null, empty_streak = 0,
last_empty_at = null, updated_at = now()
where symbol = '<심볼>';
이름으로 걸러진 것을 되살리려면 코드의 이름 규칙에서도 빼야 한다.
회전이 무너지는 날이 있다
원천이 직전 거래일의 값을 한 행씩 정정하면 유니버스가 통째로 재작성으로 표시되고, 다음 날 배치가 거의 전량 재적재가 된다.
직전 장의 거래량이 확정되면서 volume 이 유니버스 거의 전체에서 각각 한 행씩 정정되는 일이 있다. 종목당 한 행이지만 그 한 행 때문에 그 종목이 재작성으로 표시되고, 다음 실행이 그것들의 전 기간을 다시 받는다.
전 기간을 방금 받은 종목은 다시 표시하지 않는다. 그래서 그런 날 다음에는 대기가 도로 얇아진다. 실행별로 재작성이 몇 종목에서 잡혔는지, 지금 전 기간 재적재를 기다리는 종목이 몇인지 보는 질의는 8절에 있다.
시간 상한 1,800초를 넘으면 멈추고 다음 실행이 이어받는다. 데이터가 깨지는 것이 아니라 그날의 갱신이 뒤로 밀린다.
거래 달력이 자산군마다 다르다
환율, 원자재 선물, 지수에는 일요일 봉이 있다. 주식, ETF, 국채 수익률에는 없다.
거래 달력이 자산군마다 다르다| 자산군 | 주말 봉 |
|---|
fx futures index | 있다 (일요일) |
equity etf yield | 없다 |
"오늘 데이터가 없다" 의 뜻이 자산군마다 다르다. 자산군을 섞어 결측을 세지 마라. 6절의 기준일이 그것을 가르는 장치다.
그 밖
그 밖| 한계 | 내용 |
|---|
| 미국 정규 거래소만 | OTC/핑크시트는 들어 있지 않다 |
| 원가격이 아니다 | close 는 분할이 소급 반영된 값이다. 진짜 원가격을 되돌리는 계산은 아직 없다 |
| 자본이득이 비어 있다 | 계산에는 들어 있지만 원천이 그 칸을 채우지 않는다 |
| 과거 판이 없다 | 어느 날의 일봉이 무엇이었는지를 통째로 보존하지 않는다. 남는 것은 yf_restatements 의 요약과 표본 한 쌍이다 |
| 일봉뿐이다 | 분봉과 호가는 없다 |
| 인증이 없는 비공식 경로다 | 속도 제한과 응답 변경이 예고 없이 온다 |
8. 직접 확인하는 법
이 문서에서 뺀 수를 여기서 센다. 접속 문자열은 apps/a42_collector/.env 의 A42_YFINANCE_DSN 이다.
set -a; . apps/a42_collector/.env; set +a
1절의 재고
psql "$A42_YFINANCE_DSN" -c "
select count(*) filter (where collect_enabled and delisted_at is null) as 수집대상,
count(*) filter (where not (collect_enabled and delisted_at is null)) as 제외,
count(*) as 전체
from yf_symbols;"
psql "$A42_YFINANCE_DSN" -c "
select count(*) as 행수, count(distinct symbol) as 종목,
min(trade_date) as 처음, max(trade_date) as 마지막
from yf_daily_bars;"
2절의 자산군, 통화, 거래소
psql "$A42_YFINANCE_DSN" -c "
select s.asset_class,
count(distinct s.symbol) as 종목,
count(b.symbol) as 일봉_행수,
min(b.trade_date) as 처음, max(b.trade_date) as 마지막
from yf_symbols s left join yf_daily_bars b using (symbol)
group by 1 order by 3 desc;"
psql "$A42_YFINANCE_DSN" -c "
select source, count(*) as 전체,
count(*) filter (where collect_enabled and delisted_at is null) as 수집대상
from yf_symbols group by 1 order by 1;"
psql "$A42_YFINANCE_DSN" -c "
select coalesce(currency, '(없음)') as currency, count(*) as 종목
from yf_symbols group by 1 order by 2 desc;"
psql "$A42_YFINANCE_DSN" -c "
select coalesce(exchange, '(없음)') as exchange, count(*) as 종목
from yf_symbols group by 1 order by 2 desc;"
4절의 표별 행수와 조정 근거
psql "$A42_YFINANCE_DSN" -c "
select 'yf_daily_bars' as 표, count(*) from yf_daily_bars
union all select 'yf_symbols', count(*) from yf_symbols
union all select 'yf_corporate_actions', count(*) from yf_corporate_actions
union all select 'yf_restatements', count(*) from yf_restatements
union all select 'sync_runs', count(*) from sync_runs
union all select 'sync_run_symbols', count(*) from sync_run_symbols;"
조정 근거의 종류별이다. capital_gain 은 줄이 나오지 않아야 한다.
psql "$A42_YFINANCE_DSN" -c "
select action_type, count(*) as 건수, count(distinct symbol) as 종목,
min(ex_date) as 처음, max(ex_date) as 마지막
from yf_corporate_actions group by 1 order by 2 desc;"
5절의 실행 기록
실행별 소요와 성공/실패 종목 수와 실제로 쓴 행 수다.
psql "$A42_YFINANCE_DSN" -c "
select r.id, r.status, r.stop_reason,
r.started_at at time zone 'Asia/Seoul' as 시작_KST,
round(extract(epoch from (r.finished_at - r.started_at)))::int as 소요초,
count(s.symbol) filter (where s.status = 'success') as 성공,
count(s.symbol) filter (where s.status = 'failed') as 실패,
sum(s.rows_written) as 쓴_행
from sync_runs r left join sync_run_symbols s on s.run_id = r.id
where r.command_name = 'sync'
group by 1, 2, 3, 4, 5
order by r.id desc limit 5;"
회전이 무너진 날을 찾는다 (7절). 재작성이 잡힌 종목이 유니버스 크기에 가까운 실행 다음이 그런 날이다.
psql "$A42_YFINANCE_DSN" -c "
select run_id, count(distinct symbol) as 재작성_종목, count(*) as 건수,
sum(rows_changed) as 바뀐행
from yf_restatements group by 1 order by 1 desc limit 5;"
psql "$A42_YFINANCE_DSN" -c "
select count(*) filter (where needs_full_refresh) as 전기간_대기,
count(*) filter (where last_full_refresh_at::date = current_date) as 오늘_받음
from yf_symbols;"
6절의 재작성 기록
psql "$A42_YFINANCE_DSN" -c "
select field, count(*) as 건수, sum(rows_changed) as 바뀐행
from yf_restatements group by 1 order by 2 desc;"
psql "$A42_YFINANCE_DSN" -c "
select run_id, symbol, field, rows_changed,
first_trade_date, last_trade_date, sample_old, sample_new
from yf_restatements where symbol = 'LMT' and field = 'adj_close';"
조정가 대조는 DB 만 읽는다. 원천에 요청하지 않는다.
uv run a42-collector yfinance verify-adjustment --symbol SVXY --symbol AAPL
7절의 실패 목록
psql "$A42_YFINANCE_DSN" -c "
select s.symbol, s.status, s.error_message
from sync_run_symbols s
where s.run_id = (select max(id) from sync_runs where command_name = 'sync')
and s.status = 'failed'
order by s.symbol;"
psql "$A42_YFINANCE_DSN" -c "
select empty_streak, count(*) from yf_symbols group by 1 order by 1;"
psql "$A42_YFINANCE_DSN" -c "
select symbol, source, empty_streak, delisted_at
from yf_symbols
where not collect_enabled or delisted_at is not null
order by delisted_at desc nulls last, symbol;"
원천이 그 종목에 무엇을 주는지 직접 묻는 것이다. 표본 몇 개로만 한다.
uv run python -c "
import yfinance
for s in ['TWO', 'SPLG', 'AAPL']:
f = yfinance.Ticker(s).history(period='max', auto_adjust=False)
print(s, len(f), '행')
"
화면과 조회 API
화면과 조회 API (1번째 표)| 목록 화면 | http://localhost:3000/yfinance |
|---|
| 종목 화면 | http://localhost:3000/yfinance/<심볼> |
|---|
| 재고 화면 | http://localhost:3000/ |
|---|
| 목록 API | GET http://127.0.0.1:8000/api/v1/yfinance/symbols |
|---|
| 종목 API | GET http://127.0.0.1:8000/api/v1/yfinance/symbols/<심볼> |
|---|
| 일봉 API | GET http://127.0.0.1:8000/api/v1/yfinance/symbols/<심볼>/bars |
|---|
| 재고 API | GET http://127.0.0.1:8000/api/v1/datasets |
|---|
목록 API 는 한 페이지씩 준다. 기본 100개, 상한 500개이고 total 이 필터에 걸린 전체 개수다. 거르는 인자는 asset_class, exchange, delisted, q 이고 페이지 인자는 limit, offset 이다.
curl -s 'http://127.0.0.1:8000/api/v1/yfinance/symbols?limit=1'
curl -s 'http://127.0.0.1:8000/api/v1/yfinance/symbols?asset_class=index&limit=10'
curl -s 'http://127.0.0.1:8000/api/v1/yfinance/symbols/SPY/bars?from=2026-08-01'
화면과 조회 API (칸, 무엇 칸이 있는 표)| 칸 | 무엇 |
|---|
last_run | 마지막 실행의 status 와 stop_reason |
peer_trade_date | 자산군별 기준일. 뒤처짐을 이것으로 잰다 |
newest_trade_date | 유니버스 전체의 최신 거래일. 뒤처짐을 재는 데 쓰지 않는다 |
한 줄의 last_run_state 가 그 종목이 마지막 실행에서 어떻게 되었는지다.
화면과 조회 API (값, 뜻 칸이 있는 표)| 값 | 뜻 |
|---|
success | 받아왔다 |
failed | 시도했고 실패했다 |
not_reached | 차례가 오지 않았다. 실패가 아니다 |
not_collected | 실행은 끝까지 돌았는데 그 실행의 수집 대상이 아니었다 |