Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b86e6fee2c | |||
| 6c6011a62d | |||
| 53be11673c | |||
| 8ed2bcf56f | |||
| 2ccf74c410 | |||
| 54b7922167 | |||
| 4059828abf |
+60
-258
@@ -1,285 +1,87 @@
|
||||
# 🚀 K-ArtSell Aegis v16.0 - 현재 진행 로드맵
|
||||
|
||||
**상태:** 95% 완료 (Phase 2-3 구현 완료, Gate 3만 검증 필요)
|
||||
**마지막 업데이트:** 2026-08-03 02:00 KST
|
||||
**관리자:** Claude Code + 향후 Codex 연계
|
||||
**최종 갱신:** 2026-08-07 (이 문서의 이전 버전은 2026-08-03 상태로 정체되어 있었고, 그 사이 병합된 115개 커밋을 반영하지 못했습니다. 이번 갱신은 실제 코드/테스트를 직접 확인한 결과입니다.)
|
||||
|
||||
**상태 요약:** VS-03(승인 워크플로우), VS-04(감사 추적), VS-10(매도 결정), VS-12(거래 실행), VS-14(포트폴리오 대사) 백엔드 구현 + 테스트 완료. Phase 1 Shadow Run(Gate 5a, 252+ 거래일 검증)은 **아직 시작되지 않음** (과거 "RUNNING" 기록은 허위였음이 이미 문서로 정정됨). 상세 항목별 상태는 `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv` 참조.
|
||||
|
||||
---
|
||||
|
||||
## 📍 Current Sprint (이번 주)
|
||||
## ⚠️ 알려진 문서 정합성 문제 (DECISION_REQUIRED)
|
||||
|
||||
### ✅ 완료 (4개)
|
||||
|
||||
#### 1. Idempotency 버그 수정
|
||||
- **Commit:** 9a2d939
|
||||
- **파일:** RecommendationReportGenerator.cs, 3x Job classes
|
||||
- **내용:**
|
||||
- ADO pattern으로 HasReportBeenSentAsync/MarkReportSentAsync 복구
|
||||
- Daily/Weekly/Monthly 모든 Job에 idempotency 체크/마크 복구
|
||||
- CLAUDE.md blocking rule 준수: "No partial success"
|
||||
- **검증:** Build 0 errors, 모든 Job 테스트됨
|
||||
|
||||
#### 2. Serilog Telegram 알림 통합
|
||||
- **이전 커밋:** (4519fa8)
|
||||
- **파일:** TelegramSink.cs
|
||||
- **내용:**
|
||||
- ERROR/FATAL 로그 → Telegram 자동 발송
|
||||
- 동기 호출 + 오류 침묵 처리
|
||||
- Markdown 포맷 + 타임스탬프
|
||||
|
||||
#### 3. Daily/Weekly/Monthly Recommendation Reports
|
||||
- **이전 커밋:** (4519fa8)
|
||||
- **파일:** 3x Job 클래스 + RecommendationReportGenerator
|
||||
- **내용:**
|
||||
- Daily: 09:00 KST 매일
|
||||
- Weekly: 09:00 KST 토요일 (사용자 요청)
|
||||
- Monthly: 09:00 KST 1일
|
||||
- SignalEngine.sell_decisions 집계 + Telegram 발송
|
||||
|
||||
#### 4. Phase 1 API 최적화 완료
|
||||
- **Commit:** eb106d5
|
||||
- **파일:**
|
||||
- KrxDataService.cs (exponential backoff)
|
||||
- TelegramSinkAsync.cs (new, async queue)
|
||||
- DataBackfiller.cs (30-day batch)
|
||||
- ApiCallMetricsService.cs (new, 24h metrics)
|
||||
- Program.cs (TelegramSinkAsync 등록)
|
||||
- **내용:**
|
||||
- KRX: 지수 백오프 (100ms → 30s) + X-RateLimit-Remaining 모니터링
|
||||
- Telegram: 논블로킹 큐, 100ms 간격, 3회 재시도
|
||||
- DataBackfiller: 252일 → 9회 호출 (97% ↓)
|
||||
- Metrics: API별 성공/실패/레이턴시/할당량 추적
|
||||
- **효과:** Shadow run 4분 → 1초 (75% ↓), 신뢰성 ↑
|
||||
1. **VS 번호 체계 충돌:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`(원 계획)와 실제 구현/`WBS_PROGRESS_TRACKER.csv`(실행 트래커) 사이에 VS-03/VS-04/VS-12/VS-14 번호가 서로 다른 기능을 가리키는 충돌이 있습니다. 이번 세션에서 발견했고, 사용자 결정에 따라 **기존 트래커 번호를 유지**하고 충돌 사실만 각 행에 명시했습니다. 근본 해결(재번호 부여 또는 WBS_MASTER 공식 대체)은 아직 미결정입니다.
|
||||
2. **DbUp 마이그레이션 테스트 DB 권한 문제:** `kartsell` DB 사용자가 `kartsell_migration_test` 데이터베이스의 소유자가 아니어서 `DbUpMigrationTests`(12건)가 로컬에서 실패합니다. 코드 문제가 아니라 DBA 조치(소유권 부여)가 필요합니다.
|
||||
3. **frontend 빌드 산출물 재해시:** `dotnet build`를 실행할 때마다 `pnpm build`가 재실행되어 `wwwroot/assets/*` 해시 파일명이 바뀌고 git에 불필요한 변경이 쌓이는 구조적 문제가 있습니다 (아직 미해결).
|
||||
|
||||
---
|
||||
|
||||
### ⏳ 진행 중 (1개)
|
||||
## ✅ 완료 (Backend 구현 + 테스트, 2026-08-07 기준 검증됨)
|
||||
|
||||
#### Gate 3: 252+ Trading-Day Shadow Run (리허설)
|
||||
- **상태:** 🔴 검증 실패 (재시도 필요)
|
||||
- Run ID: `d14f34ea-2afe-4caf-bbb1-c9a7d74fb582` (생성됨, 미완료)
|
||||
- Hangfire Job 269: 상태 미확인 (Host 재시작 실패)
|
||||
- 근본 원인: Hangfire 분산 락 타임아웃 + 가짜 KRX API 키
|
||||
- **완료된 것:**
|
||||
- ✅ DB 격리: 테스트 appsettings.Development.json → `kartselldb_test`
|
||||
- ✅ Host 재시작: Development 환경 (DevelopmentHeaderAuthenticationHandler 활성화)
|
||||
- ✅ Hangfire 타임아웃 복원력: Program.cs 재시도 로직 추가 (DEBT-015)
|
||||
- ✅ 실KRX 데이터 서비스: KrxDataService 실연동 (Program.cs 등록)
|
||||
- ✅ 기술부채 등록: DEBT-009~015 (PBO/DSR/예측/false-exit/타임아웃/감시)
|
||||
- **현재 제약 사항 (문서화됨):**
|
||||
- PBO/Sharpe 계산: 간단한 percentile 공식 (정확한 CSCV 방법론 필요 — DEBT-009)
|
||||
- 모델 예측: 고정 수량 (실제 포지션 사이징 필요 — DEBT-010)
|
||||
- 비용 2배 시뮬레이션: 선형 공식 (정확한 재시뮬레이션 필요 — DEBT-011)
|
||||
- False-exit 분석: 미구현 (항상 0 반환 — DEBT-012)
|
||||
- **필요 조건:**
|
||||
```bash
|
||||
# Terminal 1: SSH 터널 (지속)
|
||||
ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7
|
||||
|
||||
# Terminal 2: Host 실행 (Development 환경)
|
||||
cd D:\JobRoomz\KArtSell.Aegis
|
||||
$env:ASPNETCORE_ENVIRONMENT = "Development"
|
||||
dotnet run --project src/KArtSell.Host -c Debug
|
||||
```
|
||||
- **실행 단계:**
|
||||
1. ✅ POST /api/shadow-runs (modelId, windowStart, windowEnd)
|
||||
2. ✅ 202 Accepted 반환 (Job 269 enqueue)
|
||||
3. ⏳ Hangfire Worker 처리 중 (Phase 1-5 실행)
|
||||
4. ⏳ Phase 5 완료 → model_operations.shadow_run 저장
|
||||
5. ⏳ GET /api/shadow-runs/{runId} → 200 OK (status: Completed)
|
||||
6. 목적: 데이터 계층 검증 + 실KRX 통합 확인
|
||||
- **기대 결과 (리허설용):**
|
||||
- 데이터 파이프라인 동작 확인
|
||||
- 실KRX 가격 데이터 정상 다운로드
|
||||
- model_operations.shadow_run 테이블 데이터 쓰기 성공
|
||||
- 단순화된 분석 메트릭 생성 (프로덕션 검증 아님)
|
||||
- **순서:** 다음 세션에서 실행
|
||||
### VS-03: 모델 승인 워크플로우 (Maker-Checker Governance)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/`
|
||||
- **테스트:** 20/20 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **이번 세션에서 발견/수정한 결함:** `ApprovalSql`이 Dapper로 `DateOnly` 파라미터를 바인딩하지 못해 승인 제안서 생성이 실 DB 환경에서 100% 실패하던 버그 — 병합 이후 실 DB로 한 번도 검증되지 않아 발견되지 않고 있었음
|
||||
|
||||
### VS-04: 불변 감사 추적 (Audit Trail / GDPR)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/Compliance/`
|
||||
- **테스트:** 5/5 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **이번 세션에서 발견/수정한 결함:** `ip_address`/`kis_response`류 컬럼의 Dapper 타입 캐스팅 실패, `GdprRetention.RetentionEndsAt`이 `DATE` 컬럼인데 `DateTime`으로 선언되어 있던 문제, 그리고 `KArtSell.BuildingBlocks`의 `[ModuleInitializer]`가 우연히 로드되지 않으면 모든 snake_case 컬럼이 null로 매핑되던 레이스 컨디션
|
||||
|
||||
### VS-10: 매도 결정 엔진 (Sell Decision Engine)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/SellDecision/`, `frontend/src/features/sell-decision/`
|
||||
- **테스트:** 32/32 PASS (격리 실행 기준)
|
||||
- **완료도:** Backend + Frontend 모두 존재 (VS-03/04/12/14 중 유일)
|
||||
- **⚠️ 미검증 사항:** 코드/테스트 완료 ≠ PBO/DSR 프로덕션 검증 완료. 실 시장 데이터 기반 검증은 Phase 1 Shadow Run 완료 후에만 가능
|
||||
|
||||
### VS-12: 거래 실행 시스템 (Trade Execution, KIS 연동)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/TradeExecution/`
|
||||
- **테스트:** 13/13 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **이번 세션에서 발견/수정한 결함 (심각):** `UpdateTradeStatusAsync`가 `status`/`kis_response`/`error_message`만 저장하고 `kis_order_id`, `executed_quantity`, `unit_price`, `commission`, `net_proceeds`, 체결/정산 타임스탬프는 병합 이후 매번 조용히 유실시키던 버그. 거래 체결·정산 데이터가 실제로는 저장되고 있지 않았음
|
||||
|
||||
### VS-14: 포트폴리오 대사 (Portfolio Reconciliation)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/`
|
||||
- **테스트:** 18/18 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **참고:** 이 슬라이스가 포함된 PR(#28)이 병합 당일 `model_operations.models` 테이블 누락으로 신규 DB 마이그레이션을 전부 깨뜨리는 채로 병합되었고, 같은 날 별도 PR(#29)로 긴급 수정됨 — 병합 전 fresh-install 리허설이 실제로 이루어지지 않았음을 시사
|
||||
|
||||
### AEG-X-009: 외부 데이터 소스 통합 (KRX/OpenDart/KIS)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/Infrastructure/`, market_data 스키마
|
||||
- **완료:** 소스 카탈로그/거버넌스 정책(Workstream D/E/F) + 실 API 연동(Workstream G: KRX OpenAPI/OpenDart/KIS 서비스, 일일 스케줄링, 에러 분류, LKG 폴백)
|
||||
|
||||
### 그 외 완료 항목 (VS-00 플랫폼 부트스트랩, VS-01/VS-02 슬라이스 스펙, 보안/Outbox/OpenAPI 게이트 등)
|
||||
상세는 `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`의 AEG-X-001~008, AEG-VS-00-01~07, AEG-VS-01-01, AEG-VS-02-01 행 참조.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 완료됨 (Implemented & Tested)
|
||||
## 🔴 실제로 블로킹 중인 것 (Phase 1 Shadow Run)
|
||||
|
||||
### Phase 2: 중기 최적화
|
||||
### PHASE-1-SHADOW-RUN: 252+ 거래일 검증 (Gate 5a)
|
||||
- **상태:** `BLOCKED` — **실행 중이 아님**
|
||||
- **근거:** `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`에 이미 정정되어 있음 — 과거 세션들의 "Job 893/976 RUNNING, ~20+시간 경과" 등의 기록은 실제로는 `POST /api/shadow-runs`가 `PostgresException 23514`(check_status 제약조건 위반)로 500 에러를 반환하며 실패한 것이었고, Job이 실제로 시작된 적이 없음
|
||||
- **차단 사유:** 서버 측 `dataset_manifest`, `model_version_registry`, `evidence_snapshot`, `release_evidence_bundle`에 승인/동결된 행이 없어 RunId/JobId를 생성할 수 없음. 승인된 VersionSet 대기 중
|
||||
- **재개 절차:** `PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`의 5단계 참조 (① check_status 제약조건 정합 ② 승인된 테스트 DB에서 fresh/upgrade/재실행/실패복구 리허설 ③ 증거 보존 ④ 명시적 승인 획득 ⑤ 신규 Run ID/Job ID로 재큐잉)
|
||||
- **이 상태가 바뀌려면:** 실제 RunId/JobId가 존재해야 하며, 문서에 "RUNNING"이라고 다시 적으려면 그 근거를 반드시 명시해야 함 (과거의 허위 기록을 반복하지 말 것)
|
||||
|
||||
#### 5. ✅ OpenDart 일일 배치
|
||||
- **파일:** src/KArtSell.Host/Observability/OpenDartService.cs (186 lines)
|
||||
- **Job:** OpenDartDailyBatchJob.cs (169 lines)
|
||||
- **내용:**
|
||||
- 1,000 req/day 할당량 관리
|
||||
- 3개월 캐싱 (분기별 재무제표)
|
||||
- 일 1회 배치 호출만 허용
|
||||
- **테스트:** 5개 통합 테스트 (OpenDartServiceTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 6. ✅ Gate 4: 승인 워크플로우
|
||||
- **파일:** GetApprovalQueue/Endpoint.cs, ApproveModel/Handler.cs, RejectModel/Handler.cs
|
||||
- **내용:**
|
||||
1. GET /api/approval-queue (대기 중 목록)
|
||||
2. POST /api/approval/{id}/approve (2명 승인)
|
||||
3. approved_at / approved_by 타임스탬프 추적
|
||||
- **테스트:** 32개 통합 테스트
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 7. ✅ KIS Connection Pool
|
||||
- **파일:** src/KArtSell.Host/Infrastructure/KisConnectionPool.cs (247 lines)
|
||||
- **내용:**
|
||||
- 3-5 concurrent connection pool
|
||||
- OAuth2 token refresh (55분 주기)
|
||||
- Priority queue (BUY > SELL > CANCEL)
|
||||
- **테스트:** 2개 통합 테스트 (KisConnectionPoolTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 3: 장기 고도화
|
||||
|
||||
#### 8. ✅ Central Rate Limiter (모든 API)
|
||||
- **파일:** src/KArtSell.Host/Infrastructure/RateLimiterService.cs (211 lines)
|
||||
- **내용:**
|
||||
- Token bucket pattern (모든 API 통합)
|
||||
- Per-API quota 추적
|
||||
- Fairness 보장
|
||||
- **테스트:** 4개 통합 테스트 (RateLimiterServiceTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 9. ✅ Circuit Breaker Pattern
|
||||
- **파일:** src/KArtSell.Host/Infrastructure/CircuitBreakerPolicy.cs (180 lines)
|
||||
- **내용:**
|
||||
- Polly policy 기반 구현
|
||||
- 429 에러 3회 → 5분 차단
|
||||
- 자동 복구 (시간 후)
|
||||
- **테스트:** 7개 통합 테스트 (CircuitBreakerTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 10. ✅ Gate 5: Observability Dashboard
|
||||
- **파일:** src/KArtSell.Host/Features/Observability/GetMetricsEndpoint.cs
|
||||
- **내용:**
|
||||
- Batch SLA: 작업 완료 시간
|
||||
- Data quality: 격리된 항목 수
|
||||
- Duplicate detection: 중복 경고 (DEBT-014)
|
||||
- Reconciliation: 상태 불일치 (DEBT-014)
|
||||
- Model drift: OOS 성능 추적
|
||||
- **테스트:** 6개 통합 테스트 (ObservabilityMetricsTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Production Readiness Gates
|
||||
|
||||
| Gate | 항목 | 상태 | 기한 |
|
||||
|------|------|------|------|
|
||||
| **1** | DbUp 마이그레이션 (0000-0031) | ✅ PASS | - |
|
||||
| **2** | Outbox/Inbox Crash-recovery | ✅ PASS | - |
|
||||
| **3** | 252-day Shadow Run (실KRX) | ⏳ REHEARSAL IN PROGRESS | 오늘 |
|
||||
| **4** | 승인 워크플로우 | ✅ IMPL (대기) | 이번 주 |
|
||||
| **5** | 관찰성 대시보드 (메트릭) | ✅ IMPL (대기) | 다음 주 |
|
||||
|
||||
**Go-Live 기준:** 모든 Gate PASS + 증거 수집 완료 (≤ 2주)
|
||||
|
||||
---
|
||||
|
||||
## 📊 진행률
|
||||
|
||||
```
|
||||
Infrastructure: ██████████████████░ 85% (Phase 1 완료, Phase 2-3 진행 중)
|
||||
Testing: ██████████████████░ 100% (135/135 tests PASS - 5 arch + 95 integration + 35 unit)
|
||||
Documentation: ████████████░░░░░░░ 60% (로드맵, 계약, ADR, Gate 3 가이드)
|
||||
Validation Gates: ████████░░░░░░░░░░ 50% (Gate 1-2 PASS, Gate 3 IN PROGRESS, Gate 4-5 준비)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 다음 Iteration
|
||||
|
||||
### 이번 루프 (현재, ~60초)
|
||||
- [ ] Host 준비 확인
|
||||
- [ ] Agent 1 (Gate 3) 시작 또는 계속 대기
|
||||
- [ ] Loop 30초마다 상태 모니터링
|
||||
|
||||
### Host 준비 후 (오늘, ~30분)
|
||||
- [ ] Gate 3 Shadow Run 실행
|
||||
- [ ] 252일 검증 + 메트릭 계산
|
||||
- [ ] GATE_3_EVIDENCE.md 생성
|
||||
- [ ] PASS/FAIL 판정
|
||||
|
||||
### 다음 주
|
||||
- [ ] Gate 4: 승인 워크플로우 실행
|
||||
- [ ] Phase 2: OpenDart + KIS 최적화
|
||||
- [ ] 증거 수집 완료
|
||||
|
||||
### 2주 후
|
||||
- [ ] Gate 5: 관찰성 대시보드 활성화
|
||||
- [ ] Production readiness 최종 확인
|
||||
- [ ] Go-Live 준비
|
||||
|
||||
---
|
||||
|
||||
## 📝 Codex 연계 방법
|
||||
|
||||
### 다른 환경에서 계속하기
|
||||
|
||||
1. **현재 커밋 확인**
|
||||
```bash
|
||||
git log --oneline -10
|
||||
# 최신: eb106d5 (Phase 1 API optimization)
|
||||
# 이전: 9a2d939 (idempotency fix)
|
||||
# 이전: 4519fa8 (recommendation reports)
|
||||
```
|
||||
|
||||
2. **빌드 & 테스트**
|
||||
```bash
|
||||
dotnet build KArtSell.sln -c Release
|
||||
dotnet test KArtSell.sln -c Release
|
||||
```
|
||||
|
||||
3. **Host 시작 (Gate 3 진행)**
|
||||
```bash
|
||||
ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7 # Terminal 1
|
||||
dotnet run --project src/KArtSell.Host -c Release # Terminal 2
|
||||
```
|
||||
|
||||
4. **Shadow Run 요청**
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:5002/api/shadow-runs \
|
||||
-H "X-KArtSell-User: gate3-rehearsal" \
|
||||
-H "X-KArtSell-Role: Researcher" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"modelId": "00000000-0000-0000-0000-000000000001",
|
||||
"windowStart": "2024-01-02",
|
||||
"windowEnd": "2024-10-01"
|
||||
}'
|
||||
|
||||
# 폴링 (Analyst 역할 필요)
|
||||
curl http://127.0.0.1:5002/api/shadow-runs/{runId} \
|
||||
-H "X-KArtSell-User: gate3-rehearsal" \
|
||||
-H "X-KArtSell-Role: Analyst"
|
||||
```
|
||||
|
||||
5. **다음 단계로 점프**
|
||||
- Phase 2 구현 시작 (OpenDart, KIS)
|
||||
- 로드맵 업데이트
|
||||
이 게이트는 **달력 시간이 필요한 작업**입니다 (252+ 거래일 시뮬레이션은 컴퓨팅으로 앞당길 수 없음). "최적 전략적으로 빨리 끝내기"의 대상이 될 수 없고, 남은 유일한 실행 가능 조치는 위 재개 절차를 밟아 실제로 큐잉하는 것뿐입니다.
|
||||
|
||||
---
|
||||
|
||||
## 📚 관련 문서
|
||||
|
||||
- **WBS 트래커 (항목별 상세 상태):** `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`
|
||||
- **WBS 원 계획 (번호 충돌 있음, 주의):** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`
|
||||
- **Phase 1 상태 정정 기록:** `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`
|
||||
- **Architecture:** `docs/03_ARCHITECTURE_BE_FE.md`
|
||||
- **API Rate Limits:** `docs/API_RATE_LIMIT_STRATEGY.md`
|
||||
- **Gates:** `PRODUCTION_READINESS.md`
|
||||
- **Code Guidelines:** `CLAUDE.md`
|
||||
- **Tech Debt:** `TECH_DEBT_REGISTER.md`
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Loop 상태
|
||||
## 📝 이 문서를 다시 갱신할 때
|
||||
|
||||
**현재:** `/loop` 30초마다 모니터링 (Host 준비 대기)
|
||||
**다음:** Host 준비 → Gate 3 자동 시작
|
||||
**예상:** 오늘 이내 결과
|
||||
|
||||
---
|
||||
|
||||
**최종 목표:** Production readiness (모든 Gate PASS) ✅
|
||||
**기한:** 2주 이내 (2026-08-16)
|
||||
**Status:** ON TRACK 🚀
|
||||
1. **git log를 먼저 확인하세요.** 이 문서와 `main`이 얼마나 벌어졌는지 (`git log --oneline <이-문서-마지막-커밋>..main`) 확인하지 않고 문서만 읽고 "현재 상태"를 판단하지 마세요.
|
||||
2. **테스트는 격리 실행으로 확인하세요.** 전체 스위트 실행에서 통과했다고 해서 개별 기능이 안정적으로 통과하는 것은 아닙니다 (이번 세션에서 `AuditSql`이 정확히 이 이유로 놓칠 뻔했습니다 — `--filter`로 단일 클래스만 돌려서 재확인하세요).
|
||||
3. **"완료"라고 쓰기 전에 실제 파일 경로와 테스트 결과를 직접 확인하세요.** 이 저장소에는 검증 없이 "COMPLETE"/"100%"라고 선언한 문서가 매우 많습니다 (`EXECUTION_COMPLETE_FINAL.md`, `WORK_COMPLETION_CERTIFICATE.md` 등). 그 패턴을 반복하지 마세요.
|
||||
4. **Phase 1 Shadow Run은 달력 시간 게이트입니다.** 실제로 큐잉되어 진행 중이라는 구체적 증거(RunId/JobId) 없이 "진행 중"이라고 쓰지 마세요.
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
| DEBT-017 | Duplicate VS-03 Approval Workflow implementation | High (3) | Medium (2) | Backlog | Two independent, functionally-identical VS-03 maker-checker slices exist: `ApprovalWorkflow/` (Workstream H, own `ApprovalProposal`/`IClock`/`IOutbox` types) and `Features/ApprovalWorkflow/` (Workstream G, matches documented `Features/<Slice>/` convention). Both mapped the same routes (`/approvals`, `/approvals/{id}`, `/approvals/{id}/approve`), which crashed Host startup with a duplicate-route/missing-DI error the first time the app was actually booted (2026-08-07 — apparently never booted successfully before). Old set annotated `[DontRegister]` (FastEndpoints) 2026-08-07 to unblock boot; code and its test file (`ApprovalWorkflowTests.cs`) kept for now. Needs an architect decision: delete the old slice entirely (and its test) or intentionally keep both for a reason not yet documented. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening) |
|
||||
| DEBT-018 | Outbox write not co-transactional with entity write | Medium (2) | Medium (2) | Backlog | `TradeExecution/TradeHandlers.cs` (`TradeOutboxPublisher`) and `PortfolioReconciliation/ReconcileTradeHandler.cs` open a second, separate connection/transaction to write the outbox message after the trade/holding write already committed on its own connection. A crash between the two leaves the entity updated but no outbox event emitted (silent, non-atomic). Proper fix: thread a shared `NpgsqlTransaction` through `TradeSql`/`ReconciliationSql` mutation methods so entity insert + outbox insert commit together, matching `DapperModelOperationRequestRepository`'s pattern. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening) |
|
||||
| DEBT-019 | Multiple duplicate cross-cutting abstractions (`IClock`, `IOutboxWriter`, `IKrxDataService`) | Medium (2) | Low (1) | Completed (partial) | Found and collapsed 3 separate cases where a slice reinvented an abstraction that already existed in `KArtSell.BuildingBlocks`: a second `IKrxDataService` (deleted, `ShadowRun.Services`), a second `IOutboxWriter`/`WriteAsync<T>` in `ReconcileTradeHandler.cs` (removed, switched to `BuildingBlocks.Reliability.IOutboxWriter`), and a second `IClock`/`SystemClock` in `ApprovalWorkflow/ApprovalPolicy.cs` (removed, switched to `BuildingBlocks.Time.IClock`). Root cause: successive sessions implementing a slice without searching `BuildingBlocks` first. Recommend a pre-implementation checklist step ("does this abstraction already exist in BuildingBlocks?") for future slices. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening) |
|
||||
| DEBT-020 | `model_operations.models` and `compliance` schema never created by any migration | High (3) | Low (1) | Completed | `0036`/`0038` reference `model_operations.models(id)` via FK and `OpenDartDailyBatchJob.cs` queries it directly, but no migration ever ran `CREATE TABLE model_operations.models`; `0037` wrote to `compliance.*` tables without `CREATE SCHEMA compliance`. Any fresh database — including the actual deploy target (178.104.200.7), confirmed via a live failed SCP/DbMigrator deploy on 2026-08-07 — failed at migration `0036`/`0037`. Fixed via new `0035_model_operations_models.sql` (minimal: id/ticker/published_at/correlation_id/revision only — full Model Card schema is separate future work) and `CREATE SCHEMA IF NOT EXISTS compliance;` added to `0037`. Full chain 0000→0040 now verified fresh-install + idempotent re-run clean. | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-021 | Dapper never configured for snake_case↔PascalCase column mapping | High (3) | Low (1) | Completed | `Dapper.DefaultTypeMap.MatchNamesWithUnderscores` was never set anywhere in the codebase, so every `QueryAsync<T>`/`QuerySingleOrDefaultAsync<T>` result-mapping onto a snake_case DB column (e.g. `event_type` → `EventType`) silently returned null/default for that property instead of throwing — masking the bug in every Sql class across every module. Confirmed via `ApprovalWorkflowTests.InsertAndRetrieveProposal_RoundTrips` and `AuditTrailTests.InsertAuditEvent_CreatesImmutableRecord` both getting real rows back with null fields. Fixed centrally via a `[ModuleInitializer]` in `KArtSell.BuildingBlocks/Data/DapperBootstrap.cs` (runs once per process regardless of entry point — Host/DbMigrator/tests). | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-022 | jsonb/inet columns written as plain text without an explicit cast | Medium (2) | Low (1) | Completed (partial) | Dapper does not know to cast a `string` parameter to `jsonb`/`inet` for Npgsql; `AuditSql.InsertAuditEventAsync` (`details`, `ip_address`), `AuditSql.RedactAuditEventDetailsAsync` (duplicate `SET details =` assignment, separately fixed), `TradeSql.InsertTradeAsync`/`UpdateTradeStatusAsync` (`kis_response`), and `SellDecisionSql.InsertDecisionAsync` (`oos_performance`) all failed with `42804: column "x" is of type jsonb but expression is of type text` the first time they were run against a real schema. Fixed with explicit `::jsonb`/`::inet` casts at each call site (mechanical, no behavior change). `AuditSql`'s jsonb read-back (`Dictionary<string,object>` from a jsonb column) also needed a raw-DTO + `JsonSerializer.Deserialize` mapping since Dapper has no built-in jsonb→Dictionary conversion either. **Not yet checked**: `PortfolioReconciliation`/`ApprovalWorkflow` Sql classes for the same pattern beyond what surfaced in this session's test runs — a full audit of jsonb/inet columns across all Sql classes is still open. | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-023 | `ApprovalSql.InsertProposalAsync` fails on `DateOnly` parameter | Medium (2) | Low (1) | Backlog | `ApprovalWorkflowTests.InsertAndRetrieveProposal_RoundTrips` fails with `System.NotSupportedException: The member effectiveAt of type System.DateOnly cannot be used as a parameter value` — Dapper's `LookupDbType` doesn't recognize `DateOnly` without an explicit type map (`SqlMapper.AddTypeMap`/custom `TypeHandler`). Likely affects every other `DateOnly`-typed Dapper parameter in the codebase, not just this one; needs a similar centralized fix to DEBT-021 rather than a per-call-site patch. Discovered but not fixed in this session (scope cut to unblock the live deploy). | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-024 | New integration tests don't insert FK parent rows / one pure-logic test flakes under full-suite run | Low (1) | Low (1) | Backlog | `TradeExecutionTests` constructs `Trade` with a random `sellDecisionId` that was never inserted into `sell_decisions`, so every insert now correctly fails its FK constraint (`trades_sell_decision_id_fkey`) once the schema was actually complete (see DEBT-020) — test-only gap, not a production code defect; needs the tests updated to insert a parent `models`+`sell_decisions` row first. Separately, `SellPriorityRankerTests.CalculateScore_HardImpairment_ReturnsLowestScore` (pure logic, no DB) passed in isolation but returned 1000 instead of the expected 950 (age-boost not applied) when run as part of the full suite — not yet root-caused; may be test-order/parallelization state leakage rather than a `SellPriorityRanker` bug. Also, `DbUpMigrationTests.*` (pre-existing, unrelated to this session) fail locally with `42501: must be owner of database kartsell_migration_test` — a local Postgres role permission gap, not a code issue. | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Migration 0041: model_operations.models
|
||||
-- Missing prerequisite table: referenced via FK by 0036 (approval_proposals.model_id)
|
||||
-- and 0038 (sell_decisions.model_id), and queried directly by OpenDartDailyBatchJob.cs
|
||||
-- (SELECT DISTINCT ticker ... WHERE published_at <= @now), but never created by any
|
||||
-- prior migration. Any fresh database fails at 0036 without this table.
|
||||
--
|
||||
-- Scope is intentionally minimal (only the columns actually referenced today). The full
|
||||
-- Model Card / lifecycle schema (Freeze/Mature/Score/Diagnose/.../Manual Activation per
|
||||
-- CLAUDE.md) is a separate, larger piece of work and is not guessed at here.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_operations.models (
|
||||
id UUID PRIMARY KEY,
|
||||
ticker VARCHAR(20) NOT NULL,
|
||||
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
correlation_id UUID NOT NULL,
|
||||
revision INT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_models_ticker ON model_operations.models(ticker);
|
||||
CREATE INDEX IF NOT EXISTS ix_models_published_at ON model_operations.models(published_at DESC);
|
||||
@@ -1,6 +1,8 @@
|
||||
-- Workstream I: VS-04 Audit Trail (Immutable events + GDPR compliance)
|
||||
-- Creates compliance audit trail for model operations, regulatory reporting, and GDPR redaction
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS compliance;
|
||||
|
||||
-- Audit events (immutable, INSERT-only)
|
||||
CREATE TABLE IF NOT EXISTS compliance.audit_events (
|
||||
id UUID PRIMARY KEY,
|
||||
@@ -22,11 +24,11 @@ CREATE TABLE IF NOT EXISTS compliance.audit_events (
|
||||
);
|
||||
|
||||
-- Indexes for compliance querying
|
||||
CREATE INDEX idx_audit_events_entity_id ON compliance.audit_events(entity_id);
|
||||
CREATE INDEX idx_audit_events_event_type ON compliance.audit_events(event_type);
|
||||
CREATE INDEX idx_audit_events_actor_email ON compliance.audit_events(actor_email);
|
||||
CREATE INDEX idx_audit_events_event_at ON compliance.audit_events(event_at);
|
||||
CREATE INDEX idx_audit_events_correlation_id ON compliance.audit_events(correlation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_events_entity_id ON compliance.audit_events(entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_events_event_type ON compliance.audit_events(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_events_actor_email ON compliance.audit_events(actor_email);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_events_event_at ON compliance.audit_events(event_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_events_correlation_id ON compliance.audit_events(correlation_id);
|
||||
|
||||
-- GDPR retention tracking (personal data retention policy)
|
||||
CREATE TABLE IF NOT EXISTS compliance.gdpr_retention (
|
||||
@@ -43,8 +45,8 @@ CREATE TABLE IF NOT EXISTS compliance.gdpr_retention (
|
||||
);
|
||||
|
||||
-- Indexes for GDPR processing
|
||||
CREATE INDEX idx_gdpr_retention_customer_id ON compliance.gdpr_retention(customer_id);
|
||||
CREATE INDEX idx_gdpr_retention_purge_status ON compliance.gdpr_retention(purge_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_gdpr_retention_customer_id ON compliance.gdpr_retention(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_gdpr_retention_purge_status ON compliance.gdpr_retention(purge_status);
|
||||
|
||||
-- Event types enumeration (reference, not enforced at DB level)
|
||||
CREATE TABLE IF NOT EXISTS compliance.audit_event_types (
|
||||
|
||||
@@ -2,7 +2,7 @@ WBS_ID,Sprint,Slice_ID,Task,Status,Completion_Date,Evidence_Link,Owner,Notes
|
||||
AEG-X-001,S0,Cross,Version Coverage Matrix 고도화,COMPLETED,2026-08-04,docs/contracts/platform/VERSION_COVERAGE_MATRIX.md,PM/Architect,"✅ Version matrix: v10/v12/v12.1 compatibility (Retained/Improved/Superseded 100%), Supersession registry, Breaking change assessment, Migration roadmap"
|
||||
AEG-X-002,S0,Cross,global.json 고도화,COMPLETED,2026-08-04,.gitea/workflows/ci.yml (dotnet/pnpm restore/build/test),DevOps,"✅ CI pipeline validates: dotnet restore/build/test (Release config), pnpm frozen install/build/e2e, PostgreSQL 17 health checks, Log output to .gitea/workflows/ci.yml"
|
||||
AEG-X-003,S0,Cross,Architecture tests 고도화,COMPLETED,2026-08-04,tests/KArtSell.ArchitectureTests/RepositoryRulesTests.cs (6 tests PASSING),Architect/QA,"✅ Architecture rules enforced: (1) No prohibited patterns, (2) Domain isolation from infrastructure, (3) SQL validation (no SELECT *, schema-qualified), (4) Endpoint authorization (Roles/Policies), (5) No placeholder files, (6) No duplicate aggregate IDs. All 6 tests PASS."
|
||||
AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/AEG-X-004_STATUS_CONTRACT_SLICE.md; db/migrations/0032_shadow_run_queued_status_contract.sql; tests/KArtSell.Integration.Tests/DbUpMigrationTests.cs; tests/KArtSell.Integration.Tests/DbUpRecoveryTests.cs; evidence/AEG-X-004/0032-isolated-migration.trx",DBA/BE,"✅ Queued status contract applied as append-only 0032; isolated kartsell_migration_test rehearsal targeted 1/1 and recovery 6/6 passed. kartselldb_test was not reset. Production migration/DBA approval and Phase 1 requeue remain unclaimed."
|
||||
AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/AEG-X-004_STATUS_CONTRACT_SLICE.md; db/migrations/0032_shadow_run_queued_status_contract.sql; tests/KArtSell.Integration.Tests/DbUpMigrationTests.cs; tests/KArtSell.Integration.Tests/DbUpRecoveryTests.cs; evidence/AEG-X-004/0032-isolated-migration.trx",DBA/BE,"✅ Queued status contract applied as append-only 0032; isolated kartsell_migration_test rehearsal targeted 1/1 and recovery 6/6 passed. kartselldb_test was not reset. Production migration/DBA approval and Phase 1 requeue remain unclaimed. ⚠️ 2026-08-07 regression: 12 DbUpMigrationTests (Migration0008/0009/0010/0032) now fail locally with Postgres 42501 'must be owner of database kartsell_migration_test' — the kartsell DB user no longer owns/can DROP+CREATE that database on this environment. Code-side (fix/dapper-underscore-mapping-and-build branch) is unaffected; this needs a DBA grant (ALTER DATABASE kartsell_migration_test OWNER TO kartsell, or equivalent) before the fresh/upgrade/re-run rehearsal can be re-verified."
|
||||
AEG-X-005,S0,Cross,Security auth 고도화,COMPLETED,2026-08-04,"docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests)",Security/BE,"✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'"
|
||||
AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs",BE/SRE,"✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS."
|
||||
AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db",SRE/Security,"✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253)."
|
||||
@@ -14,14 +14,16 @@ AEG-VS-00-04,S0,VS-00,Vertical Slice API/Application/SQL 구현,COMPLETED,2026-0
|
||||
AEG-VS-00-05,S0,VS-00,Event/Job/Inbox·재처리 구현,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-VS-00-05_ACCEPTANCE_EVIDENCE.md + src/KArtSell.Host/Jobs/OutboxPollerJob.cs + DownstreamConsumerJob.cs",BE/SRE,"✅ Async event pipeline complete: OutboxPollerJob (poll unprocessed), DownstreamConsumerJob (dispatch), 5 consumers (SignalR/Approval/Audit), Hangfire 8 workers, correlation tracking. Acceptance_Evidence: Idempotency verified, Job 976 replay-safe, 177/177 tests PASS."
|
||||
AEG-VS-00-06,S0,VS-00,Vue feature·Zod·Query·컴포넌트 구현,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-VS-00-06_ACCEPTANCE_EVIDENCE.md + frontend/src/features/shadow-run/",FE Lead,"✅ Vue 3 feature module complete: ShadowRunPage + ShadowRunForm + Results + Chart, Pinia store, TanStack Query, Zod validation, vee-validate, 40/40 component tests PASS. Acceptance_Evidence: All criteria verified (accessibility, responsive, state ownership, error handling)."
|
||||
AEG-VS-00-07,S0,VS-00,회귀·관제·Runbook·Rollback 증거,COMPLETED,2026-08-04,docs/operational-runbook.md + PRODUCTION_READINESS.md + scripts/*.ps1 + commit ca2aeae,QA/SRE,"Golden/integration/failure/replay/E2E + metric/alert/Owner/Secondary/rollback rehearsal complete (Acceptance_Evidence: '회귀·관제·Runbook·Rollback 증거') - 7 scenarios, 4 scripts, 18 queries verified"
|
||||
AEG-X-009,S1,Cross,Source catalog 고도화,COMPLETED,2026-08-07,"docs/CURRENT/CATALOGS/source-catalog.md; docs/CURRENT/AEG-X-009_AUTOMATION_PROPOSAL.md; contracts/data/source-approval.v1.proposed.json; docs/DECISIONS/ADR-DATA-001.md; db/migrations/0033_source_approval_contract.sql; db/migrations/0034_dataset_manifest_freeze_contract.sql; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperApprovedModelContextReader.cs",Data Governance,"✅ Workstream D/E/F COMPLETED: source-catalog.md v2.0 (KRX/OpenDart/KIS consolidated), VS-02_DATA_GOVERNANCE_POLICY.md, VS-03/04 SLICE_SPECs. All 4 unknowns resolved. Phase 2 implementation ready (Workstreams G/H/I)."
|
||||
AEG-X-009,S1,Cross,Source catalog 고도화,COMPLETED,2026-08-07,"docs/CURRENT/CATALOGS/source-catalog.md; docs/CURRENT/AEG-X-009_AUTOMATION_PROPOSAL.md; contracts/data/source-approval.v1.proposed.json; docs/DECISIONS/ADR-DATA-001.md; db/migrations/0033_source_approval_contract.sql; db/migrations/0034_dataset_manifest_freeze_contract.sql; db/migrations/0033_market_data_import_logs.sql; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperApprovedModelContextReader.cs",Data Governance,"✅ Workstream D/E/F COMPLETED: source-catalog.md v2.0 (KRX/OpenDart/KIS consolidated), VS-02_DATA_GOVERNANCE_POLICY.md, VS-03/04 SLICE_SPECs. All 4 unknowns resolved. ✅ Workstream G (commit 136665c, 2026-08-07) also now COMPLETE: live KRX OpenAPI / OpenDart / KIS service integrations (P1-P3), daily scheduling + error classification + SLA tracking + LKG fallback (P4-P6), market_data schema with append-only import logs, correlation_id-based idempotent replay. ⚠️ Note: 0033 is used by two different, unrelated migrations across branches (source_approval_contract.sql vs market_data_import_logs.sql) — confirm actual applied migration number in the target DB's kartsell_schema_versions journal before assuming both landed as authored."
|
||||
AEG-VS-01-01,S1,VS-01,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-07,docs/CURRENT/SLICE_SPECS/VS-01-SLICE_SPEC.md,PM/Architect,"✅ SLICE_SPEC produced: VS-01-SLICE_SPEC.md (identity/MFA/RBAC/maker-checker contract). Prerequisite AEG-X-001 + AEG-VS-00-02 already COMPLETED. Ready for security team review and schema implementation."
|
||||
AEG-VS-02-01,S1,VS-02,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-02-SLICE_SPEC.md; docs/CURRENT/VS-02_DATA_GOVERNANCE_POLICY.md",PM/Architect,"✅ COMPLETE: VS-02-SLICE_SPEC.md + governance policy. All 4 unknowns resolved (data source, import SLA, audit policy, schema versioning). Financial security master implementation ready for Phase 2."
|
||||
AEG-VS-03-01,S2,VS-03,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Blocked: Depends on AEG-VS-02-01. Future sprint."
|
||||
AEG-VS-04-01,S2,VS-04,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Blocked: Depends on AEG-VS-03-01. Future sprint."
|
||||
AEG-VS-05-01,S3,VS-05,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Blocked: Depends on Gate 1 (Phase 1). Waiting for Job 976 (~50-90 days)."
|
||||
AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm changes require Golden data",Quant/QA,"Gate 2 prerequisite. Blocked by Phase 1 (Job 976) completion."
|
||||
AEG-VS-09-01,S4,VS-09,BuildEvidenceSnapshot,BLOCKED,TBD,"CLAUDE.md: Evidence requires Phase 1 results",PM/Architect,"Gate 2 prerequisite. Blocked by Phase 1."
|
||||
AEG-VS-10-01,S4,VS-10,GenerateSellDecision,BLOCKED,TBD,"CLAUDE.md: Model must pass PBO/DSR validation",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1."
|
||||
AEG-VS-19-01,S5,VS-19,RunFrozenBacktest,BLOCKED,TBD,"CLAUDE.md: Requires evidence from Phase 1-4",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1."
|
||||
PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md; docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/PHASE-1_REQUEUE_READINESS.md; docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md; docs/CURRENT/PHASE-1_PREFLIGHT_20260806.md; docs/CURRENT/PHASE-1_PRODUCTION_PREFLIGHT_20260806.md; evidence/AEG-X-004/production-readonly-preflight-20260806.md; db/migrations/0032_shadow_run_queued_status_contract.sql; logs/phase-1-execution.log; logs/host-startup-20260804-173000.log",김재현/BE/SRE,"Read-only preflight: active DbUp journal public.kartsell_schema_versions contains 0032 and check_status includes Queued. Capabilities remain order/KIS/client publication OFF. Server-side dataset_manifest, model_version_registry, evidence_snapshot, and release_evidence_bundle contain no approved/frozen rows; no RunId/JobId/enqueue created. Blocked pending approved server-side VersionSet."
|
||||
AEG-VS-03-01,S2,VS-03,모델 승인 워크플로우 구현 (Maker-Checker Governance),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-03-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/ (ApprovalSql.cs, ApprovalPolicy.cs); db/migrations/0036_approval_workflow.sql; tests/KArtSell.Integration.Tests/ApprovalWorkflow/*; commit a2e742c (Workstream H, PR #23, merged to main)",PM/BE Lead,"✅ Backend implementation + tests complete: maker-checker 2-person approval gate, evidence attachment, approve/reject with justification. 20/20 tests PASS run in isolation (2026-08-07, KARTSELL_POSTGRES=kartselldb). Also fixed same day: ApprovalSql couldn't insert a DateOnly parameter through Dapper at all (see fix/dapper-underscore-mapping-and-build branch) — prior to that fix, proposal creation failed 100% of the time against a real database; this was never caught because no integration test had run against a live DB since the slice merged. ⚠️ No frontend UI yet (no frontend/src/features/approval-workflow/). ⚠️ WBS_ID COLLISION — DECISION_REQUIRED: docs/CURRENT/CATALOGS/WBS_MASTER.csv defines VS-03 as 'IngestMarketDataPIT' (market data PIT ingestion), an unrelated slice. This VS-03 label was assigned by SLICE_SPECS/VS-03-SLICE_SPEC.md independently, without checking WBS_MASTER.csv. Needs an explicit decision: renumber this slice, or formally supersede WBS_MASTER's original VS-03 definition and record why."
|
||||
AEG-VS-04-01,S2,VS-04,불변 감사 추적 구현 (Audit Trail / GDPR),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-04-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/Compliance/ (AuditSql.cs, GdprRetention.cs); tests/KArtSell.Integration.Tests/Compliance/AuditTrailTests.cs; commit 97444c9 (Workstream I, PR #24, merged to main)",PM/BE Lead,"✅ Backend implementation + tests complete: append-only audit_events table, GDPR retention tracking + redaction (actor_email→'<redacted>', customer_id→'<purged>'), PII fields (ip_address). 5/5 tests PASS run in isolation (2026-08-07). Also fixed same day (fix/dapper-underscore-mapping-and-build branch): ip_address (inet) and kis_response-style jsonb columns threw InvalidCastException when read through Dapper into a typed class; GdprRetention.RetentionEndsAt was declared DateTime against a DATE column, same failure mode; and a process-wide Dapper snake_case-mapping race condition (KArtSell.BuildingBlocks' [ModuleInitializer] only fires once that assembly loads — AuditSql doesn't reliably touch it) intermittently nulled out every column read from this table depending on unrelated test/host startup order. None of this had ever been exercised against a live database before. ⚠️ No frontend UI yet. ⚠️ WBS_ID COLLISION — DECISION_REQUIRED: WBS_MASTER.csv defines VS-04 as 'ApplyCorporateActions' (corporate actions processing), an unrelated slice. Same issue and same required decision as AEG-VS-03-01 above."
|
||||
AEG-VS-05-01,S3,VS-05,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Not started. No docs/CURRENT/SLICE_SPECS/VS-05-SLICE_SPEC.md and no src/ implementation exist yet (confirmed 2026-08-07). Previous note referenced 'Job 976 (~50-90 days)' as if Phase 1 were actively running and counting down — that was false; see PHASE-1-SHADOW-RUN row below. Correct statement: blocked behind Gate 1 (Phase 1 shadow run), which itself has not been queued yet."
|
||||
AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm changes require Golden data",Quant/QA,"Gate 2 prerequisite. Blocked by Phase 1 completion. Corrected 2026-08-07: Phase 1 (Job 893/976) has not been started, not 'in progress' — see PHASE-1-SHADOW-RUN row. No countdown is currently running."
|
||||
AEG-VS-09-01,S4,VS-09,BuildEvidenceSnapshot,BLOCKED,TBD,"CLAUDE.md: Evidence requires Phase 1 results",PM/Architect,"Gate 2 prerequisite. Blocked by Phase 1, which has not been started (confirmed 2026-08-07). No src/ implementation exists for this slice."
|
||||
AEG-VS-10-01,S4,VS-10,매도 결정 엔진 구현 (GenerateSellDecision),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-10-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/SellDecision/ (SellDecisionEndpoints.cs, SellDecisionHandler.cs, SellDecisionSql.cs, SellPriorityRanker.cs); frontend/src/features/sell-decision/; tests/KArtSell.Integration.Tests/SellDecision/SellDecisionTests.cs; commit b1e38ac (Phase 3 J, PR #28, merged to main)",BE Lead/Quant Lead,"✅ Implementation complete (code + BE + FE + tests), matches WBS_MASTER's VS-10='GenerateSellDecision' definition (no ID collision here). Sell priority ranking (HARD_IMPAIRMENT→...→REENTRY_OPTION) with age/liquidity score boosts per VS-10-SLICE_SPEC.md. 32/32 tests PASS run in isolation (2026-08-07); one test (CalculateScore_HardImpairment_ReturnsLowestScore) had a wrong input value that happened to not exercise the >365-day age-boost branch the spec defines — fixed as a test bug, not a product bug (see fix/dapper-underscore-mapping-and-build branch). ⚠️ NOT validated: this row was previously (incorrectly) marked BLOCKED with reasoning 'Model must pass PBO/DSR validation' — that Gate-3/production-readiness validation genuinely still requires real Phase 1 shadow-run data and has not happened. Distinguish 'code implemented and unit/integration-tested' (done) from 'PBO/DSR-validated against real market data' (not done, blocked on Phase 1)."
|
||||
AEG-VS-19-01,S5,VS-19,RunFrozenBacktest,BLOCKED,TBD,"CLAUDE.md: Requires evidence from Phase 1-4",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1, which has not been started (confirmed 2026-08-07). No src/ implementation exists for this slice."
|
||||
AEG-VS-12-01,S2,VS-12,"거래 실행 시스템 구현 (Trade Execution, KIS Integration)",COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-12-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/TradeExecution/ (TradeEndpoints.cs, TradeHandlers.cs, TradeSql.cs, Trade.cs, KisTradeExecutionService.cs); db/migrations/0039_trades.sql; tests/KArtSell.Integration.Tests/TradeExecution/TradeExecutionTests.cs; commit b1e38ac (Phase 3 K, PR #28, merged to main)",BE Lead/Trading Ops,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Trade state machine (Pending→Submitted→Accepted→PartiallyFilled/FullyFilled→Confirmed→Reconciled), KIS order submission/poll/settlement. 13/13 tests PASS run in isolation (2026-08-07), but only after two real bugs were fixed on fix/dapper-underscore-mapping-and-build: (1) UpdateTradeStatusAsync only ever persisted status/kis_response/error_message and silently dropped kis_order_id, executed_quantity, unit_price, commission, net_proceeds and both timestamps on every single call since the slice merged — trade fills and settlements were not actually being recorded; (2) the same Dapper snake_case-mapping race condition described in AEG-VS-04-01's notes. ⚠️ No frontend UI yet (no frontend/src/features/trade-execution/). ⚠️ WBS_ID COLLISION — DECISION_REQUIRED: WBS_MASTER.csv defines VS-12 as 'RankBuyCandidates' (buy candidate ranking), an unrelated slice. This VS-12 label was assigned independently by SLICE_SPECS/VS-12-SLICE_SPEC.md."
|
||||
AEG-VS-14-01,S2,VS-14,포트폴리오 대사 구현 (Portfolio Reconciliation),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-14-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/ (Endpoints.cs, ReconcileTradeHandler.cs, ReconciliationEngine.cs, ReconciliationSql.cs, MismatchDetector.cs, CostBasisCalculator.cs); tests/KArtSell.Integration.Tests/PortfolioReconciliation/ReconciliationEngineTests.cs; tests/KArtSell.ModelOperations.UnitTests/EvaluationReconciliationPlannerTests.cs; commit b1e38ac (Phase 3 L, PR #28, merged to main); commit 4059828 (fix: missing model_operations.models table breaking every fresh DB, PR #29, merged to main same day)",BE Lead,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Sell Decision → Trade → Holdings reconciliation, cost-basis calculation, mismatch detection. 18/18 tests PASS run in isolation (2026-08-07). Note this slice's own merge (PR #28) shipped with a missing model_operations.models table that broke every fresh-database migration; that was caught and fixed same day in PR #29 — a reminder that this branch's fresh-install DbUp path had not actually been rehearsed before merge. ⚠️ No frontend UI yet. ⚠️ WBS_ID COLLISION — DECISION_REQUIRED: WBS_MASTER.csv defines VS-14 as 'GenerateDailyRecommendations' (daily client recommendation packages), an unrelated slice."
|
||||
PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md; docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/PHASE-1_REQUEUE_READINESS.md; docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md; docs/CURRENT/PHASE-1_PREFLIGHT_20260806.md; docs/CURRENT/PHASE-1_PRODUCTION_PREFLIGHT_20260806.md; evidence/AEG-X-004/production-readonly-preflight-20260806.md; db/migrations/0032_shadow_run_queued_status_contract.sql; logs/phase-1-execution.log; logs/host-startup-20260804-173000.log",김재현/BE/SRE,"Read-only preflight: active DbUp journal public.kartsell_schema_versions contains 0032 and check_status includes Queued. Capabilities remain order/KIS/client publication OFF. Server-side dataset_manifest, model_version_registry, evidence_snapshot, and release_evidence_bundle contain no approved/frozen rows; no RunId/JobId/enqueue created. Blocked pending approved server-side VersionSet. Re-confirmed 2026-08-07: still no RunId/JobId exists anywhere in this workspace or its evidence trail; nothing changed on this row this session. Any future document that claims this row is RUNNING must cite a real RunId/JobId — do not restate the earlier (already-corrected) false claim."
|
||||
|
||||
|
@@ -0,0 +1,23 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace KArtSell.BuildingBlocks.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Dapper does not map snake_case DB columns (event_type) to PascalCase C# properties
|
||||
/// (EventType) by default; every Sql class in this repo relies on that mapping, so this
|
||||
/// must be set before any query runs. A module initializer guarantees it runs once per
|
||||
/// process regardless of entry point (Host, DbMigrator, test runner) without every Sql
|
||||
/// class or Program.cs having to remember to configure it.
|
||||
/// </summary>
|
||||
internal static class DapperBootstrap
|
||||
{
|
||||
#pragma warning disable CA2255 // intentional: BuildingBlocks is this solution's internal shared layer,
|
||||
// not a distributed package, and every entry point (Host/DbMigrator/tests) needs this set
|
||||
// before its first query regardless of which one runs first.
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
public static void Initialize()
|
||||
{
|
||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,18 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Frontend Build Target: Automatically build Vite and copy to wwwroot (dev only) -->
|
||||
<!-- FrontendFiles must be globbed *after* pnpm build, inside the target: Vite emits
|
||||
content-hashed filenames each build, and a top-level ItemGroup is evaluated once
|
||||
at project load (before pnpm build runs), so it would copy stale/missing filenames. -->
|
||||
<Target Name="BuildFrontend" BeforeTargets="Build" Condition="'$(CI)' != 'true' AND Exists('$(ProjectDir)../../frontend/package.json')">
|
||||
<Exec Command="pnpm install --frozen-lockfile" WorkingDirectory="$(ProjectDir)../../frontend" ContinueOnError="false" />
|
||||
<Exec Command="pnpm build" WorkingDirectory="$(ProjectDir)../../frontend" ContinueOnError="false" />
|
||||
<ItemGroup>
|
||||
<FrontendFiles Include="../../frontend/dist/**/*" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(FrontendFiles)" DestinationFolder="$(ProjectDir)wwwroot/%(RecursiveDir)" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<FrontendFiles Include="../../frontend/dist/**/*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../KArtSell.BuildingBlocks/KArtSell.BuildingBlocks.csproj" />
|
||||
<ProjectReference Include="../KArtSell.Modules.SignalEngine/KArtSell.Modules.SignalEngine.csproj" />
|
||||
|
||||
@@ -69,7 +69,7 @@ public class ApprovalSql
|
||||
const string sql = """
|
||||
INSERT INTO model_operations.approval_proposals
|
||||
(id, model_id, status, created_by, created_at, justification, effective_at, published_at, revision, correlation_id)
|
||||
VALUES (@id, @modelId, @status, @createdBy, @createdAt, @justification, @effectiveAt, @publishedAt, 1, @correlationId)
|
||||
VALUES (@id, @modelId, @status, @createdBy, @createdAt, @justification, @effectiveAt::date, @publishedAt, 1, @correlationId)
|
||||
""";
|
||||
|
||||
await conn.ExecuteAsync(sql, new
|
||||
@@ -80,7 +80,7 @@ public class ApprovalSql
|
||||
createdBy,
|
||||
createdAt = _clock.UtcNow,
|
||||
justification,
|
||||
effectiveAt,
|
||||
effectiveAt = effectiveAt.ToString("yyyy-MM-dd"), // Dapper: DateOnly cannot be used as a parameter value directly
|
||||
publishedAt,
|
||||
correlationId
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ public class AuditSql
|
||||
error_message, details, evidence_links, ip_address, user_agent, published_at,
|
||||
correlation_id, revision)
|
||||
VALUES (@Id, @EventType, @EntityType, @EntityId, @ActorEmail, @ActorRole, @EventAt,
|
||||
@Result, @ErrorMessage, @Details, @EvidenceLinks, @IpAddress, @UserAgent,
|
||||
@Result, @ErrorMessage, @Details::jsonb, @EvidenceLinks, @IpAddress::inet, @UserAgent,
|
||||
NOW(), @CorrelationId, 1)
|
||||
""";
|
||||
|
||||
@@ -135,7 +135,7 @@ public class AuditSql
|
||||
// Get paginated results
|
||||
var sql = $"""
|
||||
SELECT id, event_type, entity_type, entity_id, actor_email, actor_role, event_at,
|
||||
result, error_message, details, evidence_links, ip_address, user_agent,
|
||||
result, error_message, details, evidence_links, ip_address::text as ip_address, user_agent,
|
||||
published_at, correlation_id, revision
|
||||
FROM compliance.audit_events
|
||||
WHERE {whereClause}
|
||||
@@ -146,7 +146,8 @@ public class AuditSql
|
||||
parameters.Add("@Skip", skip);
|
||||
parameters.Add("@Take", take);
|
||||
|
||||
var events = (await db.QueryAsync<AuditEvent>(sql, parameters)).ToList();
|
||||
var raw = await db.QueryAsync<AuditEventRaw>(sql, parameters);
|
||||
var events = raw.Select(ToAuditEvent).ToList();
|
||||
|
||||
return (events, total);
|
||||
}
|
||||
@@ -161,13 +162,54 @@ public class AuditSql
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, event_type, entity_type, entity_id, actor_email, actor_role, event_at,
|
||||
result, error_message, details, evidence_links, ip_address, user_agent,
|
||||
result, error_message, details, evidence_links, ip_address::text as ip_address, user_agent,
|
||||
published_at, correlation_id, revision
|
||||
FROM compliance.audit_events
|
||||
WHERE id = @EventId
|
||||
""";
|
||||
|
||||
return await db.QuerySingleOrDefaultAsync<AuditEvent>(sql, new { EventId = eventId });
|
||||
var raw = await db.QuerySingleOrDefaultAsync<AuditEventRaw>(sql, new { EventId = eventId });
|
||||
return raw == null ? null : ToAuditEvent(raw);
|
||||
}
|
||||
|
||||
private static AuditEvent ToAuditEvent(AuditEventRaw raw) => new()
|
||||
{
|
||||
Id = raw.Id,
|
||||
EventType = raw.EventType,
|
||||
EntityType = raw.EntityType,
|
||||
EntityId = raw.EntityId,
|
||||
ActorEmail = raw.ActorEmail,
|
||||
ActorRole = raw.ActorRole,
|
||||
EventAt = raw.EventAt,
|
||||
Result = raw.Result,
|
||||
ErrorMessage = raw.ErrorMessage,
|
||||
Details = raw.Details == null ? null : JsonSerializer.Deserialize<Dictionary<string, object>>(raw.Details),
|
||||
EvidenceLinks = raw.EvidenceLinks,
|
||||
IpAddress = raw.IpAddress,
|
||||
UserAgent = raw.UserAgent,
|
||||
PublishedAt = raw.PublishedAt,
|
||||
CorrelationId = raw.CorrelationId,
|
||||
Revision = raw.Revision
|
||||
};
|
||||
|
||||
private sealed class AuditEventRaw
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required string EventType { get; set; }
|
||||
public required string EntityType { get; set; }
|
||||
public Guid EntityId { get; set; }
|
||||
public required string ActorEmail { get; set; }
|
||||
public string? ActorRole { get; set; }
|
||||
public DateTime EventAt { get; set; }
|
||||
public required string Result { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public string? Details { get; set; }
|
||||
public string[]? EvidenceLinks { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public string? UserAgent { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public Guid CorrelationId { get; set; }
|
||||
public int Revision { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -255,12 +297,11 @@ public class AuditSql
|
||||
const string sql = """
|
||||
UPDATE compliance.audit_events
|
||||
SET details = jsonb_set(
|
||||
COALESCE(details, '{}'::jsonb),
|
||||
'{actor_email}',
|
||||
'"<redacted>"'::jsonb
|
||||
),
|
||||
details = jsonb_set(
|
||||
details,
|
||||
jsonb_set(
|
||||
COALESCE(details, '{}'::jsonb),
|
||||
'{actor_email}',
|
||||
'"<redacted>"'::jsonb
|
||||
),
|
||||
'{customer_id}',
|
||||
'"<purged>"'::jsonb
|
||||
),
|
||||
|
||||
@@ -10,7 +10,7 @@ public class GdprRetention
|
||||
public Guid EventId { get; set; }
|
||||
public Guid? CustomerId { get; set; }
|
||||
public string[]? DataCategories { get; set; } // PII, EMAIL, TRADING_HISTORY, PORTFOLIO_DATA, etc.
|
||||
public DateTime RetentionEndsAt { get; set; }
|
||||
public DateOnly RetentionEndsAt { get; set; }
|
||||
public required string PurgeStatus { get; set; } // PENDING, PURGED, EXCEPTION
|
||||
public DateTime? PurgedAt { get; set; }
|
||||
public string? ExceptionReason { get; set; }
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace KArtSell.Modules.ModelOperations;
|
||||
|
||||
/// <summary>
|
||||
/// KArtSell.BuildingBlocks.Data.DapperBootstrap sets Dapper's snake_case-to-PascalCase column
|
||||
/// mapping via its own [ModuleInitializer], but that only fires once that assembly is actually
|
||||
/// loaded into the process. Several Sql classes in this module (e.g. AuditSql, TradeSql) only
|
||||
/// have a `using` for a BuildingBlocks namespace without ever touching a type from it at
|
||||
/// runtime, so under test isolation - or any host that queries this module before touching
|
||||
/// BuildingBlocks - the load (and the mapping) can be skipped, silently nulling out every
|
||||
/// snake_case column. Every Sql class in this assembly is defined here, so a module initializer
|
||||
/// in this assembly is guaranteed to run before any of them are used, regardless of what else
|
||||
/// has loaded.
|
||||
/// </summary>
|
||||
internal static class DapperMappingBootstrap
|
||||
{
|
||||
#pragma warning disable CA2255
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
public static void Initialize()
|
||||
{
|
||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class SellDecisionSql : ISellDecisionSql
|
||||
sell_priority, target_quantity, target_price, approval_id, execution_id,
|
||||
created_at, created_by, created_justification, published_at, correlation_id, revision
|
||||
) VALUES (
|
||||
@id, @modelId, @status, @pboScore, @dsrMetric, @oosPerformance,
|
||||
@id, @modelId, @status, @pboScore, @dsrMetric, @oosPerformance::jsonb,
|
||||
@sellPriority, @targetQuantity, @targetPrice, @approvalId, @executionId,
|
||||
@createdAt, @createdBy, @createdJustification, @publishedAt, @correlationId, @revision
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ public class Trade
|
||||
public decimal? Commission { get; set; }
|
||||
public decimal? NetProceeds { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public JsonElement? KisResponse { get; set; }
|
||||
public string? KisResponse { get; set; }
|
||||
public DateTime? ExecutionTimestamp { get; set; }
|
||||
public DateTime? SettlementTimestamp { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
@@ -55,14 +55,14 @@ public class Trade
|
||||
{
|
||||
Status = TradeStatus.Submitted;
|
||||
KisOrderId = kisOrderId;
|
||||
KisResponse = response;
|
||||
KisResponse = response.ToString();
|
||||
Revision++;
|
||||
}
|
||||
|
||||
public void MarkAccepted(JsonElement response)
|
||||
{
|
||||
Status = TradeStatus.Accepted;
|
||||
KisResponse = response;
|
||||
KisResponse = response.ToString();
|
||||
Revision++;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public class Trade
|
||||
TotalAmount = executedQty * unitPrice;
|
||||
Status = executedQty >= Quantity ? TradeStatus.FullyFilled : TradeStatus.PartiallyFilled;
|
||||
ExecutionTimestamp = now;
|
||||
KisResponse = response;
|
||||
KisResponse = response.ToString();
|
||||
Revision++;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public class Trade
|
||||
public void MarkErrored(KisTradeExecutionException exception)
|
||||
{
|
||||
ErrorMessage = exception.Message;
|
||||
KisResponse = exception.KisResponse;
|
||||
KisResponse = exception.KisResponse?.ToString();
|
||||
Revision++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +57,7 @@ public class SubmitTradeHandler
|
||||
);
|
||||
|
||||
trade.MarkSubmitted(orderId, response);
|
||||
await _sql.UpdateTradeStatusAsync(
|
||||
trade.Id,
|
||||
TradeStatus.Submitted,
|
||||
response,
|
||||
null,
|
||||
command.CorrelationId,
|
||||
ct
|
||||
);
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
|
||||
await PublishEventAsync(
|
||||
"TradeSubmitted",
|
||||
@@ -84,14 +77,7 @@ public class SubmitTradeHandler
|
||||
catch (KisTradeExecutionException ex)
|
||||
{
|
||||
trade.MarkErrored(ex);
|
||||
await _sql.UpdateTradeStatusAsync(
|
||||
trade.Id,
|
||||
trade.Status,
|
||||
ex.KisResponse,
|
||||
ex.Message,
|
||||
command.CorrelationId,
|
||||
ct
|
||||
);
|
||||
await _sql.UpdateTradeStatusAsync(trade, ex.KisResponse, ex.Message, ct);
|
||||
|
||||
_logger.LogError(
|
||||
"Trade submission failed: {TradeId} {Classification}",
|
||||
@@ -163,14 +149,7 @@ public class PollTradeStatusHandler
|
||||
trade.MarkFilled(executedQty, unitPrice, response, _clock.UtcNow.UtcDateTime);
|
||||
}
|
||||
|
||||
await _sql.UpdateTradeStatusAsync(
|
||||
trade.Id,
|
||||
trade.Status,
|
||||
response,
|
||||
null,
|
||||
command.CorrelationId,
|
||||
ct
|
||||
);
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
|
||||
if (trade.Status is TradeStatus.FullyFilled)
|
||||
{
|
||||
@@ -195,14 +174,7 @@ public class PollTradeStatusHandler
|
||||
}
|
||||
catch (KisTradeExecutionException ex)
|
||||
{
|
||||
await _sql.UpdateTradeStatusAsync(
|
||||
trade.Id,
|
||||
trade.Status,
|
||||
ex.KisResponse,
|
||||
ex.Message,
|
||||
command.CorrelationId,
|
||||
ct
|
||||
);
|
||||
await _sql.UpdateTradeStatusAsync(trade, ex.KisResponse, ex.Message, ct);
|
||||
|
||||
_logger.LogError("Failed to poll trade status: {TradeId}", trade.Id);
|
||||
}
|
||||
@@ -262,14 +234,7 @@ public class ConfirmSettlementHandler
|
||||
if (success)
|
||||
{
|
||||
trade.MarkConfirmed(_clock.UtcNow.UtcDateTime, command.Commission);
|
||||
await _sql.UpdateTradeStatusAsync(
|
||||
trade.Id,
|
||||
TradeStatus.Confirmed,
|
||||
response,
|
||||
null,
|
||||
command.CorrelationId,
|
||||
ct
|
||||
);
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
|
||||
await TradeOutboxPublisher.PublishAsync(
|
||||
_connectionFactory,
|
||||
@@ -290,14 +255,7 @@ public class ConfirmSettlementHandler
|
||||
}
|
||||
catch (KisTradeExecutionException ex)
|
||||
{
|
||||
await _sql.UpdateTradeStatusAsync(
|
||||
trade.Id,
|
||||
trade.Status,
|
||||
ex.KisResponse,
|
||||
ex.Message,
|
||||
command.CorrelationId,
|
||||
ct
|
||||
);
|
||||
await _sql.UpdateTradeStatusAsync(trade, ex.KisResponse, ex.Message, ct);
|
||||
|
||||
_logger.LogError("Failed to confirm settlement: {TradeId}", trade.Id);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface ITradeSql
|
||||
Task<IEnumerable<Trade>> GetTradesByStatusAsync(TradeStatus status, Guid correlationId, CancellationToken ct = default);
|
||||
Task<IEnumerable<Trade>> GetTradesByDecisionIdAsync(Guid sellDecisionId, Guid correlationId, CancellationToken ct = default);
|
||||
Task InsertTradeAsync(Trade trade, CancellationToken ct = default);
|
||||
Task UpdateTradeStatusAsync(Guid tradeId, TradeStatus newStatus, JsonElement? kisResponse, string? errorMessage, Guid correlationId, CancellationToken ct = default);
|
||||
Task UpdateTradeStatusAsync(Trade trade, JsonElement? kisResponse, string? errorMessage, CancellationToken ct = default);
|
||||
Task<int> CountTradesByStatusAsync(TradeStatus status, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public class TradeSql : ITradeSql
|
||||
|
||||
const string sql = """
|
||||
SELECT id, sell_decision_id, kis_order_id, status, quantity, executed_quantity,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response::text as kis_response,
|
||||
execution_timestamp, settlement_timestamp, published_at, correlation_id, revision
|
||||
FROM model_operations.trades
|
||||
WHERE id = @tradeId
|
||||
@@ -61,7 +61,7 @@ public class TradeSql : ITradeSql
|
||||
|
||||
const string sql = """
|
||||
SELECT id, sell_decision_id, kis_order_id, status, quantity, executed_quantity,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response::text as kis_response,
|
||||
execution_timestamp, settlement_timestamp, published_at, correlation_id, revision
|
||||
FROM model_operations.trades
|
||||
WHERE kis_order_id = @kisOrderId
|
||||
@@ -82,7 +82,7 @@ public class TradeSql : ITradeSql
|
||||
|
||||
const string sql = """
|
||||
SELECT id, sell_decision_id, kis_order_id, status, quantity, executed_quantity,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response::text as kis_response,
|
||||
execution_timestamp, settlement_timestamp, published_at, correlation_id, revision
|
||||
FROM model_operations.trades
|
||||
WHERE status = @status
|
||||
@@ -102,7 +102,7 @@ public class TradeSql : ITradeSql
|
||||
|
||||
const string sql = """
|
||||
SELECT id, sell_decision_id, kis_order_id, status, quantity, executed_quantity,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response,
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response::text as kis_response,
|
||||
execution_timestamp, settlement_timestamp, published_at, correlation_id, revision
|
||||
FROM model_operations.trades
|
||||
WHERE sell_decision_id = @sellDecisionId
|
||||
@@ -126,7 +126,7 @@ public class TradeSql : ITradeSql
|
||||
unit_price, total_amount, commission, net_proceeds, error_message, kis_response,
|
||||
execution_timestamp, settlement_timestamp, published_at, correlation_id, revision)
|
||||
VALUES (@id, @sellDecisionId, @kisOrderId, @status, @quantity, @executedQuantity,
|
||||
@unitPrice, @totalAmount, @commission, @netProceeds, @errorMessage, @kisResponse,
|
||||
@unitPrice, @totalAmount, @commission, @netProceeds, @errorMessage, @kisResponse::jsonb,
|
||||
@executionTimestamp, @settlementTimestamp, @publishedAt, @correlationId, @revision)
|
||||
""";
|
||||
|
||||
@@ -143,7 +143,7 @@ public class TradeSql : ITradeSql
|
||||
trade.Commission,
|
||||
trade.NetProceeds,
|
||||
trade.ErrorMessage,
|
||||
kisResponse = trade.KisResponse?.ToString(),
|
||||
trade.KisResponse,
|
||||
trade.ExecutionTimestamp,
|
||||
trade.SettlementTimestamp,
|
||||
trade.PublishedAt,
|
||||
@@ -155,11 +155,9 @@ public class TradeSql : ITradeSql
|
||||
}
|
||||
|
||||
public async Task UpdateTradeStatusAsync(
|
||||
Guid tradeId,
|
||||
TradeStatus newStatus,
|
||||
Trade trade,
|
||||
JsonElement? kisResponse,
|
||||
string? errorMessage,
|
||||
Guid correlationId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
using var connection = await _dataSource.OpenConnectionAsync(ct);
|
||||
@@ -167,13 +165,21 @@ public class TradeSql : ITradeSql
|
||||
const string sql = """
|
||||
INSERT INTO model_operations.trade_status_history
|
||||
(id, trade_id, old_status, new_status, transitioned_at, kis_response, error_message, published_at, correlation_id)
|
||||
SELECT @id, id, status, @newStatus, NOW(), @kisResponse, @errorMessage, NOW(), @correlationId
|
||||
SELECT @id, id, status, @newStatus, NOW(), @kisResponse::jsonb, @errorMessage, NOW(), @correlationId
|
||||
FROM model_operations.trades
|
||||
WHERE id = @tradeId;
|
||||
|
||||
UPDATE model_operations.trades
|
||||
SET status = @newStatus,
|
||||
kis_response = COALESCE(@kisResponse, kis_response),
|
||||
kis_order_id = COALESCE(@kisOrderId, kis_order_id),
|
||||
executed_quantity = COALESCE(@executedQuantity, executed_quantity),
|
||||
unit_price = COALESCE(@unitPrice, unit_price),
|
||||
total_amount = COALESCE(@totalAmount, total_amount),
|
||||
commission = COALESCE(@commission, commission),
|
||||
net_proceeds = COALESCE(@netProceeds, net_proceeds),
|
||||
execution_timestamp = COALESCE(@executionTimestamp, execution_timestamp),
|
||||
settlement_timestamp = COALESCE(@settlementTimestamp, settlement_timestamp),
|
||||
kis_response = COALESCE(@kisResponse::jsonb, kis_response),
|
||||
error_message = COALESCE(@errorMessage, error_message),
|
||||
revision = revision + 1
|
||||
WHERE id = @tradeId
|
||||
@@ -182,14 +188,22 @@ public class TradeSql : ITradeSql
|
||||
await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
id = Guid.NewGuid(),
|
||||
tradeId,
|
||||
newStatus = newStatus.ToString(),
|
||||
tradeId = trade.Id,
|
||||
newStatus = trade.Status.ToString(),
|
||||
kisOrderId = trade.KisOrderId,
|
||||
executedQuantity = trade.ExecutedQuantity,
|
||||
unitPrice = trade.UnitPrice,
|
||||
totalAmount = trade.TotalAmount,
|
||||
commission = trade.Commission,
|
||||
netProceeds = trade.NetProceeds,
|
||||
executionTimestamp = trade.ExecutionTimestamp,
|
||||
settlementTimestamp = trade.SettlementTimestamp,
|
||||
kisResponse = kisResponse?.ToString(),
|
||||
errorMessage,
|
||||
correlationId
|
||||
correlationId = trade.CorrelationId
|
||||
});
|
||||
|
||||
_logger.LogInformation("Updated trade {TradeId} status to {Status}", tradeId, newStatus);
|
||||
_logger.LogInformation("Updated trade {TradeId} status to {Status}", trade.Id, trade.Status);
|
||||
}
|
||||
|
||||
public async Task<int> CountTradesByStatusAsync(TradeStatus status, CancellationToken ct = default)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace KArtSell.Modules.SignalEngine;
|
||||
|
||||
/// <summary>
|
||||
/// See KArtSell.Modules.ModelOperations.DapperMappingBootstrap for the full rationale: Dapper's
|
||||
/// snake_case-to-PascalCase column mapping is a process-wide static flag set via
|
||||
/// KArtSell.BuildingBlocks.Data.DapperBootstrap's [ModuleInitializer], which only fires once
|
||||
/// that assembly is loaded. This mirrors it locally so every Sql/reader class defined in this
|
||||
/// assembly is guaranteed the mapping is on before its first query, regardless of load order.
|
||||
/// </summary>
|
||||
internal static class DapperMappingBootstrap
|
||||
{
|
||||
#pragma warning disable CA2255
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
public static void Initialize()
|
||||
{
|
||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ namespace KArtSell.Integration.Tests.ApprovalWorkflow;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Xunit;
|
||||
using KArtSell.BuildingBlocks.Time;
|
||||
using KArtSell.Modules.ModelOperations.ApprovalWorkflow;
|
||||
@@ -165,6 +166,7 @@ public class ApprovalWorkflowTests : IAsyncLifetime
|
||||
var id = Guid.NewGuid();
|
||||
var modelId = Guid.NewGuid();
|
||||
var correlationId = Guid.NewGuid();
|
||||
await SeedModelAsync(modelId);
|
||||
|
||||
// Act
|
||||
await _sql.InsertProposalAsync(
|
||||
@@ -185,6 +187,15 @@ public class ApprovalWorkflowTests : IAsyncLifetime
|
||||
Assert.Equal(modelId, retrieved.ModelId);
|
||||
Assert.Equal(correlationId, retrieved.CorrelationId);
|
||||
}
|
||||
|
||||
private async Task SeedModelAsync(Guid modelId)
|
||||
{
|
||||
await using var conn = new Npgsql.NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO model_operations.models (id, ticker, correlation_id) VALUES (@Id, @Ticker, @CorrelationId)",
|
||||
new { Id = modelId, Ticker = "TEST", CorrelationId = Guid.NewGuid() });
|
||||
}
|
||||
}
|
||||
|
||||
public class InMemoryOutbox : IOutbox
|
||||
|
||||
@@ -106,6 +106,11 @@ public class AuditTrailTests : IAsyncLifetime
|
||||
var customerId = Guid.NewGuid();
|
||||
var retentionId = Guid.NewGuid();
|
||||
|
||||
await _sql.InsertAuditEventAsync(
|
||||
_db, eventId, AuditEventTypes.ModelActivated, AuditEntityTypes.Model,
|
||||
Guid.NewGuid(), "customer@company.com", null, DateTime.UtcNow, "SUCCESS", null,
|
||||
null, null, null, null, Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
// Act
|
||||
await _sql.InsertGdprRetentionAsync(
|
||||
_db, retentionId, eventId, customerId,
|
||||
@@ -175,7 +180,8 @@ public class AuditTrailTests : IAsyncLifetime
|
||||
// Assert
|
||||
var @event = await _sql.GetAuditEventByIdAsync(_db, eventId, CancellationToken.None);
|
||||
Assert.NotNull(@event);
|
||||
Assert.Contains("<redacted>", @event.Details?.ToString() ?? "");
|
||||
Assert.Equal("<redacted>", @event.Details?["actor_email"].ToString());
|
||||
Assert.Equal("<purged>", @event.Details?["customer_id"].ToString());
|
||||
}
|
||||
|
||||
private const string TestConnectionString =
|
||||
|
||||
@@ -118,8 +118,8 @@ public class SellPriorityRankerTests
|
||||
[Fact]
|
||||
public void CalculateScore_HardImpairment_ReturnsLowestScore()
|
||||
{
|
||||
var score = _ranker.CalculateScore(SellPriority.HardImpairment, fundAgeDays: 200, liquidityPercent: 0.5m);
|
||||
Assert.Equal(950m, score); // 1000 - 50 (age boost)
|
||||
var score = _ranker.CalculateScore(SellPriority.HardImpairment, fundAgeDays: 400, liquidityPercent: 0.5m);
|
||||
Assert.Equal(950m, score); // 1000 - 50 (age boost, fundAgeDays > 365 per VS-10-SLICE_SPEC.md)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using KArtSell.Modules.ModelOperations.TradeExecution;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
@@ -31,11 +32,31 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task<Guid> SeedSellDecisionAsync()
|
||||
{
|
||||
await using var connection = await _dataSource.OpenConnectionAsync();
|
||||
var modelId = Guid.NewGuid();
|
||||
await connection.ExecuteAsync(
|
||||
"INSERT INTO model_operations.models (id, ticker, correlation_id) VALUES (@Id, @Ticker, @CorrelationId)",
|
||||
new { Id = modelId, Ticker = "TEST", CorrelationId = Guid.NewGuid() });
|
||||
|
||||
var sellDecisionId = Guid.NewGuid();
|
||||
await connection.ExecuteAsync(
|
||||
"""
|
||||
INSERT INTO model_operations.sell_decisions
|
||||
(id, model_id, status, created_by, published_at, correlation_id)
|
||||
VALUES (@Id, @ModelId, 'PENDING', 'test@company.com', NOW(), @CorrelationId)
|
||||
""",
|
||||
new { Id = sellDecisionId, ModelId = modelId, CorrelationId = Guid.NewGuid() });
|
||||
|
||||
return sellDecisionId;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateTrade_WithValidData_ShouldInsertSuccessfully()
|
||||
{
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var sellDecisionId = Guid.NewGuid();
|
||||
var sellDecisionId = await SeedSellDecisionAsync();
|
||||
var correlationId = Guid.NewGuid();
|
||||
|
||||
var trade = Trade.Create(sellDecisionId, 1000, correlationId, DateTime.UtcNow);
|
||||
@@ -55,14 +76,14 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
{
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var correlationId = Guid.NewGuid();
|
||||
var trade = Trade.Create(Guid.NewGuid(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade = Trade.Create(await SeedSellDecisionAsync(), 1000, correlationId, DateTime.UtcNow);
|
||||
|
||||
await sql.InsertTradeAsync(trade);
|
||||
|
||||
var response = JsonDocument.Parse("{}").RootElement;
|
||||
trade.MarkSubmitted("KIS-ORDER-123", response);
|
||||
|
||||
await sql.UpdateTradeStatusAsync(trade.Id, TradeStatus.Submitted, response, null, correlationId);
|
||||
await sql.UpdateTradeStatusAsync(trade, response, null);
|
||||
|
||||
var retrieved = await sql.GetTradeByIdAsync(trade.Id, correlationId);
|
||||
|
||||
@@ -76,14 +97,14 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
{
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var correlationId = Guid.NewGuid();
|
||||
var trade = Trade.Create(Guid.NewGuid(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade = Trade.Create(await SeedSellDecisionAsync(), 1000, correlationId, DateTime.UtcNow);
|
||||
|
||||
await sql.InsertTradeAsync(trade);
|
||||
|
||||
var response = JsonDocument.Parse("{}").RootElement;
|
||||
trade.MarkFilled(1000, 49.95m, response, DateTime.UtcNow);
|
||||
|
||||
await sql.UpdateTradeStatusAsync(trade.Id, TradeStatus.FullyFilled, response, null, correlationId);
|
||||
await sql.UpdateTradeStatusAsync(trade, response, null);
|
||||
|
||||
var retrieved = await sql.GetTradeByIdAsync(trade.Id, correlationId);
|
||||
|
||||
@@ -100,8 +121,8 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var correlationId = Guid.NewGuid();
|
||||
|
||||
var trade1 = Trade.Create(Guid.NewGuid(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade2 = Trade.Create(Guid.NewGuid(), 2000, correlationId, DateTime.UtcNow);
|
||||
var trade1 = Trade.Create(await SeedSellDecisionAsync(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade2 = Trade.Create(await SeedSellDecisionAsync(), 2000, correlationId, DateTime.UtcNow);
|
||||
|
||||
await sql.InsertTradeAsync(trade1);
|
||||
await sql.InsertTradeAsync(trade2);
|
||||
@@ -118,14 +139,14 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
{
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var correlationId = Guid.NewGuid();
|
||||
var trade = Trade.Create(Guid.NewGuid(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade = Trade.Create(await SeedSellDecisionAsync(), 1000, correlationId, DateTime.UtcNow);
|
||||
|
||||
await sql.InsertTradeAsync(trade);
|
||||
|
||||
trade.TotalAmount = 49950m;
|
||||
trade.MarkConfirmed(DateTime.UtcNow, 50m);
|
||||
|
||||
await sql.UpdateTradeStatusAsync(trade.Id, TradeStatus.Confirmed, null, null, correlationId);
|
||||
await sql.UpdateTradeStatusAsync(trade, null, null);
|
||||
|
||||
var retrieved = await sql.GetTradeByIdAsync(trade.Id, correlationId);
|
||||
|
||||
@@ -140,11 +161,16 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
{
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var correlationId = Guid.NewGuid();
|
||||
var trade = Trade.Create(Guid.NewGuid(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade = Trade.Create(await SeedSellDecisionAsync(), 1000, correlationId, DateTime.UtcNow);
|
||||
|
||||
await sql.InsertTradeAsync(trade);
|
||||
await sql.UpdateTradeStatusAsync(trade.Id, TradeStatus.Submitted, null, null, correlationId);
|
||||
await sql.UpdateTradeStatusAsync(trade.Id, TradeStatus.Accepted, null, null, correlationId);
|
||||
|
||||
var response = JsonDocument.Parse("{}").RootElement;
|
||||
trade.MarkSubmitted("KIS-1", response);
|
||||
await sql.UpdateTradeStatusAsync(trade, response, null);
|
||||
|
||||
trade.MarkAccepted(response);
|
||||
await sql.UpdateTradeStatusAsync(trade, response, null);
|
||||
|
||||
var retrieved = await sql.GetTradeByIdAsync(trade.Id, correlationId);
|
||||
|
||||
@@ -158,8 +184,8 @@ public class TradeExecutionTests : IAsyncLifetime
|
||||
var sql = new TradeSql(_dataSource, _logger);
|
||||
var correlationId = Guid.NewGuid();
|
||||
|
||||
var trade1 = Trade.Create(Guid.NewGuid(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade2 = Trade.Create(Guid.NewGuid(), 2000, correlationId, DateTime.UtcNow);
|
||||
var trade1 = Trade.Create(await SeedSellDecisionAsync(), 1000, correlationId, DateTime.UtcNow);
|
||||
var trade2 = Trade.Create(await SeedSellDecisionAsync(), 2000, correlationId, DateTime.UtcNow);
|
||||
|
||||
await sql.InsertTradeAsync(trade1);
|
||||
await sql.InsertTradeAsync(trade2);
|
||||
|
||||
Reference in New Issue
Block a user