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

# Plan - REVIEW_REVIEW_REVIEW_LIVE_RISK

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

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

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

## 배경

plan=2 구현은 account별 submit mutex로 same-account concurrent submit을 직렬화해 count-limit broker 진입 문제를 보완했다. 이번 리뷰에서는 두 가지 남은 Required가 확인됐다.

- account mutex를 기다리기 전에 `policy`/`killSwitch`를 snapshot해서, 대기 중 kill switch나 policy가 바뀌면 queued submit이 stale state로 broker에 도달할 수 있다.
- `NewService(nil)`의 기본 halted kill switch 상태에서 submit이 `ErrBrokerUnavailable` 대신 `ErrRiskBlocked`로 분류될 수 있어 nil broker unavailable contract가 깨진다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 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_2.log`
- `agent-task/m-live-trading-boundary/03+01,02_risk_kill_switch/code_review_cloud_G07_2.log`
- `services/worker/internal/livetrading/service.go`
- `services/worker/internal/livetrading/service_test.go`
- `packages/domain/trading/risk.go`
- `services/worker/internal/socket/live.go`
- `services/worker/internal/socket/live_mapping.go`
- `services/worker/internal/socket/live_test.go`
- `services/api/internal/socket/live.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/output.go`

### 테스트 환경 규칙

`test_env=local`이다. 이번 follow-up의 직접 수정 범위는 worker live trading service이므로 worker smoke 기준 `go test ./services/worker/...`를 baseline으로 둔다. 기존 cross-domain contract/API/CLI/client surface는 plan=1/2 로그에서 검증됐으므로, 이번 수정이 그 surface를 건드리지 않으면 focused worker checks와 `bin/lint`로 닫는다. 만약 contracts/API/CLI/client 파일을 추가로 건드리면 해당 domain smoke 명령도 다시 실행하고 review stub에 실제 stdout/stderr를 남긴다.

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

- account mutex 대기 중 kill switch가 `halted=true`로 바뀌는 queued submit case가 없다.
- default halted kill switch 상태의 `NewService(nil)` submit이 unavailable을 우선 반환하는지 검증하지 않는다.

### 범위 결정 근거

이번 follow-up은 `services/worker/internal/livetrading`의 submit guard ordering만 수정한다. Protobuf schema, parser maps, API forwarding, CLI output, Flutter parser map은 기존 루프에서 추가 및 검증됐고 이번 Required와 직접 관련이 없으므로 변경하지 않는다.

### 빌드 등급

Build lane `cloud-G07`, review lane `cloud-G07`. live order submission의 money-moving guard ordering과 concurrency timing을 다루는 반복 Required이므로 cloud-G07 route를 유지한다.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_REVIEW_LIVE_RISK-1] account mutex 대기 후 현재 policy/kill switch로 risk를 평가하게 한다.
- [ ] [REVIEW_REVIEW_REVIEW_LIVE_RISK-2] nil broker submit은 기본 kill switch 상태에서도 `ErrBrokerUnavailable`을 우선 반환하게 한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REVIEW_LIVE_RISK-1] Current Risk State Under Account Lock

#### 문제

`services/worker/internal/livetrading/service.go:211`에서 `policy`와 `killSwitch`를 복사한 뒤 account mutex를 기다린다. 같은 account의 첫 submit이 broker 안에서 mutex를 잡고 있는 동안 두 번째 submit이 stale unhalted state를 복사한 채 대기하면, 그 사이 kill switch가 halted로 바뀌어도 두 번째 submit은 stale state로 risk를 통과할 수 있다.

#### 해결 방법

account mutex를 먼저 획득한 뒤, usage 계산과 `trading.EvaluateRisk` 직전에 `policyMu` 아래에서 현재 `policy`와 `killSwitch`를 복사한다.

Before:

```go
s.policyMu.RLock()
policy := s.policy
ks := s.killSwitch
s.policyMu.RUnlock()

acctMu := s.submitLockForAccount(req.AccountID)
acctMu.Lock()
defer acctMu.Unlock()
```

After:

```go
acctMu := s.submitLockForAccount(req.AccountID)
acctMu.Lock()
defer acctMu.Unlock()

s.policyMu.RLock()
policy := s.policy
ks := s.killSwitch
s.policyMu.RUnlock()
```

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

- [ ] `services/worker/internal/livetrading/service.go`에서 policy/kill switch snapshot을 account mutex 획득 후로 이동한다.
- [ ] `services/worker/internal/livetrading/service_test.go`에 queued submit이 account mutex 대기 중 kill switch 변경을 반영하는 테스트를 추가한다.
- [ ] 기존 concurrent max daily/open tests가 계속 통과하는지 확인한다.

#### 테스트 작성

작성한다. 예시 이름:

- `TestSubmitLiveOrderQueuedKillSwitchBlocksBeforeBroker`: 첫 submit을 blocking broker 안에 진입시킨 뒤 두 번째 submit을 시작한다. 두 번째 submit이 account mutex에서 대기 중일 때 `svc.SetKillSwitch(trading.KillSwitchState{Halted: true, Reason: "operator halt"})`를 호출하고 첫 broker를 release한다. 기대 결과는 broker call count 1, 첫 submit 성공, 두 번째 submit `ErrRiskBlocked`, rejected order status, risk reason `"operator halt"`다.

#### 중간 검증

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

기대 결과: exit code 0.

### [REVIEW_REVIEW_REVIEW_LIVE_RISK-2] Broker Unavailable Precedence

#### 문제

`SubmitLiveOrder`는 risk evaluation 뒤에 nil broker를 확인한다. `NewService(nil)`은 기본 kill switch가 halted라서 submit이 `ErrBrokerUnavailable` 대신 `ErrRiskBlocked` + rejected order로 반환될 수 있다. 이는 nil broker mode의 unavailable contract와 typed unavailable surface를 흐린다.

#### 해결 방법

request validation이 끝난 직후, account mutex 획득과 risk evaluation 전에 `s.broker == nil`을 검사한다. nil broker이면 order record를 만들지 않고 `ErrBrokerUnavailable`을 반환한다.

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

- [ ] `services/worker/internal/livetrading/service.go`에서 nil broker check를 validation 직후로 이동한다.
- [ ] `services/worker/internal/livetrading/service_test.go`에 default kill switch 상태에서도 nil broker가 unavailable을 우선 반환하는 테스트를 추가한다.
- [ ] 기존 `TestSubmitLiveOrderBrokerUnavailable`가 새 테스트와 중복되면 하나로 정리하되 unavailable precedence를 명시한다.

#### 테스트 작성

작성한다. 예시 이름:

- `TestSubmitLiveOrderBrokerUnavailableWithDefaultKillSwitch`: `svc := NewService(nil)`에서 kill switch를 변경하지 않고 confirmed request를 submit한다. 기대 결과는 `errors.Is(err, ErrBrokerUnavailable)`이고 `ErrRiskBlocked`가 아니어야 한다.

#### 중간 검증

```bash
go test ./services/worker/internal/livetrading -run 'TestSubmitLiveOrderBrokerUnavailableWithDefaultKillSwitch|TestSubmitLiveOrderBrokerUnavailable' -count=1 -v
```

기대 결과: exit code 0.

## 최종 검증

```bash
gofmt -l services/worker/internal/livetrading/service.go services/worker/internal/livetrading/service_test.go
go test ./services/worker/internal/livetrading
go test ./services/worker/...
bin/lint
```

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

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `services/worker/internal/livetrading/service.go` | REVIEW_REVIEW_REVIEW_LIVE_RISK-1, REVIEW_REVIEW_REVIEW_LIVE_RISK-2 |
| `services/worker/internal/livetrading/service_test.go` | REVIEW_REVIEW_REVIEW_LIVE_RISK-1, REVIEW_REVIEW_REVIEW_LIVE_RISK-2 |
| `agent-task/m-live-trading-boundary/03+01,02_risk_kill_switch/CODE_REVIEW-cloud-G07.md` | 구현 결과와 검증 evidence 기록 |
