# 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<IReadOnlyList<OhlcvBar>> FetchOhlcvAsync(...)
{
    // Returns simulated data
    return new List<OhlcvBar> { ... }.AsReadOnly();
}
```

### New State (Real API)
```csharp
public async Task<IReadOnlyList<OhlcvBar>> FetchOhlcvAsync(...)
{
    var results = new List<OhlcvBar>();
    
    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<KrxPriceResponse>(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<KrxApiOptions>(configuration.GetSection("KrxApi"));
services.AddHttpClient<IKrxDataService, KrxDataService>()
    .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<OhlcvBar> 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`
