<!-- task=m-live-trading-boundary/03+01,02_risk_kill_switch plan=0 tag=LIVE_RISK -->

# Plan - LIVE_RISK

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수다. 구현 후 검증을 실행하고 실제 변경 내용, 설계 결정, 명령 출력(stdout/stderr)을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 전용이다.

구현 중 사용자만 결정할 수 있는 사항, 사용자 소유 외부 환경/secret, 또는 계획 범위 충돌 없이는 안전하게 진행할 수 없으면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행 명령/출력, 재개 조건을 기록하고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

Live order lifecycle이 생긴 뒤에는 실거래 주문 제출 전 risk guard와 kill switch가 반드시 있어야 한다. 사용자는 수익률을 포기하지 말라고 결정했지만, 실거래 경계에는 손실 확대를 막는 최소 정책값과 즉시 차단 장치가 필요하다. 이 계획은 수치를 설계에 고정하지 않고 조정 가능한 기본값으로 두며, 기능 capability는 닫지 않는다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 중 직접 사용자 프롬프트는 금지이며, code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/trading-expansion/milestones/live-trading-boundary.md`
- Task ids:
  - `risk-limits`: 실거래 전 최소 risk limit과 kill switch가 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/trading-expansion/PHASE.md`
- `agent-roadmap/phase/trading-expansion/milestones/live-trading-boundary.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/domain-model/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-test/local/rules.md`
- `agent-test/local/domain-model-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-test/local/client-smoke.md`
- `packages/domain/backtest/types.go`
- `packages/domain/backtest/types_test.go`
- `packages/domain/market/types.go`
- `packages/contracts/proto/alt/v1/paper_trading.proto`
- `bin/contracts-gen`
- `bin/contracts-check`
- `services/worker/internal/papertrading/service.go`
- `services/worker/internal/papertrading/service_test.go`
- `services/worker/internal/socket/backtest.go`
- `services/worker/internal/socket/handlers.go`
- `services/worker/internal/socket/paper.go`
- `services/worker/internal/socket/paper_mapping.go`
- `services/worker/internal/socket/paper_test.go`
- `services/api/internal/workerclient/client.go`
- `services/api/internal/socket/paper.go`
- `services/api/internal/socket/paper_test.go`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/runner_paper_test.go`
- `apps/cli/internal/operator/output.go`

### 테스트 환경 규칙

`test_env=local`이다. domain/contracts/worker/api/client smoke profile을 적용한다. `.proto` 확장이 있으면 `bin/contracts-gen`, `bin/contracts-check`, `go test ./packages/contracts/gen/go/...`, `bin/lint`를 실행한다. Worker/API/CLI 변경은 각각 `go test ./services/worker/...`, `go test ./services/api/...`, `go test ./apps/cli/...`로 검증한다.

### 테스트 커버리지 공백

- Live risk policy defaults: 기존 테스트 없음. 새 domain/worker tests가 필요하다.
- Kill switch default deny와 operator override flow: 기존 paper risk는 short selling만 막는다(`services/worker/internal/papertrading/service.go:277`). live kill switch 테스트가 필요하다.
- Submit path risk guard: 02의 live submit path에 risk check가 붙어야 하므로 통합 worker socket test가 필요하다.
- CLI risk/kill output: 기존 paper output은 `risk=clear/blocked`만 있다(`apps/cli/internal/operator/output.go:52`). live risk decision fields가 필요하다.

### 심볼 참조

Renamed/removed symbols: none. 새 risk/kill switch symbols만 추가한다.

### 분할 판단

공통 task group은 `m-live-trading-boundary`다. sibling split은 `01_broker_port`, `02+01_order_lifecycle`, `03+01,02_risk_kill_switch`, `04+01_account_sync`, `05+02,03,04_audit_trail`이다. 이 subtask는 predecessor `01`과 `02`가 필요하다. 현재 active `01_broker_port/complete.log`와 `02+01_order_lifecycle/complete.log`는 없으므로 구현 시작 전 두 predecessor PASS가 필요하다.

### 범위 결정 근거

이 subtask는 account sync와 durable audit을 구현하지 않는다. Risk policy 값은 기본값으로 제안하되 config/test fixture로 분리한다. 구체 주문 유형을 제한하지 않으며, risk check는 notional/quantity/order count/kill switch처럼 주문 type과 독립적인 정책을 먼저 평가한다.

### 빌드 등급

Build lane `cloud-G06`, review lane `cloud-G06`. 실거래 제출 차단 경계와 protocol/API/CLI 영향이 있으나 storage나 external broker network는 없다.

## 의존 관계 및 구현 순서

Directory dependency source of truth: `03+01,02_risk_kill_switch` means predecessor indices `01` and `02`. 구현은 `01_broker_port`와 `02+01_order_lifecycle`의 `complete.log`가 생긴 뒤 시작한다.

## 구현 체크리스트

- [ ] [LIVE_RISK-1] live risk policy domain/config defaults를 추가한다.
- [ ] [LIVE_RISK-2] worker submit path에 risk guard와 kill switch를 적용한다.
- [ ] [LIVE_RISK-3] risk/kill switch contract, API forwarder, CLI actions/output을 추가한다.
- [ ] [LIVE_RISK-4] focused verification과 affected module tests를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [LIVE_RISK-1] Risk Policy Defaults

#### 문제

`packages/domain/backtest/types.go:395`의 `CheckRisk`는 paper account short selling만 평가한다. Live risk는 order notional, order count, kill switch를 표현할 domain/config 경계가 없다.

#### 해결 방법

`packages/domain/trading`에 `RiskPolicy`, `RiskDecision`, `KillSwitchState`를 추가한다. 기본값은 조정 가능한 값으로 둔다: kill switch default `halted=true`, max notional KRW `1000000`, USD `1000`, max daily orders `5`, max open live orders `3`, allow short selling `false`. 구현자가 config struct와 test fixture로 이 값을 주입하게 해서 후속 수치 조정은 코드 구조 변경 없이 가능하게 한다.

Before:

```go
// packages/domain/backtest/types.go:395
func CheckRisk(account PaperAccount, order OrderIntent) RiskDecision {
	if account.RiskSettings.AllowShortSelling {
		return RiskDecision{Allowed: true, Reason: "risk check passed"}
	}
```

After:

```go
type RiskPolicy struct {
	MaxOrderNotionalByCurrency map[market.Currency]market.Decimal
	MaxDailyOrders             int
	MaxOpenOrders              int
	AllowShortSelling          bool
}

type KillSwitchState struct {
	Halted bool
	Reason string
}
```

#### 수정 파일 및 체크리스트

- [ ] `packages/domain/trading/types.go` 또는 `risk.go`에 risk types를 추가한다.
- [ ] `packages/domain/trading/types_test.go` 또는 `risk_test.go`에 default/boundary tests를 추가한다.
- [ ] `services/worker/internal/livetrading/config.go`에 worker runtime 기본값을 둔다.

#### 테스트 작성

작성한다. Notional below/above threshold, kill switch halted, custom order type unaffected, empty/invalid default handling을 검증한다.

#### 중간 검증

```bash
go test ./packages/domain/...
```

기대 결과: exit code 0.

### [LIVE_RISK-2] Worker Guard

#### 문제

02의 live submit path는 operator confirmation 뒤 broker port를 호출한다. Risk guard가 없으면 confirmation만으로 live broker 호출이 가능해진다.

#### 해결 방법

`livetrading.Service.SubmitLiveOrder`에서 operator confirmation 후, broker call 전 risk guard를 실행한다. Kill switch가 halted이면 invalid_request 또는 risk_blocked typed error로 응답하고 broker를 호출하지 않는다. Risk denial은 order status `rejected` 또는 `blocked`로 저장하고 reason을 반환한다. Error vocabulary는 기존 `invalid_request`, `unavailable`, `not_found`, `timeout`, `internal`을 재사용하거나 live-specific code를 contract에 추가하되 API/CLI tests와 일치시킨다.

#### 수정 파일 및 체크리스트

- [ ] `services/worker/internal/livetrading/service.go`에 risk evaluator를 주입한다.
- [ ] `services/worker/internal/livetrading/service_test.go`에 broker-not-called tests를 추가한다.
- [ ] `services/worker/internal/socket/live.go`에 risk error mapping을 추가한다.
- [ ] `services/worker/internal/socket/live_test.go`에 submit risk blocked tests를 추가한다.

#### 테스트 작성

작성한다. `TestSubmitLiveOrderKillSwitchBlocksBeforeBroker`, `TestSubmitLiveOrderMaxNotionalBlocks`, `TestSubmitLiveOrderRiskAllowedCallsBroker`를 포함한다.

#### 중간 검증

```bash
go test ./services/worker/internal/livetrading ./services/worker/internal/socket
```

기대 결과: exit code 0.

### [LIVE_RISK-3] Contract/API/CLI Surface

#### 문제

Operator가 현재 risk policy와 kill switch 상태를 조회/전환할 headless command가 없다. Live submit 실패 이유도 CLI output에 안정적으로 남지 않는다.

#### 해결 방법

`live_trading.proto`에 `GetLiveRiskPolicyRequest/Response`, `GetLiveKillSwitchRequest/Response`, `SetLiveKillSwitchRequest/Response`, `LiveRiskDecision`을 추가한다. API는 thin forwarder로 구현한다. CLI는 `get_live_risk_policy`, `get_live_kill_switch`, `set_live_kill_switch` actions와 live submit risk output fields를 추가한다.

#### 수정 파일 및 체크리스트

- [ ] `packages/contracts/proto/alt/v1/live_trading.proto`를 확장한다.
- [ ] `bin/contracts-gen`/generated output을 갱신한다.
- [ ] worker/API/CLI parser map에 새 messages를 등록한다.
- [ ] `services/api/internal/workerclient/client.go`와 `services/api/internal/socket/live.go`를 확장한다.
- [ ] `apps/cli/internal/operator/scenario.go`, `runner.go`, `output.go`, `client.go`를 확장한다.
- [ ] `apps/cli/testdata/operator/live_risk_kill_switch.yaml`과 expected JSONL을 추가한다.

#### 테스트 작성

작성한다. API forwarder tests, CLI validation/output tests, fixture parse tests를 추가한다.

#### 중간 검증

```bash
bin/contracts-gen
bin/contracts-check
go test ./services/api/internal/socket ./apps/cli/internal/operator
```

기대 결과: exit code 0.

### [LIVE_RISK-4] Verification

#### 문제

Risk guard는 money-moving boundary에 직접 영향을 주므로 domain/worker/API/CLI를 함께 검증해야 한다.

#### 해결 방법

focused와 affected module tests를 실행한다.

#### 수정 파일 및 체크리스트

- [ ] `bin/contracts-check`를 실행한다.
- [ ] `go test ./packages/domain/...`를 실행한다.
- [ ] `go test ./packages/contracts/gen/go/...`를 실행한다.
- [ ] `go test ./services/worker/...`를 실행한다.
- [ ] `go test ./services/api/...`를 실행한다.
- [ ] `go test ./apps/cli/...`를 실행한다.

#### 테스트 작성

추가 테스트는 LIVE_RISK-1부터 LIVE_RISK-3에 포함한다.

#### 중간 검증

```bash
bin/contracts-check
go test ./packages/domain/...
go test ./packages/contracts/gen/go/...
go test ./services/worker/...
go test ./services/api/...
go test ./apps/cli/...
bin/lint
```

기대 결과: 모두 exit code 0. Go test cache output은 최종 검증에서 허용한다. Flutter/Dart toolchain 부재로 `bin/lint`가 실행 불가하면 pass가 아니며 차단 근거를 기록한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/domain/trading/risk.go` | LIVE_RISK-1 |
| `packages/domain/trading/risk_test.go` | LIVE_RISK-1 |
| `services/worker/internal/livetrading/config.go` | LIVE_RISK-1 |
| `services/worker/internal/livetrading/service.go` | LIVE_RISK-2 |
| `services/worker/internal/livetrading/service_test.go` | LIVE_RISK-2 |
| `services/worker/internal/socket/live.go` | LIVE_RISK-2, LIVE_RISK-3 |
| `services/worker/internal/socket/live_test.go` | LIVE_RISK-2 |
| `packages/contracts/proto/alt/v1/live_trading.proto` | LIVE_RISK-3 |
| `services/worker/internal/contracts/parser_map.go` | LIVE_RISK-3 |
| `services/api/internal/contracts/parser_map.go` | LIVE_RISK-3 |
| `apps/cli/internal/operator/parser_map.go` | LIVE_RISK-3 |
| `services/api/internal/workerclient/client.go` | LIVE_RISK-3 |
| `services/api/internal/socket/live.go` | LIVE_RISK-3 |
| `services/api/internal/socket/live_test.go` | LIVE_RISK-3 |
| `apps/cli/internal/operator/scenario.go` | LIVE_RISK-3 |
| `apps/cli/internal/operator/runner.go` | LIVE_RISK-3 |
| `apps/cli/internal/operator/output.go` | LIVE_RISK-3 |
| `apps/cli/internal/operator/client.go` | LIVE_RISK-3 |
| `apps/cli/internal/operator/runner_live_test.go` | LIVE_RISK-3 |
| `apps/cli/testdata/operator/live_risk_kill_switch.yaml` | LIVE_RISK-3 |
| `apps/cli/testdata/operator/expected/live_risk_kill_switch.jsonl` | LIVE_RISK-3 |

## 최종 검증

```bash
bin/contracts-check
go test ./packages/domain/...
go test ./packages/contracts/gen/go/...
go test ./services/worker/...
go test ./services/api/...
go test ./apps/cli/...
bin/lint
```

기대 결과: 모든 명령 exit code 0.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
