Compare commits

..

11 Commits

Author SHA1 Message Date
990f9696ab Merge pull request 'docs: TPM 티켓 우선순위/메인 아이디에이션/무머지 세션 규칙 반영 (#277)' (#278) from feature/issue-277-tpm-priority-main-ideation-no-merge-session into main
Some checks are pending
CI / test (push) Waiting to run
2026-02-26 23:58:03 +09:00
agentson
9bf72c63ec docs: clarify no-merge-by-default server reflection rule (#277)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:57:58 +09:00
agentson
1399fa4d09 docs: enforce TPM ticket ownership and add main-agent ideation backlog (#277)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:56:25 +09:00
f63fb53289 Merge pull request 'feat: phase1 상태기계/청산엔진/kill-switch 구현 (#275)' (#276) from feature/issue-275-phase1-state-exit-killswitch into main
Some checks failed
CI / test (push) Has been cancelled
2026-02-26 23:46:11 +09:00
agentson
5050a4cf84 fix: address reviewer feedback for kill-switch enforcement and observability (#275)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:46:02 +09:00
agentson
4987b6393a feat: implement phase1 state machine, composite exits, and kill-switch orchestration (#275)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:22:58 +09:00
8faf974522 Merge pull request 'docs: multi-agent governance 운영 체계 반영 (#273)' (#274) from feature/issue-273-multi-agent-governance-docs into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #274
2026-02-26 23:19:11 +09:00
agentson
d524159ad0 docs: add runtime verifier role and replan escalation protocol (#273)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:16:44 +09:00
agentson
c7c740f446 docs: add repository enforcement checklist for strict governance (#273)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:08:16 +09:00
agentson
1333c65455 docs: add PM/TPM governance artifacts for multi-agent control (#273)
Some checks are pending
CI / test (pull_request) Waiting to run
2026-02-26 23:06:51 +09:00
9db7f903f8 Merge pull request 'docs: ouroboros 실행 지시서/검증 시스템 반영 (#271)' (#272) from feature/issue-271-docs-routing-validation into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #272
2026-02-26 22:56:17 +09:00
13 changed files with 928 additions and 2 deletions

View File

@@ -0,0 +1,68 @@
<!--
Doc-ID: DOC-PM-001
Version: 1.0.0
Status: active
Owner: strategy
Updated: 2026-02-26
-->
# 실전 시나리오 매트릭스 + 이슈 분류 체계
목표: 운영에서 바로 사용할 수 있는 형태로 Happy Path / Failure Path / Ops Incident를 추적 가능한 ID 체계(`REQ-*`, `TASK-*`, `TEST-*`)에 매핑한다.
## 1) 시나리오 매트릭스
| Scenario ID | Type | Trigger | Expected System Behavior | Primary IDs (REQ/TASK/TEST) | Ticket Priority |
|---|---|---|---|---|---|
| `SCN-HAPPY-001` | Happy Path | KR 정규 세션에서 진입 신호 발생, 블랙아웃 아님 | 주문/로그에 `session_id` 저장 후 정책에 맞는 주문 전송 | `REQ-V3-001`, `TASK-V3-001`, `TASK-V3-003`, `TEST-ACC-015` | P1 |
| `SCN-HAPPY-002` | Happy Path | 보유 포지션에서 BE/ATR/Hard Stop 조건 순차 도달 | 상태가 즉시 상위 단계로 승격, `EXITED` 우선 평가 보장 | `REQ-V2-002`, `REQ-V2-003`, `TASK-V2-004`, `TEST-ACC-001`, `TEST-ACC-010` | P0 |
| `SCN-HAPPY-003` | Happy Path | 세션 전환(KR->US) 이벤트 발생 | 리스크 파라미터 자동 재로딩, 새 세션 정책으로 즉시 전환 | `REQ-V3-002`, `TASK-V3-002`, `TEST-ACC-016` | P0 |
| `SCN-HAPPY-004` | Happy Path | 백테스트 실행 요청 | 비용/슬리피지/체결실패 옵션 누락 시 실행 거부, 포함 시 실행 | `REQ-V2-007`, `TASK-V2-012`, `TEST-ACC-014` | P1 |
| `SCN-FAIL-001` | Failure Path | 블랙아웃 중 신규 주문 신호 발생 | 신규 주문 차단 + 주문 의도 큐 적재, API 직접 호출 금지 | `REQ-V3-003`, `REQ-V3-004`, `TASK-V3-005`, `TEST-ACC-003`, `TEST-ACC-017` | P0 |
| `SCN-FAIL-002` | Failure Path | 저유동 세션에 시장가 주문 요청 | 시장가 하드 거부, 지정가 대체 또는 주문 취소 | `REQ-V3-005`, `TASK-V3-007`, `TASK-V3-008`, `TEST-ACC-004` | P0 |
| `SCN-FAIL-003` | Failure Path | Kill Switch 트리거(손실/연결/리스크 한도) | 신규주문차단->미체결취소->재조회->리스크축소->스냅샷 순서 강제 | `REQ-V2-008`, `TASK-V2-013`, `TEST-ACC-002` | P0 |
| `SCN-FAIL-004` | Failure Path | FX 버퍼 부족 상태에서 US 진입 신호 | 전략 PnL/FX PnL 분리 집계 유지, 신규 진입 제한 | `REQ-V3-007`, `TASK-V3-013`, `TASK-V3-014`, `TEST-ACC-006` | P1 |
| `SCN-OPS-001` | Ops Incident | 브로커 점검/블랙아웃 종료 직후 | 잔고/미체결/체결 동기화 후 큐 재검증 통과 주문만 집행 | `REQ-V3-004`, `TASK-V3-006`, `TEST-ACC-017` | P0 |
| `SCN-OPS-002` | Ops Incident | 정책 수치가 코드에만 반영되고 원장 미수정 | 문서 검증에서 실패 처리, PR 병합 차단 | `REQ-OPS-002`, `TASK-OPS-002`, `TEST-ACC-008` | P0 |
| `SCN-OPS-003` | Ops Incident | 타임존 누락 로그/스케줄 데이터 유입 | KST/UTC 미표기 레코드 검증 실패 처리 | `REQ-OPS-001`, `TASK-OPS-001`, `TEST-ACC-007` | P1 |
| `SCN-OPS-004` | Ops Incident | 신규 REQ 추가 후 TASK/TEST 누락 | 추적성 게이트 실패, 구현 PR 병합 차단 | `REQ-OPS-003`, `TASK-OPS-003`, `TEST-ACC-009` | P0 |
| `SCN-OPS-005` | Ops Incident | 배포 후 런타임 이상 동작(주문오류/상태전이오류/정책위반) 탐지 | Runtime Verifier가 즉시 이슈 발행, Dev 수정 후 재관측으로 클로즈 판정 | `REQ-V2-008`, `REQ-V3-003`, `REQ-V3-005`, `TEST-ACC-002`, `TEST-ACC-003`, `TEST-ACC-004` | P0 |
## 2) 이슈 분류 체계 (Issue Taxonomy)
| Taxonomy | Definition | Typical Symptoms | Default Owner | Mapping Baseline |
|---|---|---|---|---|
| `EXEC-STATE` | 상태기계/청산 우선순위 위반 | EXIT 우선순위 깨짐, 상태 역행, 갭 대응 실패 | Strategy | `REQ-V2-001`~`REQ-V2-004`, `TASK-V2-004`~`TASK-V2-006`, `TEST-ACC-000`,`001`,`010`,`011` |
| `EXEC-POLICY` | 세션/주문 정책 위반 | 블랙아웃 주문 전송, 저유동 시장가 허용 | Broker/Execution | `REQ-V3-003`~`REQ-V3-005`, `TASK-V3-004`~`TASK-V3-009`, `TEST-ACC-003`,`004`,`017` |
| `BACKTEST-MODEL` | 백테스트 현실성/검증 무결성 위반 | 비용 옵션 off로 실행, 체결가 과낙관 | Research | `REQ-V2-006`,`REQ-V2-007`,`REQ-V3-006`, `TASK-V2-010`~`012`, `TASK-V3-010`~`012`, `TEST-ACC-013`,`014`,`005` |
| `RISK-EMERGENCY` | Kill Switch/리스크 비상 대응 실패 | 순서 위반, 차단 누락, 복구 절차 누락 | Risk | `REQ-V2-008`,`REQ-V3-008`, `TASK-V2-013`~`015`, `TASK-V3-015`, `TEST-ACC-002`,`018` |
| `FX-ACCOUNTING` | 환율/통화 버퍼 정책 위반 | 전략손익/환차손익 혼합 집계, 버퍼 미적용 | Risk + Data | `REQ-V3-007`, `TASK-V3-013`,`014`, `TEST-ACC-006` |
| `OPS-GOVERNANCE` | 문서/추적성/타임존 거버넌스 위반 | 원장 미수정, TEST 누락, 타임존 미표기 | PM + QA | `REQ-OPS-001`~`003`, `TASK-OPS-001`~`003`, `TEST-ACC-007`~`009` |
| `RUNTIME-VERIFY` | 실동작 모니터링 검증 | 배포 후 이상 현상, 간헐 오류, 테스트 미포착 회귀 | Runtime Verifier + TPM | 관련 `REQ/TASK/TEST`와 런타임 로그 증적 필수 |
## 3) 티켓 생성 규칙 (Implementable)
1. 모든 이슈는 `taxonomy + scenario_id`를 제목에 포함한다.
예: `[EXEC-POLICY][SCN-FAIL-001] blackout 주문 차단 누락`
2. 본문 필수 항목: 재현절차, 기대결과, 실제결과, 영향범위, 롤백/완화책.
3. 본문에 최소 1개 `REQ-*`, 1개 `TASK-*`, 1개 `TEST-*`를 명시한다.
4. 우선순위 기준:
- P0: 실주문 위험, Kill Switch, 블랙아웃/시장가 정책, 추적성 게이트 실패
- P1: 손익 왜곡 가능성(체결/FX/시간대), 운영 리스크 증가
- P2: 보고서/관측성 품질 이슈(거래 안전성 영향 없음)
5. Runtime Verifier가 발행한 `RUNTIME-VERIFY` 이슈는 Main Agent 확인 전 클로즈 금지.
## 4) 즉시 생성 권장 티켓 (초기 백로그)
- `TKT-P0-001`: `[EXEC-POLICY][SCN-FAIL-001]` 블랙아웃 차단 + 큐적재 + 복구 재검증 e2e 점검 (`REQ-V3-003`,`REQ-V3-004`)
- `TKT-P0-002`: `[RISK-EMERGENCY][SCN-FAIL-003]` Kill Switch 순서 강제 검증 자동화 (`REQ-V2-008`)
- `TKT-P0-003`: `[OPS-GOVERNANCE][SCN-OPS-004]` REQ/TASK/TEST 누락 시 PR 차단 게이트 상시 점검 (`REQ-OPS-003`)
- `TKT-P1-001`: `[FX-ACCOUNTING][SCN-FAIL-004]` FX 버퍼 위반 시 진입 제한 회귀 케이스 보강 (`REQ-V3-007`)
- `TKT-P1-002`: `[BACKTEST-MODEL][SCN-HAPPY-004]` 비용/슬리피지 미설정 백테스트 거부 UX 명확화 (`REQ-V2-007`)
- `TKT-P0-004`: `[RUNTIME-VERIFY][SCN-OPS-005]` 배포 후 런타임 이상 탐지/재현/클로즈 판정 절차 자동화
## 5) 운영 체크포인트
- 스프린트 계획 시 `P0` 시나리오 100% 테스트 통과를 출발 조건으로 둔다.
- 배포 승인 시 `SCN-FAIL-*`, `SCN-OPS-*` 관련 `TEST-ACC-*`를 우선 확인한다.
- 정책 변경 PR은 반드시 원장(`01_requirements_registry.md`) 선수정 후 진행한다.

View File

@@ -0,0 +1,192 @@
<!--
Doc-ID: DOC-TPM-001
Version: 1.0.0
Status: active
Owner: tpm
Updated: 2026-02-26
-->
# TPM Control Protocol (Main <-> PM <-> TPM <-> Dev <-> Verifier <-> Runtime Verifier)
목적:
- PM 시나리오가 구현 가능한 단위로 분해되고, 개발/검증이 동일 ID 체계(`REQ-*`, `TASK-*`, `TEST-*`)로 닫히도록 강제한다.
- 각 단계는 Entry/Exit gate를 통과해야 다음 단계로 이동 가능하다.
- 주요 의사결정 포인트마다 Main Agent의 승인/의견 확인을 강제한다.
## Team Roles
- Main Agent: 최종 취합/우선순위/승인 게이트 오너
- PM Agent: 시나리오/요구사항/티켓 관리
- TPM Agent: PM-Dev-검증 간 구현 가능성/달성률 통제, 티켓 등록 및 구현 우선순위 지정 오너
- Dev Agent: 구현 수행, 블로커 발생 시 재계획 요청
- Verifier Agent: 문서/코드/테스트 산출물 검증
- Runtime Verifier Agent: 실제 동작 모니터링, 이상 징후 이슈 발행, 수정 후 이슈 클로즈 판정
Main Agent 아이디에이션 책임:
- 진행 중 신규 구현 아이디어를 별도 문서에 누적 기록한다.
- 기록 위치: [70_main_agent_ideation.md](./70_main_agent_ideation.md)
- 각 항목은 `IDEA-*` 식별자, 배경, 기대효과, 리스크, 후속 티켓 후보를 포함해야 한다.
## Main Decision Checkpoints (Mandatory)
- DCP-01 범위 확정: Phase 0 종료 전 Main Agent 승인 필수
- DCP-02 요구사항 확정: Phase 1 종료 전 Main Agent 승인 필수
- DCP-03 구현 착수: Phase 2 종료 전 Main Agent 승인 필수
- DCP-04 배포 승인: Phase 4 종료 후 Main Agent 최종 승인 필수
## Phase Control Gates
### Phase 0: Scenario Intake and Scope Lock
Entry criteria:
- PM 시나리오가 사용자 가치, 실패 모드, 우선순위를 포함해 제출됨
- 영향 범위(모듈/세션/KR-US 시장)가 명시됨
Exit criteria:
- 시나리오가 `REQ-*` 후보에 1:1 또는 1:N 매핑됨
- 모호한 표현("개선", "최적화")은 측정 가능한 조건으로 치환됨
- 비범위 항목(out-of-scope) 명시
Control checks:
- PM/TPM 합의 완료
- Main Agent 승인(DCP-01)
- 산출물: 시나리오 카드, 초기 매핑 메모
### Phase 1: Requirement Registry Gate
Entry criteria:
- Phase 0 산출물 승인
- 변경 대상 요구사항 문서 식별 완료
Exit criteria:
- [01_requirements_registry.md](./01_requirements_registry.md)에 `REQ-*` 정의/수정 반영
-`REQ-*`가 최소 1개 `TASK-*`, 1개 `TEST-*`와 연결 가능 상태
- 시간/정책 수치는 원장 단일 소스로 확정(`REQ-OPS-001`,`REQ-OPS-002`)
Control checks:
- `python3 scripts/validate_ouroboros_docs.py` 통과
- Main Agent 승인(DCP-02)
- 산출물: 업데이트된 요구사항 원장
### Phase 2: Design and Work-Order Gate
Entry criteria:
- 요구사항 원장 갱신 완료
- 영향 모듈 분석 완료(상태기계, 주문정책, 백테스트, 세션)
Exit criteria:
- [10_phase_v2_execution.md](./10_phase_v2_execution.md), [20_phase_v3_execution.md](./20_phase_v3_execution.md), [30_code_level_work_orders.md](./30_code_level_work_orders.md)에 작업 분해 완료
- 각 작업은 구현 위치/제약/완료 조건을 가짐
- 위험 작업(Kill Switch, blackout, session transition)은 별도 롤백 절차 포함
Control checks:
- TPM이 `REQ -> TASK` 누락 여부 검토
- Main Agent 승인(DCP-03)
- 산출물: 승인된 Work Order 세트
### Phase 3: Implementation Gate
Entry criteria:
- 승인된 `TASK-*`가 브랜치 작업 단위로 분리됨
- 변경 범위별 테스트 계획이 PR 본문에 링크됨
Exit criteria:
- 코드 변경이 `TASK-*`에 대응되어 추적 가능
- 제약 준수(`src/core/risk_manager.py` 직접 수정 금지 등) 확인
- 신규 로직마다 최소 1개 테스트 추가 또는 기존 테스트 확장
Control checks:
- PR 템플릿 내 `REQ-*`/`TASK-*`/`TEST-*` 매핑 확인
- 산출물: 리뷰 가능한 PR
### Phase 4: Verification and Acceptance Gate
Entry criteria:
- 구현 PR ready 상태
- 테스트 케이스/픽스처 준비 완료
Exit criteria:
- [40_acceptance_and_test_plan.md](./40_acceptance_and_test_plan.md)의 해당 `TEST-ACC-*` 전부 통과
- 회귀 테스트 통과(`pytest -q`)
- 문서 검증 통과(`python3 scripts/validate_ouroboros_docs.py`)
Control checks:
- Verifier가 테스트 증적(로그/리포트/실행 커맨드) 첨부
- Runtime Verifier가 스테이징/실운영 모니터링 계획 승인
- 산출물: 수용 승인 레코드
### Phase 5: Release and Post-Release Control
Entry criteria:
- Phase 4 승인
- 운영 체크리스트 준비(세션 전환, 블랙아웃, Kill Switch)
Exit criteria:
- 배포 후 초기 관찰 윈도우에서 치명 경보 없음
- 신규 시나리오/회귀 이슈는 다음 Cycle의 Phase 0 입력으로 환류
- 요구사항/테스트 문서 버전 동기화 완료
Control checks:
- PM/TPM/Dev 3자 종료 확인
- Runtime Verifier가 운영 모니터링 이슈 상태(신규/진행/해결)를 리포트
- Main Agent 최종 승인(DCP-04)
- 산출물: 릴리즈 노트 + 후속 액션 목록
## Replan Protocol (Dev -> TPM)
- 트리거:
- 구현 불가능(기술적 제약/외부 API 제약)
- 예상 대비 개발 리소스 과다(공수/인력/의존성 급증)
- 절차:
1) Dev Agent가 `REPLAN-REQUEST` 발행(영향 REQ/TASK, 원인, 대안, 추가 공수 포함)
2) TPM Agent가 1차 심사(범위 축소/단계 분할/요구사항 조정안)
3) Verifier/PM 의견 수렴 후 Main Agent 승인으로 재계획 확정
- 규칙:
- Main Agent 승인 없는 재계획은 실행 금지
- 재계획 반영 시 문서(`REQ/TASK/TEST`) 동시 갱신 필수
TPM 티켓 운영 규칙:
- TPM은 합의된 변경을 이슈로 등록하고 우선순위(`P0/P1/P2`)를 지정한다.
- PR 본문에는 TPM이 지정한 우선순위와 범위가 그대로 반영되어야 한다.
- 우선순위 변경은 TPM 제안 + Main Agent 승인으로만 가능하다.
## Runtime Verification Protocol
- Runtime Verifier는 테스트 통과 이후 실제 동작(스테이징/실운영)을 모니터링한다.
- 이상 동작/현상 발견 시 즉시 이슈 발행:
- 제목 규칙: `[RUNTIME-VERIFY][SCN-*] ...`
- 본문 필수: 재현조건, 관측 로그, 영향 범위, 임시 완화책, 관련 `REQ/TASK/TEST`
- 이슈 클로즈 규칙:
- Dev 수정 완료 + Verifier 재검증 통과 + Runtime Verifier 재관측 정상
- 최종 클로즈 승인자는 Main Agent
## Server Reflection Rule (No-Merge by Default)
- 서버 반영 기본 규칙은 `브랜치 푸시 + PR 생성/코멘트`까지로 제한한다.
- 기본 흐름에서 검증 승인 후 자동/수동 머지 실행은 금지한다.
- 예외는 사용자 명시 승인 시에만 허용되며, Main Agent가 예외 근거를 PR에 기록한다.
## Acceptance Matrix (PM Scenario -> Dev Tasks -> Verifier Checks)
| PM Scenario | Requirement Coverage | Dev Tasks (Primary) | Verifier Checks (Must Pass) |
|---|---|---|---|
| 갭 급락/급등에서 청산 우선 처리 필요 | `REQ-V2-001`,`REQ-V2-002`,`REQ-V2-003` | `TASK-V2-004`,`TASK-CODE-001` | `TEST-ACC-000`,`TEST-ACC-001`,`TEST-ACC-010`,`TEST-CODE-001`,`TEST-CODE-002` |
| 하드스탑 + BE락 + ATR + 모델보조를 한 엔진으로 통합 | `REQ-V2-004` | `TASK-V2-005`,`TASK-V2-006`,`TASK-CODE-002` | `TEST-ACC-011` |
| 라벨 누수 없는 학습데이터 생성 | `REQ-V2-005` | `TASK-V2-007`,`TASK-CODE-004` | `TEST-ACC-012`,`TEST-CODE-003` |
| 검증 프레임워크를 시계열 누수 방지 구조로 강제 | `REQ-V2-006` | `TASK-V2-010`,`TASK-CODE-005` | `TEST-ACC-013`,`TEST-CODE-004` |
| 과낙관 백테스트 방지(비용/슬리피지/실패 강제) | `REQ-V2-007` | `TASK-V2-012`,`TASK-CODE-006` | `TEST-ACC-014` |
| 장애 시 Kill Switch 실행 순서 고정 | `REQ-V2-008` | `TASK-V2-013`,`TASK-V2-014`,`TASK-V2-015`,`TASK-CODE-003` | `TEST-ACC-002`,`TEST-ACC-018` |
| 세션 전환 단위 리스크/로그 추적 일관화 | `REQ-V3-001`,`REQ-V3-002` | `TASK-V3-001`,`TASK-V3-002`,`TASK-V3-003`,`TASK-CODE-007` | `TEST-ACC-015`,`TEST-ACC-016` |
| 블랙아웃 중 주문 차단 + 복구 후 재검증 실행 | `REQ-V3-003`,`REQ-V3-004` | `TASK-V3-004`,`TASK-V3-005`,`TASK-V3-006`,`TASK-CODE-008` | `TEST-ACC-003`,`TEST-ACC-017`,`TEST-CODE-005` |
| 저유동 세션 시장가 주문 금지 | `REQ-V3-005` | `TASK-V3-007`,`TASK-V3-008`,`TASK-CODE-009` | `TEST-ACC-004`,`TEST-CODE-006` |
| 보수적 체결 모델을 백테스트 기본으로 설정 | `REQ-V3-006` | `TASK-V3-010`,`TASK-V3-011`,`TASK-V3-012`,`TASK-CODE-010` | `TEST-ACC-005`,`TEST-CODE-007` |
| 전략손익/환율손익 분리 + 통화 버퍼 통제 | `REQ-V3-007` | `TASK-V3-013`,`TASK-V3-014`,`TASK-CODE-011` | `TEST-ACC-006`,`TEST-CODE-008` |
| 오버나잇 규칙과 Kill Switch 충돌 방지 | `REQ-V3-008` | `TASK-V3-015`,`TASK-CODE-012` | `TEST-ACC-018` |
| 타임존/정책변경/추적성 문서 거버넌스 | `REQ-OPS-001`,`REQ-OPS-002`,`REQ-OPS-003` | `TASK-OPS-001`,`TASK-OPS-002`,`TASK-OPS-003` | `TEST-ACC-007`,`TEST-ACC-008`,`TEST-ACC-009` |
## 운영 규율 (TPM Enforcement Rules)
- 어떤 PM 시나리오도 `REQ-*` 없는 구현 착수 금지.
- 어떤 `REQ-*``TASK-*`,`TEST-*` 없는 승인 금지.
- Verifier는 "코드 리뷰 통과"만으로 승인 불가, 반드시 `TEST-ACC-*` 증적 필요.
- 배포 승인권자는 Phase 4 체크리스트 미충족 시 릴리즈 보류 권한을 행사해야 한다.

View File

@@ -0,0 +1,94 @@
<!--
Doc-ID: DOC-OPS-002
Version: 1.0.0
Status: active
Owner: tpm
Updated: 2026-02-26
-->
# 저장소 강제 설정 체크리스트
목표: "엄격 검증 운영"을 문서가 아니라 저장소 설정으로 강제한다.
## 1) main 브랜치 보호 (필수)
적용 항목:
- direct push 금지
- force push 금지
- branch 삭제 금지
- merge는 PR 경로만 허용
검증:
- `main`에 대해 직접 `git push origin main` 시 거부되는지 확인
## 2) 필수 상태 체크 (필수)
필수 CI 항목:
- `validate_ouroboros_docs` (명령: `python3 scripts/validate_ouroboros_docs.py`)
- `test` (명령: `pytest -q`)
설정 기준:
- 위 2개 체크가 `success` 아니면 머지 금지
- 체크 스킵/중립 상태 허용 금지
## 3) 필수 리뷰어 규칙 (권장 -> 필수)
역할 기반 승인:
- Verifier 1명 승인 필수
- TPM 또는 PM 1명 승인 필수
- Runtime Verifier 관련 변경(PR 본문에 runtime 영향 있음) 시 Runtime Verifier 승인 필수
설정 기준:
- 최소 승인 수: 2
- 작성자 self-approval 불가
- 새 커밋 푸시 시 기존 승인 재검토 요구
## 4) 워크플로우 게이트
병합 전 체크리스트:
- 이슈 연결(`Closes #N`) 존재
- PR 본문에 `REQ-*`, `TASK-*`, `TEST-*` 매핑 표 존재
- `src/core/risk_manager.py` 변경 없음
- 주요 의사결정 체크포인트(DCP-01~04) 중 해당 단계 Main Agent 확인 기록 존재
자동 점검:
- 문서 검증 스크립트 통과
- 테스트 통과
## 5) 감사 추적
필수 보존 증적:
- CI 실행 로그 링크
- 검증 실패/복구 기록
- 머지 승인 코멘트(Verifier/TPM)
분기별 점검:
- 브랜치 보호 규칙 drift 여부
- 필수 CI 이름 변경/누락 여부
## 6) 적용 순서 (운영 절차)
1. 브랜치 보호 활성화
2. 필수 CI 체크 연결
3. 리뷰어 규칙 적용
4. 샘플 PR로 거부 시나리오 테스트
5. 정상 머지 시나리오 테스트
## 7) 실패 시 조치
- 브랜치 보호 미적용 발견 시: 즉시 릴리즈 중지
- 필수 CI 우회 발견 시: 관리자 권한 점검 및 감사 이슈 발행
- 리뷰 규칙 무효화 발견 시: 규칙 복구 후 재머지 정책 시행
- Runtime 이상 이슈 미해결 상태에서 클로즈 시도 발견 시: 즉시 이슈 재오픈 + 릴리즈 중지
## 8) 재계획(Dev Replan) 운영 규칙
- Dev가 `REPLAN-REQUEST` 발행 시 TPM 심사 없이는 스코프/일정 변경 금지
- `REPLAN-REQUEST`는 Main Agent 승인 전 \"제안\" 상태로 유지
- 승인된 재계획은 `REQ/TASK/TEST` 문서를 동시 갱신해야 유효
## 9) 서버 반영 규칙 (No-Merge by Default)
- 서버 반영은 `브랜치 푸시 + PR 코멘트(리뷰/논의/검증승인)`까지를 기본으로 한다.
- 기본 규칙에서 `tea pulls merge` 실행은 금지한다.
- 사용자 명시 승인 시에만 예외적으로 머지를 허용한다(예외 근거를 PR 코멘트에 기록).

View File

@@ -0,0 +1,48 @@
<!--
Doc-ID: DOC-IDEA-001
Version: 1.0.0
Status: active
Owner: main-agent
Updated: 2026-02-26
-->
# 메인 에이전트 아이디에이션 백로그
목적:
- 구현 진행 중 떠오른 신규 구현 아이디어를 계획 반영 전 임시 저장한다.
- 본 문서는 사용자 검토 후 다음 계획 포함 여부를 결정하기 위한 검토 큐다.
운영 규칙:
- 각 아이디어는 `IDEA-*` 식별자를 사용한다.
- 필수 필드: 배경, 기대효과, 리스크, 후속 티켓 후보.
- 상태는 `proposed`, `under-review`, `accepted`, `rejected` 중 하나를 사용한다.
## 아이디어 목록
- `IDEA-001` (status: proposed)
- 제목: Kill-Switch 전역 상태를 프로세스 단일 전역에서 시장/세션 단위 상태로 분리
- 배경: 현재는 전역 block 플래그 기반이라 시장별 분리 제어가 제한될 수 있음
- 기대효과: KR/US 병행 운용 시 한 시장 장애가 다른 시장 주문을 불필요하게 막는 리스크 축소
- 리스크: 상태 동기화 복잡도 증가, 테스트 케이스 확장 필요
- 후속 티켓 후보: `TKT-P1-KS-SCOPE-SPLIT`
- `IDEA-002` (status: proposed)
- 제목: Exit Engine 입력 계약(ATR/peak/model_prob/liquidity) 표준 DTO를 데이터 파이프라인에 고정
- 배경: 현재 ATR/모델확률 일부가 fallback 기반이라 운영 일관성이 약함
- 기대효과: 백테스트-실거래 입력 동형성 강화, 회귀 분석 용이
- 리스크: 기존 스캐너/시나리오 엔진 연동 작업량 증가
- 후속 티켓 후보: `TKT-P1-EXIT-CONTRACT`
- `IDEA-003` (status: proposed)
- 제목: Runtime Verifier 자동 이슈 생성기(로그 패턴 -> 이슈 템플릿 자동화)
- 배경: 런타임 이상 리포트가 수동 작성 중심이라 누락 가능성 존재
- 기대효과: 이상 탐지 후 이슈 등록 리드타임 단축, 증적 표준화
- 리스크: 오탐 이슈 폭증 가능성, 필터링 룰 필요
- 후속 티켓 후보: `TKT-P1-RUNTIME-AUTO-ISSUE`
- `IDEA-004` (status: proposed)
- 제목: PR 코멘트 워크플로우 자동 점검(리뷰어->개발논의->검증승인 누락 차단)
- 배경: 현재 절차는 강력하지만 수행 확인이 수동
- 기대효과: 절차 누락 방지, 감사 추적 자동화
- 리스크: CLI/API 연동 유지보수 비용
- 후속 티켓 후보: `TKT-P0-WORKFLOW-GUARD`

View File

@@ -18,6 +18,10 @@ Updated: 2026-02-26
4. v3 실행 지시서: [20_phase_v3_execution.md](./20_phase_v3_execution.md) 4. v3 실행 지시서: [20_phase_v3_execution.md](./20_phase_v3_execution.md)
5. 코드 레벨 작업 지시: [30_code_level_work_orders.md](./30_code_level_work_orders.md) 5. 코드 레벨 작업 지시: [30_code_level_work_orders.md](./30_code_level_work_orders.md)
6. 수용 기준/테스트 계획: [40_acceptance_and_test_plan.md](./40_acceptance_and_test_plan.md) 6. 수용 기준/테스트 계획: [40_acceptance_and_test_plan.md](./40_acceptance_and_test_plan.md)
7. PM 시나리오/이슈 분류: [50_scenario_matrix_and_issue_taxonomy.md](./50_scenario_matrix_and_issue_taxonomy.md)
8. TPM 제어 프로토콜/수용 매트릭스: [50_tpm_control_protocol.md](./50_tpm_control_protocol.md)
9. 저장소 강제 설정 체크리스트: [60_repo_enforcement_checklist.md](./60_repo_enforcement_checklist.md)
10. 메인 에이전트 아이디에이션 백로그: [70_main_agent_ideation.md](./70_main_agent_ideation.md)
## 운영 규칙 ## 운영 규칙

71
src/core/kill_switch.py Normal file
View File

@@ -0,0 +1,71 @@
"""Kill switch orchestration for emergency risk actions.
Order is fixed:
1) block new orders
2) cancel pending orders
3) refresh order state
4) reduce risk
5) snapshot and notify
"""
from __future__ import annotations
import inspect
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable
StepCallable = Callable[[], Any | Awaitable[Any]]
@dataclass
class KillSwitchReport:
reason: str
steps: list[str] = field(default_factory=list)
errors: list[str] = field(default_factory=list)
class KillSwitchOrchestrator:
def __init__(self) -> None:
self.new_orders_blocked = False
async def _run_step(
self,
report: KillSwitchReport,
name: str,
fn: StepCallable | None,
) -> None:
report.steps.append(name)
if fn is None:
return
try:
result = fn()
if inspect.isawaitable(result):
await result
except Exception as exc: # pragma: no cover - intentionally resilient
report.errors.append(f"{name}: {exc}")
async def trigger(
self,
*,
reason: str,
cancel_pending_orders: StepCallable | None = None,
refresh_order_state: StepCallable | None = None,
reduce_risk: StepCallable | None = None,
snapshot_state: StepCallable | None = None,
notify: StepCallable | None = None,
) -> KillSwitchReport:
report = KillSwitchReport(reason=reason)
self.new_orders_blocked = True
report.steps.append("block_new_orders")
await self._run_step(report, "cancel_pending_orders", cancel_pending_orders)
await self._run_step(report, "refresh_order_state", refresh_order_state)
await self._run_step(report, "reduce_risk", reduce_risk)
await self._run_step(report, "snapshot_state", snapshot_state)
await self._run_step(report, "notify", notify)
return report
def clear_block(self) -> None:
self.new_orders_blocked = False

View File

@@ -27,6 +27,7 @@ from src.context.layer import ContextLayer
from src.context.scheduler import ContextScheduler from src.context.scheduler import ContextScheduler
from src.context.store import ContextStore from src.context.store import ContextStore
from src.core.criticality import CriticalityAssessor from src.core.criticality import CriticalityAssessor
from src.core.kill_switch import KillSwitchOrchestrator
from src.core.priority_queue import PriorityTaskQueue from src.core.priority_queue import PriorityTaskQueue
from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected, RiskManager from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected, RiskManager
from src.db import ( from src.db import (
@@ -43,11 +44,14 @@ from src.logging_config import setup_logging
from src.markets.schedule import MARKETS, MarketInfo, get_next_market_open, get_open_markets from src.markets.schedule import MARKETS, MarketInfo, get_next_market_open, get_open_markets
from src.notifications.telegram_client import NotificationFilter, TelegramClient, TelegramCommandHandler from src.notifications.telegram_client import NotificationFilter, TelegramClient, TelegramCommandHandler
from src.strategy.models import DayPlaybook, MarketOutlook from src.strategy.models import DayPlaybook, MarketOutlook
from src.strategy.exit_rules import ExitRuleConfig, ExitRuleInput, evaluate_exit
from src.strategy.playbook_store import PlaybookStore from src.strategy.playbook_store import PlaybookStore
from src.strategy.pre_market_planner import PreMarketPlanner from src.strategy.pre_market_planner import PreMarketPlanner
from src.strategy.position_state_machine import PositionState
from src.strategy.scenario_engine import ScenarioEngine from src.strategy.scenario_engine import ScenarioEngine
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
KILL_SWITCH = KillSwitchOrchestrator()
def safe_float(value: str | float | None, default: float = 0.0) -> float: def safe_float(value: str | float | None, default: float = 0.0) -> float:
@@ -784,7 +788,24 @@ async def trading_cycle(
stop_loss_threshold = stock_playbook.scenarios[0].stop_loss_pct stop_loss_threshold = stock_playbook.scenarios[0].stop_loss_pct
take_profit_threshold = stock_playbook.scenarios[0].take_profit_pct take_profit_threshold = stock_playbook.scenarios[0].take_profit_pct
if loss_pct <= stop_loss_threshold: exit_eval = evaluate_exit(
current_state=PositionState.HOLDING,
config=ExitRuleConfig(
hard_stop_pct=stop_loss_threshold,
be_arm_pct=max(0.5, take_profit_threshold * 0.4),
arm_pct=take_profit_threshold,
),
inp=ExitRuleInput(
current_price=current_price,
entry_price=entry_price,
peak_price=max(entry_price, current_price),
atr_value=0.0,
pred_down_prob=0.0,
liquidity_weak=market_data.get("volume_ratio", 1.0) < 1.0,
),
)
if exit_eval.reason == "hard_stop":
decision = TradeDecision( decision = TradeDecision(
action="SELL", action="SELL",
confidence=95, confidence=95,
@@ -800,7 +821,7 @@ async def trading_cycle(
loss_pct, loss_pct,
stop_loss_threshold, stop_loss_threshold,
) )
elif loss_pct >= take_profit_threshold: elif exit_eval.reason == "arm_take_profit":
decision = TradeDecision( decision = TradeDecision(
action="SELL", action="SELL",
confidence=90, confidence=90,
@@ -876,6 +897,15 @@ async def trading_cycle(
trade_price = current_price trade_price = current_price
trade_pnl = 0.0 trade_pnl = 0.0
if decision.action in ("BUY", "SELL"): if decision.action in ("BUY", "SELL"):
if KILL_SWITCH.new_orders_blocked:
logger.critical(
"KillSwitch block active: skip %s order for %s (%s)",
decision.action,
stock_code,
market.name,
)
return
broker_held_qty = ( broker_held_qty = (
_extract_held_qty_from_balance( _extract_held_qty_from_balance(
balance_data, stock_code, is_domestic=market.is_domestic balance_data, stock_code, is_domestic=market.is_domestic
@@ -944,6 +974,25 @@ async def trading_cycle(
except Exception as notify_exc: except Exception as notify_exc:
logger.warning("Fat finger notification failed: %s", notify_exc) logger.warning("Fat finger notification failed: %s", notify_exc)
raise # Re-raise to prevent trade raise # Re-raise to prevent trade
except CircuitBreakerTripped as exc:
ks_report = await KILL_SWITCH.trigger(
reason=f"circuit_breaker:{market.code}:{stock_code}:{exc.pnl_pct:.2f}",
snapshot_state=lambda: logger.critical(
"KillSwitch snapshot %s/%s pnl=%.2f threshold=%.2f",
market.code,
stock_code,
exc.pnl_pct,
exc.threshold,
),
)
if ks_report.errors:
logger.critical(
"KillSwitch step errors for %s/%s: %s",
market.code,
stock_code,
"; ".join(ks_report.errors),
)
raise
# 5. Send order # 5. Send order
order_succeeded = True order_succeeded = True
@@ -1845,6 +1894,15 @@ async def run_daily_session(
trade_pnl = 0.0 trade_pnl = 0.0
order_succeeded = True order_succeeded = True
if decision.action in ("BUY", "SELL"): if decision.action in ("BUY", "SELL"):
if KILL_SWITCH.new_orders_blocked:
logger.critical(
"KillSwitch block active: skip %s order for %s (%s)",
decision.action,
stock_code,
market.name,
)
continue
daily_broker_held_qty = ( daily_broker_held_qty = (
_extract_held_qty_from_balance( _extract_held_qty_from_balance(
balance_data, stock_code, is_domestic=market.is_domestic balance_data, stock_code, is_domestic=market.is_domestic
@@ -1911,6 +1969,16 @@ async def run_daily_session(
logger.warning("Fat finger notification failed: %s", notify_exc) logger.warning("Fat finger notification failed: %s", notify_exc)
continue # Skip this order continue # Skip this order
except CircuitBreakerTripped as exc: except CircuitBreakerTripped as exc:
ks_report = await KILL_SWITCH.trigger(
reason=f"daily_circuit_breaker:{market.code}:{stock_code}:{exc.pnl_pct:.2f}",
snapshot_state=lambda: logger.critical(
"Daily KillSwitch snapshot %s/%s pnl=%.2f threshold=%.2f",
market.code,
stock_code,
exc.pnl_pct,
exc.threshold,
),
)
logger.critical("Circuit breaker tripped — stopping session") logger.critical("Circuit breaker tripped — stopping session")
try: try:
await telegram.notify_circuit_breaker( await telegram.notify_circuit_breaker(
@@ -1921,6 +1989,13 @@ async def run_daily_session(
logger.warning( logger.warning(
"Circuit breaker notification failed: %s", notify_exc "Circuit breaker notification failed: %s", notify_exc
) )
if ks_report.errors:
logger.critical(
"Daily KillSwitch step errors for %s/%s: %s",
market.code,
stock_code,
"; ".join(ks_report.errors),
)
raise raise
# Send order # Send order

104
src/strategy/exit_rules.py Normal file
View File

@@ -0,0 +1,104 @@
"""Composite exit rules: hard stop, break-even lock, ATR trailing, model assist."""
from __future__ import annotations
from dataclasses import dataclass
from src.strategy.position_state_machine import PositionState, StateTransitionInput, promote_state
@dataclass(frozen=True)
class ExitRuleConfig:
hard_stop_pct: float = -2.0
be_arm_pct: float = 1.2
arm_pct: float = 3.0
atr_multiplier_k: float = 2.2
model_prob_threshold: float = 0.62
@dataclass(frozen=True)
class ExitRuleInput:
current_price: float
entry_price: float
peak_price: float
atr_value: float = 0.0
pred_down_prob: float = 0.0
liquidity_weak: bool = False
@dataclass(frozen=True)
class ExitEvaluation:
state: PositionState
should_exit: bool
reason: str
unrealized_pnl_pct: float
trailing_stop_price: float | None
def evaluate_exit(
*,
current_state: PositionState,
config: ExitRuleConfig,
inp: ExitRuleInput,
) -> ExitEvaluation:
"""Evaluate composite exit logic and return updated state."""
if inp.entry_price <= 0 or inp.current_price <= 0:
return ExitEvaluation(
state=current_state,
should_exit=False,
reason="invalid_price",
unrealized_pnl_pct=0.0,
trailing_stop_price=None,
)
unrealized = (inp.current_price - inp.entry_price) / inp.entry_price * 100.0
hard_stop_hit = unrealized <= config.hard_stop_pct
take_profit_hit = unrealized >= config.arm_pct
trailing_stop_price: float | None = None
trailing_stop_hit = False
if inp.atr_value > 0 and inp.peak_price > 0:
trailing_stop_price = inp.peak_price - (config.atr_multiplier_k * inp.atr_value)
trailing_stop_hit = inp.current_price <= trailing_stop_price
be_lock_threat = current_state in (PositionState.BE_LOCK, PositionState.ARMED) and (
inp.current_price <= inp.entry_price
)
model_exit_signal = inp.pred_down_prob >= config.model_prob_threshold and inp.liquidity_weak
next_state = promote_state(
current=current_state,
inp=StateTransitionInput(
unrealized_pnl_pct=unrealized,
be_arm_pct=config.be_arm_pct,
arm_pct=config.arm_pct,
hard_stop_hit=hard_stop_hit,
trailing_stop_hit=trailing_stop_hit,
model_exit_signal=model_exit_signal,
be_lock_threat=be_lock_threat,
),
)
if hard_stop_hit:
reason = "hard_stop"
elif trailing_stop_hit:
reason = "atr_trailing_stop"
elif be_lock_threat:
reason = "be_lock_threat"
elif model_exit_signal:
reason = "model_liquidity_exit"
elif take_profit_hit:
# Backward-compatible immediate profit-taking path.
reason = "arm_take_profit"
else:
reason = "hold"
should_exit = next_state == PositionState.EXITED or take_profit_hit
return ExitEvaluation(
state=next_state,
should_exit=should_exit,
reason=reason,
unrealized_pnl_pct=unrealized,
trailing_stop_price=trailing_stop_price,
)

View File

@@ -0,0 +1,70 @@
"""Position state machine for staged exit control.
State progression is monotonic (promotion-only) except terminal EXITED.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class PositionState(str, Enum):
HOLDING = "HOLDING"
BE_LOCK = "BE_LOCK"
ARMED = "ARMED"
EXITED = "EXITED"
_STATE_RANK: dict[PositionState, int] = {
PositionState.HOLDING: 0,
PositionState.BE_LOCK: 1,
PositionState.ARMED: 2,
PositionState.EXITED: 3,
}
@dataclass(frozen=True)
class StateTransitionInput:
unrealized_pnl_pct: float
be_arm_pct: float
arm_pct: float
hard_stop_hit: bool = False
trailing_stop_hit: bool = False
model_exit_signal: bool = False
be_lock_threat: bool = False
def evaluate_exit_first(inp: StateTransitionInput) -> bool:
"""Return True when terminal exit conditions are met.
EXITED must be evaluated before any promotion.
"""
return (
inp.hard_stop_hit
or inp.trailing_stop_hit
or inp.model_exit_signal
or inp.be_lock_threat
)
def promote_state(current: PositionState, inp: StateTransitionInput) -> PositionState:
"""Promote to highest admissible state for current tick/bar.
Rules:
- EXITED has highest precedence and is terminal.
- Promotions are monotonic (no downgrade).
"""
if current == PositionState.EXITED:
return PositionState.EXITED
if evaluate_exit_first(inp):
return PositionState.EXITED
target = PositionState.HOLDING
if inp.unrealized_pnl_pct >= inp.arm_pct:
target = PositionState.ARMED
elif inp.unrealized_pnl_pct >= inp.be_arm_pct:
target = PositionState.BE_LOCK
return target if _STATE_RANK[target] > _STATE_RANK[current] else current

55
tests/test_kill_switch.py Normal file
View File

@@ -0,0 +1,55 @@
import pytest
from src.core.kill_switch import KillSwitchOrchestrator
@pytest.mark.asyncio
async def test_kill_switch_executes_steps_in_order() -> None:
ks = KillSwitchOrchestrator()
calls: list[str] = []
async def _cancel() -> None:
calls.append("cancel")
def _refresh() -> None:
calls.append("refresh")
def _reduce() -> None:
calls.append("reduce")
def _snapshot() -> None:
calls.append("snapshot")
def _notify() -> None:
calls.append("notify")
report = await ks.trigger(
reason="test",
cancel_pending_orders=_cancel,
refresh_order_state=_refresh,
reduce_risk=_reduce,
snapshot_state=_snapshot,
notify=_notify,
)
assert report.steps == [
"block_new_orders",
"cancel_pending_orders",
"refresh_order_state",
"reduce_risk",
"snapshot_state",
"notify",
]
assert calls == ["cancel", "refresh", "reduce", "snapshot", "notify"]
assert report.errors == []
@pytest.mark.asyncio
async def test_kill_switch_collects_step_errors() -> None:
ks = KillSwitchOrchestrator()
def _boom() -> None:
raise RuntimeError("boom")
report = await ks.trigger(reason="test", cancel_pending_orders=_boom)
assert any(err.startswith("cancel_pending_orders:") for err in report.errors)

View File

@@ -13,6 +13,7 @@ from src.db import init_db, log_trade
from src.evolution.scorecard import DailyScorecard from src.evolution.scorecard import DailyScorecard
from src.logging.decision_logger import DecisionLogger from src.logging.decision_logger import DecisionLogger
from src.main import ( from src.main import (
KILL_SWITCH,
_apply_dashboard_flag, _apply_dashboard_flag,
_determine_order_quantity, _determine_order_quantity,
_extract_avg_price_from_balance, _extract_avg_price_from_balance,
@@ -77,6 +78,14 @@ def _make_sell_match(stock_code: str = "005930") -> ScenarioMatch:
) )
@pytest.fixture(autouse=True)
def _reset_kill_switch_state() -> None:
"""Prevent cross-test leakage from global kill-switch state."""
KILL_SWITCH.clear_block()
yield
KILL_SWITCH.clear_block()
class TestExtractAvgPriceFromBalance: class TestExtractAvgPriceFromBalance:
"""Tests for _extract_avg_price_from_balance() (issue #249).""" """Tests for _extract_avg_price_from_balance() (issue #249)."""
@@ -5039,3 +5048,71 @@ class TestOverseasGhostPositionClose:
and "[ghost-close]" in (c.kwargs.get("rationale") or "") and "[ghost-close]" in (c.kwargs.get("rationale") or "")
] ]
assert not ghost_close_calls, "Ghost-close must NOT be triggered for non-잔고없음 errors" assert not ghost_close_calls, "Ghost-close must NOT be triggered for non-잔고없음 errors"
@pytest.mark.asyncio
async def test_kill_switch_block_skips_actionable_order_execution() -> None:
"""Active kill-switch must prevent actionable order execution."""
db_conn = init_db(":memory:")
decision_logger = DecisionLogger(db_conn)
broker = MagicMock()
broker.get_current_price = AsyncMock(return_value=(100.0, 0.5, 0.0))
broker.get_balance = AsyncMock(
return_value={
"output1": [],
"output2": [
{
"tot_evlu_amt": "100000",
"dnca_tot_amt": "50000",
"pchs_amt_smtl_amt": "50000",
}
],
}
)
broker.send_order = AsyncMock(return_value={"msg1": "OK"})
market = MagicMock()
market.name = "Korea"
market.code = "KR"
market.exchange_code = "KRX"
market.is_domestic = True
telegram = MagicMock()
telegram.notify_trade_execution = AsyncMock()
telegram.notify_fat_finger = AsyncMock()
telegram.notify_circuit_breaker = AsyncMock()
telegram.notify_scenario_matched = AsyncMock()
settings = MagicMock()
settings.POSITION_SIZING_ENABLED = False
settings.CONFIDENCE_THRESHOLD = 80
try:
KILL_SWITCH.new_orders_blocked = True
await trading_cycle(
broker=broker,
overseas_broker=MagicMock(),
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match())),
playbook=_make_playbook(),
risk=MagicMock(),
db_conn=db_conn,
decision_logger=decision_logger,
context_store=MagicMock(
get_latest_timeframe=MagicMock(return_value=None),
set_context=MagicMock(),
),
criticality_assessor=MagicMock(
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
get_timeout=MagicMock(return_value=5.0),
),
telegram=telegram,
market=market,
stock_code="005930",
scan_candidates={},
settings=settings,
)
finally:
KILL_SWITCH.clear_block()
broker.send_order.assert_not_called()

View File

@@ -0,0 +1,38 @@
from src.strategy.exit_rules import ExitRuleConfig, ExitRuleInput, evaluate_exit
from src.strategy.position_state_machine import PositionState
def test_hard_stop_exit() -> None:
out = evaluate_exit(
current_state=PositionState.HOLDING,
config=ExitRuleConfig(hard_stop_pct=-2.0, arm_pct=3.0),
inp=ExitRuleInput(current_price=97.0, entry_price=100.0, peak_price=100.0),
)
assert out.should_exit is True
assert out.reason == "hard_stop"
def test_take_profit_exit_for_backward_compatibility() -> None:
out = evaluate_exit(
current_state=PositionState.HOLDING,
config=ExitRuleConfig(hard_stop_pct=-2.0, arm_pct=3.0),
inp=ExitRuleInput(current_price=104.0, entry_price=100.0, peak_price=104.0),
)
assert out.should_exit is True
assert out.reason == "arm_take_profit"
def test_model_assist_exit_signal() -> None:
out = evaluate_exit(
current_state=PositionState.ARMED,
config=ExitRuleConfig(model_prob_threshold=0.62, arm_pct=10.0),
inp=ExitRuleInput(
current_price=101.0,
entry_price=100.0,
peak_price=105.0,
pred_down_prob=0.8,
liquidity_weak=True,
),
)
assert out.should_exit is True
assert out.reason == "model_liquidity_exit"

View File

@@ -0,0 +1,30 @@
from src.strategy.position_state_machine import (
PositionState,
StateTransitionInput,
promote_state,
)
def test_gap_jump_promotes_to_armed_directly() -> None:
state = promote_state(
PositionState.HOLDING,
StateTransitionInput(
unrealized_pnl_pct=4.0,
be_arm_pct=1.2,
arm_pct=2.8,
),
)
assert state == PositionState.ARMED
def test_exited_has_priority_over_promotion() -> None:
state = promote_state(
PositionState.HOLDING,
StateTransitionInput(
unrealized_pnl_pct=5.0,
be_arm_pct=1.2,
arm_pct=2.8,
hard_stop_hit=True,
),
)
assert state == PositionState.EXITED