4. 시스템 설계 및 엔지니어링 목표

개요

스프링 서버와 postgreSQL 을 사용하는 것으로 구상하였고, 세부 최적화는 PoC 구현이후에 진행하고자 했습니다.
API와 데이터베이스 DDL의 상세 설계를 하였습니다.
실시간으로 재무제표와 마켓 데이터를 업데이트할 수 있는 이벤트 기반 파이프라인 또한 설계를 하였습니다.

지난 포스팅 리뷰

FMP에서 사용한 API 요약

API 구분 Endpoint (Base URL) 주요 파라미터 데이터 용도
일별 종가 데이터 /historical-price-eod/full symbol 상장 이후 전체 주가 이력 수집 (수익률 계산 및 백테스팅용)
현금흐름표 /cash-flow-statement limit=5, period=annual 최근 5년치 연간 현금흐름 분석 (영업현금흐름, FCF 확인용)
재무상태표 /balance-sheet-statement limit=5, period=annual 최근 5년치 연간 자산/부채/자본 확인 (PBR, 부채비율 계산용)

위의 표에 나와있는 api들을 이용하여 입력에 대해 출력을 뽑아낼 수 있는 데이터를 저장하려고 했다.

시스템 설계

단순한 CRUD 시스템이라고 생각이 되었고, Spring서버와 pg(PostgreSQL)를 사용한다.
흥미를 가지고 진행하기위해, 먼저 빠르게 PoC를 쳐내고, 이후에 최적화나 설계를 변경하는 방식으로 하려한다.

fmp API 상세

FMP api 명세 링크에서 지원하는 API 명세서를 확인할 수 있다.
필요한 api는 아래와 같다.

  1. Ticker의 리스트 (APPL, TSLA ... 등 주식의 id의 리스트)
  2. Ticker에 대응하는 5개년치 재무제표, 현금 흐름표
  3. Ticker에 대응하는 5개년치 가격 정보

명세

  1. Ticker의 리스트 (APPL, TSLA ... 등 주식의 id의 리스트)
    • 해당 명세는 찾을 수가 없었다. Ticker를 찾기 위해 위키피디아를 크롤링하였다.
    • S&P 500 외에도, 나스닥 100, 다우존스의 리스트를 크롤링하였다.
  2. 재무제표, 현금흐름표의 경우 걸려있는 링크와 같이 찾을 수 있었다.
  3. 마지막으로 가격정보 또한 링크와 같이 찾을 수 있었다.

예외처리

tickers_curr.json의 일부 데이터

  [
    { "ticker": "A", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AAPL", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ABBV", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ABNB", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ABT", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ACGL", "country": "Bermuda", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ACN", "country": "Ireland", "currency": "EUR", "usd_to_local_ratio": 0.8525 },
    { "ticker": "ADBE", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ADI", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ADM", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ADP", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ADSK", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AEE", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AEP", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AES", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AFL", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AIG", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AIZ", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AJG", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AKAM", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ALB", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ALGN", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ALL", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "ALLE", "country": "Ireland", "currency": "EUR", "usd_to_local_ratio": 0.8525 },
    { "ticker": "AMAT", "country": "USA", "currency": "USD", "usd_to_local_ratio": 1.0 },
    { "ticker": "AMCR", "country": "Switzerland", "currency": "CHF", "usd_to_local_ratio": 0.8842 }
    ]

데이터 스키마 설계

두가지의 테이블을 구상하였다.

PG의 타입과 연산 비교

항목 DOUBLE PRECISION DECIMAL / NUMERIC INTEGER
연산 속도 매우 빠름 (기준점) FPU 약 10~50배 느림 소프트웨어 가장 빠름 ALU
저장 효율 매우 좋음 (고정 8자) 보통 (숫자가 클수록 커짐) 4바이트로 저장하므로
DOUBLE 보다 최적화를 할 여지가 있음
정확도 근사치 (소수점 오차 가능성) 완벽한 정확도 완벽한 정확도

스키마 DDL

스키마 DDL

  -- 기존 테이블 초기화
DROP TABLE IF EXISTS market_prices CASCADE;
DROP TABLE IF EXISTS company_fundamentals CASCADE;

-- 1. Market Data (일봉 데이터 + 데일리 밸류에이션)
CREATE TABLE market_prices (
ticker VARCHAR(10),
trade_date DATE,
open_price DOUBLE PRECISION,
high_price DOUBLE PRECISION,
low_price DOUBLE PRECISION,
close_price DOUBLE PRECISION NOT NULL,
volume BIGINT,

per DOUBLE PRECISION,
pbr DOUBLE PRECISION,
psr DOUBLE PRECISION,
fcf_yield DOUBLE PRECISION,

updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (ticker, trade_date)
);

-- 2. Fundamentals
CREATE TABLE company_fundamentals (
ticker VARCHAR(10),
report_period DATE,
filing_date DATE NOT NULL,

revenue BIGINT,
operating_income BIGINT,
net_income BIGINT,
eps DOUBLE PRECISION,
shares_outstanding BIGINT,

fcf BIGINT,
total_assets BIGINT,
total_debt BIGINT,
total_equity BIGINT,

per DOUBLE PRECISION,
pbr DOUBLE PRECISION,
psr DOUBLE PRECISION,
market_cap BIGINT,

roe DOUBLE PRECISION,
roic DOUBLE PRECISION,
opm DOUBLE PRECISION,
debt_to_equity DOUBLE PRECISION,

revenue_growth DOUBLE PRECISION,
eps_growth DOUBLE PRECISION,
fcf_yield DOUBLE PRECISION,

consecutive_growth_years INT,

updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (ticker, report_period)
);
CREATE INDEX idx_market_prices_covering ON market_prices (ticker, trade_date);

- (trade_date, ticker) 인덱스를 추가하여 조회 속도 비교 필요

API 설계

goal

non-goal

명세

Field 설명
user_id 요청을 전송한 유저의 id
indicators 벡테스팅 및 원하는 주식에 대한 지표
Field 설명
yoy 지난 연도 대비 성장률
eps 주당 이익률
opm 영업이익률
opm_diff 영업이익 성장률
debt 부채
min_per 최소 주가수익비율 (주가와 순이익의 비율)
max_per 최대 주가수익비율
sell_cut 처음 산 가격대비 어느정도 떨어지면 손절할지에 대한 손절라인
year 최대 얼마나 들고 있을지에 대한 년
max_pbr 주가순자산비율 (주가와 자산의 비율)
max_psr 주가매출비율 (주가와 매출의 비율)
fcf_yield 영업현금흐름에서 자본지출을 뺀 것을 잉여현금흐름이라고 하는데,
이를 시가총액으로 나눈것
roic 부채 + 자본대비 얼마나 영업이익을 냈는지 측정하는 지표

cron 설계

주식 데이터는 실시간성이 중요하므로, 하루 주가 및 재무제표가 업데이트가 될때마다 지연시간 없이 디비에 잘 넣어주는 것이 중요하다.

마켓 데이터의 경우는 휴장일이 아닌 경우 미국 주식 시장 폐장 시간(한국 시간 새벽 4~5시)의 한시간뒤에 특정 타임윈도우 단위로 재시작하는 로직을 사용할 수 있다.
또한 오전 8시에 최종적으로 데이터를 확인하고, 누락된 배치를 마지막으로 더 돌리는 Double Check 도 사용가능하다.

재무제표 데이터의 경우는 기업마다 서로 다른 시기에 올리므로 처리가 더 까다롭다.
fmp에서 재무제표가 올라온 경우 rss-feed 형식의 api로 지원했으나, 지금은 지원을 더이상하지 않는다.

SEC-rss feed

그래서 직접 미국 정부 홈페이지에서 재무제표에 대한 알림을 받고, 그를 이벤트 기반 아키텍쳐로 저장하는 방식의 처리를 구상하였다.

  1. 아주 가벼운 웹서버를 이용해 최신 공시 이벤트를 찌른다.
  2. 최신 공시가 발생할 경우, 해당 날짜와 티커를 kafka의 key로 적용해두어 중복처리가 되지 않게 설정한다.
  3. key가 중복되지 않을 경우, kafka에 push하여 최신 공시를 처리하라는 알림을 제공한다.
  4. Batch 어플리케이션은 그 이벤트가 발생할시, SEC의 CIK 에서 ticker로 변경 및 fmp api에서 해당 공시를 확인하고 디비에 집어넣는등 무거운 작업을 처리한다.