와이프의 퀀트 공부를 돕기 위해 만든 주식 팩터 스크리너

소개

와이프가 경제 공부를 취미로 하고 있는데, 최근 퀀트 투자에 관심을 가지기 시작했습니다. 여러 조건을 직접 걸어가며 국내/미국 주식 종목을 분석해보고 싶어 했고, 그 용도로 간단한 주식 스크리너를 만들기 시작했습니다.

초기 목표는 단순했습니다.

  • 국내/미국 종목을 조회한다.

  • EPS 성장률, ROE 성장률 같은 조건을 걸 수 있다.

  • 조건을 바꿔가며 종목을 필터링한다.

  • 웹으로 만들고, 가능하면 Pake로 감싸 앱처럼 실행한다.

처음부터 native macOS 앱이나 SQLite 적재 구조를 목표로 한 것은 아니었습니다. 작업하면서 실제 사용성을 기준으로 구조가 바뀌었습니다.

Pake로 감싸는 방식은 간단했지만, 로컬 서버를 별도로 실행해야 하는 문제가 있었습니다. 와이프가 쓰는 도구라면 터미널에서 서버를 켜고 끄는 과정이 없어야 한다고 판단했습니다. 그래서 앱을 켜면 내부 서버도 자동으로 켜지고, 앱을 끄면 서버도 같이 꺼지는 native macOS wrapper 구조로 바꿨습니다.

데이터 저장 방식도 바뀌었습니다. 처음에는 매번 Yahoo Finance에서 조회하거나, 종목별 JSON 스냅샷을 저장하는 정도로 생각했습니다. 하지만 전체 종목을 대상으로 조회하면 요청 수가 많아지고, 속도도 느려지고, Yahoo에서 차단될 가능성도 있었습니다. 그래서 SQLite 기반으로 원천 재무 데이터를 누적 저장하는 구조로 바꿨습니다.

현재 구조는 다음과 같습니다.

  • pnpm 기반 로컬 웹 서비스

  • macOS native wrapper 앱

  • 앱 실행 시 내부 Node 서버 자동 시작

  • 앱 종료 시 내부 서버 종료

  • Yahoo Finance 재무 데이터 조회

  • 최초 조회 시 최근 8년치 원천 재무 데이터 저장

  • 이후 조회 시 최신 결산 구간만 확인해 새 분기/연간 데이터 추가

  • JSON 스냅샷 캐시에서 SQLite 기반 누적 적재로 전환

  • 조건 통과 여부는 저장하지 않고 조회 시점에 계산

  • UI/UX는 와이프와 화면을 보면서 조정

Codex의 /goal 기능도 초반에 사용했습니다. 한동안 Codex를 거의 쓰지 않다가 ChatGPT Pro 1개월 구독권을 등록한 김에 이번 작업에 사용해봤습니다.

초기 /goal은 최종 결과처럼 정교하지 않았습니다. “와이프가 조건을 걸어가며 주식 종목을 볼 수 있는 간단한 스크리너를 만들고, 앱처럼 실행할 수 있게 한다” 정도였습니다. 진행하면서 Pake에서 native wrapper로 바뀌고, JSON 저장에서 SQLite 적재로 바뀌었습니다.

진행 방법

주로 ChatGPT/Codex를 사용했습니다.

저는 요구사항과 사용성 피드백을 계속 전달했고, Codex는 코드 수정, 테스트, 패키징을 진행했습니다.

이번 작업은 단순 기능 하나를 추가하는 것이 아니라 다음 요소가 같이 있었습니다.

  • 웹 UI

  • 로컬 서버

  • macOS 앱 패키징

  • 앱 실행/종료 시 서버 프로세스 관리

  • Yahoo Finance 데이터 조회

  • SQLite 적재 구조

  • 조건 평가 로직

  • UI/UX 개선

  • 테스트와 패키징 검증

그래서 초반에 /goal을 사용해 큰 방향을 잡고 진행했습니다. /goal이 최종 설계를 고정해주는 기능이라기보다는, 작업 중간에 방향이 바뀌어도 흐름을 이어가는 데 도움이 됐습니다.

사용한 프롬프트 예시

처음 앱화는 Pake를 고려했었습니다. 하지만, 논의하는 과정에서

앱으로 만들면 따로 프로세스 실행 별도 터미널에서 안해도 되는거지?
아니라면 앱을 킬 때 자동으로 같이 실행되어지는게 낫지 않아?

이후 Pake 기반이 아니라 native macOS wrapper로 바꿨습니다.

현재 앱 실행 흐름은 다음과 같습니다.

Stock Factor Screener.app 실행
-> 앱 내부에서 node server.mjs 실행
-> WebKit 화면으로 로컬 UI 표시
-> 앱 종료 시 node server도 함께 종료

전체 종목 조회에 대한 확인도 있었습니다. 조건 대비 조회되는 종목이 너무 적어 확인해보니 Codex가 임의로 일부 종목 선정하여 구현 중이였습니다.

현재는 일부 종목만 랜덤으로 뽑아서 체크하는 식이고 전체 종목 확인하는게 아니였어?

이후 샘플 종목 조회가 아니라 전체 시장 조회 구조로 바꿨습니다.

비교 기준도 확장했습니다.

연간 / 분기 YoY / TTM 모두 한번에 비교하는 식이면 좋을 것 같다.
다만 옵션으로 빼서 기본은 모두 체크된 상태이지만 일부 체크 해제해서 하나만, 혹은 2개 조합으로도 볼 수 있으면 좋을 것 같다.

현재 비교 기준은 다음과 같습니다.

  • 연간: 최신 연간 결산일과 직전 연간 결산일 비교

  • 분기 YoY: 최신 분기와 전년 동기 분기 비교

  • TTM: 최근 4개 분기 합산과 이전 4개 분기 합산 비교

데이터 구조는 중간에 크게 바뀌었습니다.
조회할 때마다 스냅샷을 찍어두는 방식은 데이터도 중복되고 불필요한 반복 조회도 많으며, 추후 특정 기간 기준 조회나 분석 기능 등을 추가할 때 원천 데이터가 없는 구조가 되기에

구조를 바꾸는게 나을 것 같다. 현재 구조는 비효율적인 것 같네.
전체 새로 조회가 아니라 신규 데이터만 추가 누적하는 방식이 낫잖아?

이후 저장 구조를 다음 방향으로 정리했습니다.

현재 SQLite 구조는 대략 다음과 같습니다.

symbols
- symbol
- name
- market
- exchange
- data_status
- last_checked_at
- latest_annual_period
- latest_quarterly_period

financial_points
- symbol
- frequency        -- annual / quarterly
- metric           -- NetIncome, DilutedEPS, StockholdersEquity 등
- period_end       -- 2026-03-31
- value
- currency
- source
- fetched_at

latest_factors
- symbol
- value_json
- financial_fingerprint
- latest_annual_period
- latest_quarterly_period
- calculated_at

최초 조회 시에는 최근 8년치 원천 재무 데이터를 저장합니다.

이후 조회 시에는 매번 전체를 다시 받지 않고, 마지막 저장 결산일 기준 최근 18개월 구간만 다시 확인합니다. 새 분기/연간 데이터가 있으면 financial_points에 추가하거나 갱신하고, 최신 factor 값을 다시 계산합니다.

UI/UX는 와이프와 같이 화면을 보면서 수정했습니다.

한국 비즈니스 대시보드의 스크린샷

결과와 배운 점

실제 사용자가 있으면 기술적 판단 기준이 명확해졌습니다.

혼자 쓰는 도구였다면 터미널에서 서버를 켜도 괜찮았을 수 있습니다. 하지만 와이프가 쓰는 도구라면 앱을 열면 바로 사용할 수 있어야 했습니다. 이 기준 때문에 Pake 기반 접근에서 native macOS wrapper로 바꿨습니다.

데이터 저장 방식도 마찬가지였습니다. 단순 JSON 스냅샷은 처음에는 충분해 보였지만, 전체 시장을 반복 조회하려면 비효율적이었습니다. 그래서 SQLite 기반 원천 데이터 누적 구조로 바꿨습니다.

/goal도 도움이 됐습니다. 초기 목표는 단순했지만, 작업 중간에 요구사항과 구조가 계속 바뀌었습니다. /goal을 사용하니 작업 흐름을 이어가면서 방향을 조정하기 좋았습니다.

시행착오

1. Pake 기반 접근

처음에는 웹앱을 Pake로 감싸려고 했습니다. 하지만 로컬 서버를 별도로 실행해야 하는 문제가 있었습니다.

사용자가 앱처럼 쓰려면 서버 실행/종료가 앱 내부에서 처리되어야 했습니다. 그래서 native macOS wrapper로 전환했습니다.

2. JSON 스냅샷 캐시

처음에는 종목별 JSON 파일에 최신 계산 결과를 저장했습니다.

문제는 다음과 같았습니다.

  • 조회 시점의 스냅샷만 남음

  • 분기별 원천 데이터가 독립적으로 쌓이지 않음

  • 신규 분기만 추가하는 구조가 아님

  • 조건이 바뀌면 저장된 통과/미통과 결과가 의미 없어짐

  • 전체 종목 수천 개를 다루기엔 파일 기반 관리가 애매함

그래서 SQLite로 바꿨습니다.

3. 조건 결과와 원천 데이터 분리

조건 통과 여부는 저장하지 않는 쪽으로 정리했습니다.

조건은 사용자가 계속 바꿀 수 있습니다. 오늘은 ROE >= 15를 보고, 내일은 ROE >= 20을 볼 수 있습니다. 그러면 이전 조건 기준의 통과 여부는 의미가 없습니다.

그래서 원천 데이터와 계산 가능한 factor만 저장하고, 조건 평가는 조회 시점에 하도록 했습니다.

4. 데이터 상태와 조건 미통과 구분

전체 종목을 다루다 보니 “조건 미통과”와 “데이터 부족”은 구분해야 했습니다.

조건 미통과
- 재무 데이터가 있음
- 계산 가능함
- 사용자가 건 조건을 넘지 못함

데이터 부족
- Yahoo에서 재무 데이터가 없음
- 최신 재무 데이터가 오래됨
- 분기 데이터가 부족해 TTM 계산 불가
- ETF/ETN 등 일반 기업 재무제표 구조와 다름

이 구분을 위해 data_status 개념을 두었습니다.

앞으로의 계획

  • 앱 실행 후 백그라운드로 전체 종목 데이터 적재

  • data_status를 화면에서 더 명확히 표시

  • 데이터 부족 종목과 조건 미통과 종목을 결과에서 분리

  • 조건 프리셋 저장

  • 섹터/업종별 필터

  • 조회 결과 히스토리 저장

  • DART, SEC/EDGAR 같은 공식 데이터 소스 연동

  • 재무제표 수정 공시나 restatement 감지

  • macOS 앱 서명 및 배포용 notarization

  • 국장/미장 외에 일본도 추가 (사례글 와이프에게 써도 되냐 물어보다 추가로 숙제받음...)

4
1개의 답글
지피터스 뉴스레터

이번 주 AI, 딱 쓸 것만

매주 월요일 오후,
읽고 바로 써먹을 AI 소식만