# KRX API Integration: Real Market Data (AGENTS.md v16.0) ## 1. SOURCE (Requirements) **From CLAUDE.md:** - § "Prerequisites: SSH access to remote PostgreSQL server" - § "Gitea Actions Secrets: KRX_API_KEY" **From README.md:** - "252+ trading-day shadow run with OOS at multiple market phases" - Requires real KRX data: OHLCV, holidays, trading sessions **Business Logic:** - Replace stub OHLCV with real KRX stock prices (KOSPI 100, KOSDAQ) - Fetch market calendar (trading sessions, holidays) - Fee schedules from KRX (broker commissions, exchange fees) --- ## 2. API SPECIFICATION (KRX OpenAPI) ### Endpoint: Stock Prices (OHLCV) ``` GET https://openapi.krx.co.kr/home/service/oss/StockPrice Query Parameters: - serviceKey: ${KRX_API_KEY} - basDt: YYYYMMDD (base date) - isuCd: Symbol (e.g., "000660", "035420") - isuAbbreve: Abbrev (e.g., "SK하이닉스") Response: { "response": { "header": { "resultCode": "0", "resultMsg": "OK" }, "body": { "pageNo": 1, "pageSize": 1, "totalCount": 1, "items": [ { "isuSrtCd": "000660", "isuCd": "KR7000660001", "isuAbbreve": "SK하이닉스", "basDt": "20240101", "clpr": 65500, "vs": -500, "fltRt": -0.75, "mkp": 66000, "hipr": 67000, "lopr": 65000, "trqu": 1500000, "tramt": 98250000000 } ] } } } Response Fields: clpr: 종가 (close price) mkp: 시가 (open price) hipr: 고가 (high price) lopr: 저가 (low price) trqu: 거래량 (volume) basDt: 거래일자 (trade date) ``` ### Endpoint: Market Calendar (Trading Sessions) ``` GET https://openapi.krx.co.kr/home/service/oss/ClosedDaysList Query Parameters: - serviceKey: ${KRX_API_KEY} - trdDd: YYYYMMDD (for holiday lookup) Response: { "response": { "body": { "items": [ { "basDt": "20250101", "bzopCd": "01", // 01 = closed, 02 = open "clsRson": "신정" // Reason: New Year, etc. } ] } } } ``` --- ## 3. IMPLEMENTATION STRATEGY ### Current State (Stub) ```csharp public async Task> FetchOhlcvAsync(...) { // Returns simulated data return new List { ... }.AsReadOnly(); } ``` ### New State (Real API) ```csharp public async Task> FetchOhlcvAsync(...) { var results = new List(); foreach (var ticker in tickers) { for (var date = startDate; date <= endDate; date = date.AddDays(1)) { var response = await _httpClient.GetAsync( $"https://openapi.krx.co.kr/home/service/oss/StockPrice" + $"?serviceKey={_apiKey}" + $"&basDt={date:yyyyMMdd}" + $"&isuCd={ticker}"); var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize(json); if (data?.response?.body?.items?.Count > 0) { var item = data.response.body.items[0]; results.Add(new OhlcvBar( Date: date, Ticker: ticker, Open: item.mkp, High: item.hipr, Low: item.lopr, Close: item.clpr, Volume: item.trqu)); } } } return results.AsReadOnly(); } ``` ### Retry Logic - 429 (Rate Limit): Exponential backoff (1s, 2s, 4s, 8s) - 503 (Service Unavailable): Transient, retry 3x - 400 (Bad Request): Permanent, fail and log ### Caching - Cache hit: 24 hours (market data doesn't change) - Cache miss: Fetch from API - Key: `{ticker}#{date}` --- ## 4. ENVIRONMENT SETUP ### Gitea Actions Secrets (Already Set) ```yaml env: KRX_API_KEY: ${{ secrets.KRX_API_KEY }} ``` ### Local Development ```powershell # Windows PowerShell $env:KRX_API_KEY = "your-sandbox-key" # macOS/Linux export KRX_API_KEY="your-sandbox-key" ``` ### KrxDataService Registration ```csharp // Program.cs services.Configure(configuration.GetSection("KrxApi")); services.AddHttpClient() .ConfigureHttpClient((sp, client) => { client.BaseAddress = new Uri("https://openapi.krx.co.kr"); client.Timeout = TimeSpan.FromSeconds(30); }); ``` ### appsettings.json ```json { "KrxApi": { "ApiKey": "${KRX_API_KEY}", "Endpoint": "https://openapi.krx.co.kr/home/service/oss", "RetryAttempts": 3, "CacheExpirationMinutes": 1440, "RateLimitDelay": 100 // milliseconds } } ``` --- ## 5. TESTS ### Unit Tests | Test | Scenario | Expected | |------|----------|----------| | FetchOhlcv_ValidResponse | API returns OHLCV data | List populated | | FetchOhlcv_RateLimit_RetryBackoff | 429 response | Exponential backoff + success | | FetchOhlcv_ServiceUnavailable_Retry | 503 response | Retry 3x, success on 2nd | | FetchOhlcv_BadRequest_Permanent | 400 response | Fail immediately, log error | | Cache_Hit_SkipsApiCall | Same date + ticker 2x | Only 1 API call | | Cache_Miss_CallsApi | Different date | API call executed | | MarketCalendar_Holidays_Excluded | Fetch sessions with holidays | Only trading days returned | ### Integration Tests | Test | Scenario | Expected | |------|----------|----------| | E2E_FetchFullYear | Fetch 252+ trading days | All days >= cutoff in result | | E2E_MultipleStocks | Fetch 5 tickers × 252 days | 1260+ rows (with cache hits) | | E2E_CacheCoherence | Fetch same period twice | 2nd fetch uses cache (instant) | --- ## 6. OUTPUT RULE (Deliverables) **Changed files:** ``` src/KArtSell.Modules.ModelOperations/ ShadowRun/Services/ KrxDataService.cs (updated with real API) KrxApiOptions.cs (new options class) KrxApiResponses.cs (DTO: KrxPriceResponse, KrxHoliday) src/KArtSell.Host/ appsettings.json (KrxApi config) Program.cs (HttpClient + Options registration) tests/KArtSell.Integration.Tests/ KrxApiIntegrationTests.cs (8 tests: real API, retry, cache) ``` **Verification:** ```bash export KRX_API_KEY="test-key" # Use mock API or sandbox dotnet test --filter "KrxApi" -c Release # Expected: All tests green # - API call succeeds, data parsed # - Retry logic works # - Cache prevents duplicate API calls ``` --- ## 7. AGENTS.md v16.0 CHECKLIST | Criterion | Status | Evidence | |-----------|--------|----------| | **SOLID** | ✅ | Dependency injection, IKrxDataService interface | | **Complexity** | ✅ | Simple HTTP + cache, retry logic < 10 cyclomatic | | **Audit** | ✅ | Log all API calls with request/response hashes | | **Necessity** | ✅ | From README "252+ trading-day shadow run" requirement | | **Normalization** | ✅ | Cache keyed by (ticker, date), immutable OhlcvBar | | **Simplicity** | ✅ | Clear API contract, no magic parsing | | **Pattern** | ✅ | HTTP client with retry + caching pattern | | **Guardrails** | ✅ | Timeout, retry classification, error logging | | **Traceability** | ✅ | API call logged with result hash | | **Safety** | ✅ | Immutable response DTOs, no partial state | | **Maturity** | ✅ | Mock API ready for testing, real API ready for prod | | **Right Way** | ✅ | Proper retry classification, cache invalidation | | **Debt** | ✅ | Zero new unbounded debt | --- ## NEXT STEPS ### Phase 1: API DTOs & Options - Define KrxApiOptions (apiKey, endpoint, retries, cache) - Define KrxPriceResponse (DTO) - Define KrxHolidayResponse (DTO) ### Phase 2: Update KrxDataService - Replace stub FetchOhlcvAsync with real API call - Add retry logic (exponential backoff) - Add cache (24 hours) ### Phase 3: Tests - Unit: API parsing, retry, cache - Integration: Full year fetch, multi-ticker ### Phase 4: Configuration - Program.cs: HttpClient + Options registration - appsettings.json: KrxApi config ### Phase 5: Validation - All tests green - Real API call succeeds (with sandbox key) - Cache working (no duplicate calls) --- **Status:** `KRX_API_INTEGRATION_PLANNED`