From 8dabffc08af7a66cad3ca2644f0b96a18e2a0300 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Thu, 30 Jul 2026 12:21:32 +0900 Subject: [PATCH] chore: commit pre-existing untracked work + finish archived-doc notes Commits work that predated this session and was sitting untracked: qualitative sell strategy module (src/quant_engine), the t20 outcome ledger cron workflow, the QuantEngine masterpiece roadmap doc, a WBS execution yaml, and shared Razor Pages partials (page header, delete confirm modal) with their view models. Verified these build clean (0 errors, 0 warnings) before committing. Also includes the "ARCHIVED" header notes on 6 docs archived in the previous commit - the git-mv landed there, but the note text itself was missed from that commit's staged file list. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/t20_ledger.yml | 17 + docs/QUANTENGINE_MASTERPIECE_ROADMAP.md | 445 ++++++++++++++++++ docs/WBS_EXECUTION_20260728.yaml | 40 ++ docs/archive/CICD_ROADMAP.md | 4 + ...PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md | 4 + docs/archive/MODERNIZATION_ROADMAP_VISUAL.md | 4 + ...ODERNIZATION_STRATEGY_ROADMAP_2026-2027.md | 4 + .../ROADMAP_ENTERPRISE_TEMPLATES_WBS.md | 4 + docs/archive/ROADMAP_WBS.md | 5 + .../Shared/DeleteConfirmModalViewModel.cs | 6 + .../Pages/Shared/PageHeaderViewModel.cs | 10 + .../Pages/Shared/_DeleteConfirmModal.cshtml | 34 ++ .../Pages/Shared/_PageHeader.cshtml | 21 + .../qualitative_sell_strategy_v1.py | 377 +++++++++++++++ 14 files changed, 975 insertions(+) create mode 100644 .gitea/workflows/t20_ledger.yml create mode 100644 docs/QUANTENGINE_MASTERPIECE_ROADMAP.md create mode 100644 docs/WBS_EXECUTION_20260728.yaml create mode 100644 src/dotnet/QuantEngine.Web/Pages/Shared/DeleteConfirmModalViewModel.cs create mode 100644 src/dotnet/QuantEngine.Web/Pages/Shared/PageHeaderViewModel.cs create mode 100644 src/dotnet/QuantEngine.Web/Pages/Shared/_DeleteConfirmModal.cshtml create mode 100644 src/dotnet/QuantEngine.Web/Pages/Shared/_PageHeader.cshtml create mode 100644 src/quant_engine/qualitative_sell_strategy_v1.py diff --git a/.gitea/workflows/t20_ledger.yml b/.gitea/workflows/t20_ledger.yml new file mode 100644 index 00000000..ac730022 --- /dev/null +++ b/.gitea/workflows/t20_ledger.yml @@ -0,0 +1,17 @@ +name: Daily T+20 Outcome Ledger Build + +on: + schedule: + - cron: '0 8 * * 1-5' # KST 17:00 (Mon-Fri) + workflow_dispatch: + +jobs: + build-t20-ledger: + runs-on: self-hosted + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Build T+20 Outcome Ledger + run: | + python3 tools/build_operational_t20_outcome_ledger_v1.py diff --git a/docs/QUANTENGINE_MASTERPIECE_ROADMAP.md b/docs/QUANTENGINE_MASTERPIECE_ROADMAP.md new file mode 100644 index 00000000..4d127fff --- /dev/null +++ b/docs/QUANTENGINE_MASTERPIECE_ROADMAP.md @@ -0,0 +1,445 @@ +# QuantEngine 냉정 분석 & 마스터피스 로드맵 +**분석 기준일: 2026-07-28 | 분석 범위: 전체 프로젝트 (spec/src/tools/docs/CI/CD/서버)** + +--- + +## 🔬 Part 1: 냉정한 현실 진단 + +### 1.1 프로젝트 정체성 (What Is This?) + +| 질문 | 냉정한 답변 | +|:---|:---| +| **이 프로젝트는 무엇인가?** | 은퇴자산 포트폴리오(~5억원)를 운용하는 결정론적 퀀트 투자 엔진. GAS→Python→.NET→Vue 3으로 점진 진화 중 | +| **누가 사용하는가?** | 현재 1인(본인). 미래 확장 가능성은 있으나 현재는 개인 운용 | +| **실제 동작하는가?** | GAS+Python 파이프라인은 **실제 운용 중** (98단계 DAG, KIS 연동, 리밸런싱 엔진). .NET+Vue 3은 어드민/대시보드 수준에서 동작 | +| **수익을 내는가?** | 아직 미측정. T+20 실측 데이터가 0건(DATA_GATED). 핵심 캘리브레이션 0/191 검증됨 | + +### 1.2 아키텍처 진화 타임라인 + +```mermaid +graph LR + A["Phase 1-6
GAS + Python
2026-05~06"] --> B["Phase 7
구조 경화
2026-06~07"] + B --> C["Phase 10
.NET 10 + PostgreSQL
2026-06~07"] + C --> D["SEMP Phase 0
Vue 3 + FastEndpoints
2026-07~현재"] + + style A fill:#2d5016,stroke:#4a8c28,color:#fff + style B fill:#2d5016,stroke:#4a8c28,color:#fff + style C fill:#8c6b2a,stroke:#c49a3c,color:#fff + style D fill:#8c2a2a,stroke:#c43c3c,color:#fff +``` + +### 1.3 기술 스택 현황 (냉정 평가) + +| 레이어 | 기술 | 코드량 | 성숙도 | 냉정 평가 | +|:---|:---|:---|:---|:---| +| **데이터 수집** | GAS (18 `.gs`) + Python (KIS/Naver/Yahoo) | ~8,000 LOC | ⭐⭐⭐⭐ | ✅ **가장 안정적**. 실전 검증됨 | +| **퀀트 엔진** | Python (`src/quant_engine/`, 42 모듈) | ~14,500 LOC | ⭐⭐⭐⭐ | ✅ 공식 269개 등록, 게이트/워터폴 동작 | +| **검증 도구** | Python (`tools/`, 586 스크립트) | ~40,000+ LOC | ⭐⭐ | ⚠️ **버전 스프롤 심각**. v1~v6 난립, 정리 필요 | +| **백엔드 API** | .NET 10 / ASP.NET Core / FastEndpoints | ~5,000 LOC | ⭐⭐⭐ | 🔶 Parity 검증 완료, Application 서비스 미완 | +| **DB** | PostgreSQL 18 + Dapper / DbUp | V004까지 마이그레이션 | ⭐⭐⭐ | 🔶 3NF 정규화 PENDING | +| **프론트엔드** | Vue 3 / Vite 8 / PrimeVue / AG Grid | ~150KB (19 views) | ⭐⭐ | ⚠️ **뼈대만 존재**. 실제 데이터 연동 미검증 | +| **CI/CD** | Gitea Actions (9 워크플로) + 6 러너 | ~90KB YAML | ⭐⭐⭐ | 🔶 파이프라인 존재, 재현성 검증 중 | +| **인프라** | hz-prod-01 (Ubuntu 26.04, 2vCPU/3.7G) | systemd + Nginx | ⭐⭐⭐ | 🔶 동작하나 모니터링/알림 부재 | + +### 1.4 핵심 문제점 — 5대 구조적 약점 + +> [!CAUTION] +> 이 프로젝트의 가장 큰 위험은 **"완료 표시가 많지만 실증이 없다"**는 것입니다. + +#### 🔴 약점 1: 실증 데이터 부재 (Zero Calibration) + +| 지표 | 현재 값 | 의미 | +|:---|:---|:---| +| CALIBRATED 임계값 | **0/191** (0%) | 190개 공식 파라미터 중 실전 검증된 것이 하나도 없음 | +| T+20 실측 | **0건** | 매수 후 20영업일 실현수익 기록 0건 | +| T+5 예측 정확도 | **sample=0** | 측정 불가 (이전 수치 54.76%/35.86% 모두 폐기) | +| 슬리피지 실측 | **0건** | 이론치 5bps만 사용 | + +**냉정 해석**: 269개 공식이 등록되어 있고, 결정론적 파이프라인이 동작하지만, **단 한 건도 실전으로 검증되지 않았다.** 이 엔진은 사실상 "정교한 시뮬레이터"이지 "검증된 투자 엔진"이 아니다. + +#### 🔴 약점 2: 기술 스택 분산 (Four Language Overhead) + +``` +GAS (.gs) ←→ Python (.py) ←→ C# (.cs) ←→ TypeScript (.ts/.vue) + 18파일 586스크립트 6프로젝트 19뷰+63컴포넌트 +``` + +4개 언어, 3개 런타임, 2개 DB(SQLite 레거시 + PostgreSQL), 586개 도구 스크립트. **1인 운영자에게 이 복잡도는 지속 가능하지 않다.** + +#### 🟠 약점 3: tools/ 버전 스프롤 + +`tools/` 디렉토리에 **586개 스크립트**가 존재한다. 상당수가 `_v1`, `_v2`, `_v3` 등의 버전 접미사를 가지며, 어떤 것이 현재 canonical인지 즉시 판별하기 어렵다. + +#### 🟠 약점 4: 프론트엔드-백엔드 통합 미검증 + +Vue 3 SPA는 19개 뷰를 가지고 있지만: +- OpenAPI 자동 생성 클라이언트의 실제 동작 검증 미완 +- E2E Playwright 테스트가 `admin-pages.spec.ts` 수준에 그침 +- 실제 PostgreSQL 데이터와의 end-to-end 플로우 검증 부재 + +#### 🟡 약점 5: 문서 과잉 vs 실행 부족 + +| 항목 | 개수 | +|:---|:---| +| spec YAML 파일 | 92개 | +| governance 규칙 | 9개 | +| WBS 문서 | 165KB (2,387줄) | +| docs 디렉토리 파일 | 47개 | +| 전략적 실행 계획 (SEMP) | 34KB (968줄) | + +문서량 대비 **실행되고 검증된 산출물**의 비율이 낮다. "계약은 많고 체결은 적다." + +--- + +### 1.5 강점 — 인정할 것 + +> [!TIP] +> 이 프로젝트가 가진 강점도 냉정히 인정해야 한다. + +| 강점 | 근거 | +|:---|:---| +| **결정론적 아키텍처** | 269개 공식 ID + lifecycle 100% 등록 + 황금 테스트 커버리지 100% | +| **안전 게이트** | KIS API 거래 차단(governance/rules/06-07) — 코드 수준 강제 | +| **자체 비판 문화** | 2026-06-21 비판적 리뷰(0c절)에서 10건의 문제를 스스로 발견하고 추적 | +| **CI/CD 기반** | Gitea Actions 9개 워크플로, 6 러너, 자동 배포 + 롤백 | +| **클라우드 인프라** | hz-prod-01에 실제 배포, systemd + Nginx + PostgreSQL 운영 | +| **Parity 검증** | Python↔C# 계산기 40건 parity PASS | +| **spec 체계** | 92개 YAML spec — 의사결정 추적 가능성이 매우 높음 | + +--- + +## 🎯 Part 2: 마스터피스를 위한 전략적 재구성 + +### 2.1 마스터피스의 정의 + +> **마스터피스 = 실전 검증된 알파 생성 + 1인이 지속 운영 가능한 복잡도 + 프로 수준 UX** + +3가지 축을 동시에 달성해야 한다: + +```mermaid +graph TD + A["💰 Alpha Engine
실증 기반 수익 생성"] + B["🔧 Operational Excellence
1인 운영 가능한 단순함"] + C["🎨 Professional UX
의사결정 가시성 극대화"] + + A --> D["🏆 MASTERPIECE
은퇴자산 퀀트 엔진"] + B --> D + C --> D + + style D fill:#c9a227,stroke:#8b7019,color:#000,stroke-width:3px + style A fill:#1a5276,color:#fff + style B fill:#1a5276,color:#fff + style C fill:#1a5276,color:#fff +``` + +### 2.2 전략적 페이즈 재구성 + +기존 Phase 0~10의 WBS는 너무 분산되어 있다. **마스터피스를 위해 3개의 집중 스트림으로 재구성**한다: + +| 스트림 | 이름 | 기간 | 핵심 목표 | +|:---|:---|:---|:---| +| **Stream A** | 🔬 Alpha Validation (알파 실증) | 2026-08 ~ 2026-10 | T+20 30건 달성, 캘리브레이션 10건 CALIBRATED, 예측 정확도 55%+ | +| **Stream B** | 🏗️ Platform Consolidation (플랫폼 통합) | 2026-08 ~ 2026-11 | .NET 10 백엔드 완성, Vue 3 SPA 실동작, tools/ 정리 | +| **Stream C** | 🎨 Professional Operation (전문가 운영) | 2026-10 ~ 2026-12 | 관제 대시보드, 자동 알림, 1-click 리밸런싱 UI, 성과 리포팅 | + +``` +2026-08 2026-09 2026-10 2026-11 2026-12 +├──────────►├──────────►├──────────►├──────────►├──────────► +│ Stream A: Alpha Validation ────────────────►│ +│ ███████████████████████████████████████████ │ +│ │ +│ Stream B: Platform Consolidation ──────────────────────►│ +│ ████████████████████████████████████████████████████████ │ +│ │ +│ Stream C: Professional Operation ─────────►│ +│ ██████████████████████████████████████████ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 📋 Part 3: 상세 WBS — 마스터피스 로드맵 + +--- + +### Stream A: 🔬 Alpha Validation (알파 실증) + +> **철학: "공식 269개는 충분하다. 이제 1개라도 실전에서 증명하라."** + +#### WBS-A1: T+20 실측 파이프라인 가동 (2026-08 Week 1~2) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| A1.1 | `build_operational_t20_outcome_ledger_v1.py` 일일 자동 실행 스케줄 등록 (Gitea Actions cron) | 없음 | `.gitea/workflows/t20_ledger.yml` 존재, cron 17:00 KST | +| A1.2 | 매수 진입 이벤트 자동 캡처 — KIS 체결내역 조회 또는 HTS 수동 기록 UI | A1.1 | `Temp/t20_entry_events.json` 행 수 ≥ 1 | +| A1.3 | T+20 만기 시점 자동 Close 가격 수집 — yfinance/KIS fallback | A1.2 | `Temp/t20_outcomes.json`에 `close_t20` 필드 non-null | +| A1.4 | 30건 도달 시 `ALPHA_FEEDBACK_LOOP_V2` 자동 활성화 트리거 | A1.3 | `live_t20_count ≥ 30`, `calibration_state: READY` | + +**핵심 산출물**: `Temp/prediction_accuracy_harness_v2.json` → `t20_sample ≥ 30` + +--- + +#### WBS-A2: 캘리브레이션 실증 전환 1차 (2026-08 Week 3 ~ 2026-09 Week 2) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| A2.1 | `calibration_priority_v1.json`에서 urgency score 상위 20건 추출 | 없음 | 대상 목록 JSON 존재 | +| A2.2 | 20건에 대해 과거 1년 역사 데이터 백테스트 (replay calibration) | A2.1 | `Temp/replay_calibration_results_v1.json` gate: PASS | +| A2.3 | 백테스트 결과 기반 10건 `EXPERT_PRIOR/SPEC_DERIVED` → `CALIBRATED` 승격 | A2.2 | `spec/calibration_registry.yaml`에 `source: CALIBRATED` 10건+ | +| A2.4 | 승격된 임계값으로 엔진 재실행, 결과 비교 (before/after) | A2.3 | `Temp/calibration_impact_report_v1.json` 존재 | + +**핵심 산출물**: CALIBRATED ≥ 10/191 (5.2%+ 달성) + +--- + +#### WBS-A3: 예측 정확도 목표 달성 (2026-09 ~ 2026-10) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| A3.1 | T+20 30건 기반 첫 match_rate 산출 | A1.4 | `match_rate_pct ≥ 50%` (1차 목표) | +| A3.2 | SS001 가중치(P/V/F) 1차 재보정 | A3.1 | `Temp/alpha_calibration_v1.json` — 보정 전후 개선 ≥ 2%p | +| A3.3 | 슬리피지 실측 5건 기록 (HTS 체결 후 수동 입력) | 없음 | `outputs/execution_slippage.db` sample ≥ 5 | +| A3.4 | 슬리피지 실측값 vs 5bps 이론값 비교 및 spec 갱신 | A3.3 | `gap_bps` 보고서 존재, 3bps 초과 시 spec 갱신 | +| A3.5 | 섹터 플로우 30일 달성 후 `FLOW_CREDIT_V1` 활성화 | 없음 | `days_accumulated ≥ 30`, lifecycle → ACTIVE | + +**핵심 산출물**: `match_rate_pct ≥ 55%`, `honest_proof_score ≥ 70` + +--- + +### Stream B: 🏗️ Platform Consolidation (플랫폼 통합) + +> **철학: "복잡도를 줄여라. 1인이 유지할 수 없는 구조는 마스터피스가 아니다."** + +#### WBS-B1: tools/ 대정리 (2026-08 Week 1~2) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| B1.1 | `tools/` 586개 파일 전수 인벤토리 — canonical/deprecated/dead 3등급 분류 | 없음 | `Temp/tools_inventory_v1.json` 생성 | +| B1.2 | Dead 스크립트 → `tools/archive/` 이동 (삭제하지 않고 보존) | B1.1 | `tools/archive/` 100건+ 이동 | +| B1.3 | Canonical 스크립트에 `#!/usr/bin/env python` + docstring 표준화 | B1.2 | canonical 스크립트 100% docstring 보유 | +| B1.4 | `tools/README.md` — canonical 도구 목록 + 사용법 작성 | B1.3 | README 존재, 검증 명령 포함 | +| B1.5 | GAS 중복 정리: `src/gas/` + `src/gas_adapter_parts/` + `src/google_apps_script/` → `src/gas/` 단일화 | 없음 | 3개 디렉토리 → 1개로 통합 | +| B1.6 | `src/client/` 레거시 삭제 | 없음 | 디렉토리 미존재 | +| B1.7 | `.gitea/workflows/deploy-prod.yml.backup` 삭제 | 없음 | 파일 미존재 | + +**핵심 산출물**: `tools/` 파일 수 300개 이하, canonical 도구 목록 문서 + +--- + +#### WBS-B2: .NET 10 백엔드 완성 (2026-08 Week 3 ~ 2026-09) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| B2.1 | Application 서비스 완성 — Workspace/Approval/Collection/Formula 4개 서비스 실구현 | 없음 | `dotnet test --filter ApplicationService` 13+ PASS | +| B2.2 | 데이터 수집 오케스트레이터 — KIS-first → Naver fallback → JSON replay | B2.1 | `dotnet test --filter Collection` 4+ PASS | +| B2.3 | PostgreSQL 3NF 스키마 정규화 (V005~V008 마이그레이션) | 없음 | `dotnet-ef database update` 성공, `stocks`/`sources`/`market_data` 테이블 존재 | +| B2.4 | Repository 패턴 100% 적용 — Dapper + interface 기반 | B2.3 | 직접 SQL 호출 0건 (Service 레이어에서) | +| B2.5 | FastEndpoints API 완성 — 최소 15개 엔드포인트 (CRUD + 퀀트 결과 조회) | B2.1 | OpenAPI spec 자동 생성, endpoint 15개+ 존재 | +| B2.6 | Hangfire 스케줄러 — 일일 수집 + 주간 리밸런싱 + 월간 유니버스 갱신 | B2.2 | Hangfire 대시보드에서 3개 recurring job 확인 | +| B2.7 | 보안 강화 — BCrypt 패스워드 해싱, JWT 토큰 갱신, CSRF 방어 완전 탑재 | B2.5 | `dotnet test --filter Security` 10+ PASS | + +**핵심 산출물**: `dotnet build` 경고 0, `dotnet test` 250+ PASS, API 엔드포인트 15+ + +--- + +#### WBS-B3: Vue 3 SPA 실동작 검증 (2026-09 ~ 2026-10) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| B3.1 | OpenAPI TypeScript 클라이언트 자동 생성 + Axios interceptor 완성 | B2.5 | `src/frontend/src/api/generated/` 자동 생성, `npm run type-check` 0 에러 | +| B3.2 | 로그인 플로우 — JWT + Refresh Token + 자동 갱신 | B2.7 | Playwright E2E: 로그인→토큰갱신→인증만료 시나리오 PASS | +| B3.3 | DashboardView — 실시간 포트폴리오 요약 (PostgreSQL 데이터 연동) | B3.1 | DashboardView에서 총자산/수익률/포지션 데이터 렌더링 확인 | +| B3.4 | SnapshotAdminView — account_snapshot 편집/검증/저장/승인 4단계 플로우 | B3.1 | Playwright E2E: 편집→저장→diff preview→승인 시나리오 PASS | +| B3.5 | DatabaseView — AG Grid 기반 전체 테이블 브라우저 | B3.1 | 10,000행 렌더링 성능 P95 < 200ms | +| B3.6 | SystemSettingsView — 전체 시스템 설정 관리 UI 실연동 | B3.1 | 설정 변경 → DB 반영 → 화면 갱신 round-trip | +| B3.7 | Vitest 단위 테스트 20+ 작성, Playwright E2E 10+ 시나리오 | B3.2 | `npm run test:unit` 20+ PASS, `npm run test:e2e` 10+ PASS | + +**핵심 산출물**: Vue 3 SPA 완전 동작, E2E 테스트 10+, 타입 에러 0 + +--- + +#### WBS-B4: CI/CD 파이프라인 통합 (2026-10) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| B4.1 | CI 파이프라인 단일화 — Python/dotnet/frontend 3-stage gate 직렬 | B3.7 | `ci.yml` 실행 시간 ≤ 20분 | +| B4.2 | 배포 파이프라인 — frontend build → dotnet publish → tar.gz → 배포 → 헬스체크 | B4.1 | `deploy-prod.yml` 자동 실행, 6개 헬스체크 PASS | +| B4.3 | 배포 후 Playwright smoke 테스트 — 운영 서버 접속 + 로그인 + 대시보드 확인 | B4.2 | `tests/e2e/production-smoke.spec.ts` PASS | +| B4.4 | CI 재현성 검증 — 3회 연속 실행 결과 100% 동일 | B4.1 | `Temp/ci_reproducibility_report.json` variance < 5% | + +**핵심 산출물**: 단일 `git push` → 15~20분 내 자동 배포 + 검증 완료 + +--- + +### Stream C: 🎨 Professional Operation (전문가 운영) + +> **철학: "마스터피스는 보는 순간 신뢰감을 준다."** + +#### WBS-C1: 관제 대시보드 (2026-10 ~ 2026-11) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| C1.1 | **Portfolio Overview** — 총자산, 일일/주간/월간 수익률, KOSPI 대비 알파, MDD | B3.3 | 첫 화면에서 5초 내 전체 상황 파악 가능 | +| C1.2 | **Position Heat Map** — 종목별 손익 히트맵 + Core/Satellite/Cash 버킷 시각화 | B3.3 | 11개 포지션 히트맵 렌더링, 색상으로 건강도 즉시 인지 | +| C1.3 | **Signal Dashboard** — SS001 점수, 라우팅 게이트 상태, 매수/매도 신호 실시간 표시 | B3.3 | HOLD/SELL_READY/BLOCKED 상태 색상 chips 표시 | +| C1.4 | **Calibration Health** — 191개 임계값 중 CALIBRATED/PROVISIONAL/미검증 비율 진행바 | B3.3 | 캘리브레이션 건강도 게이지 차트 | +| C1.5 | **Engine Activity Log** — 최근 엔진 실행 이력, 성공/실패/경고 타임라인 | B3.3 | 최근 30일 실행 이력 스크롤 가능 | + +**핵심 산출물**: 한 화면에서 포트폴리오 건강도 + 신호 + 엔진 상태를 즉시 파악 + +--- + +#### WBS-C2: 자동 알림 & 모니터링 (2026-11) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| C2.1 | Telegram Bot 알림 — 일일 엔진 실행 결과, 매도 신호 발생, MDD 경고 | 없음 | Telegram 메시지 수신 확인 | +| C2.2 | 장애 자동 감지 — CI 실패, 서버 다운, 데이터 수집 중단 시 즉시 알림 | C2.1 | 의도적 장애 주입 → 5분 내 알림 수신 | +| C2.3 | Serilog + OpenTelemetry 구조화 로깅 — JSON 형식 로그 + 메트릭 수집 | B2.5 | `journalctl -u quantengine` JSON 구조 확인 | +| C2.4 | 주간 자동 리포트 — 포트폴리오 성과, 신호 변화, 캘리브레이션 진척 | C1.1 | 매주 일요일 Telegram 리포트 수신 | + +**핵심 산출물**: 수동 확인 없이 이상 상황 자동 통보 + +--- + +#### WBS-C3: 1-Click 리밸런싱 워크플로 (2026-11 ~ 2026-12) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| C3.1 | 리밸런싱 제안 화면 — 버킷 밴드 위반 시 자동 제안, 주문표 생성 | C1.2 | 리밸런싱 필요 시 자동 제안 카드 표시 | +| C3.2 | 주문 시뮬레이션 — 지정가/호가단위 정규화 결과 미리보기 | C3.1 | 시뮬레이션 결과 테이블 (ticker/수량/지정가/슬리피지) | +| C3.3 | 승인 → HTS 주문표 export (CSV/클립보드) | C3.2 | "승인" 버튼 → CSV 다운로드/클립보드 복사 | +| C3.4 | 체결 후 실측 기록 UI — 의도가/실제체결가 입력 → 슬리피지 DB 저장 | C3.3 | `execution_slippage` 레코드 생성 확인 | + +**핵심 산출물**: 리밸런싱 판단 → 주문 → 체결 기록의 완전한 루프 + +--- + +#### WBS-C4: 성과 리포팅 & 아카이빙 (2026-12) + +| WBS | 작업 | 선행 | 성공 판단 데이터 | +|:---|:---|:---|:---| +| C4.1 | 월간 성과 보고서 자동 생성 — PDF/마크다운 (수익률, 알파, MDD, 매매 이력) | C1.1 | `Temp/monthly_report_2026_12.pdf` 존재 | +| C4.2 | 벤치마크 비교 차트 — KOSPI, S&P 500 대비 누적 수익률 | C4.1 | 차트에서 3개 라인 비교 가능 | +| C4.3 | 연간 결산 보고서 — 세금 계산, 배당 수입, 실현/미실현 손익 | C4.1 | 연간 보고서 항목 100% 채움 | +| C4.4 | 이력 아카이빙 — 일일 포트폴리오 스냅샷 PostgreSQL 시계열 저장 | B2.3 | `portfolio_daily_snapshots` 테이블 행 수 ≥ 30 | + +**핵심 산출물**: 전문가급 성과 리포트 자동 생성 + +--- + +## 📊 Part 4: 완성도 매트릭스 & 마일스톤 + +### 4.1 마스터피스 완성도 KPI (2026-12-31 목표) + +| 차원 | 지표 | 현재 | 마스터피스 목표 | 판정 | +|:---|:---|:---|:---|:---| +| **Alpha** | T+20 실측 건수 | 0 | ≥ 50 | 🔴 | +| **Alpha** | CALIBRATED 임계값 | 0/191 | ≥ 30/191 (15%+) | 🔴 | +| **Alpha** | 예측 정확도 (match_rate) | DATA_GATED | ≥ 55% | 🔴 | +| **Alpha** | 슬리피지 실측 | 0건 | ≥ 10건 | 🔴 | +| **Alpha** | KOSPI 대비 알파 | 미측정 | > 0%p/분기 | 🔴 | +| **Platform** | .NET 테스트 | 214 | ≥ 300 | 🟠 | +| **Platform** | Vue 3 E2E 테스트 | ~3 | ≥ 15 | 🟠 | +| **Platform** | tools/ 파일 수 | 586 | ≤ 200 (canonical) | 🟠 | +| **Platform** | CI 재현성 | 미검증 | 3회 연속 100% 동일 | 🟠 | +| **Platform** | 배포 소요시간 | 수동 | ≤ 20분 (자동) | 🟠 | +| **Operation** | 수동 개입 | 매일 | ≤ 1회/주 | 🟡 | +| **Operation** | 장애 알림 | 없음 | 5분 내 Telegram | 🔴 | +| **Operation** | 월간 리포트 | 없음 | 자동 생성 | 🔴 | +| **Operation** | 리밸런싱 워크플로 | CLI 전용 | 웹 UI 1-Click | 🔴 | + +### 4.2 월별 마일스톤 + +| 월 | 마일스톤 | 핵심 증빙 | +|:---|:---|:---| +| **2026-08** | **M1: Alpha Pipeline Live** — T+20 수집 자동화 + tools/ 대정리 완료 | T+20 entry 10건+, tools/ 300개 이하 | +| **2026-09** | **M2: Backend Complete** — .NET Application 서비스 + PostgreSQL 3NF + API 15개 | `dotnet test` 250+, endpoint 15+ | +| **2026-10** | **M3: SPA Live** — Vue 3 전체 뷰 실동작 + CI/CD 통합 | E2E 10+, 자동 배포 동작 | +| **2026-11** | **M4: Professional Ops** — 관제 대시보드 + Telegram 알림 + 리밸런싱 UI | 대시보드 5개 패널, 알림 동작 | +| **2026-12** | **M5: Masterpiece** — 알파 실증 + 성과 리포트 + 연간 결산 | match_rate ≥ 55%, 월간 리포트 자동 | + +--- + +## 🔥 Part 5: 즉시 실행 — Sprint-0 (이번 주, 2026-07-28 ~ 2026-08-01) + +> **이번 주에 할 수 있는 가장 가치 있는 4가지** + +### Sprint-0.1: T+20 수집 자동화 파이프라인 (Day 1~2) +```bash +# 1. Gitea Actions 일일 cron 등록 +# .gitea/workflows/t20_ledger.yml +# 매 영업일 17:00 KST 자동 실행 +python tools/build_operational_t20_outcome_ledger_v1.py --auto +``` +- 이것이 **가장 시급**하다. 알파 검증의 전제조건이 데이터 누적이고, 하루라도 빨리 시작해야 한다. + +### Sprint-0.2: tools/ 파일 인벤토리 자동 분류 (Day 2~3) +```bash +python tools/build_tools_inventory_v1.py +# 586개 → canonical / deprecated / dead 3등급 분류 +# Temp/tools_inventory_v1.json 산출 +``` + +### Sprint-0.3: 슬리피지 실측 첫 기록 (Day 3~4) +```bash +# 최근 체결 이력에서 1건이라도 기록 +python tools/evaluate_execution_slippage_v1.py record \ + --ticker 005930 --side BUY \ + --intended-price 71000 --actual-price 71050 \ + --recorded-at 2026-07-28 +``` + +### Sprint-0.4: 레거시 파일 정리 (Day 4~5) +```bash +# 즉시 삭제 가능한 레거시 +rm -rf src/client/ +rm .gitea/workflows/deploy-prod.yml.backup +``` + +--- + +## 📐 Part 6: 의존성 차트 (전체) + +```mermaid +graph TD + subgraph "Stream A: Alpha Validation" + A11["A1: T+20 파이프라인"] + A21["A2: 캘리브레이션 실증"] + A31["A3: 예측 정확도"] + A11 --> A31 + A21 --> A31 + end + + subgraph "Stream B: Platform" + B11["B1: tools 정리"] + B21["B2: .NET 완성"] + B31["B3: Vue 3 SPA"] + B41["B4: CI/CD 통합"] + B21 --> B31 + B31 --> B41 + end + + subgraph "Stream C: Operation" + C11["C1: 관제 대시보드"] + C21["C2: 자동 알림"] + C31["C3: 리밸런싱 UI"] + C41["C4: 성과 리포팅"] + B31 --> C11 + C11 --> C21 + C11 --> C31 + C31 --> C41 + end + + A31 --> C41 +``` + +--- + +> [!IMPORTANT] +> **마스터피스의 핵심은 기술이 아니라 실증이다.** +> +> 269개 공식, 92개 spec, 586개 도구 — 이 모든 것은 **T+20 30건이 쌓이고, 캘리브레이션 10건이 CALIBRATED되고, match_rate가 55%를 넘는 순간** 비로소 의미를 갖는다. +> +> 지금 이 순간부터 가장 중요한 것은 **데이터 누적**이다. 하루라도 빨리 T+20 파이프라인을 돌려야 한다. + diff --git a/docs/WBS_EXECUTION_20260728.yaml b/docs/WBS_EXECUTION_20260728.yaml new file mode 100644 index 00000000..3551002c --- /dev/null +++ b/docs/WBS_EXECUTION_20260728.yaml @@ -0,0 +1,40 @@ +version: "1.0.0" +objective: "oms-wms-erp의 계약 중심 리팩토링을 안전한 증분 단위로 수행" +scope: + canonical_code_root: "oms-wms-erp" + excluded_roots: + - "src/frontend" + - "Temp" + - "archive" +work_items: + - id: "WBS-20260728-01" + title: "baseline 및 계약 경계 확인" + status: "in_progress" + success_data: + - "npm run type-check exit code 0" + - "npm run build exit code 0" + - "npm run test exit code 0" + - "python tools/validate_enterprise_crud_specification_v1.py exit code 0" + - id: "WBS-20260728-02" + title: "공통 계약·타입·오류 처리의 단일 진실원 확립" + status: "pending" + success_data: + - "중복 API 오류·감사·그리드 계약 0건" + - "기존 소비자 타입체크 통과" + - id: "WBS-20260728-03" + title: "삭제·동시성·금액 정합성 보호" + status: "pending" + success_data: + - "물리 삭제 경로 신규 추가 0건" + - "낙관적 잠금 실패가 명시적 오류 계약으로 매핑" + - "금액 계산의 부동소수점 암묵 변환 0건" + - id: "WBS-20260728-04" + title: "사후 검증·재현성·증빙 기록" + status: "pending" + success_data: + - "검증 명령과 결과가 Temp/ 증빙 파일에 존재" + - "변경 파일이 WBS 항목에 매핑됨" +constraints: + - "가격·수량·공식은 quant-engine spec을 재계산하지 않음" + - "Temp/ 산출물은 직접 편집하지 않음" + - "기존 사용자 변경을 덮어쓰지 않음" diff --git a/docs/archive/CICD_ROADMAP.md b/docs/archive/CICD_ROADMAP.md index 43b7eeb0..56bac79e 100644 --- a/docs/archive/CICD_ROADMAP.md +++ b/docs/archive/CICD_ROADMAP.md @@ -1,3 +1,7 @@ +> **ARCHIVED (2026-07-30)**: 본 문서의 CI/CD 로드맵은 2026-07-11 이후 구현 완료되었습니다. +> 현재 CI/CD 가이드는 [`docs/CICD_PIPELINE.md`](docs/CICD_PIPELINE.md) 및 +> [`docs/DEPLOYMENT_RUNBOOK.md`](docs/DEPLOYMENT_RUNBOOK.md)을 참고하세요. + # QuantEngine Gitea Actions CI/CD 개선 로드맵 **최종 목표**: 신뢰성 높은 자동화된 배포 파이프라인 구축 diff --git a/docs/archive/EXECUTION_PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md b/docs/archive/EXECUTION_PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md index 1dfdd4da..13ed8b98 100644 --- a/docs/archive/EXECUTION_PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md +++ b/docs/archive/EXECUTION_PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md @@ -1,3 +1,7 @@ +> **ARCHIVED (2026-07-30)**: 본 문서는 Phase 0-1 실행 계획(2026-07-24)으로, 현재 상태와 일부 차이가 있습니다. +> 최신 정보는 [`../CLAUDE.md`](../CLAUDE.md) Migration Status 섹션 또는 +> [`STRATEGIC_EXECUTION_MASTER_PLAN.md`](../STRATEGIC_EXECUTION_MASTER_PLAN.md)를 참고하세요. + # QuantEngine 현대화 실행 계획 **Phase 0 마무리 + Phase 1 준비** (2026-07-24 ~ 2026-09-30) diff --git a/docs/archive/MODERNIZATION_ROADMAP_VISUAL.md b/docs/archive/MODERNIZATION_ROADMAP_VISUAL.md index 34c149d1..161f3be0 100644 --- a/docs/archive/MODERNIZATION_ROADMAP_VISUAL.md +++ b/docs/archive/MODERNIZATION_ROADMAP_VISUAL.md @@ -1,3 +1,7 @@ +> **ARCHIVED (2026-07-30)**: 본 문서의 Gantt 차트는 2026-07-24 기준 Phase 0-4 일정입니다. +> 최신 마이그레이션 상태는 [`../CLAUDE.md`](../CLAUDE.md)의 Migration Status 섹션 또는 +> [`STRATEGIC_EXECUTION_MASTER_PLAN.md`](../STRATEGIC_EXECUTION_MASTER_PLAN.md)를 참고하세요. + # QuantEngine 현대화 로드맵 (시각화) ## 1. 전체 진행도 (Gantt Chart) diff --git a/docs/archive/MODERNIZATION_STRATEGY_ROADMAP_2026-2027.md b/docs/archive/MODERNIZATION_STRATEGY_ROADMAP_2026-2027.md index 2aed1854..fb595ff0 100644 --- a/docs/archive/MODERNIZATION_STRATEGY_ROADMAP_2026-2027.md +++ b/docs/archive/MODERNIZATION_STRATEGY_ROADMAP_2026-2027.md @@ -1,3 +1,7 @@ +> **ARCHIVED (2026-07-30)**: 본 문서의 Phase 0-3 전략은 2026-07-24 기준이며, 최신 버전으로 대체되었습니다. +> 현재 마이그레이션 상태는 [`../CLAUDE.md`](../CLAUDE.md) Migration Status 또는 +> [`STRATEGIC_EXECUTION_MASTER_PLAN.md`](../STRATEGIC_EXECUTION_MASTER_PLAN.md)를 참고하세요. + # QuantEngine 데이터 기반 고도화 로드맵 **2026-07-24 ~ 2027-06-30** diff --git a/docs/archive/ROADMAP_ENTERPRISE_TEMPLATES_WBS.md b/docs/archive/ROADMAP_ENTERPRISE_TEMPLATES_WBS.md index 3fb211cc..845cf9d9 100644 --- a/docs/archive/ROADMAP_ENTERPRISE_TEMPLATES_WBS.md +++ b/docs/archive/ROADMAP_ENTERPRISE_TEMPLATES_WBS.md @@ -1,3 +1,7 @@ +> **ARCHIVED (2026-07-30)**: OMS·WMS·ERP WBS의 마크다운 버전입니다. +> 권위있는 버전은 [`../../spec/60_oms_wms_erp_wbs.yaml`](../../spec/60_oms_wms_erp_wbs.yaml) (YAML 형식) 또는 +> [`OMS_WMS_ERP_PLAYBOOK.md`](OMS_WMS_ERP_PLAYBOOK.md)를 참고하세요. + # OMS·WMS·ERP 입력 컴포넌트 & 공통 CRUD 템플릿 & 상용화 제안 마스터 WBS (WBS-MASTER-2026) ## 0. 개요 및 3대 명세 통합 권위 diff --git a/docs/archive/ROADMAP_WBS.md b/docs/archive/ROADMAP_WBS.md index 69b01923..2b3ab9d0 100644 --- a/docs/archive/ROADMAP_WBS.md +++ b/docs/archive/ROADMAP_WBS.md @@ -1,3 +1,8 @@ +> **ARCHIVED (2026-07-30)**: 본 문서는 QuantEngine 전체 로드맵의 이전 버전(v8.9 기준, 2026-06-13)입니다. +> 현재의 마이그레이션 상태는 [`../CLAUDE.md`](../CLAUDE.md)의 Migration Status 섹션과 +> [`STRATEGIC_EXECUTION_MASTER_PLAN.md`](../STRATEGIC_EXECUTION_MASTER_PLAN.md) (Phase 0-3 상세) 또는 +> 개별 집중 WBS 문서들(`WBS_10_DOTNET_MIGRATION_HARDENING_2026_06_30.md` 등)을 참고하세요. + # 퀀트투자 엔진 — 전체 로드맵 & WBS & 하네스 성공 기준 > 작성일: 2026-06-13 | 엔진 버전: REBALANCE_ENGINE_V1 기준 diff --git a/src/dotnet/QuantEngine.Web/Pages/Shared/DeleteConfirmModalViewModel.cs b/src/dotnet/QuantEngine.Web/Pages/Shared/DeleteConfirmModalViewModel.cs new file mode 100644 index 00000000..a073dd34 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Pages/Shared/DeleteConfirmModalViewModel.cs @@ -0,0 +1,6 @@ +namespace QuantEngine.Web.Pages.Shared; + +public class DeleteConfirmModalViewModel +{ + public string Message { get; set; } = string.Empty; +} diff --git a/src/dotnet/QuantEngine.Web/Pages/Shared/PageHeaderViewModel.cs b/src/dotnet/QuantEngine.Web/Pages/Shared/PageHeaderViewModel.cs new file mode 100644 index 00000000..8187b21f --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Pages/Shared/PageHeaderViewModel.cs @@ -0,0 +1,10 @@ +namespace QuantEngine.Web.Pages.Shared; + +public class PageHeaderViewModel +{ + public string Title { get; set; } = string.Empty; + public string PreTitle { get; set; } = string.Empty; + public bool HasAction { get; set; } + public string ActionText { get; set; } = string.Empty; + public string ActionUrl { get; set; } = string.Empty; +} diff --git a/src/dotnet/QuantEngine.Web/Pages/Shared/_DeleteConfirmModal.cshtml b/src/dotnet/QuantEngine.Web/Pages/Shared/_DeleteConfirmModal.cshtml new file mode 100644 index 00000000..15f86ba7 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Pages/Shared/_DeleteConfirmModal.cshtml @@ -0,0 +1,34 @@ +@model QuantEngine.Web.Pages.Shared.DeleteConfirmModalViewModel + + diff --git a/src/dotnet/QuantEngine.Web/Pages/Shared/_PageHeader.cshtml b/src/dotnet/QuantEngine.Web/Pages/Shared/_PageHeader.cshtml new file mode 100644 index 00000000..42a273c3 --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Pages/Shared/_PageHeader.cshtml @@ -0,0 +1,21 @@ +@model QuantEngine.Web.Pages.Shared.PageHeaderViewModel + + diff --git a/src/quant_engine/qualitative_sell_strategy_v1.py b/src/quant_engine/qualitative_sell_strategy_v1.py new file mode 100644 index 00000000..67858def --- /dev/null +++ b/src/quant_engine/qualitative_sell_strategy_v1.py @@ -0,0 +1,377 @@ +from __future__ import annotations + +import math +from datetime import date, timedelta +from typing import Any + +# 매도 결정에 동원하는 5개 독립 팩터군. 단일 팩터의 임계값 돌파만으로는 행동을 +# 트리거하지 않는다 — 최소 CONFLUENCE_MIN개 팩터군이 동일 방향으로 합의해야 +# SELL/ADD 확신도가 성립한다. (기계적 단일 트리거 매도 금지 원칙) +FACTOR_FAMILIES: tuple[str, ...] = ( + "macro_pressure", + "fundamental_trajectory", + "short_interest_pressure", + "microstructure_pressure", + "liquidity_rotation_risk", +) +CONFLUENCE_MIN = 3 +EVENT_PRE_GUARD_DAYS = 5 # macro_event_synchronizer_v2.event_hold_gate와 동일 — HIGH 이벤트 5일 전 +EVENT_POST_GUARD_DAYS = 2 # 이벤트 후 2일 변동성 소화 구간 + +# 금리국면별 시장 성격: 금리 상승기=실적장세(펀더멘털/수출입 실적이 가격을 주도), +# 금리 보합·하락기=기술장세(수급·미시구조가 가격을 주도). 동일한 5팩터라도 +# 국면에 따라 가중치를 달리 줘야 confluence가 의미를 갖는다. +REGIME_FLAT_WEIGHTS: dict[str, float] = {family: 1.0 for family in FACTOR_FAMILIES} +REGIME_WEIGHT_TABLE: dict[str, dict[str, float]] = { + "PERFORMANCE_MARKET": { # 금리 상승기 — 실적/수출입 펀더멘털 가중 상향 + "macro_pressure": 1.2, + "fundamental_trajectory": 1.8, + "short_interest_pressure": 1.0, + "microstructure_pressure": 0.5, + "liquidity_rotation_risk": 1.0, + }, + "TECHNICAL_MARKET": { # 금리 보합·하락기 — 수급/미시구조 가중 상향 + "macro_pressure": 0.8, + "fundamental_trajectory": 0.8, + "short_interest_pressure": 1.3, + "microstructure_pressure": 1.6, + "liquidity_rotation_risk": 1.3, + }, + "NEUTRAL": REGIME_FLAT_WEIGHTS, +} + + +def classify_market_regime(rate_trend: str | None) -> str: + """금리 추세 문자열(RISING/FLAT/FALLING)을 실적장세/기술장세로 분류. + + RISING → PERFORMANCE_MARKET(실적장세): 금리 상승기엔 유동성보다 실적/펀더멘털이 + 가격을 결정. FLAT/FALLING → TECHNICAL_MARKET(기술장세): 유동성이 풍부해 수급· + 미시구조·테마성 모멘텀이 가격을 주도. 입력 결측 시 NEUTRAL(가중치 변화 없음). + """ + trend = str(rate_trend or "").upper() + if trend == "RISING": + return "PERFORMANCE_MARKET" + if trend in {"FLAT", "FALLING"}: + return "TECHNICAL_MARKET" + return "NEUTRAL" + + +def _finite(value: Any) -> bool: + return isinstance(value, (int, float)) and math.isfinite(float(value)) + + +def compute_short_interest_composite(ctx: dict[str, Any]) -> dict[str, Any]: + """SHORT_INTEREST_RISK_GAUGE_V1. + + 5요소: 공매도잔고율 변화, 공매도거래비중, 상대수익률(섹터/지수 대비), + 거래량 이상, 실적전망. 잔고율 단독으로는 매도 근거가 약함(현대로템형) — + 잔고율이 낮을 때는 거래비중·상대수익률 가중치를 자동 상향한다. + """ + missing: list[str] = [] + + short_balance_ratio = ctx.get("short_balance_ratio") # %, 현재 잔고율 + short_balance_ratio_chg_20d = ctx.get("short_balance_ratio_chg_20d") # %p, 20일 변화 + short_turnover_share = ctx.get("short_turnover_share") # 당일 거래 중 공매도 비중 % + relative_return_20d = ctx.get("relative_return_20d") # 종목수익률 - 섹터(or지수)수익률, %p + volume_ratio_5d = ctx.get("volume_ratio_5d") # 5일평균거래량 대비 비율 + earnings_outlook = str(ctx.get("earnings_outlook") or "").upper() # IMPROVING|STABLE|DETERIORATING|UNKNOWN + + for name, value in ( + ("short_balance_ratio", short_balance_ratio), + ("short_turnover_share", short_turnover_share), + ("relative_return_20d", relative_return_20d), + ): + if not _finite(value): + missing.append(name) + + if missing: + return { + "short_interest_pressure": None, + "status": "DATA_MISSING", + "missing_inputs": missing, + "note": "잔고율/거래비중/상대수익률 중 결측 — 공매도 합성 점수를 산출하지 않음(추정 금지)", + } + + low_balance_regime = float(short_balance_ratio) < 1.0 # 잔고율 1% 미만이면 '낮은 잔고율' 취급(현대로템형) + + # 잔고율 추세: 상승=매도근거 강화, 하락=매도근거 약화(혹은 매수근거) + balance_trend_signal = 0.0 + if _finite(short_balance_ratio_chg_20d): + balance_trend_signal = max(-1.0, min(1.0, float(short_balance_ratio_chg_20d) / 1.5)) + + turnover_signal = max(-1.0, min(1.0, (float(short_turnover_share) - 8.0) / 12.0)) # 8% 기준선 + relative_return_signal = max(-1.0, min(1.0, -float(relative_return_20d) / 10.0)) # 상대 약세일수록 + + volume_signal = 0.0 + if _finite(volume_ratio_5d): + volume_signal = max(-1.0, min(1.0, (float(volume_ratio_5d) - 1.0))) + + outlook_signal = { + "IMPROVING": -0.6, + "STABLE": 0.0, + "DETERIORATING": 0.7, + }.get(earnings_outlook, 0.0) + + if low_balance_regime: + # 잔고율 자체는 약한 근거 — 거래비중·상대수익률 가중치 상향, 잔고율추세 가중치 하향 + weights = {"balance": 0.10, "turnover": 0.30, "relative": 0.30, "volume": 0.10, "outlook": 0.20} + else: + weights = {"balance": 0.30, "turnover": 0.20, "relative": 0.20, "volume": 0.10, "outlook": 0.20} + + pressure = ( + balance_trend_signal * weights["balance"] + + turnover_signal * weights["turnover"] + + relative_return_signal * weights["relative"] + + volume_signal * weights["volume"] + + outlook_signal * weights["outlook"] + ) + pressure = max(-1.0, min(1.0, pressure)) + + label = "ELEVATED_SHORT_PRESSURE" if pressure >= 0.5 else "WATCH" if pressure >= 0.2 else \ + "SHORT_COVERING_SUPPORTIVE" if pressure <= -0.5 else "NEUTRAL" + + return { + "short_interest_pressure": round(pressure, 4), + "status": "OK", + "low_balance_regime": low_balance_regime, + "label": label, + "components": { + "balance_trend_signal": round(balance_trend_signal, 4), + "turnover_signal": round(turnover_signal, 4), + "relative_return_signal": round(relative_return_signal, 4), + "volume_signal": round(volume_signal, 4), + "outlook_signal": outlook_signal, + }, + "weights_used": weights, + } + + +def compute_microstructure_pressure_from_orderbook(orderbook_output1: dict[str, Any]) -> dict[str, Any]: + """MICROSTRUCTURE_PRESSURE_FROM_ORDERBOOK_V1. + + KIS Open API FHKST01010200(주식현재가 호가/예상체결) output1의 10단계 호가 잔량을 + -1(매수우위/지지)~+1(매도우위/압력)로 계량화. 실측 확인된 필드명(2026-06-21, + 005930 라이브 호출): total_askp_rsqn, total_bidp_rsqn(10단계 합계 잔량). + 이 점수는 전략 방향 결정에는 쓰지 않고 confluence가 성립한 이후의 '집행 타이밍' + 보조로만 사용한다(spec/exit/qualitative_sell_strategy_v1.yaml:factor_families. + microstructure_pressure 참조). + """ + total_askp = orderbook_output1.get("total_askp_rsqn") + total_bidp = orderbook_output1.get("total_bidp_rsqn") + try: + total_askp = float(total_askp) + total_bidp = float(total_bidp) + except (TypeError, ValueError): + return {"microstructure_pressure": None, "status": "DATA_MISSING"} + + denom = total_askp + total_bidp + if denom <= 0: + return {"microstructure_pressure": None, "status": "DATA_MISSING"} + + pressure = max(-1.0, min(1.0, (total_askp - total_bidp) / denom)) + return { + "microstructure_pressure": round(pressure, 4), + "status": "OK", + "total_askp_rsqn": total_askp, + "total_bidp_rsqn": total_bidp, + } + + +def _event_review_window( + today: date, + pressure_sign: int, + next_earnings_date: date | None, + next_macro_event_date: date | None, + macro_event_impact: str | None, + earnings_outlook: str, +) -> dict[str, Any]: + """캘린더 기반 검토 구간 산출 — 임의 날짜 고정이 아니라 실제 이벤트 일정에서 역산.""" + candidates: list[tuple[date, str]] = [] + + if next_earnings_date is not None: + if pressure_sign < 0 and earnings_outlook == "DETERIORATING": + # 실적 악화 전망 + 매도압력 → 실적발표 전 정리(서프라이즈 리스크 회피) + candidates.append((next_earnings_date - timedelta(days=EVENT_PRE_GUARD_DAYS), "PRE_EARNINGS_EXIT_BEFORE_SURPRISE_RISK")) + elif pressure_sign < 0 and earnings_outlook in {"IMPROVING", "STABLE"}: + # 단기 기술적 매도압력이지만 실적전망은 양호 → 발표 직전 매도는 가치훼손, 발표 이후로 연기 + candidates.append((next_earnings_date + timedelta(days=EVENT_POST_GUARD_DAYS), "DEFER_TO_POST_EARNINGS_AVOID_PREMATURE_EXIT")) + elif pressure_sign > 0: + # 추가매수/보유 신호 — 발표 변동성 통과 후 확신 재평가 + candidates.append((next_earnings_date + timedelta(days=EVENT_POST_GUARD_DAYS), "REASSESS_AFTER_EARNINGS_CONFIRM")) + + if next_macro_event_date is not None and str(macro_event_impact or "").upper() in {"HIGH", "VERY_HIGH"}: + if pressure_sign < 0: + candidates.append((next_macro_event_date - timedelta(days=EVENT_PRE_GUARD_DAYS), "PRE_MACRO_EVENT_DERISK")) + else: + candidates.append((next_macro_event_date + timedelta(days=EVENT_POST_GUARD_DAYS), "POST_MACRO_EVENT_CONFIRM")) + + if not candidates: + return { + "review_window_start": today.isoformat(), + "review_window_end": (today + timedelta(days=10)).isoformat(), + "window_basis": "NO_SCHEDULED_EVENT_DEFAULT_10D_REVIEW", + } + + earliest = min(candidates, key=lambda item: item[0]) + window_start = max(today, earliest[0] - timedelta(days=2)) + window_end = earliest[0] + timedelta(days=2) + return { + "review_window_start": window_start.isoformat(), + "review_window_end": window_end.isoformat(), + "window_basis": earliest[1], + } + + +def compute_qualitative_sell_strategy(ctx: dict[str, Any]) -> dict[str, Any]: + """QUALITATIVE_SELL_STRATEGY_V1. + + 매크로/실적/펀더멘털/공매도수급/호가미시구조/대내외(IPO·로테이션) 5개 + 독립 팩터군의 합의(confluence)로만 행동을 생성한다. 현금부족 사유는 + 입력에서 의도적으로 배제(cash_shortfall_excluded=True) — 가치보존이 + 유일한 목적 함수. + """ + today_raw = ctx.get("today") + today = today_raw if isinstance(today_raw, date) else date.today() + + factor_values: dict[str, float | None] = {} + missing_factors: list[str] = [] + for family in FACTOR_FAMILIES: + value = ctx.get(family) + if _finite(value): + factor_values[family] = max(-1.0, min(1.0, float(value))) + else: + factor_values[family] = None + missing_factors.append(family) + + available = {k: v for k, v in factor_values.items() if v is not None} + if len(available) < CONFLUENCE_MIN: + return { + "action": "INSUFFICIENT_DATA_NO_ACTION", + "conviction": "NONE", + "available_factors": list(available.keys()), + "missing_factors": missing_factors, + "rationale": "5개 팩터군 중 confluence 판정에 필요한 최소 데이터가 부족 — 추정으로 행동 생성 금지", + "cash_shortfall_excluded": True, + "mechanical_sell_prohibited": True, + } + + # 부호 규약: 모든 팩터군은 +1(매도압력 최대) ~ -1(보유/추가 지지 최대) 동일 스케일. + # short_interest_pressure도 동일 — ELEVATED_SHORT_PRESSURE(+) / SHORT_COVERING_SUPPORTIVE(-). + # confluence 합의 카운트는 국면 가중치와 무관하게 원시 방향성으로만 판정한다 + # (가중치는 행동 '강도'에만 영향 — 합의 성립 여부 자체를 왜곡하지 않는다). + sell_agree = [k for k, v in available.items() if v >= 0.30] + hold_add_agree = [k for k, v in available.items() if v <= -0.30] + + market_regime = classify_market_regime(ctx.get("rate_trend")) if "market_regime" not in ctx else str(ctx.get("market_regime") or "NEUTRAL").upper() + regime_weights = REGIME_WEIGHT_TABLE.get(market_regime, REGIME_FLAT_WEIGHTS) + weighted_sum = sum(available[k] * regime_weights.get(k, 1.0) for k in available) + weight_total = sum(regime_weights.get(k, 1.0) for k in available) + composite_score = weighted_sum / weight_total if weight_total else 0.0 + + earnings_outlook = str(ctx.get("earnings_outlook") or "STABLE").upper() + next_earnings_date = ctx.get("next_earnings_date") if isinstance(ctx.get("next_earnings_date"), date) else None + next_macro_event_date = ctx.get("next_macro_event_date") if isinstance(ctx.get("next_macro_event_date"), date) else None + macro_event_impact = ctx.get("macro_event_impact") + + if len(sell_agree) >= CONFLUENCE_MIN: + conviction = "HIGH" if len(sell_agree) >= 4 else "MEDIUM" + action = "EXIT_REVIEW_FULL" if composite_score >= 0.6 else "TRIM_REVIEW_PARTIAL" + pressure_sign = -1 + rationale = f"매도압력 합의({len(sell_agree)}/{len(available)} 팩터군 매도방향 합치): " + ", ".join(sell_agree) + elif len(hold_add_agree) >= CONFLUENCE_MIN: + conviction = "HIGH" if len(hold_add_agree) >= 4 else "MEDIUM" + action = "HOLD_ADD_CONVICTION" + pressure_sign = 1 + rationale = f"보유/추가 근거 합의({len(hold_add_agree)}/{len(available)} 팩터군 지지방향 합치): " + ", ".join(hold_add_agree) + else: + conviction = "LOW" + action = "HOLD_NO_CONFLUENCE" + pressure_sign = 0 + rationale = "팩터군 간 합의 미달 — 단일/소수 팩터의 임계값 돌파만으로는 매도 트리거 금지" + + window = _event_review_window( + today=today, + pressure_sign=pressure_sign, + next_earnings_date=next_earnings_date, + next_macro_event_date=next_macro_event_date, + macro_event_impact=macro_event_impact, + earnings_outlook=earnings_outlook, + ) if pressure_sign != 0 else None + + return { + "action": action, + "conviction": conviction, + "market_regime": market_regime, + "composite_score": round(composite_score, 4), + "sell_agreeing_factors": sell_agree, + "hold_add_agreeing_factors": hold_add_agree, + "missing_factors": missing_factors, + "review_window": window, + "rationale": rationale, + "cash_shortfall_excluded": True, + "mechanical_sell_prohibited": True, + } + + +def compute_satellite_candidate_score(ctx: dict[str, Any]) -> dict[str, Any]: + """SATELLITE_CANDIDATE_SCORE_V1. + + 미보유 유니버스 종목을 섹터 수출입 전망(sector_export_trend) + 펀더멘털 + 추세 + 국면적합도로 평가해 WATCH/BUY_CANDIDATE/AVOID를 산출한다. 보유종목 + 매도판단(compute_qualitative_sell_strategy)과 동일한 부호 규약을 쓰지 않고 + 별도 -1(약세)~+1(강세) 매력도 스케일을 쓴다 — 매수후보 평가와 매도판단은 + 목적함수가 다르므로 동일 점수를 재사용하지 않는다. + """ + sector_export_trend = ctx.get("sector_export_trend") # %, 섹터 수출 YoY/MoM 추세 + fundamental_trajectory = ctx.get("fundamental_trajectory") # -1(악화)~+1(개선), 매도엔진과 동일 정의역이나 부호 반대 해석 주의 + relative_return_20d = ctx.get("relative_return_20d") + market_regime = str(ctx.get("market_regime") or classify_market_regime(ctx.get("rate_trend"))).upper() + + missing = [name for name, value in ( + ("sector_export_trend", sector_export_trend), + ("fundamental_trajectory", fundamental_trajectory), + ) if not _finite(value)] + if missing: + return { + "satellite_action": "INSUFFICIENT_DATA_NO_ACTION", + "missing_inputs": missing, + "market_regime": market_regime, + } + + export_signal = max(-1.0, min(1.0, float(sector_export_trend) / 10.0)) + fundamental_signal = max(-1.0, min(1.0, -float(fundamental_trajectory))) # 매도엔진 부호(+)=악화 -> 매력도는 반전 + relative_signal = max(-1.0, min(1.0, float(relative_return_20d) / 10.0)) if _finite(relative_return_20d) else 0.0 + + if market_regime == "PERFORMANCE_MARKET": + weights = {"export": 0.45, "fundamental": 0.40, "relative": 0.15} + elif market_regime == "TECHNICAL_MARKET": + weights = {"export": 0.20, "fundamental": 0.25, "relative": 0.55} + else: + weights = {"export": 0.34, "fundamental": 0.33, "relative": 0.33} + + attractiveness = ( + export_signal * weights["export"] + + fundamental_signal * weights["fundamental"] + + relative_signal * weights["relative"] + ) + attractiveness = max(-1.0, min(1.0, attractiveness)) + + if attractiveness >= 0.5: + satellite_action = "BUY_CANDIDATE" + elif attractiveness >= 0.2: + satellite_action = "WATCH" + elif attractiveness <= -0.4: + satellite_action = "AVOID" + else: + satellite_action = "NEUTRAL_NO_EDGE" + + return { + "satellite_action": satellite_action, + "attractiveness_score": round(attractiveness, 4), + "market_regime": market_regime, + "components": { + "export_signal": round(export_signal, 4), + "fundamental_signal": round(fundamental_signal, 4), + "relative_signal": round(relative_signal, 4), + }, + "weights_used": weights, + }