3. 시스템 설계 및 엔지니어링 목표
지난 포스팅 리뷰
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, 부채비율 계산용) |
- 입력
- ROIC(마진의 간접적인 지표), FCF(현금흐름)등의 지표들 (이하 필터)
- 보유기간
- 출력
- 필터에 맞는 기업들의 평균 수익률
- 필터에 맞는 최신 기업들 리스팅
위의 표에 나와있는 api들을 이용하여 입력에 대해 출력을 뽑아낼 수 있는 데이터를 저장하려고 했다.
시스템 설계
단순한 CRUD 시스템이라고 생각이 되었고, Spring서버와 pg(PostgreSQL)를 사용한다.
흥미를 가지고 진행하기위해, 먼저 빠르게 PoC를 쳐내고, 이후에 최적화나 설계를 변경하는 방식으로 하려한다.
fmp API 상세
FMP api 명세 링크에서 지원하는 API 명세서를 확인할 수 있다.
필요한 api는 아래와 같다.
- Ticker의 리스트 (APPL, TSLA ... 등 주식의 id의 리스트)
- Ticker에 대응하는 5개년치 재무제표, 현금 흐름표
- Ticker에 대응하는 5개년치 가격 정보
명세
- Ticker의 리스트 (APPL, TSLA ... 등 주식의 id의 리스트)
- 해당 명세는 찾을 수가 없었다. Ticker를 찾기 위해 위키피디아를 크롤링하였다.
- S&P 500 외에도, 나스닥 100, 다우존스의 리스트를 크롤링하였다.
- 재무제표, 현금흐름표의 경우 걸려있는 링크와 같이 찾을 수 있었다.
- 마지막으로 가격정보 또한 링크와 같이 찾을 수 있었다.
예외처리
- Ticker 리스트에서 버크셔 해서웨이는 BRK.B 로 나와있는데, fmp와 호환되게 BRK-B 로 바꿔야한다.
- 재무제표와 현금흐름표의 경우, 해당 회사의 국가 기준으로 작성되기때문에 변환 작업이 필요하다.
- tickers_curr.json 파일을 만들어 Ticker와 달러와의 환율을 미리 저장해둔다.
- 데이터베이스에 저장할때 환율을 곱하여 달러기준으로 저장하여 데이터 일관성을 유지한다.
- 가격정보의 경우는 국가와 상관없이 달러로 표시되므로 그대로 사용하여도 무방하다.
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 }
]
데이터 스키마 설계
두가지의 테이블을 구상하였다.
- market_prices table : 매일매일의 가격을 저장하는 테이블
- company_fundamentals : 재무제표와 현금흐름표의 정보를 저장하는 테이블
또한 가격등의 정보를 저장할때 Double을 사용한다.
Double의 경우 CPU내부에 부동소수점을 담당하는 FPU가 있어 속도가 매우 빠르다.
반면 Decimal의 경우는 pg가 sw단에서 계산하기 때문에 느리다고 알려져있다.
프로젝트의 목적이 직접 트레이딩을 하는 것이 아니라, 분석의 보조 도구이기 때문에 실수의 오차 누적이 허용가능하다고 생각했다.
PG에서 DOUBLE과 DECIMAL의 연산 비교
| 항목 | DOUBLE PRECISION | DECIMAL / NUMERIC |
|---|---|---|
| 연산 속도 | 매우 빠름 (기준점) | 약 10~50배 느림 |
| 저장 효율 | 매우 좋음 (고정 8자) | 보통 (숫자가 클수록 커짐) |
| 정확도 | 근사치 (소수점 오차 가능성) | 완벽한 정확도 |
스키마 설계
이런이런 요소들 넣음
인덱스는 PK만
필터링 조건 = 컬럼
스키마 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);
예외 처리
- 휴장일