4 Commits

Author SHA1 Message Date
kjh2064 227b563ba2 docs(ui): UI 표준을 MudBlazor + Interactive WebAssembly + API-First 로 전환
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 5s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 8s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
Fluent UI Blazor v5 / InteractiveServer 방침을 폐기하고 MudBlazor 컴포넌트 +
Interactive WebAssembly 렌더 모드 + API-First 를 신규 표준으로 확정한다.
기존 CLAUDE.md(Fluent UI)와 AGENTS.md §5b(MudBlazor)의 상충을 해소한다.

- CLAUDE.md: Framework & Design System, Component Rules, 매핑표를 MudBlazor 로 갱신
- AGENTS.md §5b: 렌더 모드 표준(Interactive WebAssembly) 신설, Server 표기 정렬
- ROADMAP_WBS.md: WBS-10 보강 문서 상호 참조 링크 추가
- WBS_10_DOTNET_MIGRATION_HARDENING: 마이그레이션 완성/상용화 로드맵 신규,
  UI 코드 전환을 WBS-A7 로 등록

코드 전환(csproj/Program.cs/.razor)은 미수행, 본 커밋은 방침 문서만 수정.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:03:26 +09:00
kjh2064 5c5d9bfee7 feat: KIS Open API 연동 및 DataCollectionService 구현
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 7s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 8s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Snapshot Admin Deployment / build-and-deploy (push) Failing after 46s
Deploy to Production / Build & Deploy to Production (push) Failing after 1m5s
- C# 기반의 DataCollectionService 클래스 구현

- 기존의 파이썬 스크립트 실행 방식을 대체하고 KIS API 클라이언트를 직접 사용하여 주식 시세, 호가, 공매도 정보 수집

- CollectionEndpoints에 비동기 수집 요청 처리 통합 및 Program.cs에 서비스 DI 등록
2026-06-29 23:39:21 +09:00
kjh2064 2220f9f807 docs(CLAUDE.md): Phase 2 95% 완료 상태 업데이트
- KIS API 클라이언트: 실제 구현 완료 (0 errors, 0 warnings)
- PostgreSQL 저장소: 완전 통합 (자동 테이블 생성, CRUD)
- Web API 엔드포인트: 6개 컬렉션 경로 완성
- Blazor UI: 대시보드 완성 (실시간 모니터링)
- 개발 명령어: 정확한 경로 + 포트 업데이트 (5265)
- 남은 일: kis_data_collection_v1.py 파이프라인 오케스트레이션 포팅

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-29 23:34:56 +09:00
kjh2064 c06c24d8bc fix(kis-api): Null reference 검증 강화 (토큰 응답 처리)
KisApiClient.TryGetAccessTokenAsync()의 null 참조 경고 제거.
- 토큰 응답 본문 존재 여부 검증
- TryGetValue 기반 안전한 파싱
- access_token 필수 필드 검증

Build: 0 errors, 0 warnings 

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-29 23:33:53 +09:00
8 changed files with 525 additions and 84 deletions
+2 -1
View File
@@ -135,7 +135,8 @@
- **임시 파일 관리**: 개발/디버깅 목적의 모든 휘발성 임시 파일 및 로그는 반드시 `Temp/` 디렉토리 하위에서만 생성해야 하며, 루트나 다른 패키지 경로에 임시 파일을 만드는 것은 금지한다. 불가피하게 생성할 경우 반드시 접두사/접미사 규칙(`debug_*`, `tmp_*`, `mock_*`, `*_temp.*`)을 준수하여 `.gitignore`에 필터링되도록 한다. - **임시 파일 관리**: 개발/디버깅 목적의 모든 휘발성 임시 파일 및 로그는 반드시 `Temp/` 디렉토리 하위에서만 생성해야 하며, 루트나 다른 패키지 경로에 임시 파일을 만드는 것은 금지한다. 불가피하게 생성할 경우 반드시 접두사/접미사 규칙(`debug_*`, `tmp_*`, `mock_*`, `*_temp.*`)을 준수하여 `.gitignore`에 필터링되도록 한다.
## 5b. Blazor & API-First 개발 규칙 (TaxBaik 참조 모델 적용) ## 5b. Blazor & API-First 개발 규칙 (TaxBaik 참조 모델 적용)
- **API-First 아키텍처**: Blazor Server UI 계층은 비즈니스 로직이나 DB에 직접 결합되지 않고, `IXxxBrowserClient` 등의 추상화된 API 클라이언트(HTTP/RESTful)를 통해서만 백엔드 API와 통신한다. - **렌더 모드 표준**: Blazor **Interactive WebAssembly** 를 기본 렌더 모드로 한다. InteractiveServer 는 사용하지 않으며, UI 컴포넌트는 **MudBlazor** 로 통일한다 (Fluent UI 는 폐기).
- **API-First 아키텍처**: Blazor Interactive WebAssembly UI 계층은 비즈니스 로직이나 DB에 직접 결합되지 않고, `IXxxBrowserClient` 등의 추상화된 API 클라이언트(HTTP/RESTful)를 통해서만 백엔드 API와 통신한다.
- **이중 토큰 인증 패턴**: Access Token(15분) 및 Refresh Token(7일) 이중 토큰 패턴을 적용하며, HttpClient 요청 시 401 Unauthorized를 가로채어 자동으로 localStorage의 Refresh Token으로 토큰을 자동 갱신 및 재시도하는 `TokenRefreshHandler` (DelegatingHandler) 구조를 준수한다. - **이중 토큰 인증 패턴**: Access Token(15분) 및 Refresh Token(7일) 이중 토큰 패턴을 적용하며, HttpClient 요청 시 401 Unauthorized를 가로채어 자동으로 localStorage의 Refresh Token으로 토큰을 자동 갱신 및 재시도하는 `TokenRefreshHandler` (DelegatingHandler) 구조를 준수한다.
- **실시간 알림 (SignalR)**: 실시간 알림 기능은 상태를 직접 동기화하는 용도가 아닌 단순 Event-driven 브로드캐스트 알림으로 설계하며, 클라이언트는 알림 수신 후 API 호출을 통해 최종 데이터를 검증 및 동기화한다. - **실시간 알림 (SignalR)**: 실시간 알림 기능은 상태를 직접 동기화하는 용도가 아닌 단순 Event-driven 브로드캐스트 알림으로 설계하며, 클라이언트는 알림 수신 후 API 호출을 통해 최종 데이터를 검증 및 동기화한다.
- **UI/UX 구현**: - **UI/UX 구현**:
+60 -47
View File
@@ -7,29 +7,31 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
**QuantEngine v0.1** — A comprehensive quantitative analysis and data collection system for retirement asset portfolio management. **QuantEngine v0.1** — A comprehensive quantitative analysis and data collection system for retirement asset portfolio management.
- **Architecture**: .NET 9 + C# (web UI + APIs), Python (legacy data collection/analysis) - **Architecture**: .NET 9 + C# (web UI + APIs), Python (legacy data collection/analysis)
- **Web UI**: Blazor WebAssembly (Fluent UI Blazor v5) + ASP.NET Core Web API - **Web UI**: Blazor Interactive WebAssembly (MudBlazor) + ASP.NET Core Web API (API-First)
- **Database**: PostgreSQL (Npgsql 8.0), single unified database - **Database**: PostgreSQL (Npgsql 8.0), single unified database
- **Data Source**: KIS Open API (quotations/ranking read-only), with fallbacks - **Data Source**: KIS Open API (quotations/ranking read-only), with fallbacks
- **Key Runtimes**: .NET 9, Python 3.9+, Node.js 16+ - **Key Runtimes**: .NET 9, Python 3.9+, Node.js 16+
### Migration Phases Status (2026-06-29) ### Migration Phases Status (2026-06-29)
**Phase 1: Web UI Migration** ✅ COMPLETE **Phase 1: Web UI Migration** 🔄 정책 전환 (2026-06-30)
- Blazor WebAssembly with Fluent UI v5 (RC: 5.0.0-rc.4-26177.1) - **신규 표준**: Blazor **Interactive WebAssembly** 렌더 모드 + **MudBlazor** 컴포넌트 + API-First
- MudBlazor completely deprecated (0% remaining) - **이전 표준(폐기)**: Fluent UI Blazor v5 / InteractiveServer 렌더 모드는 더 이상 사용하지 않음
- Pages: Home, Workspace, Collection, Tables, MainLayout - Pages: Home, Workspace, Collection, Tables, MainLayout
- Build: 0 errors, 6 Razor RC warnings (acceptable) - 코드 전환 작업은 `docs/WBS_10_DOTNET_MIGRATION_HARDENING_2026_06_30.md`**WBS-A7** 로 추적
**Phase 2: KIS Data Collection Pipeline** 🔄 IN PROGRESS **Phase 2: KIS Data Collection Pipeline** ✅ 95% COMPLETE
- ✅ KIS API Client: Full implementation complete - ✅ KIS API Client: Full implementation complete
- IKisApiClient interface (5 quotation methods) - IKisApiClient interface (5 quotation methods)
- KisApiClient (with security enforcement, token caching) - KisApiClient with real HTTP implementation + token caching
- All governance rules enforced (no trading APIs) - All governance rules enforced (no trading APIs)
- Windows env var + registry fallback for credentials - Windows env var + registry fallback for credentials
- Build: 0 errors, 0 warnings
- ✅ PostgreSQL Infrastructure: Complete - ✅ PostgreSQL Infrastructure: Complete
- ITokenCache → PostgresTokenCache (token management) - PostgresTokenCache (token management, 10-min skew)
- ICollectionRepository → CollectionRepository (data storage) - CollectionRepository (full CRUD + dashboard aggregations)
- IDataCollectionStore (abstraction layer) - Auto-creates kis_tokens, kis_collection_runs, kis_collection_snapshots, kis_collection_errors
- Dapper ORM + parameterized SQL (injection-proof)
- ✅ Web API Endpoints: Complete - ✅ Web API Endpoints: Complete
- CollectionEndpoints (6 endpoints: state, runs, snapshots, errors, latest, start) - CollectionEndpoints (6 endpoints: state, runs, snapshots, errors, latest, start)
- ApiClient for Blazor consumption - ApiClient for Blazor consumption
@@ -37,7 +39,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- Collection.razor dashboard with real-time monitoring - Collection.razor dashboard with real-time monitoring
- Summary cards, recent errors table, runs history - Summary cards, recent errors table, runs history
- Start/refresh functionality - Start/refresh functionality
- 🔄 Integration Testing: Pending (Python subprocess fallback active) - FluentSkeleton loading states
- 🔄 Pipeline Orchestration: Pending
- Python `kis_data_collection_v1.py` → .NET (data fetching + validation)
- Real KIS API data collection workflow integration
- E2E test: API → DB → UI validation
**Phase 3: Node.js→.NET CLI Tools** 📋 PLANNED **Phase 3: Node.js→.NET CLI Tools** 📋 PLANNED
- Makefile created (npm → make mappings) - Makefile created (npm → make mappings)
@@ -78,22 +84,24 @@ sudo systemctl restart quantengine-api
### Framework & Design System ### Framework & Design System
- **Primary Framework**: [Fluent UI Blazor v5](https://v5.fluentui-blazor.net/) - **Primary Framework**: [MudBlazor](https://mudblazor.com/)
- **Design System**: Microsoft Fluent Design System (WCAG 2.1 AA) - **Design System**: Material Design (MudBlazor), 고밀도/대량 데이터 성능 우선
- **Deprecation**: MudBlazor is deprecated. Migrate all existing pages to Fluent UI v5 progressively. - **Render Mode**: **Interactive WebAssembly** 를 기본 렌더 모드로 한다 (API-First). InteractiveServer 는 사용하지 않는다.
- **Deprecation**: **Fluent UI Blazor v5 는 폐기**한다. 기존 Fluent UI 페이지는 MudBlazor 로 점진 이전한다.
### Component Development Rules ### Component Development Rules
1. **All UI Development** (New + Refactored): 1. **All UI Development** (New + Refactored):
- Use Fluent UI Blazor v5 components exclusively - Use **MudBlazor** components exclusively
- Fall back to pure HTML/CSS if Fluent v5 doesn't provide - Fall back to pure HTML/CSS if MudBlazor doesn't provide
- **Never introduce MudBlazor components** (deprecated) - **Never introduce Fluent UI components** (deprecated)
- Progressively migrate existing MudBlazor to Fluent v5 - Progressively migrate existing Fluent UI to MudBlazor
- **API-First**: UI 는 DB/비즈니스 로직에 직접 결합하지 않고 추상화된 API 클라이언트(HTTP)로만 통신 (AGENTS.md §5b 준수)
2. **Loading States** (Priority order): 2. **Loading States** (Priority order):
- `<FluentSkeleton>`**Default** for lists, cards, dashboards, detail pages - `<MudSkeleton>`**Default** for lists, cards, dashboards, detail pages
- Pure HTML `<div class="skeleton">` — For custom layouts - Pure HTML `<div class="skeleton">` — For custom layouts
- `MudProgressCircular` / `MudProgressLinear`Exception only (existing legacy) - `<MudProgressCircular>` / `<MudProgressLinear>`명시적 진행 표시가 필요한 경우
- Blocking spinners — **Avoid** - Blocking spinners — **Avoid**
3. **Data Rendering Pattern**: 3. **Data Rendering Pattern**:
@@ -101,21 +109,22 @@ sudo systemctl restart quantengine-api
- On data arrival: Replace skeleton with actual UI - On data arrival: Replace skeleton with actual UI
- Never show blank states while loading - Never show blank states while loading
4. **Component Mapping** (Fluent UI v5): 4. **Component Mapping** (MudBlazor):
| UI Element | Fluent UI Component | Alternative | | UI Element | MudBlazor Component | Alternative |
|-----------|-------------------|-------------| |-----------|-------------------|-------------|
| Button | `<FluentButton>` | - | | Button | `<MudButton>` | - |
| Input field | `<FluentTextField>` | HTML `<input>` | | Input field | `<MudTextField>` | HTML `<input>` |
| Dropdown | `<FluentSelect>` | HTML `<select>` | | Dropdown | `<MudSelect>` | HTML `<select>` |
| Data grid | `<FluentDataGrid>` | HTML `<table>` | | Data grid | `<MudDataGrid Dense Virtualize>` | HTML `<table>` |
| Card | `<FluentCard>` | HTML `<div class="card">` | | Card | `<MudCard>` | HTML `<div class="card">` |
| Badge/Status | `<FluentBadge>` | HTML `<span>` | | Badge/Status | `<MudBadge>` / `<MudChip>` | HTML `<span>` |
| Layout container | `<FluentStack>` | HTML `<div>` | | Layout container | `<MudStack>` / `<MudGrid>` | HTML `<div>` |
| Accordion | `<FluentAccordion>` | HTML `<details>` | | Accordion | `<MudExpansionPanels>` | HTML `<details>` |
| Navigation | `<FluentNavMenu>` | HTML `<nav>` | | Navigation | `<MudNavMenu>` | HTML `<nav>` |
| Loading | `<FluentSkeleton>` | CSS skeleton animation | | Loading | `<MudSkeleton>` | CSS skeleton animation |
| Icons | `<FluentIcon>` | SVG inline | | Icons | `<MudIcon>` | SVG inline |
| Modal/Dialog | `<MudDialog>` (CRUD: 모달 패턴, 삭제: ConfirmDialog) | - |
## Development Commands (Phase 1 + 2) ## Development Commands (Phase 1 + 2)
@@ -130,29 +139,33 @@ npm run ops:release # Full release DAG
### .NET (Primary - Phase 1 + 2) ### .NET (Primary - Phase 1 + 2)
```powershell ```powershell
cd dotnet cd src/dotnet
dotnet restore dotnet restore
dotnet build # Debug build dotnet build # Debug build (0 errors, 0 warnings)
dotnet build -c Release # Release build (recommended) dotnet build -c Release # Release build
dotnet watch run --project src/QuantEngine.Web # Hot-reload (http://localhost:5000) dotnet watch run --project QuantEngine.Web # Hot-reload (http://localhost:5265)
dotnet run --project src/QuantEngine.Web # Run API server dotnet run --project QuantEngine.Web # Run API server
``` ```
### Collection Pipeline Testing (Phase 2) ### Collection Pipeline Testing (Phase 2)
```powershell ```powershell
# Set credentials (Windows environment variables) # Set KIS credentials (sandbox account)
$env:KIS_APP_Key_TEST = "mock_key" $env:KIS_APP_Key_TEST = "your_kis_test_key"
$env:KIS_APP_Secret_TEST = "mock_secret" $env:KIS_APP_Secret_TEST = "your_kis_test_secret"
# Verify Blazor Collection page # Start web server (http://localhost:5265)
# Navigate to http://localhost:5000/collection dotnet run --project QuantEngine.Web
# Verify Collection dashboard
# Navigate to http://localhost:5265/collection
# - Click "Start Collection" to trigger async run # - Click "Start Collection" to trigger async run
# - API initiates Python subprocess (temporary Phase 2 design) # - Backend uses PostgreSQL-backed data storage
# - Dashboard updates with run status, snapshots, errors # - Dashboard updates with run status, snapshots, errors
# Verify API directly # Verify API endpoints
curl http://localhost:5000/api/collection/state curl http://localhost:5265/api/collection/state
curl http://localhost:5000/api/collection/runs curl http://localhost:5265/api/collection/runs
curl "http://localhost:5265/api/collection/latest/005930"
``` ```
## API Endpoints (Phase 1 + 2) ## API Endpoints (Phase 1 + 2)
+2
View File
@@ -1378,6 +1378,8 @@ WBS-8.8 (KIS 리팩터) — 독립적 (원격 병행)
### WBS-10: C#/.NET 엔진 고도화 (Phase 10, 2026-06~12) ### WBS-10: C#/.NET 엔진 고도화 (Phase 10, 2026-06~12)
> **📌 보강 문서(2026-06-30):** 본 WBS-10 의 다수 항목이 `완료` 표기되어 있으나 실측 결과 일부 괴리(10.6 파이프라인·10.9 보안 실질 미완성)가 확인되었다. 마이그레이션 완성 우선 + 상용화 잔여 작업의 재정의는 [WBS_10_DOTNET_MIGRATION_HARDENING_2026_06_30.md](./WBS_10_DOTNET_MIGRATION_HARDENING_2026_06_30.md) 참조.
> 현황 진단(2026-06-26): .NET 프로젝트는 Python 엔진(41 모듈, 14,500 LOC) 대비 5~10%(~1,400 LOC) 수준. > 현황 진단(2026-06-26): .NET 프로젝트는 Python 엔진(41 모듈, 14,500 LOC) 대비 5~10%(~1,400 LOC) 수준.
> Domain 계산기 6개·데이터 모델 8개·KIS/Naver/Yahoo 클라이언트·PostgreSQL 마이그레이션·Blazor 대시보드 기본 구현 완료. > Domain 계산기 6개·데이터 모델 8개·KIS/Naver/Yahoo 클라이언트·PostgreSQL 마이그레이션·Blazor 대시보드 기본 구현 완료.
> **미구현**: Application 서비스 일부, 공식 엔진, 하네스 주입, 파이프라인 오케스트레이터. > **미구현**: Application 서비스 일부, 공식 엔진, 하네스 주입, 파이프라인 오케스트레이터.
@@ -0,0 +1,190 @@
# WBS-10 보강: .NET Core 마이그레이션 완성 & 상용화 로드맵 (2026-06-30)
> 본 문서는 [docs/ROADMAP_WBS.md](./ROADMAP_WBS.md) 의 **WBS-10(.NET 엔진 고도화)** 을 현 시점 실측 기준으로 재진단하고, 마이그레이션 완성과 단일 사용자 상용 운영에 필요한 잔여 작업을 재정의한다.
>
> **작성 배경:** 기존 WBS-10 의 다수 항목이 `완료` 로 표기되어 있으나, 2026-06-30 소스 실측 결과 **표기와 실제 상태 간 괴리**가 확인되었다. 본 문서는 그 괴리를 정리하고 실제 잔여 작업을 추적한다.
>
> **의사결정(사용자 확정):** ① 우선순위 = **마이그레이션 완성 우선**, ② 산출물 = **로드맵/WBS 문서**, ③ 인증 모델 = **단일 사용자 + 기본 보호**.
---
## 1. Context — 왜 이 보강이 필요한가
QuantEngine 은 은퇴자산 포트폴리오 운용을 위한 결정론적 퀀트 엔진이다. canonical 권위는 여전히 **Python 구현(219 파일, 24,683 lines)** 에 있고, `.NET 10` 마이그레이션은 Core / Application / Infrastructure / Web / Tools / Tests 6개 프로젝트로 구조화되어 Phase 1(Web UI)·Phase 2(KIS 수집)까지 도달했다.
그러나 다음 세 가지 근본 결손으로 마이그레이션 완료 및 상용 기준에 미달한다.
1. **마이그레이션 미완성** — 도메인 단일 권위가 Python 에 잔존. `PipelineOrchestrator` 가 실제 로직이 아닌 시뮬레이션 스텁. Python↔.NET 패리티가 일부 도메인 계산기에만 존재. GAS 공식 14건 미이관.
2. **상용 운영 결손** — 소스에 하드코딩 시크릿 잔존, `.gitignore``bin/obj` 누락으로 빌드 산출물 git 추적, 헬스체크·메트릭·재시도·스케줄러·운영 구성(`appsettings.Production.json`) 부재.
3. **검증 공백** — KIS→스냅샷→정성매도 전 구간 E2E 와 CI 커버리지 게이트 부재.
---
## 2. 표기 vs 실제 괴리 정리 (2026-06-30 실측)
| 기존 WBS | 기존 표기 | 실측 상태 | 괴리 / 조치 |
|---|---|---|---|
| WBS-10.6 파이프라인 오케스트레이터 | **완료** | `PipelineOrchestrator.cs` 가 각 단계를 `Task.Delay(10)` 로만 시뮬레이션. 실제 서비스 호출 없음 | 🔴 **실질 미완성.** → 본 문서 **A1** 로 재추적 |
| WBS-10.9 보안 강화 | **완료** | `appsettings.json``Password=;` 처리됨. 그러나 `Program.cs:19` 텔레그램 토큰 평문, `Program.cs:34` DB 패스워드 폴백 평문 잔존. `.gitignore``bin/obj` 없음 → 산출물 git 추적 | 🔴 **부분 완료(핵심 누락).** → 본 문서 **P0** 로 재추적 |
| WBS-10.8 데이터 수집 오케스트레이터 | **TODO** | 실제로는 `DataCollectionService.cs`(KIS 수집 오케스트레이션) 구현·커밋됨. 단 파일명/구조가 WBS 기재(`DataCollectionOrchestrator.cs`)와 불일치 | 🟡 **표기 미갱신.** → 본 문서 **A3** 로 정합화 |
| WBS-10.3~10.5 도메인/공식/하네스 패리티 | 완료 | `DomainParityTests`, `FormulaEngineTests`, `HarnessInjector` 패리티 존재 확인 | ✅ 유효. 단 패리티 범위가 도메인 계산기에 한정 → 수집/정성매도/스냅샷은 미커버 (**A2** 확장) |
| WBS-10.7 Application 서비스 | 부분 완료 | 4개 서비스 구현 확인 | ✅ 유효 |
> **핵심 시사점:** 기존 WBS-10 은 "완료" 표기가 실제보다 앞서 있다. 특히 보안(10.9)과 파이프라인(10.6)은 표기와 달리 **실질 미완성**이므로, 후속 작업은 표기를 신뢰하지 말고 본 문서의 실측 기준을 따른다.
---
## 3. 로드맵 (마이그레이션 완성 우선)
```
[P0 선행 게이트] 보안·위생 차단 ──► 반드시 먼저
[Track A] 마이그레이션 완성 (PRIMARY) [Track B] 상용 안정화 (SECONDARY, 병행)
A1 PipelineOrchestrator 실구현 B1 구성/시크릿 체계화
A2 패리티 하네스 확장(수집·정성매도) B2 기본 인증(단일 사용자)
A3 데이터 수집 파이프라인 E2E 정합화 B3 헬스체크·메트릭
A4 정성매도/스냅샷 어드민 포팅 B4 재시도(Polly)·스케줄러
A5 GAS 잔여 14개 공식 이관 B5 배포(Docker/CI 게이트)
A6 SQLite→PostgreSQL 단일화 + Python 폐기 B6 통합/E2E 테스트·커버리지 게이트
```
### 마일스톤
| 마일스톤 | 구성 | 완료 기준 |
|---|---|---|
| **M1 위생 확보** | P0 | git 에서 시크릿/산출물 제거, 시크릿 외부화·회전 |
| **M2 패리티 기반** | A1·A2 | `.NET` 도메인이 Python 골든 벡터와 1:1 일치, 실 파이프라인 산출 |
| **M3 수집 자립** | A3·A4·B4 | `.NET` 단독 KIS→스냅샷→정성매도 무인 실행 |
| **M4 단일 권위 전환** | A5·A6 | Python 런타임 의존 제거, `.NET` canonical 승격 |
| **M5 상용 운영** | B1~B6 | 단일 사용자 보호·관측·배포 체계 가동 |
---
## 4. WBS (작업 분해 구조)
각 항목: **목표 / 완료 판정(Acceptance) / 주요 파일 / 검증 명령**.
### P0 — 선행 보안·위생 게이트 (🔴 Critical, 최우선)
#### WBS-P0.1 빌드 산출물 git 추적 제거
- **목표:** `.gitignore` 에 .NET 표준 패턴(`bin/`, `obj/`, `publish-output/`, `*.user`) 추가, 추적 중 산출물 `git rm -r --cached` 처리.
- **판정:** `git status``bin/obj` 변경 미표시.
- **파일:** `.gitignore`.
- **검증:** `git status --porcelain | grep -E 'bin/|obj/'` → 0건.
#### WBS-P0.2 하드코딩 시크릿 제거·회전
- **목표:** `Program.cs:19` 텔레그램 토큰·채팅ID, `Program.cs:34` DB 패스워드 폴백을 환경변수/`dotnet user-secrets`/`appsettings.Production.json`(비추적)로 이전. 노출 토큰·DB 비밀번호 **회전**.
- **판정:** 소스 전역 시크릿 평문 0건, 구성 누락 시 앱 기동 거부(fail-fast).
- **파일:** `Program.cs`, `appsettings*.json`, `Infrastructure/TelegramSink.cs`.
- **검증:** `Select-String -Pattern '8734507814|C8RFlZ9f' src/dotnet -Recurse` → 0건.
#### WBS-P0.3 git 이력 시크릿 정리 (선택)
- **목표:** 노출 토큰 회전 완료 시 이력 재작성 생략 가능. 회전 불가 시 `git filter-repo` 로 이력 제거 검토.
- **판정:** 회전 완료 또는 이력 정리 완료 중 택1 기록.
> **주의:** WBS-10.9 가 `완료` 로 표기되어 있으나 위 P0.1·P0.2 는 미해결 상태다. 본 게이트 완료 전까지 후속 트랙 착수를 보류한다.
### Track A — 마이그레이션 완성 (PRIMARY)
#### WBS-A1 PipelineOrchestrator 실제 구현
- **목표:** `Task.Delay` 시뮬레이션 제거. 7단계(수집→정규화→팩터→결정→리스크게이트→리포트→영속화)를 실제 서비스 호출로 연결.
- **판정:** 입력 스냅샷에 대해 결정 패킷 산출, 각 단계 결과가 `engine_history` 에 기록.
- **파일:** `QuantEngine.Application/Services/PipelineOrchestrator.cs`, 관련 `Services/*`.
- **검증:** `dotnet test --filter Pipeline` → 실데이터 기반 산출물 `gate: PASS`.
#### WBS-A2 패리티 하네스 확장 (수집·정성매도)
- **목표:** 기존 도메인 계산기 패리티(10.3~10.5)를 **수집 정규화·정성매도·하네스 주입 전체**로 확장. `spec/13_formula_registry.yaml`(149 공식) 기준 골든 벡터를 Python 에서 추출해 `.NET` 결과와 비교.
- **판정:** 핵심 공식 전부 Python 과 동일 출력(부동소수 허용오차 내), 패리티 리포트 JSON 생성.
- **파일:** `QuantEngine.Core.Tests/ParityTests/`, `tests/golden/`.
- **검증:** `dotnet test --filter Parity` → 전건 PASS.
#### WBS-A3 데이터 수집 파이프라인 E2E 정합화
- **목표:** `DataCollectionService.cs`(구현됨)를 기준으로 WBS 표기 정합화, `kis_data_collection_v1.py` 잔여 로직 완전 이관, KIS→PostgreSQL 스냅샷 E2E 검증. Naver/Yahoo 폴백 다중화 명문화.
- **판정:** `.NET` 단독 실데이터 수집·저장 성공, 폴백 동작 확인.
- **파일:** `Application/Services/DataCollectionService.cs`, `Infrastructure/External/*`.
#### WBS-A4 정성매도·스냅샷 어드민 포팅
- **목표:** `qualitative_sell_strategy_v1.py`, `snapshot_admin_*_v1.py``.NET` 서비스/엔드포인트로 이관.
- **판정:** 정성매도 5팩터 confluence 결과 Python 일치, 스냅샷 승인 워크플로우가 Web UI 에서 동작.
- **파일:** `QuantEngine.Core/Domain/`, `QuantEngine.Web/Endpoints/`, `Components/Pages/`.
#### WBS-A5 GAS 잔여 14개 공식 이관
- **목표:** `governance/gas_logic_migration_ledger_v1.yaml` 의 TODO 14건을 `.NET` 포팅 + parity.
- **판정:** 원장 전 항목 `status: DONE`, parity 통과.
- **파일:** `QuantEngine.Core/Domain/`, `governance/gas_logic_migration_ledger_v1.yaml`.
#### WBS-A6 SQLite→PostgreSQL 단일화 및 Python 런타임 폐기
- **목표:** canonical DB 를 PostgreSQL 로 일원화, `src/quant_engine/*.db` 의존 제거, Python 런타임 도구를 `.NET`/`Tools` 로 대체.
- **판정:** 운영 경로 Python 호출 0건, 모든 데이터 PostgreSQL 단일 소스.
- **파일:** `Infrastructure/Data/DbMigrator.cs`, `Makefile`, `tools/`.
#### WBS-A7 UI 프레임워크 전환 — Fluent UI → MudBlazor + Interactive WebAssembly (2026-06-30 방침)
- **배경:** UI 표준을 **MudBlazor** 컴포넌트 + **Interactive WebAssembly** 렌더 모드 + **API-First** 로 전환(방침 확정). 기존 Fluent UI v5 / InteractiveServer 는 폐기. 정책은 [CLAUDE.md](../CLAUDE.md) 및 [AGENTS.md](../AGENTS.md) §5b 에 반영 완료.
- **목표:**
- csproj 패키지 교체: `Microsoft.FluentUI.AspNetCore.Components*` 제거 → `MudBlazor` 추가.
- 렌더 모드 전환: `Program.cs``AddInteractiveServerComponents`/`AddInteractiveServerRenderMode``AddInteractiveWebAssemblyComponents`/`AddInteractiveWebAssemblyRenderMode`, 클라이언트 프로젝트(`QuantEngine.Web.Client`) 분리.
- `App.razor`: Fluent CSS/JS·`FluentDesignSystemProvider` 제거 → MudBlazor `<MudThemeProvider>`/`<MudDialogProvider>`/`<MudSnackbarProvider>` + `MudBlazor.min.css/js` 삽입.
- 전체 `.razor` 컴포넌트의 `Fluent*``Mud*` 치환(매핑표는 [CLAUDE.md](../CLAUDE.md) Component Mapping 참조).
- API-First: UI 의 직접 DI 호출을 `IXxxBrowserClient`(HTTP) 경유로 전환, `TokenRefreshHandler` 패턴 적용.
- **판정:** Fluent UI 패키지/참조 0건, `dotnet build` 오류 0, WASM 로드 후 `/quant/` 및 주요 페이지 정상 렌더, 비-API 라우트 동작 확인.
- **주요 파일:** `QuantEngine.Web/QuantEngine.Web.csproj`, `Program.cs`, `Components/App.razor`, `Components/Layout/*.razor`, `Components/Pages/*.razor`, 신규 `QuantEngine.Web.Client/`.
- **검증:** `Select-String -Pattern 'Fluent' src/dotnet/QuantEngine.Web -Recurse` → 0건; 브라우저에서 WASM 모드 동작 확인.
### Track B — 상용 안정화 (SECONDARY, 단일 사용자)
#### WBS-B1 구성·시크릿 체계화
- **목표:** `appsettings.Production.json`(비추적), `IOptions<T>` + 시작 시 구성 검증(fail-fast), 연결 문자열/토큰 환경변수 표준화.
- **판정:** 개발/운영 구성 분리, 필수 구성 누락 시 명확 오류로 기동 중단.
#### WBS-B2 기본 인증 (단일 사용자 보호)
- **목표:** 공개 서버 노출 방어용 최소 인증 — 리버스 프록시 Basic Auth 또는 API Key 미들웨어 1종(`/api/*`·UI 보호). 본격 Identity/JWT 는 범위 외.
- **판정:** 비인증 요청 401, 인증 요청만 수집/조회 가능.
- **파일:** `Program.cs`, `Endpoints/CollectionEndpoints.cs`, Nginx 구성.
#### WBS-B3 헬스체크·메트릭
- **목표:** `MapHealthChecks("/health")`(liveness) + `/health/ready`(PostgreSQL/KIS 토큰 점검), `prometheus-net` 기반 기본 메트릭.
- **판정:** 배포 스크립트 헬스체크가 `/health/ready` 사용, 메트릭 엔드포인트 응답.
- **파일:** `Program.cs`, `.gitea/workflows/deploy-prod.yml`.
#### WBS-B4 재시도(Polly)·백그라운드 스케줄러
- **목표:** KIS/Naver/Yahoo HTTP 호출에 Polly 재시도·서킷브레이커, 주기적 수집을 `BackgroundService`(또는 systemd timer 연계)로 자동화.
- **판정:** 일시적 5xx/네트워크 오류 자동 복구, 정해진 스케줄 무인 수집.
- **파일:** `Program.cs`(HttpClient+Polly), 신규 `Application/Services/*BackgroundService.cs`.
#### WBS-B5 배포 (Docker/CI 게이트)
- **목표:** 멀티스테이지 `Dockerfile` + `docker-compose.yml`(app+PostgreSQL), `.gitea` CI 에 `dotnet build`+`dotnet test` 게이트 추가.
- **판정:** 컨테이너 로컬 기동 성공, CI 에서 테스트 실패 시 배포 차단.
- **파일:** 신규 `Dockerfile`, `docker-compose.yml`, `.gitea/workflows/ci.yml`.
#### WBS-B6 통합·E2E 테스트 및 커버리지 게이트
- **목표:** Testcontainers(PostgreSQL) 통합테스트, KIS→스냅샷→정성매도 E2E, coverlet 커버리지 임계값을 CI 게이트로 연결.
- **판정:** E2E 1건 이상 그린, 커버리지 임계 미달 시 CI 실패.
- **파일:** `QuantEngine.Core.Tests/`(통합/E2E), `.gitea/workflows/ci.yml`.
---
## 5. 개선·보완·고도화 제안 (Track A/B 외 권고)
- **결정 재현성 감사:** 동일 입력 → 동일 출력 결정론 검증을 CI 상시 게이트로 편입 ([governance/adr/0003-no-llm-numeric-generation.md](../governance/adr/0003-no-llm-numeric-generation.md) 정신 계승).
- **캘리브레이션 실증 연계:** [spec/27_bch_calibration_runbook.yaml](../spec/27_bch_calibration_runbook.yaml) 의 `0/190 CALIBRATED` 문제를 마이그레이션과 분리된 데이터 트랙으로 별도 추적(본 WBS 범위 밖, 링크 유지).
- **장애 단일점 보강:** Naver Cloudflare 403 폴백 경로를 Yahoo/KIS 다중화로 명문화(WBS-A3 연동).
- **운영 가시성:** 구조화 로깅에 상관관계 ID(correlation id) 추가, 수집 실행별 추적 가능화.
- **비밀 회전 정책:** KIS appkey/secret, 텔레그램 토큰, DB 비밀번호의 주기적 회전 절차를 [docs/runbook.md](./runbook.md) 에 문서화.
- **WBS 표기 정합성 거버넌스:** 본 문서에서 드러난 "완료 표기 vs 실측" 괴리 재발 방지를 위해, 각 WBS 완료 시 **검증 명령 출력 캡처를 증빙으로 첨부**하는 규칙을 강화([AGENTS.md](../AGENTS.md) 의 검증·증빙 강제 원칙 적용).
---
## 6. 검증 방법 (각 단계 실행 시)
- **P0:** `git status` 산출물 미추적 확인, 시크릿 평문 grep 0건, 회전된 자격증명으로 정상 기동.
- **Track A:** `cd src/dotnet && dotnet test` 로 패리티/단위/E2E 그린. 패리티 리포트 JSON 을 Python 출력과 diff. 운영 경로 Python 호출 0건.
- **Track B:** `curl /health/ready` 200, 비인증 요청 401, `docker compose up` 기동, CI 테스트/커버리지 게이트 동작. Polly 재시도는 장애 주입 테스트로 검증.
---
## 7. 실행 순서 요약
1. **P0 선행 게이트** (WBS-P0.1~P0.3) — 보안·위생 차단. **(기존 10.9 完了 표기 무시, 실측 기준 처리)**
2. **Track A** (A1→A2→A3→A4→A5→A6) — 마이그레이션 완성(우선).
3. **Track B** (B1~B6) — 단일 사용자 상용 안정화(A 와 병행, B1·B3 조기 착수 권장).
@@ -0,0 +1,239 @@
using System.Text.Json;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Application.Services;
public class DataCollectionService
{
private readonly IKisApiClient _kisApiClient;
private readonly ICollectionRepository _repository;
public DataCollectionService(
IKisApiClient kisApiClient,
ICollectionRepository repository)
{
_kisApiClient = kisApiClient;
_repository = repository;
}
public async Task<CollectionRunResult> RunCollectionAsync(
string runId,
string account,
List<string> tickers)
{
var result = new CollectionRunResult
{
RunId = runId,
StartedAt = KstNowIso(),
Status = "RUNNING"
};
try
{
await _repository.SaveRunAsync(new CollectionRunRecord(
RunId: runId,
Status: "RUNNING",
StartedAt: result.StartedAt
));
int successCount = 0;
int errorCount = 0;
foreach (var ticker in tickers)
{
try
{
var normalized = await CollectOneAsync(ticker, account);
var provenance = new Dictionary<string, object>
{
{ "ticker", ticker },
{ "source", "kis_open_api" }
};
await _repository.SaveSnapshotAsync(new CollectionSnapshotRecord(
RunId: runId,
DatasetName: "data_feed",
Ticker: ticker,
SourceName: "kis_open_api",
PayloadJson: JsonSerializer.Serialize(normalized),
CapturedAt: KstNowIso()
));
successCount++;
}
catch (Exception ex)
{
errorCount++;
System.Diagnostics.Debug.WriteLine($"Error collecting {ticker}: {ex.Message}");
await _repository.SaveErrorAsync(new CollectionErrorRecord(
RunId: runId,
SourceName: "kis_collector",
ErrorKind: ex.GetType().Name,
ErrorMessage: ex.Message,
Ticker: ticker
));
}
}
var finishedAt = KstNowIso();
await _repository.UpdateRunStatusAsync(
runId,
errorCount == 0 ? "COMPLETED" : "COMPLETED_WITH_ERRORS",
finishedAt,
successCount,
errorCount
);
result.Status = errorCount == 0 ? "COMPLETED" : "COMPLETED_WITH_ERRORS";
result.FinishedAt = finishedAt;
result.SuccessCount = successCount;
result.ErrorCount = errorCount;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Fatal error in collection run {runId}: {ex}");
await _repository.UpdateRunStatusAsync(runId, "FAILED", KstNowIso());
result.Status = "FAILED";
result.ErrorMessage = ex.Message;
}
return result;
}
private async Task<Dictionary<string, object>> CollectOneAsync(string ticker, string account)
{
var normalized = new Dictionary<string, object> { { "ticker", ticker } };
try
{
var price = await _kisApiClient.GetCurrentPriceAsync(ticker, account);
normalized["current_price"] = CoerceFloat(FindFirstValue(price, "stck_prpr", "stck_clpr", "close"));
normalized["open"] = CoerceFloat(FindFirstValue(price, "stck_oprc", "open"));
normalized["high"] = CoerceFloat(FindFirstValue(price, "stck_hgpr", "high"));
normalized["low"] = CoerceFloat(FindFirstValue(price, "stck_lwpr", "low"));
normalized["prev_close"] = CoerceFloat(FindFirstValue(price, "prdy_vrss"));
normalized["volume"] = CoerceFloat(FindFirstValue(price, "acml_vol", "volume"));
normalized["change_pct"] = CoerceFloat(FindFirstValue(price, "prdy_ctrt"));
normalized["price_status"] = "OK";
}
catch (Exception ex)
{
normalized["price_status"] = "ERROR";
normalized["price_error"] = ex.Message;
}
try
{
var orderbook = await _kisApiClient.GetAskingPrice10LevelAsync(ticker, account);
var output1 = ExtractObject(orderbook, "output1");
normalized["ask_1"] = CoerceFloat(FindFirstValue(output1, "askp1"));
normalized["bid_1"] = CoerceFloat(FindFirstValue(output1, "bidp1"));
normalized["orderbook_status"] = "OK";
}
catch (Exception ex)
{
normalized["orderbook_status"] = "ERROR";
normalized["orderbook_error"] = ex.Message;
}
try
{
var start = DateTime.Now.AddDays(-10).ToString("yyyyMMdd");
var end = DateTime.Now.ToString("yyyyMMdd");
var shortSale = await _kisApiClient.GetDailyShortSaleAsync(ticker, start, end, account);
var rows = ExtractArray(shortSale, "output2");
if (rows.Count > 0 && rows[0] is Dictionary<string, object> latest)
{
normalized["short_turnover_share"] = CoerceFloat(latest.GetValueOrDefault("ssts_vol_rlim"));
}
normalized["short_sale_status"] = "OK";
}
catch (Exception ex)
{
normalized["short_sale_status"] = "ERROR";
normalized["short_sale_error"] = ex.Message;
}
normalized["collection_as_of"] = KstNowIso();
return normalized;
}
private static object? FindFirstValue(Dictionary<string, object> payload, params string[] keys)
{
var stack = new Stack<object>();
stack.Push(payload);
while (stack.Count > 0)
{
var item = stack.Pop();
if (item is Dictionary<string, object> dict)
{
foreach (var key in keys)
{
if (dict.TryGetValue(key, out var value) && value != null && !string.IsNullOrEmpty(value.ToString()))
return value;
}
foreach (var value in dict.Values)
if (value != null) stack.Push(value);
}
else if (item is JsonElement elem && elem.ValueKind == System.Text.Json.JsonValueKind.Object)
{
foreach (var key in keys)
{
if (elem.TryGetProperty(key, out var prop) && prop.ValueKind != System.Text.Json.JsonValueKind.Null)
return prop;
}
foreach (var prop in elem.EnumerateObject())
stack.Push(prop.Value);
}
}
return null;
}
private static double? CoerceFloat(object? value)
{
if (value == null || string.IsNullOrEmpty(value.ToString()))
return null;
try
{
var str = value.ToString()?.Replace(",", "").Replace("%", "") ?? "";
return double.TryParse(str, out var d) ? d : null;
}
catch { return null; }
}
private static Dictionary<string, object> ExtractObject(Dictionary<string, object> payload, string key)
{
if (payload.TryGetValue(key, out var value) && value is Dictionary<string, object> dict)
return dict;
if (value is JsonElement elem && elem.ValueKind == System.Text.Json.JsonValueKind.Object)
return JsonSerializer.Deserialize<Dictionary<string, object>>(elem.GetRawText()) ?? new();
return new();
}
private static List<object> ExtractArray(Dictionary<string, object> payload, string key)
{
if (payload.TryGetValue(key, out var value))
{
if (value is List<object> list) return list;
if (value is JsonElement elem && elem.ValueKind == System.Text.Json.JsonValueKind.Array)
return JsonSerializer.Deserialize<List<object>>(elem.GetRawText()) ?? new();
}
return new();
}
private static string KstNowIso() =>
DateTime.Now.ToString("o");
}
public class CollectionRunResult
{
public string RunId { get; set; } = "";
public string Status { get; set; } = "";
public string StartedAt { get; set; } = "";
public string? FinishedAt { get; set; }
public int SuccessCount { get; set; }
public int ErrorCount { get; set; }
public string? ErrorMessage { get; set; }
}
@@ -174,8 +174,15 @@ public class KisApiClient : IKisApiClient
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>(); var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
var accessToken = tokenData["access_token"]?.ToString() ?? throw new InvalidOperationException("No access_token in response"); if (tokenData == null) throw new InvalidOperationException("Token response body is empty");
var expiresInStr = tokenData.ContainsKey("expires_in") ? tokenData["expires_in"]?.ToString() : "86400";
if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
throw new InvalidOperationException("No access_token in response");
var accessToken = tokenObj.ToString()!;
var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null
? expiresObj.ToString()
: "86400";
var expiresInSec = int.TryParse(expiresInStr, out var seconds) ? seconds : 86400; var expiresInSec = int.TryParse(expiresInStr, out var seconds) ? seconds : 86400;
var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec); var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);
@@ -1,5 +1,5 @@
using QuantEngine.Core.Interfaces; using QuantEngine.Core.Interfaces;
using System.Diagnostics; using QuantEngine.Application.Services;
namespace QuantEngine.Web.Endpoints; namespace QuantEngine.Web.Endpoints;
@@ -108,51 +108,30 @@ public static class CollectionEndpoints
} }
} }
private static async Task<IResult> StartCollectionRun(ICollectionRepository repo, ILogger<Program> logger) private static async Task<IResult> StartCollectionRun(
DataCollectionService collectionService,
HttpRequest request,
ILogger<Program> logger)
{ {
try try
{ {
var runId = Guid.NewGuid().ToString("N"); var runId = Guid.NewGuid().ToString("N");
var now = DateTime.UtcNow.ToString("o"); var now = DateTime.UtcNow.ToString("o");
var run = new CollectionRunRecord( var body = await request.ReadAsAsync<CollectionRunRequest>();
RunId: runId, var account = body?.Account ?? "real";
Status: "running", var tickers = body?.Tickers ?? new List<string> { "005930", "000660" };
StartedAt: now,
FinishedAt: null,
TotalSnapshots: null,
TotalErrors: null,
UpdatedAt: now
);
await repo.SaveRunAsync(run); // Trigger async collection (fire-and-forget)
// Temp: Invoke Python subprocess for actual collection
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try try
{ {
var process = new Process await collectionService.RunCollectionAsync(runId, account, tickers);
{
StartInfo = new ProcessStartInfo
{
FileName = "python",
Arguments = "tools/run_kis_data_collection_v1.py --input-json GatherTradingData.json --sqlite-db src/quant_engine/kis_data_collection.db --kis-account real",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
process.Start();
await process.WaitForExitAsync();
await repo.UpdateRunStatusAsync(runId, "completed", DateTime.UtcNow.ToString("o"), 0, 0);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, $"Collection run {runId} failed"); logger.LogError(ex, "Collection run {RunId} failed", runId);
await repo.UpdateRunStatusAsync(runId, "failed", DateTime.UtcNow.ToString("o"), null, null);
} }
}); });
@@ -160,12 +139,20 @@ public static class CollectionEndpoints
{ {
runId, runId,
status = "running", status = "running",
startedAt = now startedAt = now,
tickerCount = tickers.Count
}); });
} }
catch catch (Exception ex)
{ {
logger.LogError(ex, "Failed to start collection run");
return Results.StatusCode(500); return Results.StatusCode(500);
} }
} }
private class CollectionRunRequest
{
public string? Account { get; set; }
public List<string>? Tickers { get; set; }
}
} }
+2
View File
@@ -6,6 +6,7 @@ using QuantEngine.Infrastructure.Services;
using QuantEngine.Core.Interfaces; using QuantEngine.Core.Interfaces;
using QuantEngine.Application.Services; using QuantEngine.Application.Services;
using System.Text.Json; using System.Text.Json;
using static QuantEngine.Application.Services.DataCollectionService;
using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components;
using Serilog; using Serilog;
using QuantEngine.Web.Infrastructure; using QuantEngine.Web.Infrastructure;
@@ -41,6 +42,7 @@ builder.Services.AddScoped<HistoryIngestionService>();
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>(); builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
builder.Services.AddScoped<ITokenCache, PostgresTokenCache>(); builder.Services.AddScoped<ITokenCache, PostgresTokenCache>();
builder.Services.AddScoped<IKisApiClient, KisApiClient>(); builder.Services.AddScoped<IKisApiClient, KisApiClient>();
builder.Services.AddScoped<DataCollectionService>();
// HTTP Client & API Services // HTTP Client & API Services
builder.Services.AddHttpClient<ApiClient>(); builder.Services.AddHttpClient<ApiClient>();