diff --git a/README.md b/README.md new file mode 100644 index 0000000..acaa3ee --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# stock-common + +한국 주식 분석 마이크로서비스 플랫폼의 **공유 라이브러리**. 모든 백엔드 서비스(`stock-dart-collector`, `stock-kis-collector`, `stock-screener` 등)가 이 패키지에 의존합니다. + +## 주요 기능 + +- **데이터 모델**: Pydantic v2 기반 도메인 모델 (Stock, DailyPrice, Valuation, Financial, Screening 등) +- **데이터베이스**: PostgreSQL (asyncpg) + MongoDB (motor) 커넥션 풀 관리 +- **Redis Streams**: 서비스 간 비동기 메시지 큐 (publish/consume/ack) +- **API 팩토리**: FastAPI 앱 생성 (`/health`, `/streams` 엔드포인트 자동 포함) +- **Collector 베이스 클래스**: Rate limiting, 자동 재시도, HTTP 세션 관리 +- **설정 관리**: pydantic-settings 기반 환경변수 로딩 + +## 설치 + +```bash +pip install -e . +``` + +다른 서비스에서 의존성으로 사용: + +```toml +# pyproject.toml +[project] +dependencies = ["stock-common"] +``` + +Docker 빌드 시: + +```dockerfile +COPY stock-common/ /tmp/stock-common/ +RUN pip install --no-cache-dir /tmp/stock-common/ +``` + +## 모듈 구조 + +``` +src/stock_common/ +├── __init__.py +├── config.py # Settings (pydantic-settings) - 환경변수 관리 +├── logging_config.py # structlog 기반 로깅 설정 +├── api_base.py # FastAPI 앱 팩토리 (create_app) +├── collector_base.py # BaseCollector ABC (rate limit, retry) +├── database/ +│ ├── __init__.py +│ ├── postgres.py # asyncpg 커넥션 풀 (get_pg_pool) +│ └── mongodb.py # motor 클라이언트 (get_mongo_database) +├── queue/ +│ ├── __init__.py +│ ├── redis_client.py # Redis 싱글턴 (get_redis) +│ └── streams.py # publish / consume / ack +└── models/ + ├── __init__.py + ├── stock.py # Stock, Market + ├── price.py # DailyPrice + ├── valuation.py # Valuation (PER, PBR, ROE...) + ├── financial.py # Financial, PeriodType + ├── screening.py # ScreeningResult, ScreeningStrategy + ├── analysis.py # LLMAnalysis, Recommendation + ├── disclosure.py # Disclosure, Sentiment + └── news.py # News +``` + +## 핵심 컴포넌트 + +### Settings (config.py) + +pydantic-settings로 환경변수를 자동 로딩합니다. `.env` 파일도 지원합니다. + +```python +from stock_common.config import settings + +# 데이터베이스 +settings.postgres_dsn # postgresql://stock:stock@localhost:5432/stockdb +settings.mongodb_uri # mongodb://localhost:27017 +settings.redis_url # redis://localhost:6379/0 + +# API 키 +settings.dart_api_key +settings.kis_app_key +settings.anthropic_api_key + +# Rate Limits +settings.dart_rate_limit # 10.0 req/s +settings.kis_rate_limit # 20.0 req/s +``` + +### API 팩토리 (api_base.py) + +모든 서비스는 `create_app()`으로 FastAPI 인스턴스를 생성합니다. 자동으로 포함되는 엔드포인트: + +| 엔드포인트 | 설명 | +|-----------|------| +| `GET /health` | 서비스 상태, Redis 연결, uptime | +| `GET /streams` | 관련 Redis Stream 큐 길이 | + +```python +from stock_common.api_base import create_app + +app = create_app( + title="stock-dart-collector", + streams=["queue:trigger-dart", "queue:raw-financials"], +) + +# 서비스별 엔드포인트 추가 +@app.post("/collect/financials") +async def collect_financials(...): + ... +``` + +### BaseCollector (collector_base.py) + +HTTP API 수집기의 공통 패턴: + +- **Rate Limiting**: `aiolimiter.AsyncLimiter`로 초당 요청 수 제한 +- **자동 재시도**: `tenacity`로 최대 3회, 지수 백오프 (1~30초) +- **429 처리**: `Retry-After` 헤더 존중 +- **세션 관리**: `async with` 컨텍스트 매니저 + +```python +from stock_common.collector_base import BaseCollector + +class DARTCollector(BaseCollector): + def __init__(self): + super().__init__(rate_limit=10.0, timeout=30) + + async def collect(self, **kwargs): + data = await self._request("GET", url, params=params) + binary = await self._download_binary(url) +``` + +### Redis Streams (queue/streams.py) + +서비스 간 비동기 메시지 전달: + +```python +from stock_common.queue import publish, consume, ack + +# 메시지 발행 +msg_id = await publish("queue:trigger-dart", {"type": "financials", "stock_codes": ["005930"]}) + +# 메시지 소비 (consumer group) +messages = await consume("queue:trigger-dart", "dart-collectors", "worker-1", count=10, block=5000) +for msg in messages: + process(msg.data) + await ack("queue:trigger-dart", "dart-collectors", msg.message_id) +``` + +### Database + +```python +from stock_common.database import get_pg_pool, get_mongo_database + +# PostgreSQL (asyncpg) +pool = await get_pg_pool() +async with pool.acquire() as conn: + rows = await conn.fetch("SELECT * FROM stock WHERE is_active = true") + +# MongoDB (motor) +db = get_mongo_database() +cursor = db.llm_analysis.find({"stock_code": "005930"}) +docs = await cursor.to_list(10) +``` + +## 의존성 + +| 패키지 | 용도 | +|--------|------| +| `pydantic` >= 2.0 | 데이터 모델 | +| `pydantic-settings` >= 2.0 | 환경변수 설정 | +| `asyncpg` >= 0.29 | PostgreSQL 비동기 드라이버 | +| `motor` >= 3.3 | MongoDB 비동기 드라이버 | +| `redis` >= 5.0 | Redis 클라이언트 | +| `fastapi` >= 0.115 | REST API 프레임워크 | +| `uvicorn` >= 0.34 | ASGI 서버 | +| `aiohttp` >= 3.9 | 비동기 HTTP 클라이언트 | +| `aiolimiter` >= 1.1 | Rate limiting | +| `tenacity` >= 8.2 | 재시도 로직 | +| `structlog` >= 24.0 | 구조화된 로깅 | + +## 요구사항 + +- Python >= 3.12 +- 빌드: hatchling