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

# Plan - REVIEW_LIVE_RISK

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

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

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

## 배경

첫 리뷰에서 live risk policy가 kill switch/notional만 막고 `MaxDailyOrders`와 `MaxOpenOrders`를 실제 submit guard로 쓰지 않는 것이 확인됐다. 또한 risk denial이 rejected/blocked order record와 `LiveRiskDecision` surface 없이 typed error만 반환해 headless 운영자가 차단 주문 근거를 안정적으로 볼 수 없다. `.proto` 확장으로 Dart generated output은 생겼지만 Flutter `altParserMap`도 새 live risk/kill switch messages를 등록하지 못한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 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-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.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/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/client/rules.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/operations-smoke.md`
- `agent-test/local/client-smoke.md`
- `agent-task/archive/2026/06/m-live-trading-boundary/01_broker_port/complete.log`
- `agent-task/archive/2026/06/m-live-trading-boundary/02+01_order_lifecycle/complete.log`
- `agent-task/m-live-trading-boundary/03+01,02_risk_kill_switch/plan_cloud_G06_0.log`
- `agent-task/m-live-trading-boundary/03+01,02_risk_kill_switch/code_review_cloud_G06_0.log`
- `packages/domain/trading/risk.go`
- `packages/domain/trading/risk_test.go`
- `packages/domain/trading/types.go`
- `services/worker/internal/livetrading/config.go`
- `services/worker/internal/livetrading/service.go`
- `services/worker/internal/livetrading/service_test.go`
- `services/worker/internal/socket/live.go`
- `services/worker/internal/socket/live_mapping.go`
- `services/worker/internal/socket/live_test.go`
- `services/worker/internal/contracts/parser_map.go`
- `packages/contracts/proto/alt/v1/live_trading.proto`
- `packages/contracts/gen/go/alt/v1/live_trading.pb.go`
- `services/api/internal/socket/live.go`
- `services/api/internal/socket/live_test.go`
- `services/api/internal/workerclient/client.go`
- `services/api/internal/contracts/parser_map.go`
- `services/api/internal/socket/backtest_test.go`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/output.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/internal/operator/runner_live_test.go`
- `apps/cli/internal/operator/parser_map.go`
- `apps/cli/testdata/operator/live_risk_kill_switch.yaml`
- `apps/cli/testdata/operator/expected/live_risk_kill_switch.jsonl`
- `apps/cli/testdata/operator/headless_validation.md`
- `apps/client/lib/src/contracts/alt_contracts.dart`
- `apps/client/test/contracts/alt_contracts_test.dart`
- `apps/client/lib/src/generated/alt/v1/live_trading.pb.dart`
- `apps/client/lib/src/generated/alt/v1/live_trading.pbjson.dart`

### 테스트 환경 규칙

`test_env=local`이다. 적용 profile은 domain-model, contracts, worker, api, operations, client smoke다. 관련 명령은 `bin/contracts-gen`, `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/...`, `cd apps/client && flutter test test/contracts/alt_contracts_test.dart`, `bin/lint`다. 리뷰 중 focused rerun으로 `bin/contracts-check`, domain/worker/API/CLI/client focused tests, `gofmt -l`을 실행했고 `gofmt` drift는 수정했다.

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

- `MaxDailyOrders`와 `MaxOpenOrders`가 broker call 전 차단되는 테스트가 없다.
- Risk denial이 rejected/blocked live order record와 reason/decision을 반환하는 worker/API/CLI 테스트가 없다.
- Flutter `altParserMap`이 새 live risk/kill switch generated messages를 포함하는 테스트가 없다.

### 심볼 참조

Renamed/removed symbols: none. Follow-up은 additive proto/schema/domain changes와 call-site rollout이다.

### 분할 판단

공통 task group은 `m-live-trading-boundary`이고 현재 subtask는 `03+01,02_risk_kill_switch`다. predecessor `01`과 `02`는 `agent-task/archive/2026/06/m-live-trading-boundary/01_broker_port/complete.log`, `agent-task/archive/2026/06/m-live-trading-boundary/02+01_order_lifecycle/complete.log`로 충족됐다. 이번 follow-up은 첫 루프의 Required 보완만 다루므로 같은 subtask 안에서 plan=1로 이어간다.

### 범위 결정 근거

Account sync와 durable audit trail은 sibling subtask 범위이므로 수정하지 않는다. 새 broker adapter network integration이나 실제 외부 주문 smoke는 추가하지 않는다. Risk policy 수치 자체는 기본값을 유지하고, 수치 조정 UI는 만들지 않는다.

### 빌드 등급

Build lane `cloud-G07`, review lane `cloud-G07`. 실거래 제출 차단, protobuf schema/generated output, API/CLI/client parser contract가 함께 걸린 첫 리뷰 FAIL 보완이므로 cloud에서 큰 문맥을 유지한다.

## 의존 관계 및 구현 순서

Directory dependency source of truth: `03+01,02_risk_kill_switch` depends on predecessor indices `01` and `02`; 둘 다 archive `complete.log`로 충족됐다. 구현 순서는 risk count guard -> risk decision contract/CLI rollout -> Flutter parser map -> verification이다.

## 구현 체크리스트

- [ ] [REVIEW_LIVE_RISK-1] max daily/open live order limits를 submit guard에 적용한다.
- [ ] [REVIEW_LIVE_RISK-2] risk denial을 rejected/blocked order record와 `LiveRiskDecision` contract/CLI evidence로 남긴다.
- [ ] [REVIEW_LIVE_RISK-3] Flutter contract parser map과 contract test에 live risk/kill switch messages를 등록한다.
- [ ] [REVIEW_LIVE_RISK-4] focused verification과 affected module tests를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_LIVE_RISK-1] Order Count Risk Guard

#### 문제

`services/worker/internal/livetrading/config.go:17`은 `MaxDailyOrders=5`, `MaxOpenOrders=3`을 기본값으로 노출하지만 `services/worker/internal/livetrading/service.go:199`은 `trading.EvaluateRisk`만 호출한다. `packages/domain/trading/risk.go:61`은 kill switch, short selling, notional만 평가하므로 주문 수 제한이 broker call 전에 적용되지 않는다.

#### 해결 방법

`packages/domain/trading/risk.go`에 usage context를 추가하고 daily/open count를 평가한다. `SubmitLiveOrder`는 `s.mu` 아래에서 account별 same UTC day order count와 non-terminal open order count를 계산해 risk evaluation에 전달한다. Terminal status는 `filled`, `canceled`, `rejected`로 보고, 그 외 live status는 open으로 취급한다.

Before:

```go
// services/worker/internal/livetrading/service.go:194
policy := s.policy
ks := s.killSwitch
...
if decision := trading.EvaluateRisk(policy, ks, req.Intent); !decision.Allowed {
	return LiveOrder{}, fmt.Errorf("%w: %s", ErrRiskBlocked, decision.Reason)
}
```

After:

```go
usage := s.riskUsageForAccount(req.AccountID, time.Now().UTC())
decision := trading.EvaluateRisk(policy, ks, req.Intent, usage)
if !decision.Allowed {
	return s.recordRiskRejectedOrder(req, decision)
}
```

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

- [ ] `packages/domain/trading/risk.go`에 `RiskUsage` 또는 동등한 context type과 count-limit evaluation을 추가한다.
- [ ] `packages/domain/trading/risk_test.go`에 daily/open count boundary tests를 추가한다.
- [ ] `services/worker/internal/livetrading/service.go`에 account별 usage 계산과 terminal status helper를 추가한다.
- [ ] `services/worker/internal/livetrading/service_test.go`에 `TestSubmitLiveOrderMaxDailyOrdersBlocksBeforeBroker`, `TestSubmitLiveOrderMaxOpenOrdersBlocksBeforeBroker`를 추가한다.

#### 테스트 작성

작성한다. 한도 직전은 broker 호출 허용, 한도 도달/초과는 broker 호출 전 차단을 검증한다. Rejected risk order는 open count에 포함되지 않는 것도 검증한다.

#### 중간 검증

```bash
go test ./packages/domain/... ./services/worker/internal/livetrading
```

기대 결과: exit code 0.

### [REVIEW_LIVE_RISK-2] Risk Decision Contract And Output

#### 문제

`services/worker/internal/livetrading/service.go:199`은 risk denial을 `LiveOrder{}` + error로 반환하므로 계획의 rejected/blocked order 저장이 없다. `packages/contracts/proto/alt/v1/live_trading.proto:137`의 `SubmitLiveOrderResponse`는 `order`/`error`만 있고 계획의 `LiveRiskDecision`이 없다. CLI도 live submit risk denial을 stable risk fields로 출력하지 못한다.

#### 해결 방법

Additive protobuf change로 `LiveRiskDecision`을 추가하고 `SubmitLiveOrderResponse`에 새 field number로 연결한다. Risk denial 시 worker는 broker를 호출하지 않고 in-memory live order를 `rejected` 상태로 저장하며 `RejectionReason`과 denied decision을 response에 담는다. 기존 typed error vocabulary는 유지하되, response에 rejected order와 decision도 함께 싣도록 API/CLI tests를 명시적으로 맞춘다.

Before:

```proto
// packages/contracts/proto/alt/v1/live_trading.proto:137
message SubmitLiveOrderResponse {
  LiveOrder order = 1;
  ErrorInfo error = 2;
}
```

After:

```proto
message LiveRiskDecision {
  bool allowed = 1;
  string reason = 2;
}

message SubmitLiveOrderResponse {
  LiveOrder order = 1;
  ErrorInfo error = 2;
  LiveRiskDecision risk_decision = 3;
}
```

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

- [ ] `packages/contracts/proto/alt/v1/live_trading.proto`에 `LiveRiskDecision`과 response field를 추가한다.
- [ ] `bin/contracts-gen`으로 Go/Dart generated output을 갱신한다.
- [ ] worker/API/CLI parser maps에 `LiveRiskDecision`을 등록한다.
- [ ] `services/worker/internal/socket/live_mapping.go`에 risk decision mapping을 추가한다.
- [ ] `services/worker/internal/socket/live.go`와 tests에 risk blocked response order/decision/error를 검증한다.
- [ ] `services/api/internal/socket/live.go`와 tests가 worker response의 order/decision/error를 그대로 forward하는지 검증한다.
- [ ] `apps/cli/internal/operator/runner.go`, `output.go`, `runner_live_test.go`, `live_risk_kill_switch.yaml`, expected JSONL에 risk-blocked submit evidence를 추가한다.

#### 테스트 작성

작성한다. Worker service, worker socket, API forwarder, CLI scenario tests가 broker-not-called, rejected order status, rejection reason, denied risk decision, expected typed error output을 검증해야 한다.

#### 중간 검증

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

기대 결과: exit code 0.

### [REVIEW_LIVE_RISK-3] Flutter Parser Map

#### 문제

`apps/client/lib/src/generated/alt/v1/live_trading.pb.dart:1618`에 새 live risk/kill switch classes가 생겼지만 `apps/client/lib/src/contracts/alt_contracts.dart:59`의 parser map은 기존 live order lifecycle messages까지만 등록한다. `apps/client/test/contracts/alt_contracts_test.dart:16`도 old set과 `parsers.length == 33`만 검증한다.

#### 해결 방법

`altParserMap`에 `GetLiveRiskPolicyRequest/Response`, `LiveRiskPolicy`, `GetLiveKillSwitchRequest/Response`, `SetLiveKillSwitchRequest/Response`, `LiveKillSwitchState`, `LiveRiskDecision`을 추가한다. Contract test expected list와 parser count를 새 map 크기에 맞춘다.

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

- [ ] `apps/client/lib/src/contracts/alt_contracts.dart`에 새 live risk/kill switch parser entries를 추가한다.
- [ ] `apps/client/test/contracts/alt_contracts_test.dart`에 새 messages와 expected parser count를 추가한다.

#### 테스트 작성

작성한다. 기존 round-trip parser test가 새 messages를 모두 순회하도록 확장한다.

#### 중간 검증

```bash
cd apps/client && flutter test test/contracts/alt_contracts_test.dart
```

기대 결과: exit code 0.

### [REVIEW_LIVE_RISK-4] Verification

#### 문제

첫 루프 검증은 Go focused tests와 `bin/lint`를 기록했지만, client parser map 신메시지 coverage와 risk denial decision evidence를 검증하지 못했다.

#### 해결 방법

affected module과 generated drift checks를 다시 실행하고 실제 stdout/stderr를 review stub에 남긴다.

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

- [ ] `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/...`를 실행한다.
- [ ] `cd apps/client && flutter test test/contracts/alt_contracts_test.dart`를 실행한다.
- [ ] `bin/lint`를 실행한다.
- [ ] `gofmt -l <changed-go-files>` 결과가 비어 있는지 확인한다.

#### 테스트 작성

추가 테스트는 REVIEW_LIVE_RISK-1부터 REVIEW_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/...
cd apps/client && flutter test test/contracts/alt_contracts_test.dart
bin/lint
```

기대 결과: 모두 exit code 0. `bin/lint`의 기존 Mattermost `avoid_print` info가 계속 나오면 기존 이슈로 기록하되 새 위반이 없어야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/domain/trading/risk.go` | REVIEW_LIVE_RISK-1 |
| `packages/domain/trading/risk_test.go` | REVIEW_LIVE_RISK-1 |
| `services/worker/internal/livetrading/service.go` | REVIEW_LIVE_RISK-1, REVIEW_LIVE_RISK-2 |
| `services/worker/internal/livetrading/service_test.go` | REVIEW_LIVE_RISK-1, REVIEW_LIVE_RISK-2 |
| `packages/contracts/proto/alt/v1/live_trading.proto` | REVIEW_LIVE_RISK-2 |
| `packages/contracts/gen/go/alt/v1/live_trading.pb.go` | REVIEW_LIVE_RISK-2 |
| `apps/client/lib/src/generated/alt/v1/live_trading.pb.dart` | REVIEW_LIVE_RISK-2 |
| `apps/client/lib/src/generated/alt/v1/live_trading.pbjson.dart` | REVIEW_LIVE_RISK-2 |
| `services/worker/internal/contracts/parser_map.go` | REVIEW_LIVE_RISK-2 |
| `services/api/internal/contracts/parser_map.go` | REVIEW_LIVE_RISK-2 |
| `apps/cli/internal/operator/parser_map.go` | REVIEW_LIVE_RISK-2 |
| `services/worker/internal/socket/live.go` | REVIEW_LIVE_RISK-2 |
| `services/worker/internal/socket/live_mapping.go` | REVIEW_LIVE_RISK-2 |
| `services/worker/internal/socket/live_test.go` | REVIEW_LIVE_RISK-2 |
| `services/api/internal/socket/live.go` | REVIEW_LIVE_RISK-2 |
| `services/api/internal/socket/live_test.go` | REVIEW_LIVE_RISK-2 |
| `apps/cli/internal/operator/runner.go` | REVIEW_LIVE_RISK-2 |
| `apps/cli/internal/operator/output.go` | REVIEW_LIVE_RISK-2 |
| `apps/cli/internal/operator/runner_live_test.go` | REVIEW_LIVE_RISK-2 |
| `apps/cli/testdata/operator/live_risk_kill_switch.yaml` | REVIEW_LIVE_RISK-2 |
| `apps/cli/testdata/operator/expected/live_risk_kill_switch.jsonl` | REVIEW_LIVE_RISK-2 |
| `apps/client/lib/src/contracts/alt_contracts.dart` | REVIEW_LIVE_RISK-3 |
| `apps/client/test/contracts/alt_contracts_test.dart` | REVIEW_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/...
cd apps/client && flutter test test/contracts/alt_contracts_test.dart
bin/lint
```

기대 결과: 모두 exit code 0. `bin/lint`의 기존 Mattermost `avoid_print` info가 계속 나오면 기존 이슈로 기록하고, 새 파일/변경 파일의 analyzer issue가 없어야 한다.

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