베타
← 리서치 노트 목록

리서치 노트

리서치 노트 #04 — 공개 API의 백테스트 오차가 학습 구간을 섞고 있었습니다. 고쳤고, 숫자는 나빠졌습니다

발행 2026-09-06데이터 기준일 2026-09-06대상 농·수산물 도매 바이어 · 우리 공개 API를 직접 호출해 보신 분

EcoChain 리서치 노트 시리즈. 우리가 내부에서 돌린 검증·백테스트·반증 기록을 정직하게 공개합니다. 잘 된 것과 안 된 것을 같은 무게로 씁니다.

TL;DR

  • 품목 상세 API 중 하나(/products/{id}/backtest)가 최근 90일 실측에 대해 "그 날짜를 예측했던 모든 예측"을 평균하고 있었습니다. 예측을 언제 했는지는 묻지 않았습니다.
  • 그 결과 실측이 나온 뒤에 만들어진 예측 — 즉 답을 이미 본 학습 구간 값 — 이 백테스트 오차에 섞였습니다. 감자(품목 11) 한 품목만 세어도 44건이었습니다. 이런 값은 정의상 잘 맞으므로 숫자를 좋은 쪽으로 끌어내립니다.
  • 표본이 0건이면 오차를 0%로 돌려주고 있었습니다. 0%는 "아직 모른다"가 아니라 "완벽하다"는 주장입니다.
  • 2026-08-31에 고쳤습니다. 예측 시점과 실측 시점의 간격이 정확히 7일·30일·90일인 짝만 채점하고, 표본이 없으면 값 대신 "없음"을 돌려줍니다. 고친 뒤 감자의 7일 오차는 4.79%에서 5.92%로 올라갔습니다. 그것이 참인 방향입니다.
  • 이 API는 우리 화면 어디에서도 쓰이지 않았습니다. 그래서 사이트를 보신 분은 이 숫자를 본 적이 없습니다. 하지만 인증 없이 열려 있는 공개 API였으므로, 누가 호출해 인용했는지 우리는 알 수 없습니다. 그래서 적습니다.

무엇이 틀려 있었나

백테스트는 "과거의 어느 시점에 내놓은 예측이, 그 뒤에 실제로 나온 값과 얼마나 달랐나"를 재는 일입니다. 이 정의에서 빠지면 안 되는 조건이 하나 있습니다 — 예측이 실측보다 먼저여야 합니다. 그 조건이 이 API에는 없었습니다.

  1. 예측 시점을 묻지 않았습니다. 실측 날짜가 창 안에 있으면, 그 날짜를 향해 만들어진 예측을 전부 가져와 평균했습니다. 우리 모델은 매일 다음 180일을 새로 예측하므로 한 실측 날짜에는 수십 개의 예측이 붙습니다. 그중 실측 이후에 만들어진 것들은 이미 그 값을 학습에 쓴 뒤의 적합값입니다. 감자 한 품목, 90일 창에서 예측일과 실측일의 간격은 −12일에서 +67일까지 퍼져 있었고, 간격이 0 이하 — 즉 답을 본 뒤의 값 — 이 44건이었습니다.
  2. 표본이 없으면 0%를 돌려줬습니다. 코드 한 줄이 "표본 수가 0이면 0"이었습니다. 읽는 쪽에는 "이 품목의 백테스트 오차는 0%"로 보입니다. 우리는 2026-08-07에 다른 곳에서 같은 형태의 오류를 찾아 고친 적이 있는데(표본이 없는 날을 오차 0%로 세던 집계), 이 경로 하나가 그 규칙을 따르지 않고 있었습니다.
  3. 같은 개념을 두 곳에서 따로 정의하고 있었습니다. /track-record 페이지와 품목 정확도 API는 "예측 시점 + 정확히 N일 뒤 실측"이라는 정의를 쓰는데, 이 백테스트 API만 다른 정의를 갖고 있었습니다. 같은 이름의 숫자가 경로에 따라 다른 것을 재고 있으면, 어느 쪽이 맞는지 외부에서는 알 길이 없습니다.

섞인 값을 걸러 내기 전의 숫자, 4.79%는 이 노트 본문에만 적습니다. 요약이나 제목에는 넣지 않습니다. 문맥 없이 잘려 나가면 "우리가 4.79%를 달성했다"는 전혀 다른 주장이 되기 때문입니다.

어떻게 드러났나 — 찾으려던 것이 아니었습니다

2026-08-31에 우리는 예측 테이블 정리 작업을 하고 있었습니다. 매일 180일치 예측을 새로 쓰면서 옛 예측을 지우지 않아 240만 행이 쌓였고, 데이터베이스 용량의 29%를 차지하고 있었습니다. 채점에 실제로 쓰이는 행은 그중 1.66%였습니다.

정리 계획서에 우리는 "옛 예측을 읽는 API는 없다"고 적었습니다. 검증 단계에서 그 문장이 틀렸다는 것이 확인됐습니다 — 이 백테스트 API가 옛 예측을 읽고 있었고, 정리 전후로 값이 바뀌었습니다. 값이 바뀐 것 자체를 따라가 보니 정리가 문제가 아니라 원래 정의가 임의적이었다는 것이 나왔습니다.

데이터를 느슨하게 읽던 소비처는 그 데이터를 정리할 때 드러납니다. 우리가 이 결함을 찾은 것은 정확도를 점검하다가가 아니라, 용량을 줄이려다가였습니다. 이 점을 숨기지 않고 적어 두는 것이 이 시리즈의 방식입니다.

무엇을 고쳤나 — 지우지 않고 참으로 만들었습니다

  • 예측 기간을 명시하게 했습니다. 호출할 때 7일·30일·90일 중 하나를 지정합니다(지정하지 않으면 7일). 그 기간과 "실측일 − 예측일"이 정확히 같은 짝만 채점합니다. 학습 구간 값은 따로 걸러 내지 않습니다 — 간격이 정확히 7일이어야 한다는 조건 자체가 간격 0 이하를 구조적으로 배제합니다.
  • 표본이 없으면 값을 비웁니다. 0% 대신 값 없음을 돌려주고, 표본 수는 그대로 실어 왜 비었는지 보이게 했습니다.
  • 응답이 자기 정의를 들고 다닙니다. 돌려주는 값에 채점 기간(7·30·90)을 함께 넣었습니다. 숫자만 떼어 옮기더라도 "며칠 뒤를 맞힌 오차인지"가 같이 갑니다. 기존 필드는 하나도 지우거나 이름을 바꾸지 않았습니다 — 인증 없는 공개 API라 누가 어떤 필드에 의존하는지 모르기 때문입니다.
  • 7·30·90일 외의 기간은 받지 않습니다(400 오류). 임의 기간을 허용하면 더 유연해 보이지만, 정리 정책 이후 옛 예측은 채점용 간격만 남아 있어서 예컨대 45일 같은 값은 최근 3주와 매주 월요일에 만든 예측에서만 표본이 잡힙니다. 그렇게 나온 숫자는 조용히 편향됩니다. 편향된 유연성보다 좁은 정직성을 택했습니다.

채점 기간 집합(7·30·90)은 예측 정확도 배치를 돌리는 쪽과 API 쪽이 각자 갖고 있었습니다. 이번에 한쪽이 다른 쪽 소스 파일을 읽어 대조하는 테스트를 두어, 어느 한 곳만 바뀌면 빌드가 실패하게 했습니다. 이 테스트는 자기 검증도 합니다 — 대조 방식이 조용히 죽어 "0건 매치인 채 초록불"이 되는 일을 우리는 8월 초에 한 번 겪었습니다.

숫자 — 감자(품목 11) 기준

채점 기간2026-08-31 수정 직후표본2026-09-06 현재표본
7일5.92%49건5.44%54건
30일8.36%31건7.47%36건
90일값 없음0건50.34%2건

수정 전 혼합값은 4.79%였습니다. 7일 기준으로 다시 재면 5.92%로 1.13%p 올라갑니다. 값이 나빠진 것이 아니라, 나빠 보이는 쪽이 원래 값입니다.

90일 행을 읽을 때 주의해 주십시오. 2026-09-06 현재 표본이 2건입니다. 두 개의 짝에서 나온 50.34%는 90일 예측이 얼마나 정확한지에 대해 사실상 아무것도 말해 주지 않습니다. 우리 90일 예측은 2026년 6월 초에 처음 만들어졌고, 그 예측의 90일 뒤 실측이 8월 말부터 도착하기 시작했습니다. 이 행은 표본이 수십 건으로 쌓이기 전까지 "채점이 시작됐다"는 사실 이상으로 읽지 마십시오. 우리도 이 값을 어디에도 인용하지 않습니다.

같은 품목의 /track-record 쪽 숫자와 이 표의 숫자가 다른 것은 정상입니다. 창(최근 90일 실측 vs 하루 스냅샷)과 모집단이 다릅니다. 다르다는 사실을 다음 절에 적어 둔 이유가 그것입니다.

아직 안 고친 것

  • 두 정의가 여전히 둘입니다. 이번 수정으로 채점 기간의 축은 /track-record와 같아졌지만, 어떤 시장 가격을 대표값으로 잡는지, 실측 날짜를 며칠까지 허용하는지는 두 경로가 여전히 각자 정하고 있습니다. 하나로 합치는 작업은 따로 하겠습니다. 합치면 어느 쪽 숫자가 얼마나 움직였는지 적겠습니다.
  • 30일·90일 구간은 여전히 표본이 적습니다. 노트 #01부터 반복해 적고 있는 사실입니다. 구매·계약 판단에 정작 필요한 구간이 검증 전이라는 점은 이번에도 달라지지 않았습니다.
  • 노트 #03에서 예고한 주제는 아직 다루지 못했습니다. 물량 가중 오차와 방향 편향을 다음 노트에서 다루겠다고 적었습니다. 이 노트가 먼저 나온 것은 그보다 급한 결함이 있었기 때문이고, 예고한 주제는 그대로 남아 있습니다. 발행 캘린더는 여전히 약속하지 않습니다.

Caveats — 이 노트를 어디까지 믿어야 하나

  1. 위 표는 감자 한 품목입니다. 결함은 API 전체에 있었으므로 모든 품목의 값이 같은 방향으로 바뀌었습니다. 다만 얼마나 바뀌었는지는 품목마다 다르고, 우리는 감자 외의 품목에서 수정 전 값을 기록해 두지 않았습니다. 그래서 "전 품목 평균 몇 %p 왜곡"이라는 숫자는 낼 수 없습니다.
  2. 이 API를 누가 호출했는지 우리는 모릅니다. 우리는 웹 페이지 방문 기록은 남기지만 API 호출 기록은 보존하지 않습니다. 화면에서 쓰이지 않았다는 것은 코드로 확인했지만, "누구도 이 숫자를 보지 않았다"고 말할 근거는 없으므로 그렇게 말하지 않습니다.
  3. 수치는 2026-09-06 스냅샷입니다. 표본이 매일 늘어나므로 오늘 API가 돌려주는 값은 이 표와 다릅니다. 이 노트를 인용하실 때는 기준일을 함께 적어 주십시오.
  4. 고친 정의가 유일하게 옳은 정의는 아닙니다. "예측 시점이 실측보다 먼저여야 한다"는 조건은 백테스트의 최소 조건이지 충분 조건이 아닙니다. 노트 #03에서 보였듯 매칭·모집단·집계를 어떻게 잡느냐에 따라 같은 예측이 다른 점수를 받습니다. 이번에 고친 것은 그중 가장 기본적인 한 칸입니다.

재현법

  • 직접 확인: https://ecochn.org/track-record — 우리가 표준으로 쓰는 정의(예측 시점 + 정확히 N일 뒤 실측)의 정확도가 매일 갱신되어 공개됩니다.
  • API로 확인: 품목 상세 API의 backtest 경로에 horizonDays=7, 30, 90을 각각 붙여 호출하면 위 표의 값(그날 기준)과 표본 수를 돌려받습니다. 7·30·90 외의 값을 넣으면 400 오류가 납니다 — 그것이 의도입니다. 인증은 필요 없습니다.
  • 이전 노트: 노트 #03 — 같은 예측을 여덟 가지 정의로 재채점한 표. 채점 정의가 숫자의 절반을 결정한다는 사실을 다뤘습니다. 이번 노트는 그중 한 정의가 우리 코드 안에서 어긋나 있던 사례입니다.
  • 방법 재현: 과거 예측과 실측을 짝지을 때 "실측일 − 예측일"을 먼저 계산해 보십시오. 그 값이 0 이하인 짝이 하나라도 있으면 그 백테스트는 학습 구간을 섞고 있습니다. 우리 것이 그랬습니다.

맺음 — 고쳤더니 나빠진 숫자에 대해

공개된 숫자를 고쳐서 더 나쁘게 만드는 일은 하기 싫은 일입니다. 안 고쳐도 아무도 모를 가능성이 높았습니다. 화면 어디에도 쓰이지 않는 API였습니다.

그래도 고치고 적는 이유는 하나입니다. 이 시리즈가 하는 약속이 "우리 숫자는 좋다"가 아니라 "우리 숫자는 우리가 말한 방식으로 잰 것이다"이기 때문입니다. 그 약속은 말한 방식대로 재지 않은 숫자가 하나라도 남아 있으면 통째로 깨집니다. 4.79%가 남아 있는 한 5.92%도 믿을 이유가 없습니다.

다음에 또 이런 것을 찾으면 또 적겠습니다. 찾지 못한 것이 없다고는 말하지 않겠습니다.

고지

이 노트는 투자·매매 조언이 아닙니다. EcoChain은 공개 데이터 기반의 가격 참고 정보를 제공하며, 구매·계약 의사결정의 책임은 이용자에게 있습니다.

리서치 노트 #04 — 공개 API의 백테스트 오차가 학습 구간을 섞고 있었습니다. 고쳤고, 숫자는 나빠졌습니다 — EcoChain 리서치 노트