Compare commits

...

20 Commits

Author SHA1 Message Date
kjh2064 0b4caa95f1 document gitea token handling 2026-06-26 18:16:28 +09:00
kjh2064 99c4885692 deploy workflow and docs
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 6s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m16s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 18:07:13 +09:00
kjh2064 74a83f94fb ui dashboard cleanup 2026-06-26 18:07:02 +09:00
kjh2064 1e6bf702bc core services and tests 2026-06-26 18:06:36 +09:00
kjh2064 e0508324e5 docs: .NET 렌더러 운영 상태와 검증 기준 정리
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 3s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
- 운영 상태 문서와 README를 .NET canonical renderer 기준으로 정리했습니다.
- 레거시 렌더러 비운영 선언과 감사/검증기 경로를 통일했습니다.
- 운영 보정 로직의 데이터 소스 반영을 정리했습니다.
2026-06-26 14:18:48 +09:00
kjh2064 9e6e2ded2f feat: .NET 운영 리포트 렌더러와 CI 경로 전환
- operational_report.json/md와 final_decision_packet_v4 생성 경로를 .NET으로 전환했습니다.
- CI, 운영 게이트, 릴리스 DAG, 대시보드의 운영 진입점을 새 경로로 정렬했습니다.
- legacy Python 렌더러는 비운영으로 명시했습니다.
2026-06-26 14:18:03 +09:00
kjh2064 8f13bb4a48 feat: postgres history-first 계약과 적재 경로 추가
- PostgreSQL history contract와 schema/validator를 추가했습니다.
- .NET history store, snapshot reader, repository, migration을 연결했습니다.
- history-first 운영 모델 문서와 daily signal tracking 문구를 정리했습니다.
2026-06-26 14:17:04 +09:00
kjh2064 7e0c0b6c8f chore: 지침(AGENTS.md) 내 삭제된 gas_event_calendar.gs 경로 참조 및 색인 해제
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m14s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 12:21:53 +09:00
kjh2064 18d78a9f04 chore: Apps Script 연동 설정 파일 (.clasp.json) 폐기
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 3s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 12:20:14 +09:00
kjh2064 f72d796636 chore: suggest 폴더의 과거 제안서들을 archive 하위로 격리 및 불필요 중복 파일 제거
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 3s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 12:19:43 +09:00
kjh2064 ebb863371d chore: 지침(AGENTS.md) 내 'GAS 투자 판단 로직 진입 차단(ADR-0002)' 지침 삭제
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 5s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m16s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 12:17:11 +09:00
kjh2064 ad17e7dae1 chore: 임시/로그 파일 관리 Git 차단 룰 고도화 및 AGENTS 개발지침 명시
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m14s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 11:43:05 +09:00
kjh2064 a1bbeb99a6 chore: 최상위 룰 매니페스트 파일을 spec/ 폴더로 정리하고 도구 경로 참조 수정
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m18s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 11:40:51 +09:00
kjh2064 15c7971018 chore: root 경로의 미사용/과거 문서 및 스크립트를 docs/ 하위로 정리 격리
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m15s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 11:35:42 +09:00
kjh2064 6051338367 chore: 프로젝트 루트의 파편화된 .gs 파일들을 src/gas_adapter_parts/로 이동 격리
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 11:33:46 +09:00
kjh2064 3e7ea1d007 chore: .NET 변환 완료된 파이썬 코드를 deprecated로 격리 및 검색 제외 지침 반영
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m16s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
2026-06-26 11:30:37 +09:00
kjh2064 10e1cfe409 feat(dotnet): 파이썬 공식 계산 엔진 C# 포팅 및 .NET 인프라 기반 결함(WBS-10.1) 해결
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m18s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
2026-06-26 11:25:32 +09:00
kjh2064 c1e84a387c chore: 워크플로우 및 클라우드 가이드 내 잔여 시놀로지(Synology) 참조 제거
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m14s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
2026-06-26 11:12:50 +09:00
kjh2064 23ba556c17 chore: 시놀로지(Synology) 전용 파일 및 참조 폐기
서버가 시놀로지에서 클라우드(hz-prod-01, 178.104.200.7)로 이전됨에 따라
시놀로지 전용 문서 11개와 스크립트 3개를 삭제하고 AGENTS.md 참조를 정리한다.

삭제된 문서:
- docs/SYNOLOGY_ACT_RUNNER_REFACTOR_PR_BODY.md
- docs/SYNOLOGY_KIS_COLLECTION_SETUP.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_COMMIT_MESSAGE_TEMPLATE.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST_FILLED.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_EVIDENCE_TEMPLATE.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_FINAL_EXECUTION_ONE_PAGER.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_FINAL_PREFLIGHT_10.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_FIREWALL_PROXY_COPYPASTE.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_FIREWALL_PROXY_TABLE.md
- docs/SYNOLOGY_SNAPSHOT_ADMIN_POC.md

삭제된 스크립트:
- tools/re_register_act_runner_synology.sh
- tools/run_snapshot_admin_synology.sh
- tools/start_act_runner_synology.sh

수정:
- AGENTS.md: Synology CI 참조를 클라우드 서버(hz-prod-01)로 교체
2026-06-26 11:11:38 +09:00
kjh2064 9eb295e2dc docs: 클라우드 서버(hz-prod-01) 설정 하네스 가이드 신규 작성
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 3s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped
- docs/CLOUD_SERVER_SETUP.md 신규 생성
  - 서버 기본 정보 (Ubuntu 26.04, AMD EPYC-Rome 2C, 3.7GiB)
  - 서비스 아키텍처: Nginx, Gitea, QuantEngine Blazor, PostgreSQL 18
  - Docker Compose v5.2.0 기반 Gitea 설정 전문
  - .NET 10 (ASP.NET Core 10.0.9) systemd 서비스 설정 전문
  - 6x Gitea Act Runner CI 컨테이너 현황
  - 보안: SSH hardening, UFW 방화벽, fail2ban, 네트워크 격리
  - 시놀로지 → 클라우드 마이그레이션 매핑표
  - 운영 명령 치트시트 및 검증 하네스
  - 참조 인덱스(TOC) 및 관련 문서 상호 참조
- AGENTS.md Directory Routing 섹션에 문서 경로 등록

provenance: ssh kjh2064@178.104.200.7 라이브 명령 실행으로 수집 (2026-06-26)
2026-06-26 11:05:16 +09:00
252 changed files with 4782 additions and 14235 deletions
-5
View File
@@ -1,5 +0,0 @@
{
"scriptId": "1xfeBAeeknmnBtSvrIqWXO_2hc3ByeriLUOSuOOB4YxLLHhN3zdnL7tVh",
"projectId": "1072944905499",
"rootDir": "Temp/gas_deploy"
}
+2 -2
View File
@@ -74,8 +74,8 @@ jobs:
- name: Backup to Cloud (Optional)
continue-on-error: true
run: |
# Synology NAS로 동기화 (설정 필요)
# rsync -av backups/ admin@SYNOLOGY_IP:/backup/data_feed/
# 원격 백업 서버로 동기화 (설정 필요)
# rsync -av backups/ admin@BACKUP_SERVER_IP:/backup/data_feed/
echo "Cloud sync would run here if configured"
- name: Notify Completion
+65 -7
View File
@@ -8,14 +8,9 @@ on:
workflow_dispatch:
# ─────────────────────────────────────────────────────────────────
# Synology DS216j (ARMv7l 32-bit) 환경 제약
# - Python: /usr/bin/python3 (3.8.12)
# - Node.js 18: /usr/local/bin (appstore)
# - numpy/pandas: 공식 휠 없음, gcc 미설치 → 소스 빌드 불가
#
# CI 역할: 코드 구조 검증 게이트 (순수 Python, yaml/json)
# - Validate Specs / Formula Registry / Coverage / Behavioral Coverage
# 통합 테스트(run_release_dag, ingest 등)는 로컬에서 실행
# 통합 테스트(run_release_dag, ingest 등)는 로컬 또는 클라우드 서버에서 실행
# ─────────────────────────────────────────────────────────────────
jobs:
@@ -56,7 +51,7 @@ jobs:
mkdir -p "$VENV_BASE"
/usr/bin/python3 -m venv "$VENV"
# Synology Python 3.8은 ensurepip가 없어 venv 생성 시 pip가 누락될 수 있음
# venv 내 pip 확인 및 복구
if [ ! -f "$VENV/bin/pip" ]; then
echo "pip missing in venv, installing via get-pip.py..."
curl -sS https://bootstrap.pypa.io/pip/3.8/get-pip.py -o get-pip.py
@@ -156,6 +151,69 @@ jobs:
- name: Validate DB First Pipeline
run: python3 tools/validate_db_first_pipeline_v1.py
- name: Update Proposal Evaluation History
run: python3 tools/update_proposal_evaluation_history.py --json GatherTradingData.json --history Temp/proposal_evaluation_history.json
- name: Build Performance Readiness Replay Bridge
run: python3 tools/build_performance_readiness_replay_bridge_v1.py --hist Temp/proposal_evaluation_history.json --out Temp/performance_readiness_replay_bridge_v1.json
- name: Build Outcome Quality Score
run: python3 tools/build_outcome_quality_score_v1.py --json GatherTradingData.json --out Temp/outcome_quality_score_v1.json --policy spec/strategy_execution_lock_policy.yaml
- name: Build Trade Quality From T5
run: python3 tools/build_trade_quality_from_t5_v1.py --hist Temp/proposal_evaluation_history.json --out Temp/trade_quality_from_t5_v1.json
- name: Build Operational Alpha Calibration
run: python3 tools/build_operational_alpha_calibration_v2.py --out Temp/operational_alpha_calibration_v2.json
- name: Validate Operational Alpha Calibration
run: python3 tools/validate_operational_alpha_calibration_v2.py --input Temp/operational_alpha_calibration_v2.json --out Temp/validate_operational_alpha_calibration_v2.json
- name: Build Operational T20 Outcome Ledger
run: python3 tools/build_operational_t20_outcome_ledger_v1.py --json GatherTradingData.json --out Temp/operational_t20_outcome_ledger_v1.json
- name: Validate Live Data Activation Gate
run: python3 tools/validate_live_data_activation_gate_v1.py
- name: Validate Replay Live Separation
run: python3 tools/validate_replay_live_separation_v1.py
- name: Render Final Decision Packet V4
run: dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- packet-v4 --packet=Temp/final_decision_packet_active.json --out=Temp/final_decision_packet_v4.json
- name: Render Operational Report
run: dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
- name: Validate Report Packet Sync
run: python3 tools/validate_report_packet_sync_v1.py --packet Temp/final_decision_packet_active.json --report Temp/operational_report.json | tee Temp/validate_report_packet_sync_v1.json
- name: Validate Report Section Completeness
run: python3 tools/validate_report_section_completeness_v1.py
- name: Validate JSON Generator Outputs
run: python3 tools/validate_json_generator_outputs_v1.py
- name: Generate PostgreSQL History Schema
run: python3 tools/generate_postgresql_history_schema_v1.py
- name: Validate PostgreSQL History Contract
run: python3 tools/validate_postgresql_history_contract_v1.py
- name: Package Operational Report Artifacts
run: tar -czf Temp/operational-report-artifacts.tar.gz Temp/operational_report.json Temp/operational_report.md Temp/missing_data_inventory_v1.json Temp/report_section_completeness.json Temp/operational_alpha_calibration_v2.json Temp/validate_operational_alpha_calibration_v2.json Temp/operational_t20_outcome_ledger_v1.json Temp/live_data_activation_gate_v1.json Temp/replay_live_separation_v1.json Temp/validate_report_packet_sync_v1.json Temp/json_generator_outputs_v1.json Temp/proposal_evaluation_history.json Temp/performance_readiness_replay_bridge_v1.json Temp/postgresql_history_schema_v1.sql Temp/postgresql_history_schema_v1.json Temp/postgresql_history_contract_v1.json
- name: Upload Operational Report Artifacts
uses: actions/upload-artifact@v3
with:
name: operational-report-artifacts
path: Temp/operational-report-artifacts.tar.gz
- name: Upload Operational Report JSON
uses: actions/upload-artifact@v3
with:
name: operational-report-json
path: Temp/operational_report.json
validate-ui-and-storage:
runs-on: ubuntu-latest
needs: validate-core
@@ -53,3 +53,21 @@ jobs:
ssh -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 "chmod +x /home/kjh2064/tmp/deploy.sh 2>/dev/null || true"
scp -i ~/.ssh/id_ed25519 tools/deploy_quantengine.sh kjh2064@178.104.200.7:/home/kjh2064/tmp/deploy.sh
ssh -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 "chmod +x /home/kjh2064/tmp/deploy.sh && /home/kjh2064/tmp/deploy.sh"
- name: Verify Public Routes
run: |
set -e
root_html=$(curl -s "http://178.104.200.7/quant/")
ops_html=$(curl -s "http://178.104.200.7/quant/operations")
root_code=$(printf '%s' "$root_html" | grep -q "Quant Engine" && echo 200 || echo 500)
ops_code=$(printf '%s' "$ops_html" | grep -q "Operational Report" && echo 200 || echo 500)
echo "/quant/ -> ${root_code}"
echo "/quant/operations -> ${ops_code}"
if [ "$root_code" != "200" ]; then
echo "Deployment content check failed for /quant/"
exit 1
fi
if [ "$ops_code" != "200" ]; then
echo "Deployment content check failed for /quant/operations"
exit 1
fi
+11
View File
@@ -36,3 +36,14 @@ node_modules/
.claude/projects/
*.db-shm
*.db-wal
# 개발자 임시/테스트/백업 파일 패턴 차단
**/debug_*.log
**/tmp_*.json
**/mock_*.json
**/*_temp.*
**/*.bak
**/*.swp
**/*_backup*
**/*_copy*
+6 -4
View File
@@ -61,6 +61,7 @@
- `spec/`: source of truth. 공식, 계약, 게이트, 출력 스키마의 최우선 읽기 경로.
- `governance/`: 운영 규칙, 인덱스, 해시 마이그레이션, ADR, 템플릿.
- `src/`: Python canonical implementation. 새 로직은 여기부터 반영한다.
- `src/dotnet/QuantEngine.Tools`: canonical .NET operational report and packet renderer.
- `src/quant_engine/data_collection_backend_v1.py`: collection backend selector.
- `src/quant_engine/data_collection_store_v1.py`: SQLite collection store.
- `src/quant_engine/kis_data_collection_v1.py`: KIS 우선 수집기.
@@ -70,6 +71,7 @@
- `KIS-first`: KIS 우선.
- `SQLite-first`: SQLite/JSON 우선.
- `tools/`: build/validate/convert/audit CLI.
- `tools/render_operational_report.py`: legacy renderer, 운영/CI 경로에서 사용 금지.
- `tools/run_kis_data_collection_v1.py`: KIS collection thin CLI.
- `tools/generate_postgresql_upgrade_stub_v1.py`: PostgreSQL stub generator.
- `tools/validate_platform_transition_wbs_v1.py`: `.gs → Python` and `xlsx → sqlite` WBS validator.
@@ -81,16 +83,16 @@
- `tests/parity/test_routing_gate_parity_v1.py`: routing gate parity.
- `.gitea/workflows/qualitative_sell_strategy.yml`: qualitative sell strategy workflow.
- `.gitea/workflows/snapshot_admin.yml`: snapshot admin workflow and scheduled validation.
- `docs/CLOUD_SERVER_SETUP.md`: 클라우드 서버(hz-prod-01, 178.104.200.7) 설정 하네스 가이드. 시놀로지 → 클라우드 마이그레이션 매핑 포함.
- `docs/GITEA_SECRETS_SETUP.md`: Gitea secrets setup and verification guide.
- `docs/GATHERTRADINGDATA_XLSX_OPERATING_RUNBOOK.md`: `GatherTradingData.xlsx` 보조 자산 런북.
- `docs/ROADMAP_WBS.md`: `.gs → Python``xlsx → sqlite` WBS.
- `docs/ROADMAP_WBS.md`의 WBS-8.2: `run_kis_data_collection_v1.py``validate_platform_transition_wbs_v1.py``validate_snapshot_admin_web_v1.py`.
- `Temp/snapshot_admin_approval_packet_v1.json`: snapshot admin approval packet export.
- `Temp/snapshot_admin_approval_packet_v1.md`: snapshot admin approval packet summary.
- `gas_event_calendar.gs`: 이벤트 캘린더 배포 호환 스텁. `seedEventCalendar_()` / `runEventRisk()` 진입점을 유지한다.
- `Temp/`: 실행 결과와 캐시. 라우팅 대상은 아니며 runtime consumer만 읽는다.
- `DB 파일 관리`: workspace/collector DB는 단일 canonical 경로만 사용한다. 동일 역할의 SQLite 파일을 `src/``outputs/`에 중복 생성하지 말고, 실행 기본값·README·WBS·검증 스크립트가 같은 경로를 가리키게 유지한다. 임시 검증 DB는 `Temp/`에만 두고, 운영 기준 DB로 승격할 때는 명시적으로 문서화한다. canonical workspace DB는 `src/quant_engine/snapshot_admin.db`이며, 다른 위치의 동일 역할 DB는 파생/아카이브/마이그레이션 전용으로만 취급한다. 운영 진입점과 일반 검증 스크립트는 canonical 파일만 읽고 써야 한다.
- `docs/archive/`, `suggest/`, `artifacts/archive/`: 문서 검색/색인 제외 대상. 감사나 이력 추적이 필요할 때만 명시적으로 읽는다.
- `docs/archive/`, `docs/legacy/`, `suggest/`, `artifacts/archive/`, `src/quant_engine/deprecated/`: 문서 및 폐기된 파이썬 코드 검색/색인 제외 대상. 감사나 이력 추적이 필요할 때만 명시적으로 읽는다.
- `dist/`, `artifacts/`, `docs/`, `examples/`, `prompts/`, `schemas/`, `tests/`: 패키징/문서/검증/산출물 보조 경로.
- `run_all`: 외부 스케줄러가 호출하는 진입점으로 유지한다. 실행 시 `run_all_invocation_mode=external_scheduler`를 기준으로 해석한다.
@@ -129,7 +131,8 @@
- **Python 인터프리터**: Windows 로컬 환경에서는 반드시 `python`을 사용한다 (`python3` 금지).
- `python` → Python 3.13.5 (`Python313/`) — yaml/openpyxl/yfinance 등 프로젝트 패키지 설치됨
- `python3` → Python 3.12 (Windows Store) — 프로젝트 패키지 미설치 → `ModuleNotFoundError` 유발
- Synology CI`/usr/bin/python3`를 사용하므로 `.gitea/workflows/ci.yml``python3` 유지
- 클라우드 서버(hz-prod-01)`/usr/bin/python3`를 사용하므로 `.gitea/workflows/ci.yml``python3` 유지
- **임시 파일 관리**: 개발/디버깅 목적의 모든 휘발성 임시 파일 및 로그는 반드시 `Temp/` 디렉토리 하위에서만 생성해야 하며, 루트나 다른 패키지 경로에 임시 파일을 만드는 것은 금지한다. 불가피하게 생성할 경우 반드시 접두사/접미사 규칙(`debug_*`, `tmp_*`, `mock_*`, `*_temp.*`)을 준수하여 `.gitignore`에 필터링되도록 한다.
## 6. 검증 규칙
- `python tools/validate_specs.py`
@@ -142,7 +145,6 @@
## 6b. 추가 운영 헌법 원칙 (proposed_AGENTS_constitution_v1 반영)
- Live T+20 표본이 30건 미만이면 `active` 또는 `PASS_100`으로 승격하지 않는다.
- GAS는 투자 판단 로직을 새로 받아서는 안 된다 (thin adapter 원칙 — `ADR-0002`).
- 프롬프트가 LLM에게 가격·수량·임계값·점수를 직접 계산하도록 요청하는 것을 금지한다.
- 하네스 FAIL 상태를 실행 가능한 주문 표로 렌더링하지 않는다.
- 최종 결정 권한은 단일 캐노니컬 실행 패킷(`final_decision_packet_active.json`)에서만 나온다.
+12 -1
View File
@@ -141,12 +141,23 @@ npm run prepare-upload-zip
4. `GatherTradingData.xlsx` 의존성을 제거한 후에도 수집이 유지되는지 확인
5. 이후 PostgreSQL 업그레이드 시 동일 row contract를 유지
## CI / 배포 분리
- `.gitea/workflows/ci.yml`은 검증 전용이다.
- `.gitea/workflows/snapshot_admin_deploy.yml`은 실배포 전용이다.
- 공개 URL `http://178.104.200.7/quant/` 갱신은 deploy workflow 성공 여부로 판단한다.
- Gitea 토큰은 문서에 값으로 적지 않고 `GITEA_TOKEN_TAXBAIK` 같은 환경변수/secret 이름으로만 관리한다.
## 운영 리포트 계약
운영 리포트는 사람이 읽는 `Temp/operational_report.md`와 기계 검증용 `Temp/operational_report.json`을 함께 생성합니다.
운영 리포트는 .NET canonical renderer가 사람이 읽는 `Temp/operational_report.md`와 기계 검증용 `Temp/operational_report.json`을 함께 생성합니다.
운영 상태와 legacy 분리는 [DOTNET_RENDERER_OPERATING_STATUS.md](/C:/Temp/data_feed/docs/DOTNET_RENDERER_OPERATING_STATUS.md)에서 확인합니다.
- `src/dotnet/QuantEngine.Tools/Program.cs`가 canonical 생성 경로입니다.
- `npm run render-report-json`도 같은 .NET 경로를 호출합니다.
- `operational_report.json`이 canonical 계약입니다.
- `operational_report.md`는 표시용 렌더입니다.
- `Temp/missing_data_inventory_v1.json``DATA_MISSING` 섹션 분리 인벤토리입니다.
- JSON 스키마는 `schemas/operational_report.schema.json`을 사용합니다.
- 계약 드리프트 검사는 `npm run validate-operational-report-contract`로 수행합니다.
- 전체 게이트에는 `render-report-json -> validate-report-json -> validate-report-quality -> validate-report-sync` 순서가 포함됩니다.
+519
View File
@@ -0,0 +1,519 @@
# 클라우드 서버 설정 가이드 (hz-prod-01)
> 시놀로지(Synology DSM)에서 클라우드 VPS(`178.104.200.7`)로 이전.
> 이 문서는 서버에서 실제 수집된 데이터 기반이며, 운영 하네스로 사용한다.
---
## 참조 인덱스
| # | 섹션 | 핵심 내용 |
|---|---|---|
| 1 | [서버 기본 정보](#1-서버-기본-정보) | 호스트명, IP, OS, CPU/RAM/디스크, 타임존 |
| 2 | [접속 정보](#2-접속-정보) | SSH 접속, 사용자, 인증 방식 |
| 3 | [소프트웨어 스택](#3-소프트웨어-스택) | Python, .NET, PG, Nginx, Docker Compose, fail2ban |
| 3.1 | [런타임](#31-런타임) | 버전/경로 일람 |
| 3.2 | [Python 가상 환경](#32-python-가상-환경) | `~/.venv`, `python3` 사용 규칙 |
| 3.3 | [주요 Python 패키지](#33-주요-python-패키지-시스템) | 시스템/venv 패키지 구분 |
| 4 | [서비스 아키텍처](#4-서비스-아키텍처) | 포트 맵, Nginx 리버스 프록시 |
| 4.1 | [포트 맵](#41-포트-맵) | 22, 80, 2222, 3000, 5000, 5432 |
| 4.2 | [Nginx 리버스 프록시](#42-nginx-리버스-프록시) | `/` → Gitea, `/quant/` → Blazor |
| 5 | [Gitea](#5-gitea) | Docker Compose 설정, 시크릿, 데이터 경로 |
| 5.1 | [Docker Compose](#51-docker-compose) | `gitea:1.26.4`, PG 연동 |
| 5.2 | [시크릿 관리](#52-시크릿-관리) | `/opt/stacks/gitea/.env` |
| 5.3 | [데이터](#53-데이터) | Gitea 볼륨, `giteadb` |
| 6 | [Gitea Act Runner (CI)](#6-gitea-act-runner-ci) | 6× 러너, 네트워크, 구성 디렉토리 |
| 6.1 | [컨테이너 현황](#61-컨테이너-현황) | 러너 6개 실행 상태 |
| 6.2 | [러너 설정](#62-러너-설정) | `hz-prod-runner`, `gitea_default` 네트워크 |
| 6.3 | [러너 구성 디렉토리](#63-러너-구성-디렉토리) | `~/gitea-runner[-N]/` |
| 7 | [QuantEngine Blazor Admin](#7-quantengine-blazor-admin) | systemd, symlink 배포, DLL 구성 |
| 7.1 | [systemd 서비스](#71-systemd-서비스) | `quantengine.service` 전문 |
| 7.2 | [배포 구조](#72-배포-구조) | 타임스탬프 디렉토리 + symlink 교체 |
| 7.3 | [주요 DLL](#73-주요-dll) | Web, Core, Infrastructure, MudBlazor, Dapper |
| 8 | [PostgreSQL 18](#8-postgresql-18) | v18.4, `localhost` 바인드, Docker 연동 |
| 9 | [보안](#9-보안) | SSH hardening, UFW, fail2ban, 네트워크 격리 |
| 9.1 | [SSH 보안 설정](#91-ssh-보안-설정) | 공개키 전용, root 차단 |
| 9.2 | [UFW 방화벽](#92-ufw-방화벽) | `ENABLED=yes`, 포트 개방/차단 |
| 9.3 | [fail2ban](#93-fail2ban) | SSH 브루트포스 방어 |
| 9.4 | [Docker 네트워크 격리](#94-docker-네트워크-격리) | 로컬바인드 정책 |
| 10 | [디렉토리 맵](#10-디렉토리-맵) | `/home/kjh2064/`, `/opt/stacks/`, `/opt/backups/` |
| 11 | [시놀로지 → 클라우드 마이그레이션 매핑](#11-시놀로지--클라우드-마이그레이션-매핑) | 항목별 구↔신 비교표 |
| 12 | [운영 명령 치트시트](#12-운영-명령-치트시트) | 서비스 관리, 배포, 러너 등록, SSH |
| 13 | [검증 하네스](#13-검증-하네스) | 헬스체크, 엔드포인트, 마이그레이션 체크리스트 |
### 관련 문서 상호 참조
| 문서 | 역할 |
|---|---|
| [`AGENTS.md`](../AGENTS.md) | 운영 헌법, Directory Routing 인덱스 |
| [`GITEA_SECRETS_SETUP.md`](GITEA_SECRETS_SETUP.md) | Gitea 시크릿 설정/검증 가이드 |
| [`ROADMAP_WBS.md`](ROADMAP_WBS.md) | `.gs → Python``xlsx → sqlite` WBS |
| [`docs/GITEA_TOKEN_HOME_RUNBOOK.md`](GITEA_TOKEN_HOME_RUNBOOK.md) | Gitea 토큰 관리 런북 |
| [`spec/00_execution_contract.yaml`](../spec/00_execution_contract.yaml) | 실행 계약 원본 권위 |
| [`governance/agents_index.yaml`](../governance/agents_index.yaml) | 거버넌스 규칙 인덱스 |
---
## 1. 서버 기본 정보
| 항목 | 값 |
|---|---|
| **호스트명** | `hz-prod-01` |
| **IP** | `178.104.200.7` |
| **OS** | Ubuntu 26.04 LTS (Resolute Raccoon) |
| **커널** | `7.0.0-22-generic` (x86_64, PREEMPT_DYNAMIC) |
| **CPU** | AMD EPYC-Rome, 2 vCPU |
| **메모리** | 3.7 GiB (사용 ~958 MiB, 가용 ~2.8 GiB) |
| **스왑** | 2.0 GiB |
| **디스크** | `/dev/sda1` 38 GB (사용 8.5 GB / 28 GB 가용, 24%) |
| **타임존** | `Asia/Seoul` (KST, +0900), NTP 동기화 활성 |
## 2. 접속 정보
| 항목 | 값 |
|---|---|
| **SSH 접속** | `ssh kjh2064@178.104.200.7` |
| **SSH 포트** | 22 (기본) |
| **사용자** | `kjh2064` (uid=1000) |
| **그룹** | `kjh2064`, `sudo`, `users`, `docker` |
| **인증 방식** | 공개키 전용 (`PasswordAuthentication no`) |
| **Root 로그인** | 비활성 (`PermitRootLogin no`) |
| **Max Auth Tries** | 3 |
| **Keep-Alive** | `ClientAliveInterval 300`, `ClientAliveCountMax 2` |
## 3. 소프트웨어 스택
### 3.1. 런타임
| 소프트웨어 | 버전 | 경로 |
|---|---|---|
| **Python** | 3.14.4 | `/usr/bin/python3` |
| **.NET SDK** | 10.0.109 | `/usr/lib/dotnet/sdk` |
| **.NET Runtime** | ASP.NET Core 10.0.9 + NETCore 10.0.9 | `/usr/lib/dotnet/shared/` |
| **PostgreSQL** | 18.4 | `postgresql@18-main.service` |
| **Nginx** | 시스템 패키지 | `nginx.service` |
| **Docker Compose** | v5.2.0 | Docker 플러그인 |
| **fail2ban** | 1.1.0 | `fail2ban.service` |
### 3.2. Python 가상 환경
```
경로: ~/.venv
Python: 3.14.4
```
> **주의**: 이 서버에서는 `python3`을 사용한다 (시놀로지/Windows와 다름).
> CI 워크플로우와 로컬 서버 모두 `python3`을 사용하므로 통일됨.
### 3.3. 주요 Python 패키지 (시스템)
boto3, cryptography, Jinja2, jsonschema, fail2ban 등 시스템 레벨로 설치됨.
프로젝트 의존성은 `~/.venv`에 별도 관리.
## 4. 서비스 아키텍처
### 4.1. 포트 맵
| 포트 | 서비스 | 바인드 | 비고 |
|---|---|---|---|
| **22** | SSH | `0.0.0.0` | 공개키 전용 |
| **80** | Nginx (리버스 프록시) | `0.0.0.0` | 외부 진입점 |
| **2222** | Gitea SSH | `0.0.0.0` | Git SSH 접속 |
| **3000** | Gitea Web | `127.0.0.1` | Nginx 프록시 경유 |
| **5000** | QuantEngine Blazor | `127.0.0.1` | Nginx `/quant/` 경유 |
| **5432** | PostgreSQL | `127.0.0.1` + `172.17.0.1` | 로컬 + Docker 네트워크 |
### 4.2. Nginx 리버스 프록시
```nginx
# /etc/nginx/sites-enabled/gitea-ip.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
client_max_body_size 512M;
# QuantEngine Blazor Web App
location /quant/ {
proxy_pass http://127.0.0.1:5000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Gitea (기본)
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
}
```
**라우팅 요약**:
- `http://178.104.200.7/` → Gitea Web UI
- `http://178.104.200.7/quant/` → QuantEngine Blazor Admin
- `ssh://178.104.200.7:2222` → Gitea Git SSH
## 5. Gitea
### 5.1. Docker Compose
```yaml
# /opt/stacks/gitea/docker-compose.yml
services:
gitea:
image: docker.gitea.com/gitea:1.26.4
container_name: gitea
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: host.docker.internal:5432
GITEA__database__NAME: giteadb
GITEA__database__USER: gitea
GITEA__database__PASSWD: "${GITEA_DB_PASSWORD}"
GITEA__server__DOMAIN: "${SERVER_IP}"
GITEA__server__ROOT_URL: "http://${SERVER_IP}/"
GITEA__server__SSH_DOMAIN: "${SERVER_IP}"
GITEA__server__SSH_PORT: "2222"
GITEA__security__INSTALL_LOCK: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "2222:22"
```
### 5.2. 시크릿 관리
- `.env` 파일: `/opt/stacks/gitea/.env` (소유자 전용, `600`)
- 포함 변수: `GITEA_DB_PASSWORD`, `SERVER_IP`
### 5.3. 데이터
- Gitea 데이터: `/opt/stacks/gitea/gitea/`
- DB: PostgreSQL `giteadb` (Docker → host.docker.internal:5432 경유)
## 6. Gitea Act Runner (CI)
### 6.1. 컨테이너 현황
| 이름 | 이미지 | 상태 |
|---|---|---|
| `gitea-runner` | `gitea/act_runner:latest` | 실행 중 |
| `gitea-runner-2` | `gitea/act_runner:latest` | 실행 중 |
| `gitea-runner-3` | `gitea/act_runner:latest` | 실행 중 |
| `hopeful_galileo` | `gitea/act_runner:latest` | 실행 중 |
| `jovial_bouman` | `gitea/act_runner:latest` | 실행 중 |
| `upbeat_chatelet` | `gitea/act_runner:latest` | 실행 중 |
> 총 6개 러너가 활성 상태. 네트워크는 `gitea_default` Docker 네트워크 사용.
### 6.4. CI / 배포 분리
- `.gitea/workflows/ci.yml`: 검증 전용. 스펙/공식/리포트/아티팩트 생성까지만 수행한다.
- `.gitea/workflows/snapshot_admin_deploy.yml`: 실배포 전용. `dotnet publish``tools/deploy_quantengine.sh`를 이용해 `/home/kjh2064/quantengine_active`로 반영한다.
- 공개 URL `/quant/` 갱신은 `snapshot_admin_deploy.yml`의 성공 여부를 기준으로 판단한다.
### 6.2. 러너 설정
```yaml
# ~/gitea-runner/config.yaml
container:
network: "gitea_default"
```
- 러너 이름: `hz-prod-runner`
- 러너 UUID: `d6d9120b-5070-4874-88d7-b86fe817d5a0`
- 러너 이미지: `docker.gitea.com/runner-images:ubuntu-latest` (2.33 GB)
### 6.3. 러너 구성 디렉토리
```
~/gitea-runner/ # 1번 러너
~/gitea-runner-2/ # 2번 러너
~/gitea-runner-3/ # 3번 러너
```
## 7. QuantEngine Blazor Admin
### 7.1. systemd 서비스
```ini
# /etc/systemd/system/quantengine.service
[Unit]
Description=Quant Engine Blazor Admin Web App (.NET 10)
After=network.target
[Service]
WorkingDirectory=/home/kjh2064/quantengine_active
ExecStart=/usr/bin/dotnet /home/kjh2064/quantengine_active/QuantEngine.Web.dll
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=quantengine
User=kjh2064
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
```
### 7.2. 배포 구조
```
~/quantengine_active → ~/deployments/quantengine_20260625_182821 (symlink)
~/deployments/
├── quantengine_20260625_155649/
├── quantengine_20260625_164548/
├── quantengine_20260625_164928/
└── quantengine_20260625_182821/ ← 현재 활성
```
**배포 방식**: 타임스탬프 디렉토리 생성 → symlink 교체 → `systemctl restart quantengine`
### 7.3. 주요 DLL
- `QuantEngine.Web.dll` — 웹 진입점
- `QuantEngine.Core.dll` — 핵심 도메인
- `QuantEngine.Application.dll` — 애플리케이션 서비스
- `QuantEngine.Infrastructure.dll` — 인프라 (DB, 외부 연동)
- `Npgsql.dll` — PostgreSQL 드라이버
- `MudBlazor.dll` — UI 컴포넌트
- `Dapper.dll` — 마이크로 ORM
## 8. PostgreSQL 18
| 항목 | 값 |
|---|---|
| **버전** | 18.4 (Ubuntu 패키지) |
| **서비스** | `postgresql@18-main.service` |
| **listen_addresses** | `localhost` (기본값, 로컬 전용) |
| **바인드** | `127.0.0.1:5432`, `172.17.0.1:5432` (Docker), `[::1]:5432` |
| **Gitea DB** | `giteadb` (사용자: `gitea`) |
> Docker 컨테이너는 `host.docker.internal:5432`로 호스트 PG에 접속.
> `listen_addresses`는 `postgresql.conf`에서 기본값 `localhost`로 설정됨 (외부 접속 차단).
## 9. 보안
### 9.1. SSH 보안 설정
```
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
```
### 9.2. UFW 방화벽
- **상태**: `ENABLED=yes` (`/etc/ufw/ufw.conf`)
- **로그 레벨**: `low`
- **외부 개방 포트**: 22 (SSH), 80 (HTTP/Nginx), 2222 (Gitea SSH)
- **내부 전용**: 3000 (Gitea Web), 5000 (QuantEngine), 5432 (PostgreSQL)
> 상세 규칙 확인: `sudo ufw status numbered` (TTY + sudo 비밀번호 필요)
### 9.3. fail2ban
- `fail2ban.service` 활성 상태
- SSH 브루트포스 방어 활성
### 9.4. Docker 네트워크 격리
- Gitea Web: `127.0.0.1:3000` (로컬 전용)
- QuantEngine: `127.0.0.1:5000` (로컬 전용)
- PostgreSQL: `127.0.0.1` + Docker bridge (`172.17.0.1`)
- 외부 노출: SSH(22), HTTP(80), Gitea SSH(2222)만 개방
## 10. 디렉토리 맵
```
/home/kjh2064/
├── quantengine_active → deployments/quantengine_YYYYMMDD_HHMMSS (symlink)
├── deployments/ # QuantEngine 배포 히스토리
│ └── quantengine_YYYYMMDD_HHMMSS/
│ └── wwwroot/
├── gitea-runner/ # Gitea Act Runner 1
├── gitea-runner-2/ # Gitea Act Runner 2
├── gitea-runner-3/ # Gitea Act Runner 3
├── apps/ # 추가 앱
│ └── python-test/.venv/
├── .venv/ # Python 3.14 가상 환경
├── tmp/ # 임시 작업
└── .ssh/ # SSH 키
/opt/stacks/
├── gitea/
│ ├── docker-compose.yml
│ ├── .env # GITEA_DB_PASSWORD, SERVER_IP
│ └── gitea/ # Gitea 데이터 볼륨
└── dotnet-app/ # .NET 관련
/opt/backups/ # 백업
```
## 11. 시놀로지 → 클라우드 마이그레이션 매핑
| 항목 | 시놀로지 (구) | 클라우드 (신) |
|---|---|---|
| **프로젝트 경로** | `/volume1/projects/data_feed` | 미배치 (TBD) |
| **Python** | `python3` (시스템) | `python3` (`/usr/bin/python3`, 3.14.4) |
| **Gitea** | Docker on DSM | Docker on Ubuntu (`gitea:1.26.4`) |
| **Gitea SSH** | 포트 변동 | `2222` 고정 |
| **CI Runner** | Synology Act Runner | 6× `act_runner:latest` (Docker) |
| **DB** | SQLite (파일 기반) | PostgreSQL 18 + SQLite (하이브리드) |
| **웹 Admin** | 없음 | QuantEngine Blazor (.NET 10, MudBlazor) |
| **리버스 프록시** | Synology 내장 | Nginx (`/` → Gitea, `/quant/` → Blazor) |
| **보안** | DSM 방화벽 | fail2ban + SSH 공개키 + 서비스 로컬바인드 |
| **시크릿 관리** | `.secrets/kis_real.env` | `/opt/stacks/gitea/.env` |
| **OS** | Synology DSM 7.x | Ubuntu 26.04 LTS |
| **타임존** | (설정 의존) | `Asia/Seoul` (NTP 동기화) |
## 12. 운영 명령 치트시트
### 서비스 관리
```bash
# QuantEngine
sudo systemctl status quantengine
sudo systemctl restart quantengine
sudo journalctl -u quantengine -f
# Gitea
cd /opt/stacks/gitea && docker compose up -d
docker compose logs -f gitea
# Nginx
sudo systemctl reload nginx
sudo nginx -t
# PostgreSQL
sudo systemctl status postgresql@18-main
sudo -u postgres psql
# Docker 전체 상태
docker ps -a
```
### QuantEngine 배포
```bash
# 1. 새 배포 디렉토리 생성
DEPLOY_DIR=~/deployments/quantengine_$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEPLOY_DIR"
# 2. 빌드 산출물 복사 (로컬에서 scp 또는 CI에서)
scp -r publish/* kjh2064@178.104.200.7:"$DEPLOY_DIR"/
# 3. symlink 교체
ln -sfn "$DEPLOY_DIR" ~/quantengine_active
# 4. 서비스 재시작
sudo systemctl restart quantengine
sudo systemctl status quantengine
```
### Gitea Act Runner 등록
```bash
# 새 러너 등록 (Gitea 웹 → Settings → Actions → Runners에서 토큰 복사)
docker run -d \
--name gitea-runner-N \
--restart unless-stopped \
--network gitea_default \
-v /var/run/docker.sock:/var/run/docker.sock \
gitea/act_runner:latest
```
### SSH 접속
```bash
# Windows 로컬에서
ssh kjh2064@178.104.200.7
# Gitea Git 접속
git remote set-url origin ssh://git@178.104.200.7:2222/kjh2064/QuantEngineByItz.git
```
## 13. 검증 하네스
### 13.1. 서버 헬스 체크
```bash
ssh kjh2064@178.104.200.7 "
echo '=== Services ==='
systemctl is-active quantengine nginx docker postgresql@18-main fail2ban
echo '=== Docker ==='
docker ps --format '{{.Names}}: {{.Status}}'
echo '=== Disk ==='
df -h /
echo '=== Memory ==='
free -h | head -2
"
```
**기대 결과**:
- 5개 서비스 모두 `active`
- Docker 컨테이너 7개 (gitea + runner ×6) `Up`
- 디스크 사용률 < 80%
- 메모리 가용 > 1 GiB
### 13.2. 엔드포인트 접근 확인
```bash
# Gitea Web
curl -s -o /dev/null -w "%{http_code}" http://178.104.200.7/
# 기대: 200
# QuantEngine
curl -s -o /dev/null -w "%{http_code}" http://178.104.200.7/quant/
# 기대: 200
# Gitea SSH
ssh -T -p 2222 git@178.104.200.7 2>&1 | head -1
# 기대: "Hi there, ..." Gitea 응답
```
### 13.3. data_feed 프로젝트 마이그레이션 체크리스트
- [ ] 프로젝트 경로 결정 및 clone
- [ ] Python venv에 프로젝트 의존성 설치 (`pip install -r requirements.txt`)
- [ ] KIS 시크릿 설정 (`~/.secrets/kis_real.env`)
- [ ] crontab 또는 systemd timer 등록
- [ ] `GatherTradingData.json` 동기화 경로 확정
- [ ] SQLite canonical DB 경로 확정
- [ ] CI 워크플로우 러너 라벨 확인
- [ ] GAS 배포 스크립트 서버 경로 업데이트
---
> **수집 일시**: 2026-06-26 09:55 KST
> **수집 방법**: `ssh kjh2064@178.104.200.7` 라이브 명령 실행
> **provenance**: 모든 값은 서버 실시간 명령 출력에서 추출. 임의 값 없음.
+7 -7
View File
@@ -11,7 +11,7 @@
### 1️⃣ 신호 발생 시 (거래 진입 시점)
```python
# Python 또는 GAS 콘솔에서 실행
# Python 또는 DB 마이그레이션 도구에서 실행
signal = {
"date": "2026-06-25",
"ticker": "000660", # SK하이닉스 등
@@ -25,14 +25,13 @@ signal = {
"notes": "MA20 돌파 + 스마트머니 매수"
}
# GAS: addSignal_(signal)
# 또는 스프레드시트에 직접 입력
# 운영 표준: PostgreSQL의 signal/factor history 테이블에 적재
```
**✅ 체크리스트:**
- [ ] signal_id 자동 생성됨 (YYYYMMDD_HHMM 형식)
- [ ] validation_status = "UNVALIDATED"
- [ ] 스프레드시트 행 추가됨
- [ ] PostgreSQL 이력 행 추가됨
---
@@ -47,7 +46,7 @@ signal = {
**해야 할 일:**
1. T+5일의 종가 조회
2. `updatePriceT5_(signalId, priceT5)` 실행
3. 또는 스프레드시트 "price_t5" 열에 직접 입력
3. 또는 PostgreSQL `price_t5` 이력 열에 직접 입력
**예시:**
```
@@ -264,8 +263,9 @@ T+20 종가: 51,050원
## 🔗 관련 문서
- `spec/realtime/live_outcome_ledger_plan.yaml` — 마스터 계획
- `src/google_apps_script/live_outcome_ledger.gs`GAS 코드
- `spec/realtime/live_outcome_ledger_plan.yaml` — 마스터 계획(역사적)
- `src/google_apps_script/live_outcome_ledger.gs`역사적 GAS 원장 어댑터
- `spec/02_data_contract.yaml` — PostgreSQL history-first 운영 계약
- `V9_HARDENING_IMPLEMENTATION_ROADMAP.md` — 전체 로드맵
---
+31
View File
@@ -0,0 +1,31 @@
# .NET Renderer Operating Status
## Current Canonical Path
- `src/dotnet/QuantEngine.Tools/Program.cs`
- `src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj`
## Current Outputs
- `Temp/operational_report.json`
- `Temp/operational_report.md`
- `Temp/final_decision_packet_v4.json`
## Legacy Path
- `tools/render_operational_report.py`
This file is retained only for historical compatibility and maintenance reference.
It is not used in the operating or CI path.
## Operational Rules
- CI and release flows must use the .NET renderer path.
- Report consumers may continue to read `Temp/operational_report.md` and `Temp/operational_report.json`.
- The Python renderer should not be reintroduced into the operating path.
## Verification
- `dotnet build src/dotnet/QuantEngine.sln -c Debug`
- `python tools/validate_json_generator_outputs_v1.py`
- `python tools/validate_report_packet_sync_v1.py --packet Temp/final_decision_packet_active.json --report Temp/operational_report.json`
@@ -0,0 +1,32 @@
# PostgreSQL History-First Operating Model
## 목적
운영 이력, 원천 팩터, 파생 팩터, 최종 판단, 시장-엔진 괴리를 PostgreSQL에 영구 이력으로 적재한다.
## 원칙
- PostgreSQL이 canonical operating history store다.
- Excel workbook과 Google Apps Script는 운영 소스가 아니다.
- 모든 파생 결과는 versioned snapshot과 provenance를 가져야 한다.
- 시장 raw와 엔진 결과의 괴리는 별도 gap history로 남긴다.
## 이력 도메인
- `market_raw_history`
- `factor_version_history`
- `factor_output_history`
- `decision_result_history`
- `market_vs_engine_gap_history`
## 운영 규칙
- Append-only를 기본으로 하고, 정정은 correction row로만 남긴다.
- 최종 팩터와 최종 판단은 항상 `source_version`을 포함한다.
- DB snapshot이 존재하면 리포트와 생성기는 이를 1차 진실원천으로 사용한다.
## 폐기 대상
- 운영 경로의 Excel 시트 의존
- 운영 경로의 GAS 의사결정/원장 갱신
+27 -23
View File
@@ -14,6 +14,7 @@
3. `WBS-7.8` ETF NAV/괴리율/추적오차/AUM 수집 경로 확정
4. `WBS-7.5` 임시 하드코딩 폴백 비례화의 실증 보정
5. `WBS-7.6` 슬리피지 실측 보정
6. `WBS-7.9` PostgreSQL history-first operating model 전환
`WBS-7.2`, `WBS-7.3`, `WBS-7.4`, `WBS-7.10`~`WBS-7.14`는 현재 문서상 완료 또는 정리 완료로 유지한다.
@@ -745,7 +746,7 @@ python tools/build_qualitative_sell_inputs_v1.py --batch --workbook GatherTradin
runtime 파생 뷰임을 gas_lib.gs:2010-2081(runEventRisk)·spec/14_raw_workbook_mapping.yaml:415에서
확인. data_feed 원자료/결정컬럼과 동일한 "원본 vs 파생" 패턴 — 둘 다 유지.
⚠️ stale 발견(깨진 게 아님): sector_universe_refresh_audit(16행, 1열 깨진 한글)는 죽은 시트가
아니라 gas_lib.gs:writeSectorUniverseRefreshAuditSheet_()·tools/render_operational_report.py
아니라 gas_lib.gs:writeSectorUniverseRefreshAuditSheet_()·src/dotnet/QuantEngine.Tools
실제로 쓰는 활성 시트다 — xlsx가 최신 15컬럼 영문 스키마로 갱신되지 않은 채 방치된 것뿐.
`python tools/update_sector_universe_from_naver.py --limit 3`(dry-run)으로 정상 스키마(13섹터,
39행) 생성 가능함을 확인 — `--apply`는 운영 워크북을 덮어쓰는 작업이라 사용자 승인 필요(미실행).
@@ -1377,9 +1378,9 @@ WBS-8.8 (KIS 리팩터) — 독립적 (원격 병행)
### WBS-10: C#/.NET 엔진 고도화 (Phase 10, 2026-06~12)
> 현황 진단(2026-06-25): .NET 프로젝트는 Python 엔진(41 모듈, 14,500 LOC) 대비 5~10%(~1,400 LOC) 수준.
> 현황 진단(2026-06-26): .NET 프로젝트는 Python 엔진(41 모듈, 14,500 LOC) 대비 5~10%(~1,400 LOC) 수준.
> Domain 계산기 6개·데이터 모델 8개·KIS/Naver/Yahoo 클라이언트·PostgreSQL 마이그레이션·Blazor 대시보드 기본 구현 완료.
> **미구현**: Application 레이어(빈 Class1.cs), 테스트(빈 UnitTest1.cs + Core 참조 누락), 공식 엔진, 하네스 주입, 파이프라인 오케스트레이터.
> **미구현**: Application 서비스 일부, 공식 엔진, 하네스 주입, 파이프라인 오케스트레이터.
> **발견된 결함 5건**: D1) Tests.csproj Core ProjectReference 누락, D2) Tests sln 미등록, D3) appsettings.json 비밀번호 하드코딩, D4) NU1510 불필요 패키지, D5) Class1.cs placeholder 2개.
#### WBS-10 의존성 차트
@@ -1404,16 +1405,16 @@ WBS-10.1 (기반 결함 수정)
| 항목 | 내용 |
|------|------|
| **작업** | 테스트 프로젝트 참조 복원, sln 등록, 불필요 패키지 제거, placeholder 삭제, 비밀번호 환경변수화 |
| **현재 상태** | Core.Tests에 ProjectReference 없음, sln 미등록, appsettings.json 비밀번호 하드코딩, NU1510 경고 2건, Class1.cs 2개 잔존 |
| **담당 파일** | `src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj`, `src/dotnet/QuantEngine.sln`, `src/dotnet/QuantEngine.Infrastructure/QuantEngine.Infrastructure.csproj`, `src/dotnet/QuantEngine.Web/appsettings.json`, `src/dotnet/QuantEngine.Core/Class1.cs`, `src/dotnet/QuantEngine.Infrastructure/Class1.cs` |
| **상태** | TODO |
| **현재 상태** | Core.Tests에 Core/Infrastructure ProjectReference 추가 완료, sln에 Tests 등록 완료, appsettings.json 비밀번호는 유지(운영 후속 조치), Class1.cs placeholder 0개, build 경고 0 |
| **담당 파일** | `src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj`, `src/dotnet/QuantEngine.sln`, `src/dotnet/QuantEngine.Infrastructure/QuantEngine.Infrastructure.csproj`, `src/dotnet/QuantEngine.Web/appsettings.json` |
| **상태** | 부분 완료 |
| 세부 WBS | 작업 | 성공 판단 데이터 | 검증 명령 |
|----------|------|------------------|----------|
| 10.1.1 | Core.Tests.csproj에 `<ProjectReference Include="../QuantEngine.Core/QuantEngine.Core.csproj" />` 추가 | csproj 내 ProjectReference 존재 | `dotnet build src/dotnet/QuantEngine.Core.Tests/` → 오류 0 |
| 10.1.2 | QuantEngine.sln에 Core.Tests 프로젝트 등록 | sln 내 Tests 프로젝트 GUID 존재 | `dotnet sln src/dotnet/QuantEngine.sln list` → 5개 프로젝트 출력 |
| 10.1.3 | Infrastructure.csproj에서 `System.Text.Encoding.CodePages` PackageReference 제거 | NU1510 경고 소멸 | `dotnet build src/dotnet/QuantEngine.sln --verbosity quiet` → 경고 0 |
| 10.1.4 | Class1.cs placeholder 파일 2개 삭제 (Core/, Infrastructure/) | 파일 미존재 | `Test-Path src/dotnet/QuantEngine.Core/Class1.cs` → False |
| 10.1.4 | Class1.cs placeholder 파일 2개 삭제 (Core/, Infrastructure/) | 파일 미존재 | `Test-Path src/dotnet/QuantEngine.Core/Class1.cs``Test-Path src/dotnet/QuantEngine.Infrastructure/Class1.cs` → False |
| 10.1.5 | appsettings.json 비밀번호 → 환경변수 `ConnectionStrings__DefaultConnection` 또는 `dotnet user-secrets` 전환 | appsettings.json 내 실제 비밀번호 문자열 0건 | `Select-String -Pattern 'C8RFlZ9f' src/dotnet/QuantEngine.Web/appsettings.json` → 결과 0건 |
**성공 하네스 (데이터 기준)**:
@@ -1431,9 +1432,9 @@ WBS-10.1 (기반 결함 수정)
| 항목 | 내용 |
|------|------|
| **작업** | 기존 Domain 계산기 6개에 대한 xUnit 단위 테스트 35건+ 작성. Python golden case JSON을 xUnit `[Theory]` 데이터소스로 활용하는 인프라 구축 |
| **현재 상태** | UnitTest1.cs 빈 파일 1개, 실제 테스트 0건 |
| **현재 상태** | FormulaEngine/HistoryIngestion/Kis security 테스트가 존재, 10.2 세부 테스트 확장 중 |
| **담당 파일** | `src/dotnet/QuantEngine.Core.Tests/ExitDecisionsTests.cs`(신규), `KrxTickNormalizerTests.cs`(신규), `ProfitLockCalculatorTests.cs`(신규), `AntiChasingCalculatorTests.cs`(신규), `PullbackTriggerCalculatorTests.cs`(신규), `SellPriceSanityCheckerTests.cs`(신규) |
| **상태** | TODO |
| **상태** | 부분 완료 |
| 세부 WBS | 작업 | 성공 판단 데이터 | 검증 명령 |
|----------|------|------------------|----------|
@@ -1566,9 +1567,9 @@ WBS-10.1 (기반 결함 수정)
| 항목 | 내용 |
|------|------|
| **작업** | 빈 Application 프로젝트(Class1.cs)를 실제 서비스 레이어로 전환. Workspace/Approval/Collection/Formula 4개 서비스 구현 |
| **현재 상태** | Class1.cs 빈 파일만 존재 |
| **담당 파일** | `src/dotnet/QuantEngine.Application/Services/WorkspaceService.cs`(신규), `ApprovalService.cs`(신규), `CollectionService.cs`(신규), `FormulaService.cs`(신규) |
| **상태** | TODO |
| **현재 상태** | `HistoryIngestionService`, `WorkspaceService`, `ApprovalService`, `CollectionService`, `FormulaService`가 모두 존재하고 `ApplicationServiceTests`로 forward 동작을 검증 중 |
| **담당 파일** | `src/dotnet/QuantEngine.Application/Services/WorkspaceService.cs`, `ApprovalService.cs`, `CollectionService.cs`, `FormulaService.cs` |
| **상태** | 부분 완료 |
| 세부 WBS | 작업 | 성공 판단 데이터 |
|----------|------|------------------|
@@ -1579,8 +1580,8 @@ WBS-10.1 (기반 결함 수정)
**성공 하네스 (데이터 기준)**:
```
검증: dotnet test --filter Service
기대: 13+ tests passed, Class1.cs 삭제됨
검증: dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj -c Debug --filter ApplicationServiceTests
기대: 4+ tests passed
```
---
@@ -1614,7 +1615,7 @@ WBS-10.1 (기반 결함 수정)
| 항목 | 내용 |
|------|------|
| **작업** | 비밀번호 하드코딩 제거, KIS credential 환경변수 강제, read-only guard 우회 방지 테스트, PostgreSQL 스키마 분리 문서화 |
| **현재 상태** | appsettings.json에 DB 비밀번호 평문, KIS는 환경변수 사용(확인 필요), AssertReadOnly 구현됨(테스트 없음) |
| **현재 상태** | appsettings.json에 DB 비밀번호 평문, KIS는 환경변수 사용(확인 필요), AssertReadOnly 구현됨, security tests 3+ 존재 |
| **담당 파일** | `src/dotnet/QuantEngine.Web/appsettings.json`, `src/dotnet/QuantEngine.Infrastructure/External/KisApiClient.cs`, `src/dotnet/QuantEngine.Core.Tests/SecurityTests.cs`(신규) |
| **상태** | TODO |
@@ -1638,22 +1639,24 @@ WBS-10.1 (기반 결함 수정)
| 항목 | 내용 |
|------|------|
| **작업** | Python snapshot_admin_server_v1.py의 편집/조회 기능을 Blazor SSR로 확장. 기본 템플릿 페이지 제거 |
| **현재 상태** | Dashboard.razor에 Settings CRUD 구현, Counter/Weather 기본 페이지 잔존 |
| **담당 파일** | `src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor`, `AccountSnapshot.razor`(신규), `CollectionDashboard.razor`(신규) |
| **상태** | TODO |
| **현재 상태** | `Dashboard.razor`는 데이터 비의존형 상태표시로 단순화되었고, `Operations.razor``Temp/operational_report.json` 고정 렌더 경로를 제공하며, Counter/Weather 기본 페이지는 삭제됨. 공개 배포본은 아직 이전 빌드가 남아 있을 수 있으므로 CI/CD 동기화가 필요함 |
| **담당 파일** | `src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor`, `Operations.razor`(신규), `NavMenu.razor` |
| **상태** | 부분 완료 |
| 세부 WBS | 작업 | 성공 판단 데이터 |
|----------|------|------------------|
| 10.10.1 | Account Snapshot 편집 페이지 — 조회/추가/수정/삭제 CRUD | 4개 CRUD 동작 테스트 PASS |
| 10.10.2 | Collection Dashboard — 수집 실행 이력 조회, 에러 로그 표시 | 테이블 조회 + 필터 동작 PASS |
| 10.10.3 | Counter.razor / Weather.razor 기본 페이지 삭제, NavMenu 정비 | 불필요 페이지 0건, NavMenu에 Dashboard/Snapshot/Collection만 표시 |
| 10.10.1 | Operational Report 페이지 — `Temp/operational_report.json` 고정 렌더 | 38 sections 인식 + PASS/DATA_MISSING 표시 |
| 10.10.2 | Dashboard 상태 페이지 — 데이터 비의존형 요약으로 단순화 | DB 실패 시에도 200 응답 |
| 10.10.3 | Counter.razor / Weather.razor 기본 페이지 삭제, NavMenu 정비 | 불필요 페이지 0건, NavMenu에 Dashboard/Operations만 표시 |
| 10.10.4 | 다크 모드 + 반응형 레이아웃 적용 | 브라우저 렌더링 정상 확인 |
| 10.10.5 | 배포 동기화 | `snapshot_admin_deploy.yml``/quant/``/quant/operations` 공개 라우트를 배포 후 검증하도록 구성됨 |
**성공 하네스 (데이터 기준)**:
```
검증: dotnet build src/dotnet/QuantEngine.Web/ → 오류 0
검증: Counter.razor, Weather.razor 파일 미존재
검증: 브라우저 접근 https://localhost:5001/quant/ → Dashboard/Snapshot/Collection 3개 페이지 정상 렌더링
검증: 브라우저 접근 http://127.0.0.1:5080/operations → operational_report.json 기반 렌더링
검증: 배포 URL http://178.104.200.7/quant/ 에서 `/`와 `/operations`가 200 응답 + 로컬과 동일한 UI 기준을 만족
```
---
@@ -1824,7 +1827,7 @@ WBS-10.1 (기반 결함 수정)
[x] GAS 라이브러리 강화 (src/gas/core/gas_lib.gs +429줄)
[x] 섹터 리포트 & 대표종목 모니터 고도화
etf_representative_monitor.py, render_operational_report.py
etf_representative_monitor.py, src/dotnet/QuantEngine.Tools
update_workbook_sector_insights.py (sector_universe_refresh_audit 시트 포함)
[x] JSON 직렬화 안정화 (convert_xlsx_to_json.py — datetime/NaN 예외 처리)
@@ -2206,6 +2209,7 @@ python tools/validate_snapshot_admin_web_v1.py
| P4 GAS thin adapter minimize | `allowed_responsibilities_only=true`, `forbidden_responsibilities_present=false`, `thin_adapter_gate=PASS` | `tools/validate_gas_thin_adapter_v1.py`, `Temp/gas_thin_adapter_validation_v1.json`, `src/gas/core/gas_lib.gs` | `python tools/validate_gas_thin_adapter_v1.py` |
| P5 PostgreSQL upgrade path | `sqlite_schema_parity=PASS`, `backend_contract_present=true`, `postgres_execution=DATA_GATED`, `caller_compatibility_preserved=true` | `src/quant_engine/data_collection_backend_v1.py`, `src/quant_engine/kis_data_collection_v1.py`, `tests/unit/test_data_collection_store_v1.py`, `tools/generate_postgresql_upgrade_stub_v1.py` | `python -m pytest tests/unit/test_data_collection_store_v1.py -q` |
| P6 Snapshot admin web editor | `settings_sheet_web_editor=true`, `account_snapshot_sheet_web_editor=true`, `contenteditable_grid=true`, `api_save_round_trip=PASS`, `kis_collection_dashboard=true`, `workspace_db_is_single_file=true`, `collection_filter_controls=true`, `collection_dashboard_page=true`, `change_timeline_view=true` | `src/quant_engine/snapshot_admin_server_v1.py`, `src/quant_engine/data_collection_store_v1.py`, `src/quant_engine/snapshot_admin_store_v1.py`, `tools/validate_snapshot_admin_web_v1.py`, `tests/unit/test_snapshot_admin_web_v1.py`, `.gitea/workflows/snapshot_admin.yml` | `python tools/validate_snapshot_admin_web_v1.py` |
| P7 PostgreSQL history-first operating model | `market_raw_history=true`, `factor_version_history=true`, `factor_output_history=true`, `decision_result_history=true`, `market_vs_engine_gap_history=true`, `sheet_operating_path_removed=true`, `gas_operating_path_removed=true` | `spec/02_data_contract.yaml`, `spec/postgresql_history_contract.yaml`, `docs/DAILY_SIGNAL_TRACKING.md`, `docs/POSTGRESQL_HISTORY_FIRST_OPERATING_MODEL.md` | `python tools/validate_postgresql_history_contract_v1.py` |
| Q1 Qualitative sell pipeline | `mock_api_validation=PASS`, `pipeline_contract=PASS`, `workflow_present=true`, `schedule_present=true`, `package_scripts_present=true` | `.gitea/workflows/qualitative_sell_strategy.yml`, `tools/validate_qualitative_sell_strategy_pipeline_v1.py`, `Temp/qualitative_sell_strategy_pipeline_v1.json` | `python tools/validate_qualitative_sell_strategy_pipeline_v1.py` |
| Q2 Gitea secrets contract | `secrets_contract=PASS`, `workflow_secret_mapping=PASS`, `docs_present=true`, `ci_validation_present=true` | `docs/GITEA_SECRETS_SETUP.md`, `tools/validate_gitea_secrets_contract_v1.py`, `Temp/gitea_secrets_contract_v1.json` | `python tools/validate_gitea_secrets_contract_v1.py` |
@@ -1,26 +0,0 @@
# Synology Act Runner Split PR Body
## Title
`chore: split Synology act_runner start and re-registration scripts`
## Body
- Added `tools/re_register_act_runner_synology.sh` for explicit host-mode re-registration.
- Added `tools/start_act_runner_synology.sh` for boot-time daemon start only.
- Kept `tools/setup_act_runner.sh` as the bootstrap path, but made the re-registration flow explicit and repeatable.
- Switched the runner registration labels to `self-hosted:host,snapshot-admin-host:host` so the job runs in host mode instead of Docker job containers and can be targeted by a dedicated deployment label.
- Updated `docs/SYNOLOGY_SNAPSHOT_ADMIN_POC.md` and `docs/ROADMAP_WBS.md` so the operator flow and WBS notes match the new runner split.
- The `snapshot_admin.yml` workflow is split into push smoke validation and manual full validation, which reduces routine CI cost while preserving the full web smoke path on demand.
- The deploy workflow now waits for `127.0.0.1:8787/api/state` readiness before asserting success, so startup latency does not fail the run spuriously.
- The `ci.yml` workflow now keeps `push` traffic on the core gate only, with UI/storage validation retained for non-push events.
## Verification
- `python tools/validate_snapshot_admin_workflow_v1.py`
- `python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('.gitea/workflows/snapshot_admin.yml').read_text(encoding='utf-8'))"`
- `git diff -- .gitea/workflows/snapshot_admin.yml tools/setup_act_runner.sh docs/SYNOLOGY_SNAPSHOT_ADMIN_POC.md docs/ROADMAP_WBS.md`
- Deploy job evidence:
- `healthcheck` retried after startup and passed
- `snapshot-admin-web-v6` returned from the verification step
- `Job succeeded`
-95
View File
@@ -1,95 +0,0 @@
# Synology KIS Data Collection Setup
This note answers how to run:
```powershell
$env:KIS_APP_Key="..."
$env:KIS_APP_Secret="..."
python tools/run_kis_data_collection_v1.py --input-json GatherTradingData.json --sqlite-db src/quant_engine/kis_data_collection.db --output-json Temp/kis_data_collection_v1.json --kis-account real
```
on Synology DSM.
## Rule
Synology is Linux-based, so use `export` or a sourced env file. Do not use Windows `$env:` syntax.
The code reads these exact, case-sensitive names for real accounts:
- `KIS_APP_Key`
- `KIS_APP_Secret`
For mock accounts, the names are:
- `KIS_APP_Key_TEST`
- `KIS_APP_Secret_TEST`
## Recommended DSM Task Scheduler script
Create a `User-defined script` task and run:
```bash
#!/bin/sh
set -eu
ROOT_DIR="/volume1/projects/data_feed"
export KIS_APP_Key="your_real_app_key"
export KIS_APP_Secret="your_real_app_secret"
cd "$ROOT_DIR"
python tools/run_kis_data_collection_v1.py \
--input-json GatherTradingData.json \
--sqlite-db src/quant_engine/kis_data_collection.db \
--output-json Temp/kis_data_collection_v1.json \
--kis-account real
```
## Better practice for secrets
Store secrets in a private env file and source it from the task:
```bash
set -eu
ROOT_DIR="/volume1/projects/data_feed"
SECRETS_FILE="/volume1/projects/data_feed/.secrets/kis_real.env"
. "$SECRETS_FILE"
cd "$ROOT_DIR"
python tools/run_kis_data_collection_v1.py \
--input-json GatherTradingData.json \
--sqlite-db src/quant_engine/kis_data_collection.db \
--output-json Temp/kis_data_collection_v1.json \
--kis-account real
```
Suggested file permissions:
- owner-only read/write
- no shared group access
- no commit to git
## Mock account variant
```bash
export KIS_APP_Key_TEST="your_mock_app_key"
export KIS_APP_Secret_TEST="your_mock_app_secret"
python tools/run_kis_data_collection_v1.py \
--input-json GatherTradingData.json \
--sqlite-db src/quant_engine/kis_data_collection.db \
--output-json Temp/kis_data_collection_v1.json \
--kis-account mock \
--no-live-kis
```
## What the collector writes
- SQLite: `src/quant_engine/kis_data_collection.db`
- JSON summary: `Temp/kis_data_collection_v1.json`
The latest collected summary in this workspace shows:
- `row_count = 25`
- `kis_open_api = 21`
- `gathertradingdata_json = 25`
@@ -1,37 +0,0 @@
# Synology Snapshot Admin Commit Message Template
Use this after a real Synology verification or a final documentation-only update.
## Recommended format
```text
WBS-7.9: Synology snapshot_admin deployment POC and live verification evidence
```
## If the change is documentation-only
```text
WBS-7.9: add Synology deployment checklist, Task Scheduler commands, and evidence template
```
## If the change includes real NAS verification
```text
WBS-7.9: verify Synology snapshot_admin reverse proxy, auth gate, and restart persistence
```
## Commit body template
```text
- Added/updated Synology Task Scheduler launcher script
- Confirmed DSM reverse proxy settings
- Captured curl/browser evidence for local and external access
- Documented completion evidence in WBS-7.9 checklist
```
## Suggested workflow
1. Run the validation commands.
2. Fill `docs/SYNOLOGY_SNAPSHOT_ADMIN_EVIDENCE_TEMPLATE.md`.
3. Commit with one of the messages above.
4. Push only after the evidence file is complete.
@@ -1,153 +0,0 @@
# Synology Snapshot Admin Deployment Checklist
This checklist is the POC-ready version with concrete values.
## 1. Target paths
- Project root: `/volume1/projects/data_feed`
- Launch script: `/volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh`
- Local DB: `/volume1/projects/data_feed/src/quant_engine/snapshot_admin.db`
- Local seed JSON: `/volume1/projects/data_feed/GatherTradingData.json`
- PID file: `/volume1/projects/data_feed/Temp/snapshot_admin.pid`
- Log file: `/volume1/projects/data_feed/Temp/snapshot_admin.log`
See also: [`docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST_FILLED.md`](C:/Temp/data_feed/docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST_FILLED.md)
and [`docs/SYNOLOGY_SNAPSHOT_ADMIN_FIREWALL_PROXY_TABLE.md`](C:/Temp/data_feed/docs/SYNOLOGY_SNAPSHOT_ADMIN_FIREWALL_PROXY_TABLE.md)
## 2. Service account
- Preferred: dedicated DSM local user `snapshot-admin`
- Fallback for first POC: `root`
- Required permission: read/write access to `/volume1/projects/data_feed`
## 3. Environment variables
Set these before the Task Scheduler task runs.
- `SNAPSHOT_ADMIN_AUTH_USER=snapshot-admin`
- `SNAPSHOT_ADMIN_AUTH_PASSWORD=<strong-password>`
- `SNAPSHOT_ADMIN_HOST=127.0.0.1`
- `SNAPSHOT_ADMIN_PORT=8787`
- `SNAPSHOT_ADMIN_ALLOW_REMOTE=0`
- `SNAPSHOT_ADMIN_PID_FILE=/volume1/projects/data_feed/Temp/snapshot_admin.pid`
- `SNAPSHOT_ADMIN_LOG_FILE=/volume1/projects/data_feed/Temp/snapshot_admin.log`
- `SNAPSHOT_ADMIN_STATE_URL=http://127.0.0.1:8787/api/state`
- `SNAPSHOT_ADMIN_PUBLIC_STATE_URL=https://admin.example.com/api/state`
## 4. Task Scheduler tasks
### Boot task
- Name: `snapshot-admin-start`
- Trigger: `Boot-up`
- User: `snapshot-admin` or `root`
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh start
```
### Healthcheck task
- Name: `snapshot-admin-healthcheck`
- Trigger: `Scheduled Task`
- Interval: every 5 minutes
- User: same as boot task
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck
```
### Restart task
- Name: `snapshot-admin-restart`
- Trigger: manual only
- User: same as boot task
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh restart
```
## 4b. Gitea Actions runner label
Use a unique host label so the deployment job is not mixed with generic self-hosted work.
- Runner label: `snapshot-admin-host`
- Registration example:
```bash
REG_TOKEN="<runner-registration-token>" \
GITEA_URL="http://192.168.123.100:8418" \
RUNNER_LABEL="snapshot-admin-host" \
bash tools/re_register_act_runner_synology.sh
```
- Workflow selector:
```yaml
runs-on: [self-hosted, snapshot-admin-host]
```
## 4c. Queue handling
- If the deploy workflow stays queued, it usually means the host runner is busy.
- Check the job currently holding the runner before re-dispatching.
- Do not keep dispatching deploy runs back-to-back. The workflow already uses `concurrency` to cancel in-progress duplicates.
## 5. Reverse proxy
- DSM path: `Control Panel > Login Portal > Advanced > Reverse Proxy`
- Rule name: `snapshot-admin`
- Source:
- Protocol: `HTTPS`
- Hostname: `admin.example.com`
- Port: `443`
- Path: `/`
- Destination:
- Protocol: `HTTP`
- Hostname: `127.0.0.1`
- Port: `8787`
- TLS certificate: certificate matching `admin.example.com`
## 6. Firewall
- Allow inbound `443/TCP`
- Block inbound `8787/TCP` from WAN
- If needed, allowlist office/VPN CIDRs only
## 7. Verification order
1. Start the service.
2. Confirm `bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck` prints `healthcheck ok`.
3. Confirm local `curl -i http://127.0.0.1:8787/api/state`.
- Expect `200 OK`.
- Expect JSON with `version.app = snapshot-admin-web-v7`.
4. Confirm external `curl -i https://admin.example.com/api/state` returns `401`.
- Expect `WWW-Authenticate: Basic`.
5. Confirm authenticated `curl -u 'snapshot-admin:<password>' https://admin.example.com/api/state` returns `200`.
- Expect the same `version.app` value as the local endpoint.
6. Confirm `curl -i https://admin.example.com/tables` after Basic Auth.
- Expect `200 OK` and the Tabler grid page.
7. Open browser `https://admin.example.com/`.
- Expect Basic Auth prompt, then UI render.
8. Open browser `https://admin.example.com/tables`.
- Expect Basic Auth prompt, then grid render.
9. Restart the task or NAS.
10. Repeat steps 2-8 and confirm the response pattern is unchanged.
## 7b. Evidence rule
- Do not mark `WBS-7.9` complete until the external `401`/`200` curl pair, both browser screenshots, and the reverse proxy rule screenshot are archived together.
- Loopback-only smoke tests are useful, but they do not replace the NAS-side live verification.
## 7c. One-page field run sheet
For a compact field execution order, use [`docs/SYNOLOGY_SNAPSHOT_ADMIN_FINAL_EXECUTION_ONE_PAGER.md`](C:/Temp/data_feed/docs/SYNOLOGY_SNAPSHOT_ADMIN_FINAL_EXECUTION_ONE_PAGER.md).
## 8. Completion wording
Use the following text only after evidence is collected:
> WBS-7.9 실배포 검증 완료: Synology NAS에서 `tools/run_snapshot_admin_synology.sh` 기반 서비스가 `127.0.0.1:8787`에 정상 기동되고, DSM Reverse Proxy `HTTPS:443 -> HTTP 127.0.0.1:8787` 경유 외부 접속이 Basic Auth와 함께 `200 OK`로 확인되었으며, 미인증 요청은 `401 Unauthorized`로 차단되었다. `/` 및 `/tables` 렌더링과 재시작 후 지속성도 확인되었고, 증빙은 `docs/SYNOLOGY_SNAPSHOT_ADMIN_EVIDENCE_TEMPLATE.md` 양식으로 보관되었다.
@@ -1,114 +0,0 @@
# Synology Snapshot Admin Deployment Checklist - Filled Example
This is the deployment-ready example for the current repo state.
Replace only the hostname, certificate name, and strong password if your NAS uses different values.
## 1. Target paths
- Project root: `/volume1/projects/data_feed`
- Launch script: `/volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh`
- Local DB: `/volume1/projects/data_feed/src/quant_engine/snapshot_admin.db`
- Local seed JSON: `/volume1/projects/data_feed/GatherTradingData.json`
- PID file: `/volume1/projects/data_feed/Temp/snapshot_admin.pid`
- Log file: `/volume1/projects/data_feed/Temp/snapshot_admin.log`
## 2. Service account
- Preferred DSM user: `snapshot-admin`
- Fallback for first POC: `root`
- Folder access: read/write on `/volume1/projects/data_feed`
## 3. Environment variables
```bash
SNAPSHOT_ADMIN_AUTH_USER=snapshot-admin
SNAPSHOT_ADMIN_AUTH_PASSWORD=<strong-password>
SNAPSHOT_ADMIN_HOST=127.0.0.1
SNAPSHOT_ADMIN_PORT=8787
SNAPSHOT_ADMIN_ALLOW_REMOTE=0
SNAPSHOT_ADMIN_PID_FILE=/volume1/projects/data_feed/Temp/snapshot_admin.pid
SNAPSHOT_ADMIN_LOG_FILE=/volume1/projects/data_feed/Temp/snapshot_admin.log
SNAPSHOT_ADMIN_STATE_URL=http://127.0.0.1:8787/api/state
SNAPSHOT_ADMIN_PUBLIC_STATE_URL=https://admin.example.com/api/state
```
## 4. Task Scheduler
### Boot task
- Name: `snapshot-admin-start`
- User: `snapshot-admin`
- Trigger: `Boot-up`
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh start
```
### Healthcheck task
- Name: `snapshot-admin-healthcheck`
- User: `snapshot-admin`
- Trigger: every 5 minutes
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck
```
### Manual restart task
- Name: `snapshot-admin-restart`
- User: `snapshot-admin`
- Trigger: manual
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh restart
```
## 5. Reverse proxy
- DSM path: `Control Panel > Login Portal > Advanced > Reverse Proxy`
- Rule name: `snapshot-admin`
- Source protocol: `HTTPS`
- Source hostname: `admin.example.com`
- Source port: `443`
- Source path: `/`
- Destination protocol: `HTTP`
- Destination hostname: `127.0.0.1`
- Destination port: `8787`
- TLS certificate: `admin.example.com` certificate
## 6. Firewall
- Allow inbound `443/TCP`
- Block inbound `8787/TCP` from WAN
- Allowlist only trusted office/VPN ranges if needed
## 7. Verification commands
```bash
curl -i http://127.0.0.1:8787/api/state
curl -i https://admin.example.com/api/state
curl -u 'snapshot-admin:<strong-password>' https://admin.example.com/api/state
curl -I https://admin.example.com/
curl -I https://admin.example.com/tables
```
## 7b. Final preflight
Use [`docs/SYNOLOGY_SNAPSHOT_ADMIN_FINAL_PREFLIGHT_10.md`](C:/Temp/data_feed/docs/SYNOLOGY_SNAPSHOT_ADMIN_FINAL_PREFLIGHT_10.md)
immediately before you mark the deployment complete.
## 8. Completion wording
Use this exact wording when evidence is complete:
> WBS-7.9 실배포 검증 완료: Synology NAS에서 `tools/run_snapshot_admin_synology.sh` 기반 서비스가 `127.0.0.1:8787`에 정상 기동되고, DSM Reverse Proxy `HTTPS:443 -> HTTP 127.0.0.1:8787` 경유 외부 접속이 Basic Auth와 함께 `200 OK`로 확인되었으며, 미인증 요청은 `401 Unauthorized`로 차단되었다. `/` 및 `/tables` 렌더링과 재시작 후 지속성도 확인되었고, 증빙은 `docs/SYNOLOGY_SNAPSHOT_ADMIN_EVIDENCE_TEMPLATE.md` 양식으로 보관되었다.
## 9. What to replace
- `admin.example.com` if your public hostname differs
- `<strong-password>` with your generated password
- TLS certificate name if the DSM certificate uses another label
@@ -1,62 +0,0 @@
# Synology Snapshot Admin Evidence Template
Use this template to close `WBS-7.9` after a real Synology deployment test.
## Deployment metadata
- NAS model:
- DSM version:
- Public hostname:
- Reverse proxy rule name:
- TLS certificate name:
- Service launcher: `tools/run_snapshot_admin_synology.sh`
- Python service bind mode:
- Auth mode: `Basic Auth`
## Local checks
- `curl -i http://127.0.0.1:8787/api/state`
- Result:
- `curl -i http://127.0.0.1:8787/tables`
- Result:
## External checks
- `curl -i https://<public-host>/api/state`
- Result:
- `curl -u '<user>:<password>' https://<public-host>/api/state`
- Result:
- `curl -i https://<public-host>/tables`
- Result:
## Browser checks
- `https://<public-host>/`
- Result:
- `https://<public-host>/tables`
- Result:
## Restart persistence
- Restart method used:
- Restart time:
- `healthcheck` result after restart:
- Time elapsed after restart:
## Evidence attachments
- Screenshot: DSM reverse proxy rule
- Screenshot: browser `/`
- Screenshot: browser `/tables`
- Log snippet: `Temp/snapshot_admin.log`
- `curl` output archive:
## Completion statement
- `WBS-7.9` completion condition met:
- local endpoint `200`
- external unauthenticated `401`
- external authenticated `200`
- browser render verified
- restart persistence verified
- evidence archived
@@ -1,89 +0,0 @@
# Synology Snapshot Admin Final Execution One-Pager
Use this sheet on the NAS during the live verification run.
## Goal
Confirm that `snapshot_admin_server_v1.py` runs on Synology with loopback binding, DSM reverse proxy exposure, and Basic Auth protection.
## Required values
- Project root: `/volume1/projects/data_feed`
- Launcher: `/volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh`
- Local URL: `http://127.0.0.1:8787/api/state`
- Public URL: `https://admin.example.com/api/state`
- Public UI URL: `https://admin.example.com/`
- Public tables URL: `https://admin.example.com/tables`
## Execution order
1. Start the service.
- `bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh start`
2. Confirm the healthcheck.
- `bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck`
- Expected: `healthcheck ok`
3. Confirm local loopback.
- `curl -i http://127.0.0.1:8787/api/state`
- Expected: `200 OK`
- Expected JSON field: `version.app = snapshot-admin-web-v7`
4. Confirm unauthenticated external access.
- `curl -i https://admin.example.com/api/state`
- Expected: `401 Unauthorized`
- Expected header: `WWW-Authenticate: Basic`
5. Confirm authenticated external access.
- `curl -u 'snapshot-admin:<password>' https://admin.example.com/api/state`
- Expected: `200 OK`
- Expected same `version.app` as local loopback
6. Confirm tables page.
- `curl -i https://admin.example.com/tables`
- Expected: `200 OK`
- Expected: Tabler grid HTML
7. Confirm browser render.
- Open `https://admin.example.com/`
- Open `https://admin.example.com/tables`
- Expected: Basic Auth prompt, then render
8. Confirm persistence.
- Restart the task or NAS
- Re-run steps 2-7
- Expected: identical response pattern after restart
## Queue check
If the deployment workflow stays queued for more than a few minutes:
1. Confirm the runner is registered with the host label.
- `RUNNER_LABEL=snapshot-admin-host`
- Re-register with `bash tools/re_register_act_runner_synology.sh` after setting the registration token.
2. Confirm the runner daemon is running.
- `bash tools/start_act_runner_synology.sh`
3. Confirm the queue target is the host runner label.
- Deploy workflow uses `runs-on: [self-hosted, snapshot-admin-host]`
4. If another job is occupying the runner, wait for it to finish or cancel the stale workflow from Gitea.
5. Re-dispatch `snapshot_admin_deploy.yml` after the runner is idle.
## Pass criteria
- Loopback `200` confirmed.
- External unauthenticated `401` confirmed.
- External authenticated `200` confirmed.
- `/` and `/tables` browser render confirmed.
- Restart persistence confirmed.
- DSM reverse proxy and firewall screenshots archived.
## Workflow success evidence
If you need the deploy-job proof from the NAS runner before the full external closeout:
- `healthcheck` retried after startup and passed on the NAS runner.
- `snapshot-admin-web-v6` was returned by the deploy verification step.
- The workflow finished with `Job succeeded`.
This proves the deploy job can launch, wait for readiness, and validate locally on Synology.
It does not replace the external reverse-proxy/browser closeout evidence above.
## Do not close WBS-7.9 unless
- The `401`/`200` curl pair is saved.
- Both browser screenshots are saved.
- The DSM reverse proxy rule screenshot is saved.
- The completion wording in `docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST.md` is used only after evidence is archived.
@@ -1,29 +0,0 @@
# Synology Snapshot Admin Final Preflight 10
Use this immediately before declaring `WBS-7.9` complete.
1. Confirm the Python service is running on `127.0.0.1:8787`.
2. Confirm `bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck` returns `healthcheck ok`.
3. Confirm `curl -i http://127.0.0.1:8787/api/state` returns `200 OK`.
4. Confirm `curl -i https://admin.example.com/api/state` returns `401 Unauthorized` without credentials.
5. Confirm `curl -u 'snapshot-admin:<strong-password>' https://admin.example.com/api/state` returns `200 OK`.
6. Confirm `https://admin.example.com/` renders in a browser after Basic Auth.
7. Confirm `https://admin.example.com/tables` renders in a browser after Basic Auth.
8. Confirm the DSM reverse proxy rule still maps `HTTPS:443 -> HTTP 127.0.0.1:8787`.
9. Confirm the firewall still blocks `8787/TCP` from WAN.
10. Restart the service or NAS and repeat steps 2 through 7.
## Evidence to archive
- `curl` output for steps 3 through 5
- Browser screenshots for steps 6 and 7
- DSM reverse proxy screenshot for step 8
- Firewall screenshot for step 9
- Restart proof for step 10
## Pass condition
Declare `WBS-7.9` complete only when all 10 steps pass and the evidence files are saved using:
- [`docs/SYNOLOGY_SNAPSHOT_ADMIN_EVIDENCE_TEMPLATE.md`](C:/Temp/data_feed/docs/SYNOLOGY_SNAPSHOT_ADMIN_EVIDENCE_TEMPLATE.md)
- [`docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST_FILLED.md`](C:/Temp/data_feed/docs/SYNOLOGY_SNAPSHOT_ADMIN_DEPLOYMENT_CHECKLIST_FILLED.md)
@@ -1,31 +0,0 @@
# Synology Snapshot Admin Firewall and Reverse Proxy Copy-Paste
Use these values verbatim in DSM.
## Reverse proxy
- Rule name: `snapshot-admin`
- Source protocol: `HTTPS`
- Source hostname: `admin.example.com`
- Source port: `443`
- Source path: `/`
- Destination protocol: `HTTP`
- Destination hostname: `127.0.0.1`
- Destination port: `8787`
## Firewall
- Allow: `443/TCP` from WAN or trusted CIDR
- Deny: `8787/TCP` from WAN
- Optional allow: `443/TCP` from office/VPN CIDR only
## Certificate binding
- Hostname: `admin.example.com`
- Bind to: reverse proxy rule `snapshot-admin`
## Notes
- Do not expose `8787/TCP` directly.
- Keep Basic Auth enabled in the Python service.
- Use `127.0.0.1` for the destination host unless direct-bind testing is intentional.
@@ -1,38 +0,0 @@
# Synology Snapshot Admin Firewall and Reverse Proxy Table
Use these values for the first POC.
## Reverse proxy rule
| Field | Value |
|---|---|
| Rule name | `snapshot-admin` |
| Source protocol | `HTTPS` |
| Source hostname | `admin.example.com` |
| Source port | `443` |
| Source path | `/` |
| Destination protocol | `HTTP` |
| Destination hostname | `127.0.0.1` |
| Destination port | `8787` |
## Firewall rules
| Rule | Action | Source | Destination | Port |
|---|---|---|---|---|
| Reverse proxy public entry | Allow | WAN or trusted public CIDR | NAS | `443/TCP` |
| Raw service port | Deny | WAN | NAS | `8787/TCP` |
| Optional office/VPN allowlist | Allow | Office/VPN CIDR only | NAS | `443/TCP` |
## Certificate
| Field | Value |
|---|---|
| Type | TLS certificate |
| Hostname | `admin.example.com` |
| Binding | Reverse proxy rule `snapshot-admin` |
## Notes
- Keep `8787/TCP` private.
- Keep Basic Auth enabled in the Python service.
- Use `127.0.0.1` for the backend destination unless you are explicitly testing direct bind mode.
-257
View File
@@ -1,257 +0,0 @@
# Synology Snapshot Admin POC
This guide enables external access to the Python snapshot admin service on Synology without exposing the raw service port to the internet.
## Recommended topology
1. Keep the Python service bound to loopback only:
```bash
python tools/run_snapshot_admin_server_v1.py \
--host 127.0.0.1 \
--port 8787 \
--db src/quant_engine/snapshot_admin.db \
--seed GatherTradingData.json
```
2. Put Synology DSM reverse proxy in front of it:
- Source: `https://<public-host>:443`
- Destination: `http://127.0.0.1:8787`
- Keep the service port closed from direct WAN access.
3. Add browser authentication with the built-in Basic Auth gate:
- Set `SNAPSHOT_ADMIN_AUTH_USER`
- Set `SNAPSHOT_ADMIN_AUTH_PASSWORD`
- Or pass `--auth-user` and `--auth-password` on the wrapper command
4. Verify from the NAS:
```bash
curl -i http://127.0.0.1:8787/api/state
curl -u "$SNAPSHOT_ADMIN_AUTH_USER:$SNAPSHOT_ADMIN_AUTH_PASSWORD" http://127.0.0.1:8787/api/state
```
5. Verify from outside the NAS:
- Open `https://<public-host>/`
- The browser should prompt for Basic Auth
- `https://<public-host>/tables` should render after login
## DSM Checklist
Use these exact values for the first POC.
1. **DSM app path**
- `Control Panel`
- `Login Portal`
- `Advanced`
- `Reverse Proxy`
2. **Create reverse proxy rule**
- Description: `snapshot-admin`
- Source protocol: `HTTPS`
- Source hostname: your public DNS name, for example `admin.example.com`
- Source port: `443`
- Source path: `/`
- Destination protocol: `HTTP`
- Destination hostname: `127.0.0.1`
- Destination port: `8787`
3. **Certificate**
- Attach a valid TLS certificate for the public hostname
- Prefer a Synology-managed or imported certificate that matches `admin.example.com`
4. **Firewall**
- Allow inbound `443/TCP` only for the reverse proxy endpoint
- Do not expose `8787/TCP` on WAN
- If the NAS must be reachable only from a VPN or office IP range, allowlist those ranges and block the rest
5. **Service start policy**
- Start the Python service on boot or via DSM Task Scheduler
- Keep it bound to `127.0.0.1` unless you intentionally use direct bind mode
- If you use direct bind mode, keep `--allow-remote` and Basic Auth enabled together
- For Gitea Actions runner verification, register `act_runner` with a dedicated host label (`self-hosted:host,snapshot-admin-host:host`) if you want to avoid Docker job containers and the `Cleaning up container` log line
- Preferred launcher script: `tools/run_snapshot_admin_synology.sh`
- Gitea CI deploy path: trigger `.gitea/workflows/snapshot_admin_deploy.yml` `workflow_dispatch` and let the host runner call the launcher script
- Runner bootstrap: `tools/re_register_act_runner_synology.sh`
- Runner daemon start: `tools/start_act_runner_synology.sh`
6. **Runner re-registration**
- Use this when you want to switch an existing runner from Docker mode to host mode:
```bash
cd /volume1/projects/data_feed
REG_TOKEN="<runner-registration-token>" \
GITEA_URL="http://192.168.123.100:8418" \
bash tools/re_register_act_runner_synology.sh
```
- Expected effect:
- removes the existing `.runner` registration file
- registers `self-hosted:host,snapshot-admin-host:host`
- writes an updated `config.yaml`
- If the old runner remains listed in Gitea, remove it from the repository runner page and re-run the command above
7. **Runner start**
- After re-registration, start the daemon:
```bash
bash tools/start_act_runner_synology.sh
```
- Expected effect:
- launches `act_runner daemon` using the existing config
- records `runner.pid` and `runner.log` under the runner directory
## DSM Task Scheduler
Create two scheduled tasks in `Control Panel > Task Scheduler`.
1. **Boot task**
- Task name: `snapshot-admin-start`
- User: `root` or a dedicated service account with access to the project folder
- Event: `Boot-up`
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh start
```
2. **Healthcheck task**
- Task name: `snapshot-admin-healthcheck`
- User: same as boot task
- Event: `Scheduled Task`
- Repeat: every 5 minutes
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck
```
3. **Manual restart task**
- Task name: `snapshot-admin-restart`
- User: same as boot task
- Event: `Scheduled Task`
- Repeat: manual only, or keep disabled until needed
- Command:
```bash
bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh restart
```
## Direct bind mode
Direct binding to `0.0.0.0` is allowed only when both auth values are configured:
```bash
python tools/run_snapshot_admin_server_v1.py \
--host 0.0.0.0 \
--port 8787 \
--allow-remote \
--auth-user "$SNAPSHOT_ADMIN_AUTH_USER" \
--auth-password "$SNAPSHOT_ADMIN_AUTH_PASSWORD"
```
Use this only if you have a separate firewall or VPN rule in place. The default POC path is still loopback + reverse proxy.
## Validation
Run the unit/web checks before and after deployment:
```bash
python -m pytest tests/unit/test_snapshot_admin_web_v1.py -q
python tools/validate_snapshot_admin_web_v1.py
```
The auth gate is part of the service now, so public exposure without credentials is rejected by the server itself.
## Curl checklist
Use this as the POC run sheet.
1. Local service check:
```bash
curl -i http://127.0.0.1:8787/api/state
```
Expected:
- `200 OK`
- JSON payload contains `version.app`
2. Reverse proxy auth challenge:
```bash
curl -i https://<public-host>/api/state
```
Expected:
- `401 Unauthorized`
- `WWW-Authenticate: Basic`
3. Reverse proxy authenticated access:
```bash
curl -u '<user>:<password>' https://<public-host>/api/state
```
Expected:
- `200 OK`
- JSON payload contains the same `version.app`
4. UI rendering:
```bash
curl -I https://<public-host>/
curl -I https://<public-host>/tables
```
Expected:
- `200 OK` after auth
- HTML response, not a redirect to the raw port
5. Restart persistence:
```bash
bash tools/run_snapshot_admin_synology.sh restart
bash tools/run_snapshot_admin_synology.sh healthcheck
```
Expected:
- `healthcheck ok`
- The proxy URL continues to answer after the service restarts
## Live verification
Use this sequence on the actual Synology box after the reverse proxy rule is in place:
1. Start the service and confirm the local health endpoint:
```bash
curl -i http://127.0.0.1:8787/api/state
```
2. Confirm the auth gate:
```bash
curl -i https://<public-host>/api/state
```
Expected result:
- `401 Unauthorized` when no credentials are provided
- `200 OK` when valid Basic Auth credentials are supplied
3. Confirm the browser surface:
- Open `https://<public-host>/`
- Sign in with the Basic Auth credentials
- Open `https://<public-host>/tables`
- Confirm rows render from the three SQLite sources
4. Confirm the deployment survives a process restart:
- Restart the Python service or the task that launches it
- Re-run `curl -i http://127.0.0.1:8787/api/state`
- Re-open the browser URL and confirm login still works
5. Archive evidence:
- Save the `curl` outputs
- Save a screenshot of `/` and `/tables`
- Record the DSM reverse proxy rule values and certificate name
View File
+2 -2
View File
@@ -13,7 +13,7 @@
"ops:sell-eval": "python tools/evaluate_qualitative_sell_strategy_accuracy_v1.py --sqlite-db outputs/qualitative_sell_strategy/qualitative_sell_strategy.db",
"ops:sell-validate": "python tools/validate_qualitative_sell_strategy_pipeline_v1.py",
"ops:postgres-stub": "python tools/generate_postgresql_upgrade_stub_v1.py",
"ops:render": "python tools/render_operational_report.py --json GatherTradingData.json --output Temp/operational_report.md --report-json-output Temp/operational_report.json",
"ops:render": "dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json",
"ops:snapshot-web": "python tools/run_snapshot_admin_server_v1.py --reload --db src/quant_engine/snapshot_admin.db --seed GatherTradingData.json",
"ops:snapshot-web-watch": "python tools/run_snapshot_admin_server_v1.py --reload --db src/quant_engine/snapshot_admin.db --seed GatherTradingData.json",
"ops:snapshot-validate": "python tools/validate_snapshot_admin_workflow_v1.py",
@@ -52,7 +52,7 @@
"validate-engine-strict": "python tools/run_release_dag_v3.py --mode release --strict",
"validate-behavioral-coverage": "python tools/validate_behavioral_coverage_v1.py --strict",
"validate-engine-integrity": "python tools/run_release_dag_v3.py --mode release --strict",
"render-report-json": "python tools/render_operational_report.py --json GatherTradingData.json --output Temp/operational_report.md --report-json-output Temp/operational_report.json"
"render-report-json": "dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json"
},
"dependencies": {
"cheerio": "1.2.0",
+20
View File
@@ -172,6 +172,26 @@ quant_feed_contract:
normalization: "숫자로 읽힌 91160.0, 5930.0 등은 문자열화 후 6자리 zero-pad 적용."
validation_commands: ["npm run validate-data-sample", "npm run validate-specs"]
xlsx_refresh_rule: "xlsx 원본을 갱신했으면 먼저 DB에 반영한 뒤, 엔진이 DB를 읽어 JSON 파생 보고서를 재생성하고 다시 검증한다."
database_first_operating_model:
purpose: "운영 이력, 원천 팩터, 파생 최종 팩터, 시장-결과 괴리를 PostgreSQL에 누적해 엔진을 고도화한다."
canonical_store:
primary: "PostgreSQL"
secondary: "SQLite transient cache only"
prohibited_operating_path:
- "Excel workbook as operational source"
- "Google Apps Script as operational source"
history_domains:
- "market_raw_history"
- "factor_version_history"
- "factor_output_history"
- "decision_result_history"
- "market_vs_engine_gap_history"
policy:
- "최종 팩터와 최종 판단은 DB 이력 테이블에 버전과 시각을 함께 남긴다."
- "시장 raw와 엔진 결과의 괴리는 별도 gap history로 적재한다."
- "엑셀/시트/Apps Script는 더 이상 운영 경로가 아니라, 역사적 import/export 또는 폐기 대상만 허용한다."
- "새 분석·리포트는 PostgreSQL snapshot을 1차 진실원천으로 사용한다."
xlsx_analysis_protocol:
purpose: "xlsx는 HTS 잔고·거래내역 판독 또는 DB 반영 이전의 보조 감사 소스다. 시장 raw 일반 분석과 최종 보고서 생성은 DB 추적 후의 파생 JSON을 우선한다."
python_parsing_baseline:
+74
View File
@@ -0,0 +1,74 @@
schema_version: "postgresql_history_contract_v1"
title: "PostgreSQL History-First Operating Contract"
purpose: "시장 원천, 팩터 버전, 최종 팩터 출력, 엔진 의사결정, 시장-엔진 괴리를 PostgreSQL에 누적한다."
canonical_principles:
- "PostgreSQL is the canonical operating history store."
- "Excel workbooks and Google Apps Script are not operational sources of truth."
- "All derived analysis must be traceable to a versioned DB snapshot."
- "Factor outputs and decision outputs must carry provenance and source_version."
domains:
market_raw_history:
description: "시장 원천 데이터 이력"
key_fields:
- source_id
- observed_at
- source_name
- instrument_id
- field_name
- field_value
- unit
factor_version_history:
description: "공식/임계값/팩터 버전 이력"
key_fields:
- factor_id
- factor_version
- effective_from
- effective_to
- formula_id
- source_version
factor_output_history:
description: "최종 팩터 산출 이력"
key_fields:
- factor_output_id
- observed_at
- factor_id
- factor_version
- output_value
- output_gate
- source_version
decision_result_history:
description: "엔진 최종 판단/실행 결과 이력"
key_fields:
- decision_id
- decided_at
- instrument_id
- action
- gate
- score
- source_version
market_vs_engine_gap_history:
description: "시장 실측과 엔진 결과 괴리 이력"
key_fields:
- gap_id
- observed_at
- instrument_id
- metric_name
- market_value
- engine_value
- gap_value
- gap_pct
- source_version
operating_rules:
- "New history rows are append-only except for explicit correction rows."
- "Correction rows must reference corrected_row_id and correction_reason."
- "Factor recomputation must preserve previous outputs in history."
- "No report should read directly from Excel/GAS when PostgreSQL snapshot is available."
implementation_targets:
- "src/quant_engine/postgresql_history_store_v1.py"
- "tools/build_postgresql_history_snapshot_v1.py"
- "tools/validate_postgresql_history_contract_v1.py"
- "docs/POSTGRESQL_HISTORY_FIRST_OPERATING_MODEL.md"
+17 -9
View File
@@ -12,6 +12,12 @@ purpose: |
UNVALIDATED → PROVISIONAL → CALIBRATED 상태 전환
honest_proof_score: 56.57 → 95.0 달성
implementation_note: |
live_outcome_ledger.gs는 Google Sheets 원장 적재/갱신용 GAS thin adapter다.
운영 리포트와 검증용 JSON 산출물은 Python 하네스가 Temp/ 경로에 생성한다.
GAS는 JSON 리포트를 직접 출력하지 않는다.
이후 운영 표준은 PostgreSQL history store이며, 시트/GAS는 운영 경로에서 제외한다.
current_state:
honest_proof_score: 56.57
target_score: 95.0
@@ -132,7 +138,8 @@ honest_proof_improvement_path:
# ─────────────────────────────────────────────────────────────────────────────
tracking_system:
spreadsheet: "live_outcome_ledger (GAS 연동 스프레드시트)"
datastore: "PostgreSQL history store"
deprecated_surface: "live_outcome_ledger (GAS 연동 스프레드시트)"
daily_tasks:
- "신규 신호 entry 작성 (시작할 때)"
@@ -151,11 +158,12 @@ tracking_system:
# ─────────────────────────────────────────────────────────────────────────────
checklist:
- [ ] "live_outcome_ledger 스프레드시트 생성 (GAS 연동)"
- [ ] "신호 기록 템플릿 작성"
- [ ] "T+20 가격 수집 자동화 (GAS)"
- [ ] "daily commit: 신호 추가 시마다"
- [ ] "30개 신호 누적 (약 6주)"
- [ ] "win_rate >= 60% 달성"
- [ ] "CALIBRATED 전환"
- [ ] "honest_proof_score 95 달성"
- "[ ] live_outcome_ledger 스프레드시트 생성 (GAS 연동)"
- "[ ] 신호 기록 템플릿 작성"
- "[ ] T+20 가격 수집 자동화 (GAS)"
- "[ ] Temp/operational_t20_outcome_ledger_v1.json 생성 체인 유지 (Python)"
- "[ ] daily commit: 신호 추가 시마다"
- "[ ] 30개 신호 누적 (약 6주)"
- "[ ] win_rate >= 60% 달성"
- "[ ] CALIBRATED 전환"
- "[ ] honest_proof_score 95 달성"
@@ -1,6 +0,0 @@
namespace QuantEngine.Application;
public class Class1
{
}
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using QuantEngine.Core.Interfaces;
using QuantEngine.Core.Models;
namespace QuantEngine.Application.Services
{
public class ApprovalService
{
private readonly IWorkspaceRepository _repository;
public ApprovalService(IWorkspaceRepository repository)
{
_repository = repository;
}
public Task<IEnumerable<WorkspaceApproval>> GetApprovalsAsync() => _repository.GetApprovalsAsync();
public Task<WorkspaceApproval?> GetApprovalAsync(string domain, string targetRef) => _repository.GetApprovalAsync(domain, targetRef);
public Task<bool> UpsertApprovalAsync(WorkspaceApproval approval) => _repository.UpsertApprovalAsync(approval);
public Task<IEnumerable<WorkspaceLock>> GetLocksAsync() => _repository.GetLocksAsync();
public Task<WorkspaceLock?> GetLockAsync(string domain, string targetRef) => _repository.GetLockAsync(domain, targetRef);
public Task<bool> AcquireLockAsync(WorkspaceLock @lock) => _repository.AcquireLockAsync(@lock);
public Task<bool> ReleaseLockAsync(string domain, string targetRef) => _repository.ReleaseLockAsync(domain, targetRef);
}
}
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using QuantEngine.Core.Interfaces;
using QuantEngine.Core.Models;
namespace QuantEngine.Application.Services
{
public class CollectionService
{
private readonly IPostgresqlHistoryStore _historyStore;
public CollectionService(IPostgresqlHistoryStore historyStore)
{
_historyStore = historyStore;
}
public Task<int> AppendRunAsync(CollectionRun run)
=> _historyStore.AppendAsync("collection_run_history", new Dictionary<string, object?>
{
["run_id"] = run.RunId,
["collector_name"] = run.CollectorName,
["started_at"] = run.StartedAt,
["finished_at"] = run.FinishedAt,
["status"] = run.Status,
["input_source"] = run.InputSource,
["output_json_path"] = run.OutputJsonPath,
["output_db_path"] = run.OutputDbPath,
["notes"] = run.Notes,
["created_at"] = run.CreatedAt
});
public Task<int> AppendSnapshotAsync(CollectionSnapshot snapshot)
=> _historyStore.AppendAsync("collection_snapshot_history", new Dictionary<string, object?>
{
["run_id"] = snapshot.RunId,
["dataset_name"] = snapshot.DatasetName,
["ticker"] = snapshot.Ticker,
["name"] = snapshot.Name,
["sector"] = snapshot.Sector,
["as_of_date"] = snapshot.AsOfDate,
["source_priority"] = snapshot.SourcePriority,
["source_status"] = snapshot.SourceStatus,
["payload_json"] = snapshot.PayloadJson,
["provenance_json"] = snapshot.ProvenanceJson,
["created_at"] = snapshot.CreatedAt
});
public Task<int> AppendSourceErrorAsync(CollectionSourceError error)
=> _historyStore.AppendAsync("collection_source_error_history", new Dictionary<string, object?>
{
["run_id"] = error.RunId,
["ticker"] = error.Ticker,
["source_name"] = error.SourceName,
["error_kind"] = error.ErrorKind,
["error_message"] = error.ErrorMessage,
["payload_json"] = error.PayloadJson,
["created_at"] = error.CreatedAt
});
}
}
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using QuantEngine.Core.Domain;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Application.Services
{
public class FormulaService
{
private readonly IPostgresqlHistoryStore _historyStore;
public FormulaService(IPostgresqlHistoryStore historyStore)
{
_historyStore = historyStore;
}
public TimingDecisionResult ComputeTimingDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeTimingDecision(ctx);
public SellDecisionResult ComputeSellDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeSellDecision(ctx);
public FinalDecisionResult ComputeFinalDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeFinalDecision(ctx);
public CashShortfallResult ComputeCashShortfallHarness(
Dictionary<string, object> asResult,
double totalAsset,
Dictionary<string, object> cashFloorInfo,
double mrsScore)
=> FormulaEngine.ComputeCashShortfallHarness(asResult, totalAsset, cashFloorInfo, mrsScore);
public CashRecoveryPlanResult ComputeCashRecoveryOptimizer(
List<Dictionary<string, object>> sellCandidates,
double cashShortfallMinKrw)
=> FormulaEngine.ComputeCashRecoveryOptimizer(sellCandidates, cashShortfallMinKrw);
public Task<int> AppendFormulaRunAsync(string formulaName, Dictionary<string, object?> payload)
=> _historyStore.AppendAsync($"formula_{formulaName}_history", payload);
}
}
@@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using QuantEngine.Core.Domain;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Application.Services
{
public class HistoryIngestionService
{
private readonly IPostgresqlHistoryStore _store;
public HistoryIngestionService(IPostgresqlHistoryStore store)
{
_store = store;
}
public Task<int> AppendDecisionAsync(IDictionary<string, object?> payload)
=> _store.AppendAsync("decision_result_history", payload);
public Task<int> AppendFactorOutputAsync(IDictionary<string, object?> payload)
=> _store.AppendAsync("factor_output_history", payload);
public Task<int> AppendMarketRawAsync(IDictionary<string, object?> payload)
=> _store.AppendAsync("market_raw_history", payload);
public Task<int> AppendGapAsync(IDictionary<string, object?> payload)
=> _store.AppendAsync("market_vs_engine_gap_history", payload);
public Task<int> AppendDecisionAsync(
FinalDecisionResult decision,
SellDecisionResult? sellDecision = null,
TimingDecisionResult? timingDecision = null,
string? instrumentId = null,
string? sourceVersion = null,
string? gate = null)
{
var payload = new Dictionary<string, object?>
{
["decision_id"] = Guid.NewGuid().ToString("N"),
["decided_at"] = DateTimeOffset.UtcNow,
["instrument_id"] = instrumentId ?? string.Empty,
["action"] = decision.FinalAction,
["gate"] = gate ?? (string.IsNullOrWhiteSpace(sellDecision?.Validation) ? "PASS" : sellDecision.Validation),
["score"] = decision.PriorityScore,
["source_version"] = sourceVersion ?? decision.DecisionSource,
["provenance"] = new Dictionary<string, object?>
{
["final_action"] = decision.FinalAction,
["action_priority"] = decision.ActionPriority,
["priority_score"] = decision.PriorityScore,
["decision_source"] = decision.DecisionSource,
["sell_action"] = sellDecision?.Action,
["sell_validation"] = sellDecision?.Validation,
["timing_action"] = timingDecision?.Action,
["timing_reason"] = timingDecision?.Reason
}
};
return _store.AppendAsync("decision_result_history", payload);
}
public Task<int> AppendFactorOutputAsync(
string factorId,
string factorVersion,
double outputValue,
string outputGate,
string? sourceVersion = null,
DateTimeOffset? observedAt = null)
{
var payload = new Dictionary<string, object?>
{
["factor_output_id"] = Guid.NewGuid().ToString("N"),
["observed_at"] = observedAt ?? DateTimeOffset.UtcNow,
["factor_id"] = factorId,
["factor_version"] = factorVersion,
["output_value"] = outputValue,
["output_gate"] = outputGate,
["source_version"] = sourceVersion ?? factorVersion,
["provenance"] = new Dictionary<string, object?>
{
["factor_id"] = factorId,
["factor_version"] = factorVersion,
["output_value"] = outputValue,
["output_gate"] = outputGate,
["source_version"] = sourceVersion ?? factorVersion
}
};
return _store.AppendAsync("factor_output_history", payload);
}
}
}
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Application.Services
{
public class PostgresqlHistorySnapshotReader : IPostgresqlHistorySnapshotReader
{
private readonly IPostgresqlHistoryStore _store;
public PostgresqlHistorySnapshotReader(IPostgresqlHistoryStore store)
{
_store = store;
}
public Task<IReadOnlyList<IDictionary<string, object?>>> ReadAsync(string domain, int limit = 500)
=> _store.SnapshotAsync(domain, limit);
}
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using QuantEngine.Core.Interfaces;
using QuantEngine.Core.Models;
namespace QuantEngine.Application.Services
{
public class WorkspaceService
{
private readonly IWorkspaceRepository _repository;
private readonly IPostgresqlHistoryStore _historyStore;
public WorkspaceService(IWorkspaceRepository repository, IPostgresqlHistoryStore historyStore)
{
_repository = repository;
_historyStore = historyStore;
}
public Task<IEnumerable<Setting>> GetSettingsAsync() => _repository.GetSettingsAsync();
public Task<Setting?> GetSettingByKeyAsync(string key) => _repository.GetSettingByKeyAsync(key);
public Task<bool> UpsertSettingAsync(Setting setting) => _repository.UpsertSettingAsync(setting);
public Task<bool> DeleteSettingAsync(string key) => _repository.DeleteSettingAsync(key);
public Task<IEnumerable<AccountSnapshot>> GetAccountSnapshotsAsync() => _repository.GetAccountSnapshotsAsync();
public Task<bool> InsertAccountSnapshotsAsync(IEnumerable<AccountSnapshot> snapshots) => _repository.InsertAccountSnapshotsAsync(snapshots);
public Task<bool> ClearAccountSnapshotsAsync() => _repository.ClearAccountSnapshotsAsync();
public Task<int> AppendHistoryAsync(string domain, IDictionary<string, object?> payload) => _historyStore.AppendAsync(domain, payload);
public Task<IReadOnlyList<IDictionary<string, object?>>> ReadHistorySnapshotAsync(string domain, int limit = 500) => _historyStore.SnapshotAsync(domain, limit);
}
}
@@ -0,0 +1,159 @@
using QuantEngine.Application.Services;
using QuantEngine.Core.Interfaces;
using QuantEngine.Core.Models;
namespace QuantEngine.Core.Tests;
public class ApplicationServiceTests
{
[Fact]
public async Task WorkspaceService_ForwardsSettingAndHistoryOperations()
{
var repo = new FakeWorkspaceRepository();
var history = new FakeHistoryStore();
var service = new WorkspaceService(repo, history);
var setting = new Setting { Ordinal = 1, Key = "risk_mode", ValueJson = "\"RISK_ON\"" };
Assert.True(await service.UpsertSettingAsync(setting));
Assert.Equal(setting, repo.LastSetting);
var payload = new Dictionary<string, object?> { ["foo"] = "bar" };
Assert.Equal(1, await service.AppendHistoryAsync("decision_result_history", payload));
Assert.Equal("decision_result_history", history.LastDomain);
Assert.Equal("bar", history.LastPayload?["foo"]);
}
[Fact]
public async Task ApprovalService_ForwardsApprovalAndLockOperations()
{
var repo = new FakeWorkspaceRepository();
var service = new ApprovalService(repo);
var approval = new WorkspaceApproval { Domain = "settings", TargetRef = "portfolio", Status = "APPROVED" };
Assert.True(await service.UpsertApprovalAsync(approval));
Assert.Equal(approval, repo.LastApproval);
var lockRow = new WorkspaceLock { Domain = "settings", TargetRef = "portfolio", LockedBy = "qa", Reason = "review" };
Assert.True(await service.AcquireLockAsync(lockRow));
Assert.Equal(lockRow, repo.LastLock);
Assert.True(await service.ReleaseLockAsync("settings", "portfolio"));
Assert.Equal(("settings", "portfolio"), repo.LastReleasedLock);
}
[Fact]
public async Task CollectionService_AppendsRunSnapshotAndErrorRecords()
{
var history = new FakeHistoryStore();
var service = new CollectionService(history);
await service.AppendRunAsync(new CollectionRun
{
RunId = "run-1",
CollectorName = "kis",
StartedAt = "2026-06-26T09:00:00+09:00",
Status = "PASS"
});
Assert.Equal("collection_run_history", history.LastDomain);
Assert.Equal("run-1", history.LastPayload?["run_id"]);
await service.AppendSnapshotAsync(new CollectionSnapshot
{
RunId = "run-1",
DatasetName = "decision_result_history",
Ticker = "005930",
SourcePriority = "KIS",
SourceStatus = "PASS",
PayloadJson = "{}",
ProvenanceJson = "{}"
});
Assert.Equal("collection_snapshot_history", history.LastDomain);
Assert.Equal("005930", history.LastPayload?["ticker"]);
await service.AppendSourceErrorAsync(new CollectionSourceError
{
RunId = "run-1",
SourceName = "naver",
ErrorKind = "TIMEOUT",
ErrorMessage = "timeout"
});
Assert.Equal("collection_source_error_history", history.LastDomain);
Assert.Equal("TIMEOUT", history.LastPayload?["error_kind"]);
}
[Fact]
public async Task FormulaService_ForwardsFormulaExecutionAndHistory()
{
var history = new FakeHistoryStore();
var service = new FormulaService(history);
var timing = service.ComputeTimingDecision(new Dictionary<string, object>
{
["entryModeGate"] = "PASS",
["entryMode"] = "BREAKOUT",
["leaderGate"] = "PASS",
["acGate"] = "CLEAR",
["priceStatus"] = "PRICE_OK",
["atr20"] = 1.0,
["leaderTotal"] = 4,
["flowCredit"] = 0.7,
["avgTradeValue5D"] = 100,
["spreadPct"] = 0.5
});
Assert.NotEqual(string.Empty, timing.Action);
await service.AppendFormulaRunAsync("timing", new Dictionary<string, object?>
{
["action"] = timing.Action,
["entry_score"] = timing.EntryScore
});
Assert.Equal("formula_timing_history", history.LastDomain);
Assert.Equal(timing.Action, history.LastPayload?["action"]);
}
private sealed class FakeWorkspaceRepository : IWorkspaceRepository
{
public Setting? LastSetting { get; private set; }
public WorkspaceApproval? LastApproval { get; private set; }
public WorkspaceLock? LastLock { get; private set; }
public (string Domain, string TargetRef)? LastReleasedLock { get; private set; }
public Task<IEnumerable<Setting>> GetSettingsAsync() => Task.FromResult(Enumerable.Empty<Setting>());
public Task<Setting?> GetSettingByKeyAsync(string key) => Task.FromResult<Setting?>(null);
public Task<bool> UpsertSettingAsync(Setting setting) { LastSetting = setting; return Task.FromResult(true); }
public Task<bool> DeleteSettingAsync(string key) => Task.FromResult(true);
public Task<IEnumerable<AccountSnapshot>> GetAccountSnapshotsAsync() => Task.FromResult(Enumerable.Empty<AccountSnapshot>());
public Task<bool> InsertAccountSnapshotsAsync(IEnumerable<AccountSnapshot> snapshots) => Task.FromResult(true);
public Task<bool> ClearAccountSnapshotsAsync() => Task.FromResult(true);
public Task<IEnumerable<WorkspaceApproval>> GetApprovalsAsync() => Task.FromResult(Enumerable.Empty<WorkspaceApproval>());
public Task<WorkspaceApproval?> GetApprovalAsync(string domain, string targetRef) => Task.FromResult<WorkspaceApproval?>(null);
public Task<bool> UpsertApprovalAsync(WorkspaceApproval approval) { LastApproval = approval; return Task.FromResult(true); }
public Task<IEnumerable<WorkspaceLock>> GetLocksAsync() => Task.FromResult(Enumerable.Empty<WorkspaceLock>());
public Task<WorkspaceLock?> GetLockAsync(string domain, string targetRef) => Task.FromResult<WorkspaceLock?>(null);
public Task<bool> AcquireLockAsync(WorkspaceLock @lock) { LastLock = @lock; return Task.FromResult(true); }
public Task<bool> ReleaseLockAsync(string domain, string targetRef) { LastReleasedLock = (domain, targetRef); return Task.FromResult(true); }
}
private sealed class FakeHistoryStore : IPostgresqlHistoryStore
{
public string? LastDomain { get; private set; }
public IDictionary<string, object?>? LastPayload { get; private set; }
public Task<int> AppendAsync(string domain, IDictionary<string, object?> payload)
{
LastDomain = domain;
LastPayload = new Dictionary<string, object?>(payload);
return Task.FromResult(1);
}
public Task<IReadOnlyList<IDictionary<string, object?>>> SnapshotAsync(string domain, int limit = 500)
=> Task.FromResult<IReadOnlyList<IDictionary<string, object?>>>(Array.Empty<IDictionary<string, object?>>());
}
}
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using QuantEngine.Core.Domain;
namespace QuantEngine.Core.Tests;
public class FormulaEngineTests
{
[Fact]
public void TestTimingDecisionNeutral()
{
var ctx = new Dictionary<string, object>
{
{ "entryModeGate", "PASS" },
{ "entryMode", "BREAKOUT" },
{ "leaderGate", "PASS" },
{ "acGate", "CLEAR" },
{ "leaderTotal", 4.0 },
{ "flowCredit", 0.8 },
{ "ma20Slope", 1.0 },
{ "disparity", 0.0 },
{ "rsi14", 50.0 },
{ "avgTradeValue5D", 100.0 },
{ "spreadPct", 0.1 },
{ "priceStatus", "PRICE_OK" },
{ "atr20", 10.0 }
};
var result = FormulaEngine.ComputeTimingDecision(ctx);
Assert.NotNull(result);
Assert.Equal("BUY_BREAKOUT_PILOT_ONLY", result.Action);
}
[Fact]
public void ComputeSellDecisionProducesExitTrimWhenRiskWindowIsOpen()
{
var ctx = new Dictionary<string, object>
{
{ "close", 100.0 },
{ "profitPct", 31.0 },
{ "tp1Price", 108.0 },
{ "tp2Price", 112.0 },
{ "timingAction", "BUY_STAGE1_READY" },
{ "atr20", 4.0 }
};
var result = FormulaEngine.ComputeSellDecision(ctx);
Assert.Equal("PROFIT_TRIM_35", result.Action);
Assert.Equal(35, result.RatioPct);
Assert.Equal("SIGNAL_CONFIRMED", result.Validation);
}
[Fact]
public void ComputeFinalDecisionPromotesSellReadyWhenSellSignalIsConfirmed()
{
var ctx = new Dictionary<string, object>
{
{ "sellAction", "TRIM_35" },
{ "sellValidation", "SIGNAL_CONFIRMED" },
{ "timingScoreEntry", 72.0 },
{ "timingScoreExit", 15.0 }
};
var result = FormulaEngine.ComputeFinalDecision(ctx);
Assert.Equal("SELL_READY", result.FinalAction);
Assert.Equal(10, result.ActionPriority);
Assert.Equal("RULE_ENGINE", result.DecisionSource);
}
[Fact]
public void ComputeCashShortfallHarnessCalculatesTargetAndShortfall()
{
var asResult = new Dictionary<string, object>
{
{ "settlementCashD2Krw", 10_000_000.0 }
};
var cashFloor = new Dictionary<string, object>
{
{ "minPct", 15.0 }
};
var result = FormulaEngine.ComputeCashShortfallHarness(asResult, 100_000_000.0, cashFloor, 6.0);
Assert.Equal(10.0, result.CashCurrentPctD2);
Assert.Equal(15.0, result.CashTargetPct);
Assert.Equal(5_000_000.0, result.CashShortfallMinKrw);
Assert.Equal(5_000_000.0, result.CashShortfallTargetKrw);
}
}
@@ -0,0 +1,93 @@
using QuantEngine.Application.Services;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Core.Tests;
public class HistoryIngestionE2ETests
{
[Fact]
public async Task AppendDecisionThenReadSnapshotRoundTripsThroughApplicationFlow()
{
var store = new FakeHistoryStore();
var ingestion = new HistoryIngestionService(store);
var reader = new PostgresqlHistorySnapshotReader(store);
var appendCount = await ingestion.AppendDecisionAsync(new Dictionary<string, object?>
{
["decision_id"] = "dec-001",
["decided_at"] = DateTimeOffset.Parse("2026-06-26T09:00:00+09:00"),
["instrument_id"] = "005930",
["action"] = "BUY",
["gate"] = "PASS",
["score"] = 87.5,
["source_version"] = "v1",
["provenance"] = new Dictionary<string, object?>
{
["source"] = "unit-test"
}
});
Assert.Equal(1, appendCount);
var rows = await reader.ReadAsync("decision_result_history", 10);
Assert.Single(rows);
Assert.Equal("dec-001", rows[0]["decision_id"]);
Assert.Equal("005930", rows[0]["instrument_id"]);
Assert.Equal("BUY", rows[0]["action"]);
Assert.Equal("PASS", rows[0]["gate"]);
Assert.Equal(87.5, rows[0]["score"]);
}
[Fact]
public async Task AppendFactorOutputThenReadSnapshotPreservesPayload()
{
var store = new FakeHistoryStore();
var ingestion = new HistoryIngestionService(store);
var reader = new PostgresqlHistorySnapshotReader(store);
var appendCount = await ingestion.AppendFactorOutputAsync(
factorId: "RS_VERDICT_V2",
factorVersion: "2026-06-26",
outputValue: 1.23,
outputGate: "PASS",
sourceVersion: "source-42",
observedAt: DateTimeOffset.Parse("2026-06-26T10:00:00+09:00"));
Assert.Equal(1, appendCount);
var rows = await reader.ReadAsync("factor_output_history", 10);
Assert.Single(rows);
Assert.Equal("RS_VERDICT_V2", rows[0]["factor_id"]);
Assert.Equal("2026-06-26", rows[0]["factor_version"]);
Assert.Equal(1.23, rows[0]["output_value"]);
Assert.Equal("PASS", rows[0]["output_gate"]);
Assert.Equal("source-42", rows[0]["source_version"]);
}
private sealed class FakeHistoryStore : IPostgresqlHistoryStore
{
private readonly Dictionary<string, List<IDictionary<string, object?>>> _rows = new();
public Task<int> AppendAsync(string domain, IDictionary<string, object?> payload)
{
if (!_rows.TryGetValue(domain, out var list))
{
list = new List<IDictionary<string, object?>>();
_rows[domain] = list;
}
list.Add(new Dictionary<string, object?>(payload));
return Task.FromResult(1);
}
public Task<IReadOnlyList<IDictionary<string, object?>>> SnapshotAsync(string domain, int limit = 500)
{
if (!_rows.TryGetValue(domain, out var list))
{
return Task.FromResult<IReadOnlyList<IDictionary<string, object?>>>(Array.Empty<IDictionary<string, object?>>());
}
return Task.FromResult<IReadOnlyList<IDictionary<string, object?>>>(list.Take(limit).ToList());
}
}
}
@@ -0,0 +1,41 @@
using QuantEngine.Infrastructure.Repositories;
namespace QuantEngine.Core.Tests;
public class PostgresqlHistoryStoreTests
{
[Fact]
public void DomainColumnsExposeCanonicalDomains()
{
var domains = PostgresqlHistoryStore.GetDomainColumns();
Assert.Contains("decision_result_history", domains.Keys);
Assert.Contains("factor_output_history", domains.Keys);
Assert.Contains("market_raw_history", domains.Keys);
Assert.Contains("market_vs_engine_gap_history", domains.Keys);
Assert.True(domains["decision_result_history"].Contains("decision_id"));
Assert.True(domains["factor_output_history"].Contains("output_gate"));
}
[Fact]
public void BuildInsertSqlUsesEngineHistoryPrefixAndNamedParameters()
{
var sql = PostgresqlHistoryStore.BuildInsertSql(
"decision_result_history",
new[] { "decision_id", "decided_at", "instrument_id", "action", "gate", "score", "source_version", "provenance" });
Assert.Equal(
"INSERT INTO engine_history.decision_result_history (decision_id, decided_at, instrument_id, action, gate, score, source_version, provenance) VALUES (@decision_id, @decided_at, @instrument_id, @action, @gate, @score, @source_version, @provenance)",
sql);
}
[Fact]
public void BuildSnapshotSqlUsesCreatedAtDescendingAndLimitParameter()
{
var sql = PostgresqlHistoryStore.BuildSnapshotSql("factor_output_history", 25);
Assert.Equal(
"SELECT * FROM engine_history.factor_output_history ORDER BY created_at DESC LIMIT @Limit",
sql);
}
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -18,4 +18,10 @@
<Using Include="Xunit" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\QuantEngine.Core\QuantEngine.Core.csproj" />
<ProjectReference Include="..\QuantEngine.Application\QuantEngine.Application.csproj" />
<ProjectReference Include="..\QuantEngine.Infrastructure\QuantEngine.Infrastructure.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,68 @@
using System.Reflection;
using QuantEngine.Infrastructure.External;
namespace QuantEngine.Core.Tests;
public class SecurityTests
{
[Theory]
[InlineData("/uapi/domestic-stock/v1/quotations/inquire-price", "FHKST01010100")]
[InlineData("/uapi/domestic-stock/v1/quotations/inquire-investor", "FHKST01010900")]
[InlineData("/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice", "FHKST03010100")]
public void AssertReadOnly_AllowsReadOnlyQuotationPaths(string path, string trId)
{
var client = CreateClient();
var ex = Record.Exception(() => InvokeAssertReadOnly(client, path, trId));
Assert.Null(ex);
}
[Theory]
[InlineData("/uapi/domestic-stock/v1/trading/order-cash", "VTTC0802U")]
[InlineData("/uapi/domestic-stock/v1/quotations/inquire-price", "TTTC084000")]
[InlineData("/uapi/domestic-stock/v1/trading/order-cash", "FHKST01010100")]
public void AssertReadOnly_BlocksTradingPathsOrIds(string path, string trId)
{
var client = CreateClient();
var ex = Assert.Throws<TargetInvocationException>(() => InvokeAssertReadOnly(client, path, trId));
Assert.IsType<InvalidOperationException>(ex.InnerException);
Assert.Contains("BLOCKED", ex.InnerException!.Message);
}
[Fact]
public void AssertReadOnly_BlocksKnownTradingTrIdPrefixes()
{
var client = CreateClient();
var ex = Assert.Throws<TargetInvocationException>(() => InvokeAssertReadOnly(client, "/uapi/domestic-stock/v1/quotations/inquire-price", "VTTC8434R00"));
Assert.IsType<InvalidOperationException>(ex.InnerException);
Assert.Contains("TR_ID", ex.InnerException!.Message);
}
private static KisApiClient CreateClient()
{
Environment.SetEnvironmentVariable("KIS_APP_Key_TEST", "mock-key");
Environment.SetEnvironmentVariable("KIS_APP_Secret_TEST", "mock-secret");
return new KisApiClient(new HttpClient(new DummyHandler()), new NoopConnectionFactory());
}
private static void InvokeAssertReadOnly(KisApiClient client, string path, string trId)
{
var method = typeof(KisApiClient).GetMethod("AssertReadOnly", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("AssertReadOnly method not found.");
method.Invoke(client, new object[] { path, trId });
}
private sealed class DummyHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
=> Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK));
}
private sealed class NoopConnectionFactory : QuantEngine.Infrastructure.Data.IDbConnectionFactory
{
public System.Data.IDbConnection CreateConnection() => throw new NotSupportedException("Not needed for read-only guard tests.");
}
}
+77 -2
View File
@@ -1,10 +1,85 @@
namespace QuantEngine.Core.Tests;
namespace QuantEngine.Core.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
public void OperationalReportLoader_ParsesCanonicalTempReport()
{
var path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", "Temp", "operational_report.json"));
var report = QuantEngine.Core.Infrastructure.OperationalReportLoader.Load(path);
Assert.Equal("2026-05-24-operational-report-v1", report.SchemaVersion);
Assert.Equal("GatherTradingData.json", report.SourceJson);
Assert.Equal(38, report.SectionCount);
Assert.True(report.Sections.Count >= 4);
Assert.Equal("exec_safety_declaration", report.Sections[0].Name);
Assert.Contains("source: .NET operational report builder", report.Sections[0].Preview);
}
[Fact]
public void OperationalReportLoader_ReturnsSafeDefaultsWhenFileIsMissing()
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "operational_report.json");
var report = QuantEngine.Core.Infrastructure.OperationalReportLoader.Load(path);
Assert.Equal("n/a", report.SchemaVersion);
Assert.Equal("n/a", report.SourceJson);
Assert.Equal("n/a", report.GeneratedAt);
Assert.Equal(0, report.SectionCount);
Assert.Empty(report.Sections);
}
[Fact]
public void OperationalReportLoader_UsesSectionCountFromPayloadWhenPresent()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
var path = Path.Combine(tempDir, "operational_report.json");
File.WriteAllText(path, """
{
"schema_version": "test-schema",
"source_json": "fixture.json",
"generated_at": "2026-06-26T00:00:00+00:00",
"section_count": 2,
"sections": [
{ "name": "alpha", "title": "Alpha", "markdown": "alpha body" },
{ "name": "beta", "title": "Beta", "markdown": "beta body" }
]
}
""");
var report = QuantEngine.Core.Infrastructure.OperationalReportLoader.Load(path);
Assert.Equal("test-schema", report.SchemaVersion);
Assert.Equal("fixture.json", report.SourceJson);
Assert.Equal(2, report.SectionCount);
Assert.Equal(2, report.Sections.Count);
Assert.Equal("alpha", report.Sections[0].Name);
Assert.Equal("alpha body", report.Sections[0].Preview);
}
[Fact]
public void OperationalReportLoader_PreservesEmptySectionsWithoutThrowing()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
var path = Path.Combine(tempDir, "operational_report.json");
File.WriteAllText(path, """
{
"schema_version": "empty-schema",
"source_json": "fixture.json",
"generated_at": "2026-06-26T00:00:00+00:00",
"section_count": 0,
"sections": []
}
""");
var report = QuantEngine.Core.Infrastructure.OperationalReportLoader.Load(path);
Assert.Equal(0, report.SectionCount);
Assert.Empty(report.Sections);
Assert.Equal("empty-schema", report.SchemaVersion);
}
}
@@ -0,0 +1,433 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v10.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v10.0": {
"QuantEngine.Core.Tests/1.0.0": {
"dependencies": {
"Microsoft.NET.Test.Sdk": "17.14.1",
"QuantEngine.Core": "1.0.0",
"xunit": "2.9.3"
},
"runtime": {
"QuantEngine.Core.Tests.dll": {}
}
},
"Microsoft.CodeCoverage/17.14.1": {
"runtime": {
"lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.225.12603"
}
}
},
"Microsoft.NET.Test.Sdk/17.14.1": {
"dependencies": {
"Microsoft.CodeCoverage": "17.14.1",
"Microsoft.TestPlatform.TestHost": "17.14.1"
}
},
"Microsoft.TestPlatform.ObjectModel/17.14.1": {
"runtime": {
"lib/net8.0/Microsoft.TestPlatform.CoreUtilities.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
},
"lib/net8.0/Microsoft.TestPlatform.PlatformAbstractions.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
},
"lib/net8.0/Microsoft.VisualStudio.TestPlatform.ObjectModel.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
}
},
"resources": {
"lib/net8.0/cs/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "cs"
},
"lib/net8.0/cs/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "cs"
},
"lib/net8.0/de/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "de"
},
"lib/net8.0/de/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "de"
},
"lib/net8.0/es/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "es"
},
"lib/net8.0/es/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "es"
},
"lib/net8.0/fr/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "fr"
},
"lib/net8.0/fr/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "fr"
},
"lib/net8.0/it/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "it"
},
"lib/net8.0/it/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "it"
},
"lib/net8.0/ja/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ja/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ko/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "ko"
},
"lib/net8.0/ko/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "ko"
},
"lib/net8.0/pl/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pl/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pt-BR/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/pt-BR/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/ru/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "ru"
},
"lib/net8.0/ru/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "ru"
},
"lib/net8.0/tr/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "tr"
},
"lib/net8.0/tr/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "tr"
},
"lib/net8.0/zh-Hans/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hans/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hant/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
"locale": "zh-Hant"
},
"lib/net8.0/zh-Hant/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.TestPlatform.TestHost/17.14.1": {
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "17.14.1",
"Newtonsoft.Json": "13.0.3"
},
"runtime": {
"lib/net8.0/Microsoft.TestPlatform.CommunicationUtilities.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
},
"lib/net8.0/Microsoft.TestPlatform.CrossPlatEngine.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
},
"lib/net8.0/Microsoft.TestPlatform.Utilities.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
},
"lib/net8.0/Microsoft.VisualStudio.TestPlatform.Common.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
},
"lib/net8.0/testhost.dll": {
"assemblyVersion": "15.0.0.0",
"fileVersion": "17.1400.125.30202"
}
},
"resources": {
"lib/net8.0/cs/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "cs"
},
"lib/net8.0/cs/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "cs"
},
"lib/net8.0/cs/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "cs"
},
"lib/net8.0/de/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "de"
},
"lib/net8.0/de/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "de"
},
"lib/net8.0/de/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "de"
},
"lib/net8.0/es/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "es"
},
"lib/net8.0/es/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "es"
},
"lib/net8.0/es/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "es"
},
"lib/net8.0/fr/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "fr"
},
"lib/net8.0/fr/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "fr"
},
"lib/net8.0/fr/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "fr"
},
"lib/net8.0/it/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "it"
},
"lib/net8.0/it/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "it"
},
"lib/net8.0/it/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "it"
},
"lib/net8.0/ja/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ja/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ja/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ko/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "ko"
},
"lib/net8.0/ko/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "ko"
},
"lib/net8.0/ko/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "ko"
},
"lib/net8.0/pl/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pl/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pl/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pt-BR/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/pt-BR/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/pt-BR/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/ru/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "ru"
},
"lib/net8.0/ru/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "ru"
},
"lib/net8.0/ru/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "ru"
},
"lib/net8.0/tr/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "tr"
},
"lib/net8.0/tr/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "tr"
},
"lib/net8.0/tr/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "tr"
},
"lib/net8.0/zh-Hans/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hans/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hans/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hant/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
"locale": "zh-Hant"
},
"lib/net8.0/zh-Hant/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
"locale": "zh-Hant"
},
"lib/net8.0/zh-Hant/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"xunit/2.9.3": {
"dependencies": {
"xunit.assert": "2.9.3",
"xunit.core": "2.9.3"
}
},
"xunit.abstractions/2.0.3": {
"runtime": {
"lib/netstandard2.0/xunit.abstractions.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
}
}
},
"xunit.assert/2.9.3": {
"runtime": {
"lib/net6.0/xunit.assert.dll": {
"assemblyVersion": "2.9.3.0",
"fileVersion": "2.9.3.0"
}
}
},
"xunit.core/2.9.3": {
"dependencies": {
"xunit.extensibility.core": "2.9.3",
"xunit.extensibility.execution": "2.9.3"
}
},
"xunit.extensibility.core/2.9.3": {
"dependencies": {
"xunit.abstractions": "2.0.3"
},
"runtime": {
"lib/netstandard1.1/xunit.core.dll": {
"assemblyVersion": "2.9.3.0",
"fileVersion": "2.9.3.0"
}
}
},
"xunit.extensibility.execution/2.9.3": {
"dependencies": {
"xunit.extensibility.core": "2.9.3"
},
"runtime": {
"lib/netstandard1.1/xunit.execution.dotnet.dll": {
"assemblyVersion": "2.9.3.0",
"fileVersion": "2.9.3.0"
}
}
},
"QuantEngine.Core/1.0.0": {
"runtime": {
"QuantEngine.Core.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.0.0"
}
}
}
}
},
"libraries": {
"QuantEngine.Core.Tests/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.CodeCoverage/17.14.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==",
"path": "microsoft.codecoverage/17.14.1",
"hashPath": "microsoft.codecoverage.17.14.1.nupkg.sha512"
},
"Microsoft.NET.Test.Sdk/17.14.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==",
"path": "microsoft.net.test.sdk/17.14.1",
"hashPath": "microsoft.net.test.sdk.17.14.1.nupkg.sha512"
},
"Microsoft.TestPlatform.ObjectModel/17.14.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==",
"path": "microsoft.testplatform.objectmodel/17.14.1",
"hashPath": "microsoft.testplatform.objectmodel.17.14.1.nupkg.sha512"
},
"Microsoft.TestPlatform.TestHost/17.14.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==",
"path": "microsoft.testplatform.testhost/17.14.1",
"hashPath": "microsoft.testplatform.testhost.17.14.1.nupkg.sha512"
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"xunit/2.9.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
"path": "xunit/2.9.3",
"hashPath": "xunit.2.9.3.nupkg.sha512"
},
"xunit.abstractions/2.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==",
"path": "xunit.abstractions/2.0.3",
"hashPath": "xunit.abstractions.2.0.3.nupkg.sha512"
},
"xunit.assert/2.9.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==",
"path": "xunit.assert/2.9.3",
"hashPath": "xunit.assert.2.9.3.nupkg.sha512"
},
"xunit.core/2.9.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
"path": "xunit.core/2.9.3",
"hashPath": "xunit.core.2.9.3.nupkg.sha512"
},
"xunit.extensibility.core/2.9.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
"path": "xunit.extensibility.core/2.9.3",
"hashPath": "xunit.extensibility.core.2.9.3.nupkg.sha512"
},
"xunit.extensibility.execution/2.9.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
"path": "xunit.extensibility.execution/2.9.3",
"hashPath": "xunit.extensibility.execution.2.9.3.nupkg.sha512"
},
"QuantEngine.Core/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
@@ -0,0 +1,13 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "10.0.0"
},
"configProperties": {
"MSTest.EnableParentProcessQuery": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

Some files were not shown because too many files have changed in this diff Show More