Skip to content

Commit 6526496

Browse files
committed
Standardize overseas stock symbol formats across systems
- Introduce symbol conversion utilities (`to_kis_symbol`, `to_yahoo_symbol`, `to_db_symbol`) in `symbol.py` for consistent formatting between DB, KIS API, and Yahoo Finance. - Migrate existing DB data to standardized `.` format using `migrate_symbols_to_dot_format.sql`. - Update services and tasks to handle symbol normalization automatically: KIS APIs, Yahoo Finance fetch, trading services, and manual holdings comparisons. - Add comprehensive tests for symbol conversion utilities and real-world scenario validations.
1 parent 0962250 commit 6526496

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed

CLAUDE.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,47 @@ stock_info (마스터 테이블) stock_analysis_results (분석 결과)
156156
- 최신 분석: Correlated Subquery 또는 Window Function 사용
157157
- 히스토리: `stock_info_id`로 JOIN하여 시간순 정렬
158158

159+
### 해외주식 심볼 변환 시스템
160+
161+
**배경:** 해외주식 심볼은 서비스마다 다른 구분자를 사용함 (예: 버크셔 해서웨이 B)
162+
- Yahoo Finance: `BRK-B` (하이픈)
163+
- 한국투자증권 API: `BRK/B` (슬래시)
164+
- DB 저장 형식: `BRK.B` (점) ← **기준**
165+
166+
**구조:**
167+
```
168+
app/core/symbol.py # 심볼 변환 유틸리티
169+
├── to_kis_symbol() # DB → KIS API (. → /)
170+
├── to_yahoo_symbol() # DB → Yahoo Finance (. → -)
171+
└── to_db_symbol() # 외부 → DB (- 또는 / → .)
172+
```
173+
174+
**적용된 파일:**
175+
- `app/services/kis.py` - KIS API 호출 시 자동 변환
176+
- `app/services/yahoo.py` - Yahoo Finance 호출 시 자동 변환
177+
- `app/tasks/kis.py` - 심볼 비교 시 정규화
178+
- `app/services/kis_holdings_service.py` - 보유주식 조회 시 정규화
179+
- `app/services/kis_trading_service.py` - 매도 주문 시 정규화
180+
181+
**DB 테이블 (해외주식 심볼 저장):**
182+
| 테이블 | 컬럼 | 설명 |
183+
|--------|------|------|
184+
| `stock_info` | `symbol` | 종목 마스터 |
185+
| `manual_holdings` | `ticker` | 수동 잔고 (토스 등) |
186+
| `stock_aliases` | `ticker` | 종목 별칭 매핑 |
187+
| `symbol_trade_settings` | `symbol` | 종목별 거래 설정 |
188+
189+
**마이그레이션:** 기존 데이터가 `-` 또는 `/` 형식이면 `.` 형식으로 변환 필요
190+
```bash
191+
# scripts/migrate_symbols_to_dot_format.sql 실행
192+
psql -d your_db -f scripts/migrate_symbols_to_dot_format.sql
193+
```
194+
195+
**테스트:**
196+
```bash
197+
uv run pytest tests/test_symbol_conversion.py -v
198+
```
199+
159200
### API 서비스 클라이언트
160201

161202
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- 해외주식 심볼 형식 정규화 마이그레이션
2+
-- 하이픈(-) 또는 슬래시(/) 형식을 점(.) 형식으로 변환
3+
--
4+
-- 실행 전 백업 권장:
5+
-- pg_dump -t stock_info -t manual_holdings -t stock_aliases -t symbol_trade_settings your_db > backup.sql
6+
7+
-- 1. stock_info 테이블 (해외주식만 - instrument_type이 equity_us인 경우)
8+
UPDATE stock_info
9+
SET symbol = REPLACE(REPLACE(symbol, '-', '.'), '/', '.')
10+
WHERE instrument_type = 'equity_us'
11+
AND (symbol LIKE '%-%' OR symbol LIKE '%/%');
12+
13+
-- 2. manual_holdings 테이블 (해외주식만 - market_type이 US인 경우)
14+
UPDATE manual_holdings
15+
SET ticker = REPLACE(REPLACE(ticker, '-', '.'), '/', '.')
16+
WHERE market_type = 'US'
17+
AND (ticker LIKE '%-%' OR ticker LIKE '%/%');
18+
19+
-- 3. stock_aliases 테이블 (해외주식만 - market_type이 US인 경우)
20+
UPDATE stock_aliases
21+
SET ticker = REPLACE(REPLACE(ticker, '-', '.'), '/', '.')
22+
WHERE market_type = 'US'
23+
AND (ticker LIKE '%-%' OR ticker LIKE '%/%');
24+
25+
-- 4. symbol_trade_settings 테이블 (해외주식만 - instrument_type이 equity_us인 경우)
26+
UPDATE symbol_trade_settings
27+
SET symbol = REPLACE(REPLACE(symbol, '-', '.'), '/', '.')
28+
WHERE instrument_type = 'equity_us'
29+
AND (symbol LIKE '%-%' OR symbol LIKE '%/%');
30+
31+
-- 변경된 레코드 확인
32+
SELECT 'stock_info' as table_name, COUNT(*) as count
33+
FROM stock_info
34+
WHERE instrument_type = 'equity_us' AND symbol LIKE '%.%'
35+
UNION ALL
36+
SELECT 'manual_holdings', COUNT(*)
37+
FROM manual_holdings
38+
WHERE market_type = 'US' AND ticker LIKE '%.%'
39+
UNION ALL
40+
SELECT 'stock_aliases', COUNT(*)
41+
FROM stock_aliases
42+
WHERE market_type = 'US' AND ticker LIKE '%.%'
43+
UNION ALL
44+
SELECT 'symbol_trade_settings', COUNT(*)
45+
FROM symbol_trade_settings
46+
WHERE instrument_type = 'equity_us' AND symbol LIKE '%.%';

tests/test_symbol_conversion.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""
2+
해외주식 심볼 변환 유틸리티 테스트
3+
4+
심볼 형식:
5+
- DB (기준): `.` (예: BRK.B)
6+
- Yahoo Finance: `-` (예: BRK-B)
7+
- KIS API: `/` (예: BRK/B)
8+
"""
9+
import pytest
10+
11+
from app.core.symbol import to_db_symbol, to_kis_symbol, to_yahoo_symbol
12+
13+
14+
class TestToKisSymbol:
15+
"""to_kis_symbol 함수 테스트 (DB → KIS API)"""
16+
17+
def test_converts_dot_to_slash(self):
18+
"""`.`을 `/`로 변환"""
19+
assert to_kis_symbol("BRK.B") == "BRK/B"
20+
assert to_kis_symbol("BRK.A") == "BRK/A"
21+
22+
def test_simple_symbol_unchanged(self):
23+
"""구분자 없는 심볼은 그대로 유지"""
24+
assert to_kis_symbol("AAPL") == "AAPL"
25+
assert to_kis_symbol("NVDA") == "NVDA"
26+
assert to_kis_symbol("TSLA") == "TSLA"
27+
28+
def test_multiple_dots(self):
29+
"""다중 `.` 처리"""
30+
assert to_kis_symbol("A.B.C") == "A/B/C"
31+
32+
def test_empty_string(self):
33+
"""빈 문자열 처리"""
34+
assert to_kis_symbol("") == ""
35+
36+
37+
class TestToYahooSymbol:
38+
"""to_yahoo_symbol 함수 테스트 (DB → Yahoo Finance)"""
39+
40+
def test_converts_dot_to_hyphen(self):
41+
"""`.`을 `-`로 변환"""
42+
assert to_yahoo_symbol("BRK.B") == "BRK-B"
43+
assert to_yahoo_symbol("BRK.A") == "BRK-A"
44+
45+
def test_simple_symbol_unchanged(self):
46+
"""구분자 없는 심볼은 그대로 유지"""
47+
assert to_yahoo_symbol("AAPL") == "AAPL"
48+
assert to_yahoo_symbol("NVDA") == "NVDA"
49+
50+
def test_multiple_dots(self):
51+
"""다중 `.` 처리"""
52+
assert to_yahoo_symbol("A.B.C") == "A-B-C"
53+
54+
def test_empty_string(self):
55+
"""빈 문자열 처리"""
56+
assert to_yahoo_symbol("") == ""
57+
58+
59+
class TestToDbSymbol:
60+
"""to_db_symbol 함수 테스트 (외부 형식 → DB)"""
61+
62+
def test_converts_hyphen_to_dot(self):
63+
"""Yahoo 형식 `-`를 `.`으로 변환"""
64+
assert to_db_symbol("BRK-B") == "BRK.B"
65+
assert to_db_symbol("BRK-A") == "BRK.A"
66+
67+
def test_converts_slash_to_dot(self):
68+
"""KIS 형식 `/`를 `.`으로 변환"""
69+
assert to_db_symbol("BRK/B") == "BRK.B"
70+
assert to_db_symbol("BRK/A") == "BRK.A"
71+
72+
def test_simple_symbol_unchanged(self):
73+
"""구분자 없는 심볼은 그대로 유지"""
74+
assert to_db_symbol("AAPL") == "AAPL"
75+
assert to_db_symbol("NVDA") == "NVDA"
76+
77+
def test_already_dot_format(self):
78+
"""이미 `.` 형식인 심볼은 그대로 유지"""
79+
assert to_db_symbol("BRK.B") == "BRK.B"
80+
81+
def test_mixed_separators(self):
82+
"""혼합 구분자 처리 (모두 `.`로 변환)"""
83+
assert to_db_symbol("A-B/C") == "A.B.C"
84+
85+
def test_empty_string(self):
86+
"""빈 문자열 처리"""
87+
assert to_db_symbol("") == ""
88+
89+
90+
class TestRoundTrip:
91+
"""왕복 변환 테스트"""
92+
93+
@pytest.mark.parametrize("symbol", [
94+
"AAPL",
95+
"BRK.B",
96+
"NVDA",
97+
"TSLA",
98+
"A.B.C",
99+
])
100+
def test_kis_roundtrip(self, symbol: str):
101+
"""DB → KIS → DB 왕복 변환"""
102+
kis_symbol = to_kis_symbol(symbol)
103+
back_to_db = to_db_symbol(kis_symbol)
104+
assert back_to_db == symbol
105+
106+
@pytest.mark.parametrize("symbol", [
107+
"AAPL",
108+
"BRK.B",
109+
"NVDA",
110+
"TSLA",
111+
])
112+
def test_yahoo_roundtrip(self, symbol: str):
113+
"""DB → Yahoo → DB 왕복 변환"""
114+
yahoo_symbol = to_yahoo_symbol(symbol)
115+
back_to_db = to_db_symbol(yahoo_symbol)
116+
assert back_to_db == symbol
117+
118+
119+
class TestRealWorldSymbols:
120+
"""실제 사용되는 심볼 테스트"""
121+
122+
@pytest.mark.parametrize("db_symbol,kis_symbol,yahoo_symbol", [
123+
("BRK.B", "BRK/B", "BRK-B"), # 버크셔 해서웨이 B
124+
("BRK.A", "BRK/A", "BRK-A"), # 버크셔 해서웨이 A
125+
("AAPL", "AAPL", "AAPL"), # 애플 (구분자 없음)
126+
("NVDA", "NVDA", "NVDA"), # 엔비디아 (구분자 없음)
127+
("CONY", "CONY", "CONY"), # CONY ETF (구분자 없음)
128+
])
129+
def test_symbol_conversions(self, db_symbol: str, kis_symbol: str, yahoo_symbol: str):
130+
"""실제 심볼 변환 검증"""
131+
assert to_kis_symbol(db_symbol) == kis_symbol
132+
assert to_yahoo_symbol(db_symbol) == yahoo_symbol
133+
assert to_db_symbol(kis_symbol) == db_symbol
134+
assert to_db_symbol(yahoo_symbol) == db_symbol

0 commit comments

Comments
 (0)