<!-- task=m-paper-trading-command-workflow/03+02_order_lifecycle plan=0 tag=PAPER_ORDER -->

# Code Review Reference - PAPER_ORDER

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Complete implementation-owned sections, paste actual verification output, and stop with active files in place for review.
> Do not ask the user directly; use `사용자 리뷰 요청` for user-only blockers.

## 개요

date=2026-06-05
task=m-paper-trading-command-workflow/03+02_order_lifecycle, plan=0, tag=PAPER_ORDER

## Roadmap Targets

- Milestone: `agent-roadmap/phase/trading-expansion/milestones/paper-trading-command-workflow.md`
- Task ids:
  - `paper-order-command`: virtual order lifecycle command
- Completion mode: check-on-pass

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** PASS/WARN/FAIL 판정과 archive는 리뷰 에이전트 전용이다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [PAPER_ORDER-1] paper order contract | [x] |
| [PAPER_ORDER-2] worker order lifecycle state | [x] |
| [PAPER_ORDER-3] API/CLI lifecycle scenario | [x] |

## 구현 체크리스트

- [x] [PAPER_ORDER-1] protobuf contract에 paper order command/query messages를 additive하게 추가하고 parser maps/client methods를 갱신한다.
- [x] [PAPER_ORDER-2] worker paper service가 virtual order submit/cancel/fill simulation state를 in-memory로 관리하고 typed status를 반환한다.
- [x] [PAPER_ORDER-3] API pass-through와 CLI scenario action이 order id, status transition, fill summary를 출력한다. 검증: `bin/contracts-check`, `go test ./services/worker/...`, `go test ./services/api/...`, `go test ./apps/cli/internal/operator`, `bin/test` 통과.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] PASS/WARN/FAIL 판정을 append한다.
- [x] active files를 `.log`로 아카이브하고 PASS이면 `complete.log` 작성 후 task directory를 archive로 이동한다.
- [ ] PASS이면 roadmap completion metadata를 보고한다.

## 계획 대비 변경 사항

- 계획은 worker registry 키를 `account/order id`로만 명시했으나, 구현은 계정별 `orderBook`(seed run state + live portfolio + manual fills + orders map)으로 모델링했다. 이유: fill simulation이 `CheckRisk`/`FillOrderOnDailyBar`/`ApplyFill` 도메인 헬퍼를 그대로 재사용하려면 계정 portfolio가 필요한데, 이를 별도 starting cash 없이 얻으려면 이미 시작된 paper run의 terminal state를 seed로 삼는 것이 가장 자연스럽다. order book은 처음 submit 시 `StartPaperTrading`이 기록한 state에서 lazy seed된다(미시작 계정은 `ErrAccountNotFound`).
- 계획에 없던 동작으로 `GetPaperTradingState`가 order book이 있으면 live snapshot을 반환하도록 했다. 이유: manual fill 후 계정 view가 stale run terminal state가 아닌 실제 cash/position을 반영해야 운영자가 lifecycle 결과를 동일 query로 확인할 수 있다. order book이 없으면 기존 동작(run terminal state)을 그대로 유지하므로 `paper-status-command`/`paper-loop-smoke` fixture는 영향받지 않는다.
- `FillPaperOrderResponse`에 `state`(PaperTradingState) 필드를 추가했다. 계획의 "fill summary" 요구는 `PaperOrder.fill`(BacktestTrade)로 충족하지만, fill이 portfolio에 미치는 cash/position 효과를 추가 query 없이 노출하기 위해 응답에 계정 snapshot을 함께 실었다.
- 범위 축소(submit/cancel/fill 중 일부만)는 하지 않았다. 세 동작 모두 구현했고 plan 범위 내에서 안전하게 진행 가능했으므로 `사용자 리뷰 요청`은 `없음`이다.

## 주요 설계 결정

- **paper-only, 추가(additive) 계약**: `paper_trading.proto`에 `PaperOrder`, `Submit/Cancel/Fill` request/response를 추가했다. side/type/status는 기존 `PaperRiskRejection.side`와 동일하게 string으로 두어 새 enum 도입 없이 additive를 유지했다. field 번호 재사용/삭제 없음. 실거래 broker 동작은 도입하지 않았다.
- **request 오류 vs business rejection 구분**: 계정/주문 부재, 비-pending 전이, market 주문의 fill_price 누락은 typed error(`not_found`/`invalid_request`)로 반환한다. 반면 risk gate 차단과 cash/position 부족은 error가 아니라 주문을 `status=rejected`(reason 포함)로 전이시키고 성공 응답으로 돌려준다. lifecycle command의 목적이 status transition을 보여주는 것이므로 rejection은 정상 흐름의 한 상태로 표현했다.
- **fill 가격 결정**: market 주문은 운영자가 준 `fill_price`로, limit 주문은 자신의 limit price로 단일-price 합성 bar를 만들어 기존 `FillOrderOnDailyBar`로 체결한다. 운영자가 "fill"을 호출한 것 자체가 시장이 그 가격에 도달했다는 가정이므로, command 경로에서는 limit crossing 재판정 없이 limit price로 체결한다.
- **결정성**: order id는 계정별 증가 counter로 `paper-order-<account>-<n>` 형식이며 주입 가능한 clock(`now`)을 사용해 테스트가 결정적이다.
- **rail 경계 유지**: worker가 runtime state와 proto<->domain 매핑을 소유하고, API는 shape 검증만 한 뒤 worker로 forward하는 thin pass-through를 유지했다. CLI는 API 내부 패키지를 import하지 않고 자체 ParserMap으로 같은 rail을 사용한다.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- No real broker behavior is introduced.
- Order state transitions are deterministic and tested.
- API remains pass-through; worker owns runtime state.

## 검증 결과

검증은 현재 local checkout에서 실행했다. 커밋된 generated 코드는 protoc v5.29.3로 만들어졌으므로(시스템 protoc은 3.21.12), contract 재생성/검사에는 `PATH="/tmp/protoc293/bin:$PATH"`로 protoc v5.29.3을 앞세웠다(`protoc-gen-go` v1.36.11, dart plugin은 flutter dart 사용). DB/Redis가 필요한 import 기반 end-to-end operations smoke(`paper_order_lifecycle.yaml`의 실 API run)는 remote runner 대상이며 local에서는 실행하지 않았다. fixture는 dry-run validation과 expected JSONL key 검사(`TestPaperOrderLifecycleFixtureIsValid`)로 검증했고, lifecycle 흐름은 in-process fake API 라운드트립(`TestRunPaperOrderLifecycle`)으로 검증했다.

### PAPER_ORDER-1 중간 검증
```text
$ PATH="/tmp/protoc293/bin:$PATH" bin/contracts-check
$ echo $?
0
```
드리프트 출력 없음 = generated Go/Dart가 최신. 새 메시지 5종 확인:
```text
$ grep -c "type PaperOrder struct\|type SubmitPaperOrderRequest struct\|type CancelPaperOrderRequest struct\|type FillPaperOrderRequest struct\|type FillPaperOrderResponse struct" \
    packages/contracts/gen/go/alt/v1/paper_trading.pb.go
5
```

### PAPER_ORDER-2 중간 검증
```text
$ go test ./services/worker/...
ok  	git.toki-labs.com/toki/alt/services/worker/internal/papertrading	0.002s
ok  	git.toki-labs.com/toki/alt/services/worker/internal/socket	0.072s
...(전 패키지 ok / no test files)
```
신규 order lifecycle service 테스트:
```text
--- PASS: TestServiceSubmitOrderRecordsPending (0.00s)
--- PASS: TestServiceSubmitOrderRequiresStartedAccount (0.00s)
--- PASS: TestServiceSubmitOrderRejectsInvalidIntent (0.00s)
--- PASS: TestServiceFillMarketOrderUpdatesPortfolio (0.00s)
--- PASS: TestServiceFillLimitOrderUsesLimitPrice (0.00s)
--- PASS: TestServiceFillMarketOrderRequiresFillPrice (0.00s)
--- PASS: TestServiceFillRejectedByRisk (0.00s)
--- PASS: TestServiceFillRejectedByInsufficientCash (0.00s)
--- PASS: TestServiceCancelTransitionsPendingOrder (0.00s)
--- PASS: TestServiceCancelMissingOrder (0.00s)
--- PASS: TestServiceGetStateReflectsOrderBook (0.00s)
ok  	git.toki-labs.com/toki/alt/services/worker/internal/papertrading	0.002s
```

### PAPER_ORDER-3 중간 검증
```text
$ go test ./services/api/...
ok  	git.toki-labs.com/toki/alt/services/api/internal/socket	0.021s
ok  	git.toki-labs.com/toki/alt/services/api/internal/workerclient	0.413s
...(전 패키지 ok / no test files)

$ go test ./apps/cli/internal/operator
ok  	git.toki-labs.com/toki/alt/apps/cli/internal/operator	0.170s
```

### go vet (touched packages)
```text
$ go vet ./services/worker/internal/papertrading/ ./services/worker/internal/socket/ \
    ./services/api/internal/socket/ ./services/api/internal/workerclient/ ./apps/cli/internal/operator/
$ echo $?
0
```

### 최종 검증
```text
$ PATH="/tmp/protoc293/bin:$PATH" bin/test
...(contracts-check + 5 Go 모듈 go test ./... + flutter test)
00:02 +60: All tests passed!
$ echo $?
0
```

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가:
  - correctness: Fail
  - completeness: Fail
  - test coverage: Fail
  - API contract: Fail
  - code quality: Pass
  - plan deviation: Pass
  - verification trust: Pass
- 발견된 문제:
  - Required - `services/worker/internal/papertrading/service.go:127`: `StartPaperTrading` replaces `states[accountID]` but leaves an existing `books[accountID]` intact. After any submit/fill creates an order book, restarting the same paper account still makes `GetPaperTradingState` return the old order-book snapshot and makes subsequent orders continue the old sequence/portfolio. Fix by clearing the account's order book when a new paper run is recorded, and add a regression test that starts, fills or submits, starts the same account again, then verifies state/order ids are seeded from the new run.
  - Required - `services/worker/internal/papertrading/service.go:450`, `services/worker/internal/socket/paper_mapping.go:49`: order lifecycle decimal validation only checks that quantity/price strings are non-empty, so non-parsable or negative `quantity`, `limit_price`, and `fill_price` values can enter the lifecycle before the order is recorded or filled. That lets malformed orders enter `pending` and later become `rejected` or even an internal fill error instead of a typed `invalid_request`. Fix by validating decimal syntax and sign before storing/filling orders, and cover zero/negative/non-numeric quantity plus malformed limit/fill prices in worker/service and socket or CLI tests.
- 다음 단계: FAIL 후속 `PLAN-*-G??.md`와 `CODE_REVIEW-*-G??.md`를 작성한다.
