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

# Plan - REVIEW_REVIEW_LIVE_RISK

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

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

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

## 배경

G07 리뷰에서 `MaxDailyOrders`와 `MaxOpenOrders`가 순차 submit에는 적용되지만, concurrent submit에서는 `riskUsageForAccount` 조회와 broker 호출/order 저장 사이에 reservation이 없어 동일 account의 여러 요청이 같은 pre-submit count를 보고 broker까지 도달할 수 있음이 확인됐다. 이 follow-up은 기존 contract/API/CLI surface를 넓히지 않고 worker submit guard의 동시성 보장만 보완한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active `CODE_REVIEW-*-G??.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-task/m-live-trading-boundary/03+01,02_risk_kill_switch/plan_cloud_G07_1.log`
- `agent-task/m-live-trading-boundary/03+01,02_risk_kill_switch/code_review_cloud_G07_1.log`
- `services/worker/internal/livetrading/service.go`
- `services/worker/internal/livetrading/service_test.go`
- `packages/domain/trading/risk.go`
- `packages/domain/trading/types.go`
- `services/worker/internal/socket/live.go`
- `apps/cli/testdata/operator/expected/live_risk_kill_switch.jsonl`

### 테스트 환경 규칙

`test_env=local`이다. 적용 profile은 worker smoke를 중심으로, risk guard가 contract/CLI output을 건드리지 않았는지 확인하기 위해 contracts, API, operations, client focused checks를 유지한다.

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

- `MaxDailyOrders`와 `MaxOpenOrders` tests가 순차 호출만 검증한다.
- broker call이 지연된 상태에서 같은 account의 두 submit이 동시에 시작될 때, broker call count와 rejected order record를 검증하는 테스트가 없다.

### 범위 결정 근거

이번 follow-up은 worker submit path의 count-limit 동시성 보장만 다룬다. Protobuf schema, API/CLI action surface, Flutter parser map은 G07에서 추가됐고 reviewer 검증을 통과했으므로 변경하지 않는다. CLI fixture reason/documentation은 concurrent guard 구현 중 실제 output을 재확인해 필요하면 함께 정렬하되, 새 기능 범위로 확장하지 않는다.

### 빌드 등급

Build lane `cloud-G07`, review lane `cloud-G07`. live order submission과 risk guard의 동시성 safety를 다루므로 기존 cloud-G07 route를 유지한다.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_LIVE_RISK-1] same-account concurrent submit에서도 max daily/open live order guard가 broker 호출 전 원자적으로 적용되게 한다.
- [ ] [REVIEW_REVIEW_LIVE_RISK-2] focused verification과 affected module checks를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_LIVE_RISK-1] Atomic Risk Reservation

#### 문제

`services/worker/internal/livetrading/service.go:199`에서 usage를 읽은 뒤 lock을 해제하고, `service.go:212`에서 broker를 호출한 다음 `service.go:217` 이후에 order를 저장한다. 같은 account에서 두 submit이 동시에 들어오면 둘 다 기존 count만 보고 allowed가 되어 broker call까지 도달할 수 있다.

#### 해결 방법

같은 account의 submit risk evaluation과 order count 반영이 broker 호출 전후로 순서화되게 만든다. 구현 방식은 아래 중 하나를 선택하되, 테스트로 broker 전 한도 보장을 증명해야 한다.

- account별 submit mutex를 두고, 같은 account의 `SubmitLiveOrder`는 risk usage 계산부터 broker result 저장까지 직렬화한다.
- 또는 broker 호출 전에 `pending` 상태의 provisional live order를 in-memory registry에 원자적으로 reserve하고, broker result로 record를 갱신한다. broker error 시 provisional record 처리 정책도 테스트로 명확히 한다.

선호안은 account별 submit mutex다. 전체 `s.mu`를 broker 호출 동안 잡지 말고, account별 lock으로 같은 account submit만 직렬화해 cancel/get이나 다른 account submit의 불필요한 blocking을 줄인다.

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

- [ ] `services/worker/internal/livetrading/service.go`에 account별 submit/risk serialization 또는 broker 전 reservation을 추가한다.
- [ ] 기존 `riskUsageForAccount` 호출과 `recordRiskRejectedOrder`가 새 locking 규칙에서 data race나 double lock 없이 동작하게 정리한다.
- [ ] `services/worker/internal/livetrading/service_test.go`에 concurrent max open order test를 추가한다.
- [ ] `services/worker/internal/livetrading/service_test.go`에 concurrent max daily order test를 추가한다.
- [ ] blocking fake broker를 사용해 한 요청이 broker 안에서 대기 중일 때 두 번째 요청이 broker에 도달하지 않는지 검증한다.
- [ ] `gofmt`를 실행한다.

#### 테스트 작성

작성한다. 최소 테스트:

- `TestSubmitLiveOrderConcurrentMaxOpenOrdersBlocksBeforeBroker`: `MaxOpenOrders=1`, `MaxDailyOrders=0`, kill switch off. 첫 submit이 fake broker 안에서 block된 동안 두 번째 submit을 시작한다. 첫 submit을 release한 뒤 broker call count가 1이고 두 번째 submit이 `ErrRiskBlocked` + rejected order가 되는지 확인한다.
- `TestSubmitLiveOrderConcurrentMaxDailyOrdersBlocksBeforeBroker`: `MaxDailyOrders=1`, `MaxOpenOrders=0`에서 동일한 보장을 확인한다.

#### 중간 검증

```bash
go test ./services/worker/internal/livetrading -run 'TestSubmitLiveOrderConcurrent(MaxOpenOrders|MaxDailyOrders)' -count=1
go test ./services/worker/internal/livetrading
```

기대 결과: exit code 0.

### [REVIEW_REVIEW_LIVE_RISK-2] Verification

#### 문제

동시성 보완은 worker submit path에 직접 닿는다. 기존 surface가 drift 나지 않았는지 affected checks를 다시 남겨야 한다.

#### 해결 방법

worker focused test와 affected module smoke를 실행하고 실제 stdout/stderr를 review stub에 붙인다.

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

- [ ] `gofmt -l <changed-go-files>` 결과가 비어 있는지 확인한다.
- [ ] `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`를 실행한다.

#### 테스트 작성

추가 테스트는 REVIEW_REVIEW_LIVE_RISK-1에 포함한다.

#### 중간 검증

```bash
gofmt -l packages/domain/trading/risk.go packages/domain/trading/risk_test.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 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/client.go apps/cli/internal/operator/client_test.go apps/cli/internal/operator/output.go apps/cli/internal/operator/parser_map.go apps/cli/internal/operator/runner.go apps/cli/internal/operator/runner_live_test.go apps/cli/internal/operator/scenario.go
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는 기존 이슈로 기록하되 새 위반이 없어야 한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `services/worker/internal/livetrading/service.go` | REVIEW_REVIEW_LIVE_RISK-1 |
| `services/worker/internal/livetrading/service_test.go` | REVIEW_REVIEW_LIVE_RISK-1 |
| `agent-task/m-live-trading-boundary/03+01,02_risk_kill_switch/CODE_REVIEW-cloud-G07.md` | REVIEW_REVIEW_LIVE_RISK-2 |
