Compare commits

..

24 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
kjh2064 d0bbb779c0 docs(deploy): Update DEPLOYMENT_SSH_GUIDE.md with final environment configuration
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
Complete rewrite with v9 production environment details:

Environment Configuration:
- Server: hz-prod-01
- Public IP: 178.104.200.7
- Internal IP: 172.17.0.1 (Docker gateway, internal access only)
- SSH user: kjh2064
- SSH endpoint: ssh kjh2064@178.104.200.7

Deployment Architecture:
- Nginx reverse proxy on port 80 (already configured)
- Location /quant/ → proxy_pass http://127.0.0.1:5000/
- Quantengine service runs on localhost:5000
- systemd service: /etc/systemd/system/quantengine.service

Deployment Paths:
- Active deployment: /home/kjh2064/quantengine_active
- Backup location: /home/kjh2064/quantengine_backup
- Nginx config: /etc/nginx/sites-available/gitea-ip.conf

Key Procedures:
1. SSH Setup: ssh-keygen, ssh-copy-id, key validation
2. Environment Check: System info, deployment paths, service status
3. Release Build: dotnet publish -c Release
4. Deployment Methods:
   - Auto: deploy-production.sh (recommended)
   - Auto: deploy-manual.sh (interactive)
   - Manual: Step-by-step SSH procedures
5. Verification: Health checks, logs, MudBlazor validation
6. Rollback: Automated backup restoration

Troubleshooting Guide:
- SSH connection failures
- Service startup issues
- Nginx proxy errors
- File permission problems

Complete deployment flow diagrams and examples for all scenarios.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-25 18:24:01 +09:00
kjh2064 a2acaa70d8 feat(deploy): Add production deployment scripts for v9 quantengine
Add deploy-production.sh (new):
- Automated deployment to hz-prod-01 (178.104.200.7)
- Service lifecycle management: systemctl stop/start quantengine
- Automatic backup to /home/kjh2064/quantengine_backup
- File transfer via rsync to /home/kjh2064/quantengine_active
- Health checks against public URL and service status
- Rollback instructions with backup restoration

Update deploy-manual.sh:
- Interactive deployment with user confirmation
- Updated for quantengine service (not nginx)
- Deployment path: /home/kjh2064/quantengine_active
- Backup path: /home/kjh2064/quantengine_backup
- Nginx reverse proxy structure documentation
- Comprehensive rollback procedures

Both scripts:
- SSH connection validation (178.104.200.7)
- Environment diagnostics
- Comprehensive logging and error handling
- Support for internal and public IP access
- Pre/post deployment validation

Deployment Architecture:
Public: http://178.104.200.7/quant/
  → Nginx (reverse proxy)
  → localhost:5000 (quantengine service)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-25 18:23:54 +09:00
kjh2064 762335286c chore(ci): Update Gitea Actions CI/CD pipeline for v9 production deployment
- Configure internal IP deployment: 172.17.0.1 (hz-prod-01)
- Set deployment path to /home/kjh2064/quantengine_active
- Use quantengine systemd service for app management
- Implement service lifecycle (stop → backup → extract → start)
- Add health checks against localhost:5000 (quantengine)
- Update Nginx reverse proxy verification (already configured)
- Add comprehensive deployment report and Slack notifications
- Include post-deployment performance metrics collection

CI/CD Flow:
1. Build & Test: Release build, validation, .tar.gz creation
2. Deploy: Service stop, backup, file transfer, service start
3. Health Check: localhost:5000 verification via Nginx proxy
4. Post-Deploy: Performance metrics and deployment checklist

Environment: hz-prod-01 (Public: 178.104.200.7 / Internal: 172.17.0.1)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-25 18:23:49 +09:00
kjh2064 55a7b044d8 feat(diagnosis): Add environment diagnosis script and guide
환경 진단 도구:

diagnose-environment.sh:
  - 네트워크 정보 (공인 IP, 내부 IP)
  - 디렉토리 구조 (/var/www 경로 확인)
  - Nginx 설정 확인
  - 파일 권한 및 소유자
  - 포트 상태
  - 시스템 정보
  - Sudo 권한
  - Git/Gitea 정보

ENVIRONMENT_DIAGNOSIS.md:
  - 진단 절차 가이드
  - 실행 방법 (3가지)
  - 출력 결과 분석
  - 결과 보고 양식
  - 빠른 진단 명령어
  - 수정 후 다음 단계

목표:
  - 정확한 내부 IP 확인 (172.x.x.x)
  - 실제 웹 서버 경로 파악
  - 웹 서버 사용자 확인
  - Nginx 설정 파악
  - 권한 구조 파악

결과 수집 후:
  - deploy-manual.sh 맞춤 수정
  - 모든 배포 문서 업데이트
  - 배포 실행

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-25 18:15:31 +09:00
254 changed files with 6040 additions and 14690 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
+92 -56
View File
@@ -6,12 +6,14 @@ on:
workflow_dispatch:
env:
DEPLOY_HOST: 192.168.123.100
# NOTE: Gitea와 운영서버가 원격지의 같은 서버에 있으므로 내부 IP 사용
# Gitea (CI/CD) → SSH (내부 192.168.123.100) → 운영서버
# 외부 사용자: 178.104.200.7 → nginx 포트포워딩 → 192.168.123.100/quant
DEPLOY_HOST: 172.17.0.1
# NOTE: Gitea와 운영서버가 같은 호스트에 있음 (hz-prod-01)
# 구조: 공인 IP 178.104.200.7/quant → Nginx reverse proxy → localhost:5000 (quantengine)
# 배포: .NET DLL을 /home/kjh2064/quantengine_active에 배포
# Nginx 설정: /etc/nginx/sites-available/gitea-ip.conf (이미 구성됨)
DEPLOY_USER: kjh2064
DEPLOY_PATH: /var/www/quant
DEPLOY_PATH: /home/kjh2064/quantengine_active
SERVICE_NAME: quantengine
DOTNET_VERSION: '10.0.x'
jobs:
@@ -105,24 +107,36 @@ jobs:
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Create Backup
- name: Stop Service and Create Backup
run: |
echo "📦 Creating backup on production server..."
echo "📦 Stopping service and creating backup..."
ssh -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF'
set -e
BACKUP_DIR="/var/www/quant_backup"
BACKUP_NAME="quant_backup_$(date +%Y%m%d_%H%M%S)"
BACKUP_DIR="/home/kjh2064/quantengine_backup"
BACKUP_NAME="quantengine_$(date +%Y%m%d_%H%M%S)"
sudo mkdir -p $BACKUP_DIR
if [ -d ${{ env.DEPLOY_PATH }}/publish ]; then
sudo cp -r ${{ env.DEPLOY_PATH }}/publish "$BACKUP_DIR/$BACKUP_NAME"
# Stop service
echo "⏹️ Stopping quantengine service..."
sudo systemctl stop ${{ env.SERVICE_NAME }}
sleep 2
# Create backup
mkdir -p $BACKUP_DIR
if [ -d ${{ env.DEPLOY_PATH }} ]; then
cp -r ${{ env.DEPLOY_PATH }} "$BACKUP_DIR/$BACKUP_NAME"
echo "✅ Backup created: $BACKUP_DIR/$BACKUP_NAME"
# Keep only last 5 backups
ls -t $BACKUP_DIR | tail -n +6 | xargs -I {} sudo rm -rf "$BACKUP_DIR/{}"
echo "🧹 Old backups cleaned"
BACKUP_COUNT=$(ls -1 $BACKUP_DIR | wc -l)
if [ "$BACKUP_COUNT" -gt 5 ]; then
OLD_BACKUPS=$(ls -1t $BACKUP_DIR | tail -n +6)
for backup in $OLD_BACKUPS; do
rm -rf "$BACKUP_DIR/$backup"
done
echo "🧹 Old backups cleaned"
fi
else
echo "⚠️ No existing deployment found, skipping backup"
echo "⚠️ No existing deployment found"
fi
EOF
@@ -152,38 +166,41 @@ jobs:
ARCHIVE_NAME=$(ls -1 /tmp/quant-deploy/quant-engine-release-*.tar.gz | head -1)
# Create deployment directory
sudo mkdir -p "$DEPLOY_PATH/publish"
sudo chmod 777 "$DEPLOY_PATH/publish"
mkdir -p "$DEPLOY_PATH"
# Extract new package
tar -xzf "$ARCHIVE_NAME" -C "$DEPLOY_PATH/publish"
echo "✅ Package extracted"
tar -xzf "$ARCHIVE_NAME" -C "$DEPLOY_PATH"
echo "✅ Package extracted to $DEPLOY_PATH"
# Set permissions
sudo chown -R www-data:www-data "$DEPLOY_PATH/publish"
sudo chmod -R 755 "$DEPLOY_PATH/publish"
echo "✅ Permissions set"
# Verify key files
if [ -f "$DEPLOY_PATH/QuantEngine.Web.dll" ]; then
echo "✅ QuantEngine.Web.dll verified"
else
echo "❌ QuantEngine.Web.dll not found!"
exit 1
fi
# Cleanup temp
rm -rf /tmp/quant-deploy
EOF
- name: Restart Services
- name: Start Service
run: |
echo "🔄 Restarting services..."
echo "🔄 Starting quantengine service..."
ssh -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF'
set -e
# Restart nginx
sudo systemctl restart nginx
sleep 2
# Start service
sudo systemctl start ${{ env.SERVICE_NAME }}
sleep 3
# Check status
if sudo systemctl is-active --quiet nginx; then
echo "✅ nginx restarted successfully"
if sudo systemctl is-active --quiet ${{ env.SERVICE_NAME }}; then
echo "✅ ${{ env.SERVICE_NAME }} started successfully"
sudo systemctl status ${{ env.SERVICE_NAME }} | head -5
else
echo "❌ nginx failed to start"
sudo systemctl status nginx
echo "❌ ${{ env.SERVICE_NAME }} failed to start"
sudo systemctl status ${{ env.SERVICE_NAME }}
exit 1
fi
EOF
@@ -192,13 +209,13 @@ jobs:
run: |
echo "🧪 Running health checks..."
# Wait for service to be ready
# Wait for service to be ready (localhost:5000 through Nginx)
for i in {1..30}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
http://${{ env.DEPLOY_HOST }}/quant/ || echo "000")
"http://127.0.0.1:5000/" || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "✅ Health check passed (HTTP $HTTP_CODE)"
echo "✅ Health check passed (HTTP $HTTP_CODE at localhost:5000)"
break
fi
@@ -208,6 +225,9 @@ jobs:
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ Health check failed after 60 seconds"
echo "Service logs:"
ssh -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
"sudo journalctl -u ${{ env.SERVICE_NAME }} -n 20" || true
exit 1
fi
@@ -215,8 +235,9 @@ jobs:
run: |
echo "📊 Verifying deployment..."
# Check MudBlazor is loaded
MUDBLAZOR_CHECK=$(curl -s http://${{ env.DEPLOY_HOST }}/quant/ | grep -c "MudBlazor" || echo "0")
# Check MudBlazor is loaded (via public IP)
PUBLIC_IP="178.104.200.7"
MUDBLAZOR_CHECK=$(curl -s "http://$PUBLIC_IP/quant/" | grep -c "MudBlazor" || echo "0")
if [ "$MUDBLAZOR_CHECK" -gt "0" ]; then
echo "✅ MudBlazor UI loaded successfully"
@@ -225,7 +246,7 @@ jobs:
fi
# Get page title
PAGE_TITLE=$(curl -s http://${{ env.DEPLOY_HOST }}/quant/ | grep -o "<title>.*</title>" | head -1)
PAGE_TITLE=$(curl -s "http://$PUBLIC_IP/quant/" | grep -o "<title>.*</title>" | head -1)
echo "📄 Page title: $PAGE_TITLE"
- name: Generate Deployment Report
@@ -233,7 +254,7 @@ jobs:
run: |
cat > deployment-report.txt << EOF
═══════════════════════════════════════════════════════
Quant Engine Deployment Report
Quant Engine v9 Deployment Report
═══════════════════════════════════════════════════════
Deployment Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
@@ -241,27 +262,42 @@ jobs:
Commit: ${{ github.sha }}
Branch: ${{ github.ref }}
Target Server: ${{ env.DEPLOY_HOST }}
🎯 Target Environment
Server: hz-prod-01
Internal IP: ${{ env.DEPLOY_HOST }}
Public IP: 178.104.200.7
Deploy Path: ${{ env.DEPLOY_PATH }}
Service: ${{ env.SERVICE_NAME }}
Status: COMPLETED
📊 Deployment Status: COMPLETED
✅ Release Build: Successful
✅ Package Created: 24MB
✅ Backup Created: /var/www/quant_backup/
✅ Package Deployed: ${{ env.DEPLOY_PATH }}/publish
✅ Services Restarted: nginx
✅ Health Check: PASS
✅ MudBlazor UI: Verified
✅ Package Created: 24MB+
✅ Backup Created: /home/kjh2064/quantengine_backup/
✅ Package Deployed: ${{ env.DEPLOY_PATH }}
✅ Service Started: ${{ env.SERVICE_NAME }}
✅ Health Check: PASS (localhost:5000)
✅ MudBlazor UI: Verified via public IP
Access: http://${{ env.DEPLOY_HOST }}/quant/
🌐 Access Information
Public URL: http://178.104.200.7/quant/
Service Port: 127.0.0.1:5000
Nginx Config: /etc/nginx/sites-available/gitea-ip.conf
Logs:
- Deployment: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- nginx: ssh kjh2064@${{ env.DEPLOY_HOST }} 'sudo tail -50 /var/log/nginx/error.log'
📝 Service Architecture
- Nginx (reverse proxy) listens on port 80/443
- /quant/ path → localhost:5000 (quantengine service)
- quantengine runs as user kjh2064
- WorkingDirectory: /home/kjh2064/quantengine_active
Rollback Command (if needed):
ssh kjh2064@${{ env.DEPLOY_HOST }} 'LATEST=\$(ls -t /var/www/quant_backup | head -1); sudo cp -r /var/www/quant_backup/\$LATEST/* /var/www/quant/publish/ && sudo systemctl restart nginx'
🔍 Monitoring & Logs
- Service: sudo systemctl status ${{ env.SERVICE_NAME }}
- Logs: sudo journalctl -u ${{ env.SERVICE_NAME }} -f
- Nginx: sudo tail -f /var/log/nginx/error.log
- Deployment Log: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
🔄 Rollback Command (if needed):
ssh kjh2064@${{ env.DEPLOY_HOST }} 'LATEST=\$(ls -t /home/kjh2064/quantengine_backup | head -1); cp -r /home/kjh2064/quantengine_backup/\$LATEST/* /home/kjh2064/quantengine_active/ && sudo systemctl restart ${{ env.SERVICE_NAME }}'
═══════════════════════════════════════════════════════
EOF
@@ -293,12 +329,12 @@ jobs:
-d "{
\"attachments\": [{
\"color\": \"$COLOR\",
\"title\": \"$EMOJI Quant Engine Deployment\",
\"title\": \"$EMOJI Quant Engine v9 Deployment\",
\"text\": \"Run #${{ github.run_number }}\",
\"fields\": [
{\"title\": \"Status\", \"value\": \"$STATUS\", \"short\": true},
{\"title\": \"Server\", \"value\": \"${{ env.DEPLOY_HOST }}\", \"short\": true},
{\"title\": \"URL\", \"value\": \"http://${{ env.DEPLOY_HOST }}/quant/\", \"short\": false}
{\"title\": \"Service\", \"value\": \"${{ env.SERVICE_NAME }}\", \"short\": true},
{\"title\": \"URL\", \"value\": \"http://178.104.200.7/quant/\", \"short\": false}
],
\"ts\": $(date +%s)
}]
@@ -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`)에서만 나온다.
-347
View File
@@ -1,347 +0,0 @@
# 🔐 SSH 배포 가이드
**목표**: SSH로 원격 서버에 직접 접속하여 환경을 파악한 후 배포
**대상**: 178.104.200.7 (공인 IP) / 192.168.123.100 (내부 IP)
---
## 📋 사전 준비
### 1. SSH 키 설정 (최초 1회)
#### 1.1 로컬에서 SSH 키 생성
```bash
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
```
#### 1.2 공개 키를 원격 서버에 등록
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub kjh2064@178.104.200.7
# 또는 내부 IP
ssh-copy-id -i ~/.ssh/id_ed25519.pub kjh2064@192.168.123.100
```
#### 1.3 SSH 연결 테스트
```bash
ssh -i ~/.ssh/id_ed25519 kjh2064@192.168.123.100 "echo '✅ 연결 성공'"
```
---
## 🔍 Step 1: 환경 파악
### 시스템 정보 확인
```bash
# SSH 접속
ssh -i ~/.ssh/id_ed25519 kjh2064@192.168.123.100
# 원격 서버에서 실행:
# ─────────────────────────────────────────────
# 1. 시스템 정보
uname -a
lsb_release -a
# 2. 디스크 상태
df -h
# 3. 웹 서버 상태
sudo systemctl status nginx
# 4. 웹 서버 경로
ls -la /var/www/quant/
ls -la /var/www/quant/publish/
# 5. Nginx 설정 확인
sudo cat /etc/nginx/sites-available/default | grep -A 10 "location"
# 6. 포트 상태
sudo netstat -tuln | grep :80
# 7. 사용자 권한 확인
id
sudo -l
```
### 예상 환경
```
✓ Linux (Ubuntu 20.04 또는 그 이상)
✓ nginx (1.18 이상)
✓ /var/www/quant/ 디렉토리 존재 또는 생성 가능
✓ www-data 또는 유사 웹 서버 사용자
✓ sudo 권한 (webmaster 그룹)
```
---
## 📦 Step 2: Release 빌드
```bash
# 로컬에서 실행
cd /c/Temp/data_feed
# Release 빌드
dotnet publish -c Release \
-o src/dotnet/QuantEngine.Web/publish
# 결과 확인
ls -lh src/dotnet/QuantEngine.Web/publish/
```
---
## 🚀 Step 3: 배포 스크립트 실행
### 방법 1: 자동 배포 스크립트 (권장)
```bash
# 스크립트에 실행 권한 부여
chmod +x deploy-manual.sh
# 배포 실행
./deploy-manual.sh 192.168.123.100
# 또는
./deploy-manual.sh 178.104.200.7
```
**스크립트가 자동으로:**
- ✓ SSH 연결 확인
- ✓ 원격 환경 파악
- ✓ 백업 생성
- ✓ 파일 전송 (rsync)
- ✓ 권한 설정
- ✓ nginx 재시작
- ✓ 헬스 체크
### 방법 2: 수동 배포 (단계별)
```bash
# 1. SSH 접속
ssh -i ~/.ssh/id_ed25519 kjh2064@192.168.123.100
# 원격 서버에서:
# ─────────────────────────────────────────────
# 2. 백업 생성
sudo mkdir -p /var/www/quant_backup
sudo cp -r /var/www/quant/publish \
/var/www/quant_backup/quant_$(date +%Y%m%d_%H%M%S)
echo "✓ 백업 생성 완료"
# 3. 배포 디렉토리 준비
sudo mkdir -p /var/www/quant/publish
sudo chmod 777 /var/www/quant/publish
# 4. 권한 설정
sudo chown -R www-data:www-data /var/www/quant/publish
sudo chmod -R 755 /var/www/quant/publish
# 5. SSH 종료
exit
```
```bash
# 로컬에서: 파일 전송
rsync -avz --delete \
-e "ssh -i ~/.ssh/id_ed25519" \
src/dotnet/QuantEngine.Web/publish/ \
kjh2064@192.168.123.100:/var/www/quant/publish/
```
```bash
# 다시 SSH 접속
ssh -i ~/.ssh/id_ed25519 kjh2064@192.168.123.100
# 원격 서버에서:
# ─────────────────────────────────────────────
# 6. nginx 재시작
sudo systemctl restart nginx
# 7. 상태 확인
sudo systemctl status nginx
# 종료
exit
```
---
## ✅ Step 4: 배포 검증
```bash
# 1. HTTP 상태 확인
curl -I http://178.104.200.7/quant/
# 또는
curl -I http://192.168.123.100/quant/
# 기대: HTTP/1.1 200 OK
# 2. MudBlazor 로드 확인
curl -s http://178.104.200.7/quant/ | grep -c "MudBlazor"
# 기대: > 0
# 3. 페이지 제목 확인
curl -s http://178.104.200.7/quant/ | grep -o "<title>.*</title>"
# 기대: <title>Quant Engine - Dashboard</title>
# 4. nginx 로그 확인
ssh -i ~/.ssh/id_ed25519 kjh2064@192.168.123.100 \
'sudo tail -20 /var/log/nginx/error.log'
# 5. 브라우저 테스트
# http://178.104.200.7/quant/ 접속
```
---
## 🔄 롤백 (배포 실패 시)
```bash
ssh -i ~/.ssh/id_ed25519 kjh2064@192.168.123.100 << 'EOF'
# 최신 백업 확인
ls -lt /var/www/quant_backup/
# 최신 백업으로 복원
LATEST=$(ls -t /var/www/quant_backup | head -1)
echo "복원 중: $LATEST"
sudo cp -r /var/www/quant_backup/$LATEST/* /var/www/quant/publish/
# 권한 재설정
sudo chown -R www-data:www-data /var/www/quant/publish
sudo chmod -R 755 /var/www/quant/publish
# nginx 재시작
sudo systemctl restart nginx
echo "✅ 롤백 완료"
EOF
```
---
## 📊 배포 체크리스트
### 배포 전
```
[ ] SSH 키 설정 완료 (~/.ssh/id_ed25519)
[ ] SSH 연결 테스트 성공
[ ] Release 빌드 완료 (24MB)
[ ] 배포 스크립트 준비
```
### 배포 중
```
[ ] 환경 파악 완료
[ ] 백업 생성 완료
[ ] 파일 전송 완료
[ ] 권한 설정 완료
[ ] nginx 재시작 완료
```
### 배포 후
```
[ ] HTTP 200 OK 확인
[ ] MudBlazor 리소스 로드됨
[ ] 페이지 제목 확인
[ ] nginx 로그 에러 없음
[ ] 브라우저 접속 테스트
```
---
## 🆘 문제 해결
### SSH 연결 타임아웃
```bash
# 원인: IP 주소 오류 또는 방화벽
# 해결:
1. IP 확인: 178.104.200.7 또는 192.168.123.100?
2. SSH 포트 확인: 22 (기본값)
3. 방화벽 규칙 확인
4. 공개 키 등록 재확인
```
### 권한 오류 (sudo 불가)
```bash
# 원인: sudo 권한 없음
# 확인:
sudo -l
# 해결: 관리자에게 webmaster 그룹 추가 요청
```
### nginx 재시작 실패
```bash
# 로그 확인
sudo systemctl status nginx
sudo journalctl -u nginx -n 20
# 설정 테스트
sudo nginx -t
# 포트 충돌 확인
sudo netstat -tuln | grep :80
```
### 파일 권한 문제
```bash
# 현재 권한 확인
ls -la /var/www/quant/publish/
# 권한 수정
sudo chown -R www-data:www-data /var/www/quant/publish
sudo chmod -R 755 /var/www/quant/publish
```
---
## 📚 관련 파일
```
배포 스크립트:
├── deploy.sh (자동 배포, bash)
└── deploy-manual.sh (대화형 배포, 이 파일)
배포 가이드:
├── DEPLOYMENT_GUIDE.md (전체 가이드)
├── DEPLOYMENT_STEPS.md (단계별 지침)
├── DEPLOYMENT_CHECKLIST.md (운영 체크리스트)
└── DEPLOYMENT_SSH_GUIDE.md (이 파일)
CI/CD:
├── .gitea/workflows/deploy-prod.yml (자동화)
└── CI_CD_PIPELINE.md (CI/CD 문서)
```
---
## ⚡ 빠른 배포 명령어
### 한 번에 배포
```bash
chmod +x deploy-manual.sh && ./deploy-manual.sh 192.168.123.100
```
### 내부 IP 사용
```bash
# Gitea에서 배포할 때 (자동 CI/CD)
DEPLOY_HOST=192.168.123.100
```
### 외부 접속
```bash
# 사용자가 접속할 때
http://178.104.200.7/quant/
```
---
**배포 준비 완료!** 🚀
deploy-manual.sh 스크립트를 실행하거나, 위의 수동 단계를 따라 배포하세요.
+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
+450
View File
@@ -0,0 +1,450 @@
# 🔐 SSH 배포 가이드 (v9)
**목표**: SSH로 원격 서버에 직접 접속하여 배포
**환경**: hz-prod-01 (공인 IP 178.104.200.7 / 내부 IP 172.17.0.1)
---
## 📋 사전 준비
### 1. SSH 키 설정 (최초 1회)
#### 1.1 로컬에서 SSH 키 생성
```bash
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
```
#### 1.2 공개 키를 원격 서버에 등록
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub kjh2064@178.104.200.7
```
#### 1.3 SSH 연결 테스트
```bash
ssh -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 "echo '✅ 연결 성공'"
```
---
## 🔍 Step 1: 환경 파악
### 원격 서버 정보 확인
```bash
ssh kjh2064@178.104.200.7 << 'EOF'
# 1. 시스템 정보
echo "=== 시스템 정보 ==="
hostname
uname -a
lsb_release -a
# 2. 배포 경로
echo -e "\n=== 배포 경로 ==="
ls -la /home/kjh2064/quantengine_active/ || echo "아직 없음 (첫 배포)"
ls -la /home/kjh2064/quantengine_backup/
# 3. 서비스 상태
echo -e "\n=== quantengine 서비스 ==="
sudo systemctl status quantengine --no-pager
# 4. Nginx 설정
echo -e "\n=== Nginx /quant 설정 ==="
cat /etc/nginx/sites-available/gitea-ip.conf | grep -A 10 "location /quant"
# 5. 포트 상태
echo -e "\n=== 포트 상태 ==="
sudo netstat -tuln | grep -E ":80|:443|:5000"
# 6. 디스크 상태
echo -e "\n=== 디스크 ==="
df -h
EOF
```
### 예상 환경
```
✓ Linux (Ubuntu 20.04+)
✓ nginx 1.28.3 (reverse proxy)
✓ /home/kjh2064/quantengine_active/ 배포 경로
✓ quantengine systemd 서비스
✓ 포트 5000에서 .NET 앱 실행
✓ sudo 권한 (quantengine 서비스 제어)
```
---
## 🏗️ 배포 아키텍처
```
┌────────────────────────────────────────────────────┐
│ 사용자 (외부 인터넷) │
│ http://178.104.200.7/quant/ │
└─────────────────────┬────────────────────────────────┘
│ 공인 IP (포트 80)
┌─────────────────────▼────────────────────────────────┐
│ Nginx (reverse proxy) │
│ /etc/nginx/sites-available/gitea-ip.conf │
│ location /quant/ → proxy_pass http://127.0.0.1:5000/
└─────────────────────┬────────────────────────────────┘
│ localhost:5000
┌─────────────────────▼────────────────────────────────┐
│ quantengine (systemd 서비스) │
│ /home/kjh2064/quantengine_active/ │
│ QuantEngine.Web.dll (실행 중) │
└────────────────────────────────────────────────────┘
```
---
## 📦 Step 2: Release 빌드
```bash
# 로컬 개발 머신에서 실행
cd /c/Temp/data_feed
# Release 빌드
dotnet publish -c Release \
-o src/dotnet/QuantEngine.Web/publish
# 결과 확인
ls -lh src/dotnet/QuantEngine.Web/publish/
du -sh src/dotnet/QuantEngine.Web/publish/
```
---
## 🚀 Step 3: 배포 방법
### 방법 1: 자동 배포 스크립트 (권장)
```bash
# 스크립트에 실행 권한 부여
chmod +x deploy-production.sh
# 배포 실행
./deploy-production.sh
# 또는
./deploy-manual.sh 178.104.200.7
```
**스크립트가 자동으로:**
- ✓ SSH 연결 확인
- ✓ 원격 환경 파악
- ✓ 서비스 중지
- ✓ 백업 생성
- ✓ 파일 전송 (rsync)
- ✓ 파일 검증
- ✓ 서비스 시작
- ✓ 헬스 체크
### 방법 2: 수동 배포 (단계별)
#### Step 2-1: SSH 접속
```bash
ssh -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7
```
#### Step 2-2: 서비스 중지 및 백업
```bash
# 원격 서버에서 실행:
set -e
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
BACKUP_PATH="/home/kjh2064/quantengine_backup"
BACKUP_NAME="quantengine_$(date +%Y%m%d_%H%M%S)"
# 서비스 중지
sudo systemctl stop $SERVICE_NAME
sleep 2
echo "✓ 서비스 중지"
# 백업 생성
mkdir -p $BACKUP_PATH
if [ -d $DEPLOY_PATH ]; then
cp -r $DEPLOY_PATH "$BACKUP_PATH/$BACKUP_NAME"
echo "✓ 백업: $BACKUP_PATH/$BACKUP_NAME"
else
mkdir -p $DEPLOY_PATH
echo "⚠️ 첫 배포"
fi
```
#### Step 2-3: SSH 종료
```bash
exit
```
#### Step 2-4: 파일 전송 (로컬에서)
```bash
rsync -avz --delete \
-e "ssh -i ~/.ssh/id_ed25519" \
src/dotnet/QuantEngine.Web/publish/ \
kjh2064@178.104.200.7:/home/kjh2064/quantengine_active/
```
#### Step 2-5: 서비스 시작
```bash
ssh -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 << 'EOF'
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
# 파일 검증
if [ -f $DEPLOY_PATH/QuantEngine.Web.dll ]; then
echo "✓ 파일 확인됨"
else
echo "❌ 파일 없음"
exit 1
fi
# 서비스 시작
sudo systemctl start $SERVICE_NAME
sleep 3
# 상태 확인
if sudo systemctl is-active --quiet $SERVICE_NAME; then
echo "✓ 서비스 시작됨"
else
echo "❌ 서비스 시작 실패"
exit 1
fi
EOF
```
---
## ✅ Step 4: 배포 검증
### HTTP 상태 확인
```bash
# 공인 IP로 접근 (외부 사용자 기준)
curl -I http://178.104.200.7/quant/
# 기대: HTTP/1.1 200 OK
# localhost:5000 직접 확인 (서버에서)
ssh kjh2064@178.104.200.7 'curl -I http://127.0.0.1:5000/'
# 기대: HTTP/1.1 200 OK
```
### MudBlazor 리소스 확인
```bash
curl -s http://178.104.200.7/quant/ | grep -c "MudBlazor"
# 기대: > 0
```
### 페이지 제목 확인
```bash
curl -s http://178.104.200.7/quant/ | grep -o "<title>.*</title>"
# 기대: <title>Quant Engine - Dashboard</title>
```
### 로그 확인
```bash
# 서비스 로그
ssh kjh2064@178.104.200.7 'sudo journalctl -u quantengine -n 50'
# Nginx 에러 로그
ssh kjh2064@178.104.200.7 'sudo tail -f /var/log/nginx/error.log'
# 실시간 모니터링
ssh kjh2064@178.104.200.7 'sudo journalctl -u quantengine -f'
```
### 브라우저 테스트
```
http://178.104.200.7/quant/
```
---
## 🔄 롤백 (배포 실패 시)
### 자동 롤백 스크립트
```bash
ssh kjh2064@178.104.200.7 << 'EOF'
set -e
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
BACKUP_PATH="/home/kjh2064/quantengine_backup"
echo "🔄 최신 백업 찾는 중..."
LATEST=$(ls -t $BACKUP_PATH | head -1)
echo "롤백 대상: $LATEST"
# 서비스 중지
sudo systemctl stop $SERVICE_NAME
sleep 2
# 백업 복원
cp -r "$BACKUP_PATH/$LATEST"/* "$DEPLOY_PATH/"
echo "✓ 백업 복원 완료"
# 서비스 시작
sudo systemctl start $SERVICE_NAME
sleep 3
# 확인
if sudo systemctl is-active --quiet $SERVICE_NAME; then
echo "✅ 롤백 완료"
else
echo "❌ 롤백 실패"
exit 1
fi
EOF
```
---
## 📊 배포 체크리스트
### 배포 전
```
[ ] SSH 키 설정 완료 (~/.ssh/id_ed25519)
[ ] SSH 연결 테스트 성공
[ ] Release 빌드 완료 (24MB+)
[ ] 배포 스크립트 준비
```
### 배포 중
```
[ ] 환경 파악 완료
[ ] 서비스 중지 확인
[ ] 백업 생성 확인
[ ] 파일 전송 완료 (rsync)
[ ] 파일 검증 완료
[ ] 서비스 시작 완료
```
### 배포 후
```
[ ] HTTP 200 OK 확인
[ ] localhost:5000 응답 확인
[ ] MudBlazor 리소스 로드됨
[ ] Nginx 에러 로그 확인
[ ] 브라우저 접속 테스트
[ ] 페이지 로드 시간 < 2s
```
---
## 🆘 문제 해결
### SSH 연결 타임아웃
```bash
# 확인:
1. IP 주소: 178.104.200.7 또는 172.17.0.1?
2. SSH 포트: 22 (기본값)
3. 방화벽 규칙
4. 공개 키 등록 확인
# 해결:
ssh-copy-id -i ~/.ssh/id_ed25519.pub kjh2064@178.104.200.7
```
### 서비스 시작 실패
```bash
# 로그 확인
ssh kjh2064@178.104.200.7 'sudo journalctl -u quantengine -n 50'
# 설정 확인
ssh kjh2064@178.104.200.7 'cat /etc/systemd/system/quantengine.service'
# 파일 검증
ssh kjh2064@178.104.200.7 'ls -la /home/kjh2064/quantengine_active/'
```
### Nginx 프록시 오류
```bash
# Nginx 설정 테스트
ssh kjh2064@178.104.200.7 'sudo nginx -t'
# 설정 파일 확인
ssh kjh2064@178.104.200.7 'cat /etc/nginx/sites-available/gitea-ip.conf'
# 포트 확인
ssh kjh2064@178.104.200.7 'sudo netstat -tuln | grep 5000'
```
### 파일 권한 문제
```bash
# 현재 권한 확인
ssh kjh2064@178.104.200.7 'ls -la /home/kjh2064/quantengine_active/'
# 권한 설정 (필요시)
ssh kjh2064@178.104.200.7 'chmod +x /home/kjh2064/quantengine_active/QuantEngine.Web.dll'
```
---
## 📚 관련 파일
```
배포 스크립트:
├── deploy-production.sh (권장)
└── deploy-manual.sh (대화형)
배포 문서:
├── DEPLOYMENT_GUIDE.md (전체)
├── DEPLOYMENT_STEPS.md (단계별)
├── DEPLOYMENT_SSH_GUIDE.md (이 파일)
└── DEPLOYMENT_CHECKLIST.md (체크리스트)
CI/CD:
├── .gitea/workflows/deploy-prod.yml
└── CI_CD_PIPELINE.md
환경:
└── ENVIRONMENT_DIAGNOSIS.md
```
---
## ⚡ 빠른 배포 명령어
### 한 번에 배포
```bash
chmod +x deploy-production.sh && ./deploy-production.sh
```
### 내부 IP 사용 (선택)
```bash
./deploy-manual.sh 172.17.0.1
```
### 공인 IP 사용 (권장)
```bash
./deploy-manual.sh 178.104.200.7
```
### 상태 확인
```bash
ssh kjh2064@178.104.200.7 'sudo systemctl status quantengine'
```
### 로그 모니터링
```bash
ssh kjh2064@178.104.200.7 'sudo journalctl -u quantengine -f'
```
---
**배포 준비 완료!** 🚀
`deploy-production.sh` 또는 `deploy-manual.sh` 스크립트를 실행하거나, 위의 수동 단계를 따라 배포하세요.
+210
View File
@@ -0,0 +1,210 @@
# 🔍 원격 서버 환경 진단
**목표**: SSH로 접속하여 원격 서버의 정확한 구조와 설정을 파악한 후 배포 스크립트를 맞춤형으로 작성
---
## 📋 진단 절차
### Step 1: SSH 접속
```bash
# 원격 서버에 SSH 접속
ssh kjh2064@178.104.200.7
# 또는 이미 내부 IP를 알고 있다면
ssh kjh2064@172.x.x.x
```
### Step 2: 진단 스크립트 실행
```bash
# 로컬에서 스크립트를 원격으로 실행
ssh kjh2064@178.104.200.7 'bash -s' < diagnose-environment.sh
# 또는 원격에 접속한 후 실행
bash < <(curl -s https://raw.githubusercontent.com/.../diagnose-environment.sh)
# 또는 직접 실행
chmod +x diagnose-environment.sh
./diagnose-environment.sh
```
### Step 3: 출력 결과 확인
진단 스크립트가 다음 정보를 제공합니다:
```
1. 네트워크 정보
- 공인 IP: 178.104.200.7 (확인됨)
- 내부 IP: 172.x.x.x (여기서 확인!)
- 호스트명
- 네트워크 인터페이스
2. 웹 서버 디렉토리 구조
- /var/www 여부
- /var/www/quant 여부
- /var/www/quant/publish 여부
- 실제 경로 (다를 수 있음)
3. Nginx 설정
- Nginx 설치 확인
- 설정 파일 위치
- /quant 관련 설정
4. 파일 권한 및 소유자
- 웹 서버 사용자 (www-data? nobody? 다른 사용자?)
- 디렉토리 권한
5. 포트 상태
- 80, 443 포트 상태
- 바인딩된 주소
6. 시스템 정보
- OS 종류 및 버전
- 디스크 공간
7. Sudo 권한
- 현재 사용자의 sudo 권한
- systemctl 사용 가능 여부
8. Git/Gitea 정보
- Gitea 설치 위치
- Gitea 데이터 저장소
```
---
## 📊 진단 결과 분석
### 예상되는 출력 값들
| 항목 | 예상값 | 실제값 |
|------|--------|--------|
| **공인 IP** | 178.104.200.7 | ✓ |
| **내부 IP** | 172.x.x.x | ? |
| **웹 서버 경로** | /var/www/quant | ? |
| **웹 서버 사용자** | www-data | ? |
| **Nginx 설정** | /etc/nginx/sites-available/default | ? |
| **OS** | Ubuntu 20.04+ | ? |
### 확인할 핵심 정보
1. **내부 IP 주소** (172로 시작)
```
ip addr show | grep "inet"
→ inet 172.x.x.x/xx
```
2. **웹 서버 경로**
```
ls -la /var/www/quant/
→ 실제 배포 경로 확인
```
3. **웹 서버 사용자**
```
ps aux | grep nginx | head -1
→ nginx 12345 0.0 0.1 ...
```
4. **Nginx 설정**
```
grep -r "quant" /etc/nginx/
→ location /quant 설정 확인
```
5. **Sudo 권한**
```
sudo -l
→ systemctl restart nginx 권한 확인
```
---
## 🔧 스크립트 결과 보고 양식
진단 스크립트 실행 후 다음 정보를 제공해주세요:
### 네트워크 정보
- 내부 IP: `172.x.x.x` 또는 다른 주소?
- 호스트명: ?
- 기본 게이트웨이: ?
### 디렉토리 구조
- /var/www 존재: O / X
- /var/www/quant 존재: O / X
- /var/www/quant/publish 존재: O / X
- 실제 웹 서빙 경로: ?
### Nginx 설정
- Nginx 버전: ?
- 설정 파일: /etc/nginx/sites-available/default 또는 다른 경로?
- /quant 설정 있음: O / X
- 루트 경로: ?
### 파일 권한
- 웹 서버 사용자: www-data 또는 ?
- /var/www/quant 소유자: ?
- /var/www/quant 권한: ?
### 시스템 정보
- OS: Ubuntu 20.04 또는 ?
- 디스크 여유: ?MB
### Sudo 권한
- sudo -l 출력:
```
복사해주세요
```
---
## 📝 수집 후 수행할 작업
위 정보를 받은 후:
1. ✅ 정확한 내부 IP로 배포 스크립트 수정
2. ✅ 실제 경로로 deploy-manual.sh 수정
3. ✅ 웹 서버 사용자로 권한 설정 수정
4. ✅ Nginx 설정에 맞게 배포 절차 수정
5. ✅ 모든 문서 (DEPLOYMENT_SSH_GUIDE.md, CI_CD_PIPELINE.md 등) 업데이트
---
## 🚀 빠른 진단 (한 줄 명령어)
```bash
# SSH 접속 후 한 번에 필요한 정보만 추출
echo "=== 내부 IP ===" && ip addr show | grep "inet " | grep -v 127.0.0.1 && \
echo "=== 웹 서버 경로 ===" && ls -la /var/www/ && \
echo "=== Nginx 사용자 ===" && ps aux | grep nginx | head -1 && \
echo "=== Sudo 권한 ===" && sudo -l | head -5
```
---
## ⚡ 진단 후 다음 단계
1. **진단 결과 공유**
- 위의 "스크립트 결과 보고 양식" 내용을 제공해주세요
2. **배포 스크립트 수정**
- 정확한 정보를 바탕으로 deploy-manual.sh 맞춤 수정
- 내부 IP, 경로, 사용자 등 정확히 반영
3. **배포 실행**
```bash
chmod +x deploy-manual.sh
./deploy-manual.sh [실제_내부_IP]
```
4. **검증**
```bash
curl -I http://178.104.200.7/quant/
```
---
**진단을 완료한 후 결과를 공유해주세요!**
정확한 환경 정보를 바탕으로 완벽하게 맞춤형 배포 스크립트를 작성하겠습니다. 🎯
@@ -1,6 +1,8 @@
#!/bin/bash
# Quant Engine Manual Deployment Script
# 원격 서버에 직접 SSH 접속하여 환경 파악 후 배포
# Quant Engine Manual Deployment Script (v9)
# 환경: hz-prod-01 (178.104.200.7/172.17.0.1)
# 배포 경로: /home/kjh2064/quantengine_active
# 서비스: quantengine (systemd)
set -e
@@ -8,18 +10,23 @@ set -e
# 설정
# ═══════════════════════════════════════════════════════════════
DEPLOY_HOST="${1:-192.168.123.100}"
DEPLOY_HOST="${1:-178.104.200.7}"
DEPLOY_USER="kjh2064"
SSH_KEY="${HOME}/.ssh/id_ed25519"
LOCAL_PUBLISH_DIR="$(pwd)/src/dotnet/QuantEngine.Web/publish"
REMOTE_DEPLOY_PATH="/var/www/quant"
REMOTE_DEPLOY_PATH="/home/kjh2064/quantengine_active"
REMOTE_BACKUP_PATH="/home/kjh2064/quantengine_backup"
SERVICE_NAME="quantengine"
echo "🚀 Quant Engine Manual Deployment"
echo "🚀 Quant Engine v9 Manual Deployment"
echo "═══════════════════════════════════════════════════════════════"
echo "Deploy Host: $DEPLOY_HOST"
echo "Deploy User: $DEPLOY_USER"
echo "Local Path: $LOCAL_PUBLISH_DIR"
echo "Remote Path: $REMOTE_DEPLOY_PATH"
echo "Deploy Host: $DEPLOY_HOST"
echo "Deploy User: $DEPLOY_USER"
echo "Local Path: $LOCAL_PUBLISH_DIR"
echo "Remote Path: $REMOTE_DEPLOY_PATH"
echo "Backup Path: $REMOTE_BACKUP_PATH"
echo "Service: $SERVICE_NAME"
echo "Public URL: http://178.104.200.7/quant/"
echo "═══════════════════════════════════════════════════════════════"
echo ""
@@ -33,6 +40,7 @@ ssh -i "$SSH_KEY" "$DEPLOY_USER@$DEPLOY_HOST" << 'ENVCHECK'
echo "✓ SSH 연결 성공"
echo ""
echo "시스템 정보:"
hostname
uname -a
echo ""
@@ -41,25 +49,25 @@ df -h | grep -E "^/dev|Filesystem|/$"
echo ""
echo "서비스 상태:"
sudo systemctl status nginx --no-pager 2>/dev/null | grep -E "Active:|Loaded:" || echo "⚠️ nginx 상태 확인 필요"
sudo systemctl status "$SERVICE_NAME" --no-pager 2>/dev/null | grep -E "Active:|Loaded:" || echo "⚠️ 서비스 상태 확인 필요"
echo ""
echo "웹 서버 디렉토리:"
if [ -d /var/www/quant/publish ]; then
echo "✓ /var/www/quant/publish 존재"
ls -lh /var/www/quant/publish | head -5
echo "배포 디렉토리:"
if [ -d "/home/kjh2064/quantengine_active" ]; then
echo "✓ /home/kjh2064/quantengine_active 존재"
ls -lh /home/kjh2064/quantengine_active | head -5
echo "..."
else
echo "✗ /var/www/quant/publish 없음 (첫 배포)"
echo "✗ /home/kjh2064/quantengine_active 없음 (첫 배포)"
fi
echo ""
echo "웹 서버 권한:"
ls -ld /var/www/quant 2>/dev/null || echo "⚠️ /var/www/quant 없음"
echo "Nginx 포트 확인:"
sudo netstat -tuln 2>/dev/null | grep ":80\|:443" || echo "⚠️ 포트 확인 필요"
echo ""
echo "Nginx 포트 확인:"
sudo netstat -tuln 2>/dev/null | grep :80 || echo "⚠️ 포트 80 확인 필요"
echo "Nginx 설정:"
cat /etc/nginx/sites-available/gitea-ip.conf | grep -A 5 "location /quant" || echo "⚠️ Nginx 설정 확인 필요"
ENVCHECK
echo ""
@@ -105,20 +113,27 @@ fi
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 4: 백업 생성
# Step 4: 서비스 중지 및 백업 생성
# ═══════════════════════════════════════════════════════════════
echo "💾 Step 3: 백업 생성..."
echo "💾 Step 3: 서비스 중지 및 백업 생성..."
ssh -i "$SSH_KEY" "$DEPLOY_USER@$DEPLOY_HOST" << 'BACKUP'
set -e
BACKUP_DIR="/var/www/quant_backup"
BACKUP_NAME="quant_backup_$(date +%Y%m%d_%H%M%S)"
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
BACKUP_DIR="/home/kjh2064/quantengine_backup"
BACKUP_NAME="quantengine_$(date +%Y%m%d_%H%M%S)"
if [ -d /var/www/quant/publish ]; then
sudo mkdir -p "$BACKUP_DIR"
sudo cp -r /var/www/quant/publish "$BACKUP_DIR/$BACKUP_NAME"
echo " 서비스 중지 중..."
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
sleep 2
echo " ✓ 서비스 중지"
mkdir -p "$BACKUP_DIR"
if [ -d "$DEPLOY_PATH" ]; then
cp -r "$DEPLOY_PATH" "$BACKUP_DIR/$BACKUP_NAME"
echo "✓ 백업 생성: $BACKUP_DIR/$BACKUP_NAME"
# 최근 5개만 유지
@@ -126,12 +141,13 @@ if [ -d /var/www/quant/publish ]; then
if [ "$BACKUP_COUNT" -gt 5 ]; then
OLD_BACKUPS=$(ls -1t "$BACKUP_DIR" | tail -n +6)
for backup in $OLD_BACKUPS; do
sudo rm -rf "$BACKUP_DIR/$backup"
rm -rf "$BACKUP_DIR/$backup"
echo "🧹 오래된 백업 삭제: $backup"
done
fi
else
echo "⚠️ 기존 배포 없음 (첫 배포)"
mkdir -p "$DEPLOY_PATH"
fi
BACKUP
@@ -146,7 +162,7 @@ echo "📤 Step 4: 파일 전송 (rsync)..."
rsync -avz --delete \
--rsh="ssh -i $SSH_KEY" \
"$LOCAL_PUBLISH_DIR/" \
"$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_DEPLOY_PATH/publish/"
"$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_DEPLOY_PATH/"
echo "✓ 파일 전송 완료"
echo ""
@@ -155,26 +171,31 @@ echo ""
# Step 6: 권한 설정 및 서비스 재시작
# ═══════════════════════════════════════════════════════════════
echo "🔧 Step 5: 권한 설정 및 서비스 시작..."
echo "🔧 Step 5: 파일 검증 및 서비스 시작..."
ssh -i "$SSH_KEY" "$DEPLOY_USER@$DEPLOY_HOST" << 'FINALIZE'
set -e
DEPLOY_PATH="/var/www/quant"
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
echo " 권한 설정 중..."
sudo chown -R www-data:www-data "$DEPLOY_PATH/publish" 2>/dev/null || true
sudo chmod -R 755 "$DEPLOY_PATH/publish" 2>/dev/null || true
echo " ✓ 권한 설정 완료"
echo " nginx 재시작 중..."
sudo systemctl restart nginx 2>/dev/null || echo " ⚠️ nginx 재시작 실패 (sudo 권한 확인)"
sleep 2
if sudo systemctl is-active --quiet nginx 2>/dev/null; then
echo " ✓ nginx 재시작 완료"
echo " 파일 검증 중..."
if [ -f "$DEPLOY_PATH/QuantEngine.Web.dll" ]; then
echo " ✓ QuantEngine.Web.dll 확인됨"
else
echo " ⚠️ nginx 상태 확인 필요"
echo " ❌ QuantEngine.Web.dll 없음 (배포 실패)"
exit 1
fi
echo " 서비스 시작 중..."
sudo systemctl start "$SERVICE_NAME" 2>/dev/null || echo " ⚠️ 서비스 시작 실패"
sleep 3
if sudo systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
echo "$SERVICE_NAME 시작 완료"
else
echo " ⚠️ 서비스 상태 확인"
sudo systemctl status "$SERVICE_NAME" || true
fi
FINALIZE
@@ -186,16 +207,16 @@ echo ""
echo "🧪 Step 6: 헬스 체크..."
for i in {1..5}; do
for i in {1..30}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
http://$DEPLOY_HOST/quant/ 2>/dev/null || echo "000")
"http://$DEPLOY_HOST/quant/" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "✓ Health check PASS (HTTP 200)"
break
fi
echo " 시도 $i/5: HTTP $HTTP_CODE (대기 중...)"
echo " 시도 $i/30: HTTP $HTTP_CODE (대기 중...)"
sleep 2
done
@@ -210,17 +231,23 @@ echo "✅ 배포 완료!"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo "📊 배포 정보:"
echo " URL: http://$DEPLOY_HOST/quant/"
echo " 경로: $REMOTE_DEPLOY_PATH/publish"
echo " 크기: $PACKAGE_SIZE"
echo " 공인 URL: http://$DEPLOY_HOST/quant/"
echo " 배포 경로: $REMOTE_DEPLOY_PATH"
echo " 백업 경로: $REMOTE_BACKUP_PATH"
echo " 서비스: $SERVICE_NAME"
echo " 패키지 크기: $PACKAGE_SIZE"
echo ""
echo "🌐 구조:"
echo " Nginx: reverse proxy /quant/ → localhost:5000"
echo " 설정: /etc/nginx/sites-available/gitea-ip.conf"
echo ""
echo "🔍 로그 확인:"
echo " ssh -i $SSH_KEY $DEPLOY_USER@$DEPLOY_HOST 'sudo tail -50 /var/log/nginx/error.log'"
echo " ssh -i $SSH_KEY $DEPLOY_USER@$DEPLOY_HOST 'sudo journalctl -u $SERVICE_NAME -f'"
echo ""
echo "🔄 롤백 (필요시):"
echo " ssh -i $SSH_KEY $DEPLOY_USER@$DEPLOY_HOST << 'EOF'"
echo " LATEST=\$(ls -t /var/www/quant_backup | head -1)"
echo " sudo cp -r /var/www/quant_backup/\$LATEST/* /var/www/quant/publish/"
echo " sudo systemctl restart nginx"
echo " LATEST=\$(ls -t $REMOTE_BACKUP_PATH | head -1)"
echo " cp -r $REMOTE_BACKUP_PATH/\$LATEST/* $REMOTE_DEPLOY_PATH/"
echo " sudo systemctl restart $SERVICE_NAME"
echo " EOF"
echo ""
+225
View File
@@ -0,0 +1,225 @@
#!/bin/bash
# Quant Engine Production Deployment Script (v9)
# 환경: hz-prod-01, 공인IP 178.104.200.7, 내부 172.17.0.1
# 배포 경로: /home/kjh2064/quantengine_active
# Nginx 설정: /etc/nginx/sites-available/gitea-ip.conf (reverse proxy → localhost:5000)
set -e
# ═══════════════════════════════════════════════════════════════
# 설정
# ═══════════════════════════════════════════════════════════════
DEPLOY_HOST="178.104.200.7"
DEPLOY_INTERNAL_IP="172.17.0.1"
DEPLOY_USER="kjh2064"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
SERVICE_NAME="quantengine"
BACKUP_PATH="/home/kjh2064/quantengine_backup"
LOCAL_PUBLISH_DIR="$(pwd)/src/dotnet/QuantEngine.Web/publish"
echo "🚀 Quant Engine v9 Production Deployment"
echo "═══════════════════════════════════════════════════════════════"
echo "Public URL: http://$DEPLOY_HOST/quant/"
echo "Internal IP: $DEPLOY_INTERNAL_IP"
echo "Deploy Path: $DEPLOY_PATH"
echo "Service: $SERVICE_NAME"
echo "Backup Path: $BACKUP_PATH"
echo "Hostname: hz-prod-01"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 1: 배포 파일 준비
# ═══════════════════════════════════════════════════════════════
echo "📦 Step 1: 배포 파일 확인..."
if [ ! -d "$LOCAL_PUBLISH_DIR" ]; then
echo "❌ 오류: $LOCAL_PUBLISH_DIR 없음"
echo "먼저 'dotnet publish -c Release'를 실행하세요"
exit 1
fi
PACKAGE_SIZE=$(du -sh "$LOCAL_PUBLISH_DIR" | cut -f1)
FILE_COUNT=$(find "$LOCAL_PUBLISH_DIR" -type f | wc -l)
echo "✓ 배포 패키지:"
echo " 크기: $PACKAGE_SIZE"
echo " 파일 수: $FILE_COUNT"
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 2: SSH 연결 확인
# ═══════════════════════════════════════════════════════════════
echo "🔐 Step 2: SSH 연결 확인..."
if ! ssh -o ConnectTimeout=10 "$DEPLOY_USER@$DEPLOY_HOST" "echo '✅ SSH 연결 성공'" &>/dev/null; then
echo "❌ SSH 연결 실패"
exit 1
fi
echo "✓ SSH 연결 확인됨"
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 3: 배포 전 확인
# ═══════════════════════════════════════════════════════════════
echo "✅ 배포 전 확인:"
echo " [ ] Release 빌드 완료됨 ($PACKAGE_SIZE)"
echo " [ ] SSH 연결 가능"
echo ""
read -p "배포를 진행하시겠습니까? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "❌ 배포 취소됨"
exit 1
fi
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 4: 서비스 중지 및 백업 생성
# ═══════════════════════════════════════════════════════════════
echo "🛑 Step 3: 서비스 중지 및 백업 생성..."
ssh "$DEPLOY_USER@$DEPLOY_HOST" << 'EOF'
set -e
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
BACKUP_PATH="/home/kjh2064/quantengine_backup"
BACKUP_NAME="quantengine_$(date +%Y%m%d_%H%M%S)"
echo " 서비스 중지 중..."
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
sleep 2
echo " ✓ 서비스 중지 완료"
echo " 백업 생성 중..."
mkdir -p "$BACKUP_PATH"
if [ -d "$DEPLOY_PATH" ]; then
cp -r "$DEPLOY_PATH" "$BACKUP_PATH/$BACKUP_NAME"
echo " ✓ 백업 생성: $BACKUP_PATH/$BACKUP_NAME"
# 최근 5개만 유지
BACKUP_COUNT=$(ls -1 "$BACKUP_PATH" | wc -l)
if [ "$BACKUP_COUNT" -gt 5 ]; then
OLD_BACKUPS=$(ls -1t "$BACKUP_PATH" | tail -n +6)
for backup in $OLD_BACKUPS; do
rm -rf "$BACKUP_PATH/$backup"
echo " 🧹 오래된 백업 삭제: $backup"
done
fi
else
echo " ⚠️ 기존 배포 없음 (첫 배포)"
mkdir -p "$DEPLOY_PATH"
fi
EOF
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 5: 파일 전송
# ═══════════════════════════════════════════════════════════════
echo "📤 Step 4: 파일 전송 (rsync)..."
rsync -avz --delete \
--rsh="ssh" \
"$LOCAL_PUBLISH_DIR/" \
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
echo "✓ 파일 전송 완료"
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 6: 서비스 시작
# ═══════════════════════════════════════════════════════════════
echo "🚀 Step 5: 서비스 시작..."
ssh "$DEPLOY_USER@$DEPLOY_HOST" << 'EOF'
set -e
SERVICE_NAME="quantengine"
DEPLOY_PATH="/home/kjh2064/quantengine_active"
echo " 파일 검증 중..."
if [ -f "$DEPLOY_PATH/QuantEngine.Web.dll" ]; then
echo " ✓ QuantEngine.Web.dll 확인됨"
else
echo " ❌ QuantEngine.Web.dll 없음 (배포 실패)"
exit 1
fi
echo " 서비스 시작 중..."
sudo systemctl start "$SERVICE_NAME"
sleep 3
if sudo systemctl is-active --quiet "$SERVICE_NAME"; then
echo "$SERVICE_NAME 시작 완료"
else
echo "$SERVICE_NAME 시작 실패"
sudo systemctl status "$SERVICE_NAME" || true
exit 1
fi
EOF
echo ""
# ═══════════════════════════════════════════════════════════════
# Step 7: 헬스 체크
# ═══════════════════════════════════════════════════════════════
echo "🧪 Step 6: 헬스 체크..."
for i in {1..30}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"http://$DEPLOY_HOST/quant/" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "✓ Health check PASS (HTTP 200)"
break
fi
echo " 시도 $i/30: HTTP $HTTP_CODE (대기 중...)"
sleep 2
done
echo ""
# ═══════════════════════════════════════════════════════════════
# 배포 완료
# ═══════════════════════════════════════════════════════════════
echo "═══════════════════════════════════════════════════════════════"
echo "✅ 배포 완료!"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo "📊 배포 정보:"
echo " 공인 URL: http://$DEPLOY_HOST/quant/"
echo " 내부 IP: $DEPLOY_INTERNAL_IP"
echo " 배포 경로: $DEPLOY_PATH"
echo " 서비스: $SERVICE_NAME"
echo " 백업: $BACKUP_PATH"
echo ""
echo "🔍 로그 확인:"
echo " ssh $DEPLOY_USER@$DEPLOY_HOST 'sudo journalctl -u $SERVICE_NAME -f'"
echo ""
echo "🔄 롤백 (필요시):"
echo " ssh $DEPLOY_USER@$DEPLOY_HOST << 'ROLLBACK'"
echo " LATEST=\$(ls -t $BACKUP_PATH | head -1)"
echo " cp -r $BACKUP_PATH/\$LATEST/* $DEPLOY_PATH/"
echo " sudo systemctl restart $SERVICE_NAME"
echo " ROLLBACK"
echo ""
echo "🌐 Nginx 역방향 프록시 구조:"
echo " 공인 IP:178.104.200.7/quant/ → localhost:5000 (Nginx reverse proxy)"
echo " Nginx 설정: /etc/nginx/sites-available/gitea-ip.conf"
echo ""
View File
+202
View File
@@ -0,0 +1,202 @@
#!/bin/bash
# 원격 서버 환경 진단 스크립트
# SSH로 접속한 후 이 스크립트를 실행하여 환경 정보를 수집합니다.
echo "═══════════════════════════════════════════════════════════════"
echo " 원격 서버 환경 진단"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# 1. 네트워크 정보
echo "1️⃣ 네트워크 정보"
echo "───────────────────────────────────────────────────────────────"
echo "공인 IP (외부에서 접속 가능):"
curl -s https://api.ipify.org
echo ""
echo "내부 IP 목록:"
ip addr show | grep -E "inet |inet6 " | grep -v "127.0.0.1"
echo ""
echo "호스트명:"
hostname
echo ""
echo "네트워크 인터페이스:"
ip link show | grep -E "^[0-9]+:|UP|DOWN"
echo ""
# 2. 디렉토리 구조
echo "2️⃣ 웹 서버 디렉토리 구조"
echo "───────────────────────────────────────────────────────────────"
# /var/www 확인
if [ -d /var/www ]; then
echo "✓ /var/www 존재"
ls -la /var/www/ | head -20
else
echo "✗ /var/www 없음"
fi
echo ""
# /var/www/quant 확인
if [ -d /var/www/quant ]; then
echo "✓ /var/www/quant 존재"
ls -la /var/www/quant/
du -sh /var/www/quant/*
else
echo "✗ /var/www/quant 없음"
fi
echo ""
# /var/www/quant/publish 확인
if [ -d /var/www/quant/publish ]; then
echo "✓ /var/www/quant/publish 존재"
ls -la /var/www/quant/publish/ | head -10
du -sh /var/www/quant/publish
else
echo "✗ /var/www/quant/publish 없음 (첫 배포)"
fi
echo ""
# 3. Nginx 설정
echo "3️⃣ Nginx 설정"
echo "───────────────────────────────────────────────────────────────"
if command -v nginx &> /dev/null; then
echo "✓ Nginx 설치됨"
nginx -v
echo ""
echo "Nginx 설정 파일 위치:"
nginx -T 2>/dev/null | grep "configuration file" | head -1
echo ""
echo "Nginx 실행 사용자:"
ps aux | grep nginx | grep -v grep | head -1
echo ""
echo "/quant 관련 설정:"
cat /etc/nginx/sites-available/default 2>/dev/null | grep -A 10 -B 2 "quant" || echo "quant 관련 설정 없음"
echo ""
else
echo "✗ Nginx 미설치"
fi
echo ""
# 4. 웹 서버 권한
echo "4️⃣ 파일 권한 및 소유자"
echo "───────────────────────────────────────────────────────────────"
echo "웹 서버 사용자:"
ps aux | grep -E "nginx|apache" | grep -v grep | head -1 | awk '{print $1}' || echo "확인 필요"
echo ""
echo "/var/www 권한:"
ls -ld /var/www
echo ""
if [ -d /var/www/quant ]; then
echo "/var/www/quant 권한:"
ls -ld /var/www/quant
echo ""
fi
if [ -d /var/www/quant/publish ]; then
echo "/var/www/quant/publish 권한:"
ls -ld /var/www/quant/publish
echo ""
fi
echo ""
# 5. 포트 상태
echo "5️⃣ 포트 상태"
echo "───────────────────────────────────────────────────────────────"
netstat -tuln 2>/dev/null | grep -E "^Proto|:80|:443" || ss -tuln | grep -E "LISTEN|:80|:443"
echo ""
echo ""
# 6. 시스템 정보
echo "6️⃣ 시스템 정보"
echo "───────────────────────────────────────────────────────────────"
echo "OS:"
uname -a
echo ""
echo "Linux 배포판:"
lsb_release -a 2>/dev/null || cat /etc/os-release | head -3
echo ""
echo "디스크 공간:"
df -h | grep -E "^/dev|Filesystem"
echo ""
echo ""
# 7. Sudo 권한
echo "7️⃣ 현재 사용자 정보"
echo "───────────────────────────────────────────────────────────────"
echo "현재 사용자:"
whoami
echo ""
echo "사용자 그룹:"
groups
echo ""
echo "Sudo 권한:"
sudo -l 2>/dev/null | grep -E "NOPASSWD|nginx|systemctl" || echo "sudo 권한 확인 필요"
echo ""
echo ""
# 8. Git/Gitea 정보
echo "8️⃣ Git/Gitea 정보"
echo "───────────────────────────────────────────────────────────────"
if command -v git &> /dev/null; then
echo "✓ Git 설치됨"
git --version
else
echo "✗ Git 미설치"
fi
echo ""
if command -v gitea &> /dev/null; then
echo "✓ Gitea 설치됨"
gitea -v
else
echo "✗ Gitea 미설치"
fi
echo ""
if [ -d /var/lib/gitea ] || [ -d /home/git/gitea-repositories ]; then
echo "Gitea 데이터 위치:"
[ -d /var/lib/gitea ] && echo " /var/lib/gitea"
[ -d /home/git/gitea-repositories ] && echo " /home/git/gitea-repositories"
else
echo "Gitea 데이터 위치: 확인 필요"
fi
echo ""
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo "✅ 진단 완료"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo "위 정보를 바탕으로 배포 스크립트를 업데이트합니다."
echo ""
echo "특히 확인할 사항:"
echo " 1. 내부 IP 주소 (172로 시작하는 IP)"
echo " 2. /var/www/quant 경로 (또는 다른 경로?)"
echo " 3. 웹 서버 사용자 (www-data? nobody? 다른 사용자?)"
echo " 4. Nginx 설정 파일 위치"
echo " 5. /quant에 대한 nginx 설정"
echo ""
+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