sossost.dev

AI 에이전트로 시장을 분석한다는 것 / 3

market-analyst — RS 80은 종목의 점수가 아니다

퍼센타일은 누구와 비교했는가의 함수다. 모집단은 정의된 적이 없었고, 기준은 내가 계속 바꿨다. '매일 같은 기준으로 본다'는 말이 어디까지 참인지에 대한 글

5 min read

2편에서 재료를 믿게 됐다고 적었다. 정확히는, 못 믿을 때 시스템이 스스로 말하게 됐다고. 이제 그 재료로 Phase·RS·SEPA·브레드스를 계산하는 층으로 올라간다.

1편에서 이 시스템의 존재 이유를 한 문장으로 적었다. 사람이 아니라 코드가, 미국 전 종목을, 매일 같은 기준으로 본다고. 이 글은 그 문장 중 "같은 기준"이라는 네 글자가 어디까지 참인지에 대한 글이다.

결론부터 적으면, 생각보다 훨씬 좁은 범위에서만 참이었다.

퍼센타일은 종목의 속성이 아니다

1편 용어집에서 RS를 이렇게 정의했다. 전체 종목 대비 모멘텀 퍼센타일, 0에서 100. RS 80이면 상위 20%.

이 정의를 다시 읽어보면 이상한 점이 있다. "종목 대비"의 종목이 누구인가. 퍼센타일은 절대값이 아니다. 어떤 종목의 12개월 수익률은 그 종목의 속성이지만, 그 수익률이 상위 몇 퍼센트인지는 비교 대상의 속성이다. 종목이 한 틱도 움직이지 않아도 비교 대상이 바뀌면 점수가 바뀐다.

이걸 머리로는 알고 있었다. 그런데 시스템을 만들면서, 그 비교 대상이 정확히 누구인지를 나는 한 번도 정의한 적이 없었다.

모집단은 정의된 적이 없었다

과거 횡단면을 조사하다가 발견했다. 넷플릭스, 마이크론, 서비스나우, 팔로알토, 셰브론, 스타벅스 같은 종목 499개가 약 3년 구간의 가격 이력에서 통째로 빠져 있었다. 미국 시장을 대표하는 대형주들이다. 그 구간 동안 RS는 이들 없이 계산됐다.

왜 빠졌는지보다 더 중요한 건, 빠졌는데 왜 아무 일도 안 일어났느냐였다.

랭킹 쿼리를 다시 읽었다. 유니버스 필터가 없었다. "그날 가격 테이블에 행이 있는 심볼"이 곧 그날의 모집단이었다. 12개월·6개월·3개월 수익률을 어떻게 가중하는지, 퍼센타일을 어떻게 구하는지는 코드에 명시돼 있었다. 그런데 누구를 줄 세우는지는 어디에도 명시돼 있지 않았다. 기준은 코드에 적혀 있었고, 모집단은 우연에 적혀 있었다.

그래서 종목 499개가 빠져도 시스템은 멈추지 않았다. 남은 종목들로 퍼센타일을 구했을 뿐이다. 그리고 빠진 499개가 대체로 강한 대형주였기 때문에, 남은 종목들은 실제보다 높은 퍼센타일을 받았다. RS 80 게이트를 실제보다 쉽게 통과한 후보들이 그 구간에 있었다는 뜻이다.

여기서 한 가지 결정을 했는데, 그건 마지막에 적겠다. 사건이 하나 더 있다.

10달러짜리 Phase 2

브레드스 지표 중에 phase2_ratio가 있다. 전 종목 중 Phase 2에 있는 종목의 비율. 시장이 얼마나 넓게 상승 추세에 있는지를 보는 숫자다. 이 숫자가 부풀어 있었다.

범인은 SPAC이었다. 상장 후 합병 전까지 10달러 근처에 고정된 껍데기 회사들. 가격이 평탄하고, 그래서 MA150도 평탄하고, 가격이 MA150 바로 위에 붙어 있다. 규칙으로 보면 완벽한 Phase 2 초입이다. 오래 바닥을 다지고 막 상승 추세로 들어선 종목의 모양과 구분이 안 된다.

규칙이 틀린 게 아니다. 규칙은 옳았다. 규칙은 SPAC과 바닥을 다지는 종목을 구분할 수 없다 — 그건 규칙의 일이 아니다. 누가 심사받을 자격이 있는가는 규칙의 일이 아니라 모집단의 일이다. 그리고 모집단은, 다시 한번, 정의돼 있지 않았다.

덤으로 하나 더 나왔다. 같은 phase2_ratio를 리포트와 검증 경로가 각자 계산하고 있었는데, 서로 다른 모집단 위에서 계산하고 있었다. 숫자가 안 맞았다. 세 번에 걸쳐 고쳤다. 같은 이름의 지표가 두 개의 모집단 위에서 두 개의 값으로 살고 있었다.

그리고 기준은 내가 바꿨다

모집단은 내가 건드리지 않았는데 움직였다. 기준은 내가 건드려서 움직였다. 매번 옳은 이유로.

4월. SEPA 등급이 없는 종목이 아예 보이지 않는다는 걸 발견했다. 조인이 INNER였다. 재무 데이터가 아직 쌓이지 않은 신생 종목, 정확히 조기 포착의 대상이 되어야 할 종목들이 투명인간이었다. LEFT JOIN으로 바꾸고 게이트를 B등급까지 완화했다. 옳았다.

7월. B등급이 노이즈라는 게 분명해졌다. 편입 기준을 S/A 하드게이트로 되돌렸다. 옳았다.

5월. 업종 RS를 테마 RS로 통째로 갈아엎었다. 제공처의 업종 분류가 시장이 실제로 종목을 묶는 방식과 달랐다. 광통신은 업종이 아니라 테마고, 시장은 테마로 움직였다. 옳았다.

그리고 그 사이 어느 날, RS 80이라는 숫자가 코드 여섯 군데에 각각 SQL 리터럴로 박혀 있다는 걸 발견했다. 뉴스 적재에 하나, 이상 종목 탐지에 하나, 검증에 하나, 편입 자격에 하나. "같은 기준"이 여섯 개의 사본으로 살고 있었고, 어느 하나가 바뀌어도 나머지는 모른다. 단일 소스로 승격했다.

매 변경이 옳았다. 그리고 매 변경이 어제의 "RS 80 통과"와 오늘의 "RS 80 통과"를 다른 것으로 만들었다. 4월의 후보 리스트와 7월의 후보 리스트는 같은 시스템이 만든 게 아니다. 같은 이름의 다른 시스템이 만든 것이다.

"같은 기준"은 미래에 대한 약속이다

여기서 반론이 자연스럽게 나온다. 그럼 기준을 고정해라. 한 번 정하고 건드리지 마라. 그래야 비교가 된다.

맞는 말이고, 이 시스템에서는 틀린 말이다. 기준을 고정하면 4월의 투명인간이 다시 생긴다. 이 시스템의 목표는 다음 사이클의 주도주를 초입에서 잡는 것이고, 초입에 있는 종목은 정의상 기준이 아직 모르는 종목이다. 시장을 배우면서 기준이 바뀌는 건 결함이 아니라 목표의 대가다. 기준을 고정한 시스템은 일관되지만, 일관되게 늦는다.

그래서 "매일 같은 기준으로 본다"의 뜻을 다시 정의했다. 시간을 가로지르는 비교 가능성이 아니다. 그건 애초에 없었고, 앞으로도 없다. 대신 이렇게 정의했다 — 오늘 어떤 기준과 어떤 모집단으로 봤는지를 시스템이 항상 말할 수 있다는 것.

구체적으로는 이렇게 바꿨다. phase2_ratio를 모집단을 인자로 받는 순수함수로 만들어, 어떤 모집단 위에서 계산됐는지가 호출부에 드러나게 했다. 임계값을 단일 소스로 올려 "RS 80"이 한 곳에서만 살게 했다. 모집단에 결손이 있으면 리포트 상단에 경고를 띄워, 그날의 브레드스가 몇 종목 위에서 계산됐는지 사람이 읽게 했다.

그리고 아까 미뤄둔 결정. 499종목이 빠졌던 구간의 RS를 다시 계산하지 않았다. 대신 얼마나 왜곡됐는지를 정량화해서 기록으로 남겼다. 과거를 조용히 고쳐 쓰면 그 시점에 시스템이 무엇을 봤는지가 사라진다. 그때의 후보 리스트는 그때의 모집단이 만든 것이고, 그건 틀렸더라도 사실이다. 틀린 사실을 지우는 것보다 틀렸다고 적어두는 게 낫다.

2편에서 데이터가 침묵할 수 없게 만들었다고 적었다. 이 층에서 한 일은 그것의 대칭이다. 기준이 침묵할 수 없게 만든 것.

같은 기준이란 없다. 있는 건, 오늘 어떤 기준이었는지 말할 수 있는 시스템과 말할 수 없는 시스템뿐이다.

종목을 보는 게 아니라 모집단을 보는 것

RS 80은 종목의 점수가 아니다. 그 종목이 그날 누구와 비교됐는지의 결과다. Phase 2는 종목의 상태가 아니다. 그 종목이 심사 대상이었다는 전제 위에서만 의미가 있는 판정이다.

이 층을 만들면서 배운 건, 지표를 계산하는 법이 아니었다. 지표 앞에 항상 "누구 중에서"라는 말이 생략돼 있고, 그 생략된 말이 지표보다 중요하다는 것이었다.

재료도 기준도 말하게 만들었으니, 다음 편에서는 그 위에서 LLM이 무슨 일을 하는지로 올라간다. 1편에서 정량 게이트는 LLM의 일이 아니라고 적었다. 그럼 LLM의 일은 무엇이었고, 그 일이 정말 필요했는지 — 그 질문에 솔직하게 답할 차례다.

지표 앞에는 항상 "누구 중에서"가 생략돼 있다. 나는 그 생략된 말을 찾는 데 다섯 달을 썼다.