# 시험 매수(trial buy) 실측 로그 — 블로그 기사용 증거

## 환경 고지 (먼저 읽는다)

| 항목 | 값 |
|---|---|
| 작업 트리 | `/Users/unolee/dev/weltiq-trial-content` (다른 트리·운영 배포를 건드리지 않았다) |
| 커밋 SHA | `f9e58918454f94c52d0d8893ede77c92f3e0d5cb` — 작업 트리 클린 (미커밋 변경 없음) |
| 실행 시각 (KST) | 2026-09-27 22:22:29 시작 → 22:23:34 종료 (62초) |
| DB | **격리 임시 PostgreSQL 클러스터** — 임시 유닉스 소켓 디렉터리(`weltiq-blog-trial-*`) · 포트 55531 · TCP 수신 끔 · DB `weltiq_blog_trial` · 종료 시 삭제 |
| 스키마 | `db/schema.sql` + `db/migrations/*.sql` (`tools/migrate.py`) |
| API | `uvicorn` 127.0.0.1:**8891** (자식 프로세스), REST 실호출 |
| 시세 | **합성 호가만** (`feed_source='blog-evidence-fixture'`, 1초 주기) — 실시간 시세도 거래소 연결도 없다 |
| 계정 | 픽스처 `blog-trial@example.invalid` · plan `pro` · API 키 스코프 `read-data`,`run-forward-tests` (키 문자열은 어디에도 남기지 않았다) |
| 아님 | 운영 DB 아님 · 운영 `.env` 아님 · 실계좌 아님 · 외부 거래소 주문 아님 · 배포·푸시·메일 없음 · 커밋 없음 |

**합성 호가**: `binance-spot BTCUSDT` 50000/50001 · `binance-usdm BTCUSDT` 50000/50001 · `us-equity AAPL` 230.00/230.02.
런 중 **단 한 번** 바꿨다 — T3 진입 체결 뒤 `binance-usdm BTCUSDT` 를 48900/48901 로 내렸다 (숏 목표가 발동을
실제로 일으키기 위해). 그 변경은 `evidence.json` 의 `events` 에 시각과 함께 남아 있다. 다른 호가는 고정이다.

**직접 넣은 상장 행**(`market.listings`, 주문 접수·시세 구독 판정이 읽는다):
`BTCUSDT` (asset_class `crypto`, price_venue `binance-spot`, alt_venues `{binance-usdm}`, tradable=true) ·
`AAPL` (asset_class `us_stock`, price_venue `us-equity`, alt_venues `{}`, tradable=true). 두 줄뿐이다.

**계좌 원본**: 픽스처가 가입 시 자동 생성된 계정 `acc_u_…`(binance-spot·paper)에 현금 원장 **한 줄**
(`cash 100000 USDT`, `ref_kind='blog_evidence_fixture'`)만 넣었다. 그 밖의 계좌·원장·의도·체결 행은 0이다.

**월 한도 환경변수 `WELTIQ_FORWARD_TEST_MONTHLY_LIMIT` 는 주지 않았다** — 자식 환경에 그 키가 없다
(`setup.env_forward_test_monthly_limit = "(unset)"`). 그 상태에서 `usage.limit = 1000` 이 나왔으므로
**제품 기본값 1,000 이 실제로 잡힌다**. 기본값 미적용 사실을 적을 일은 없었다.

케이스 실행 순서는 T0 → T1 → T2 → T3(접수) → T4 → T4b → T5 → T6 → T9 → T10 → (T1·T3 청산 추적) → T7 → T8 이다.
T9·T10 은 접수 전 거절(차감 0)이라 T8 비교에 영향이 없고, 청산 추적을 T7 앞에 두어야 내보내기에 최종 사유가 실린다.

---

## 케이스별 판정

| id | 판정 | 근거 (원문은 `evidence.json`) |
|---|---|---|
| **T0** | PASS | `usage` = `limit 1000 · used 0 · remaining 1000 · plan pro · entries_enabled true · can_enter true`, 리셋 `2026-10-01T00:00:00Z`. 한도 원장(`forward.usage`·`forward.usage_events`)은 **빈 상태**. 계좌 원장은 위의 현금 한 줄뿐 |
| **T1** | PASS | `binance-spot BTCUSDT long`, 201. **서버가 정한 `qty = 0.00013`** (`qty_basis.rule = smallest_lot_multiple_meeting_min_notional_within_price_headroom`, lot 0.00001 · min_notional 5 · reference_px 50000.5 · headroom 20%). `auto_close.effective_close_at = 2026-09-28T22:22:31.442830+09:00`·`effective_close_basis = "max_duration"`(첫 체결 전이라 상대 시계가 아직 안 돈다) · `affects_account = false` · `usage {used 1, remaining 999, counted 1}` |
| **T2** | PASS | `binance-spot` + `side="short"` → **422 `unsupported_direction`**, `detail`에 `short_venues = ["binance-usdm","bybit-linear","cme","us-equity"]`. **`us-equity` 가 목록에 들어 있다** (US_EQUITY_V4 `shortable=True`, 커밋 38b3e06). 기사 핵심 증거는 실측으로 확인됐다 |
| **T3** | PASS | `binance-usdm BTCUSDT short`, TP 49000 < SL 51000, 201. **`qty = 0.002`** (lot 0.001 · min_notional 50 · ref 50000.5 · headroom 20%). 진입 체결 `0.002 @ 49994.49995`(숏 = 매도, bid 쪽 슬리피지). 호가를 48900/48901 로 내린 0.8초 뒤 (변경 13:22:33.481Z → 발동 13:22:34.304Z) **`exit_trigger = close_reason = "take_profit"`**, `exit_trigger_px = 48901`(판정에 쓴 ask — 체결가가 아니다), `exit_trigger_at 22:22:34.304`, 청산 체결은 그 **뒤** `22:22:35.315 @ 48906.39005`. 실현손익 `+2.07731891`(수수료 0.09890089 차감) |
| **T4** | 라이선스에서 막혔다 (기록대로) | `us-equity` 기본 라이선스는 `license_pending` → **422 `market_not_licensed`** (`status: "license_pending"`). **방향 때문이 아니다** — 시장 권한 축에서 먼저 막혔다 |
| **T4b** | 숏은 열렸고, **세션이 닫혀 막혔다** | 같은 호출을 내부 검증용 강제 개방(`WELTIQ_MARKET_OPEN=us-equity`, 설정 시각 `events` 에 기록)으로 다시 → **201 접수**, `side = "short"`, **`qty = 1`** (`rule = one_share_or_contract`). 즉 **방향은 통과했다**. 그 뒤 엔진이 배치할 때 브로커가 거절 → `close_reason = "rejected:market_closed"`, 체결 0, 손익 `null`, 시험은 `ended / trial_completed`. 오늘은 2026-09-27(일) 이라 `us_equities` 세션이 `closed` 다. **구분 결론: 방향 거절 코드는 `unsupported_direction`(T2), 세션 거절은 `market_closed`, 라이선스 거절은 `market_not_licensed`(T4) — 셋이 서로 다른 코드다** |
| **T5** | PASS | T1 과 같은 `client_order_id` + 같은 본문 → **200**, `idempotent_replay = true`, 최초 응답 그대로. 직후 `usage.used = 3` 으로 T4b 직후 값과 **동일** — 한도를 다시 쓰지 않았다 |
| **T6** | PASS | 같은 `client_order_id` + `memo` 한 글자만 다름 → **409 `client_order_id_conflict`**, `detail: "same client_order_id with a different trial buy"` |
| **T7** | PASS | 상세·CSV·JSON 셋 다 200. **CSV 머리글 30열** 전체: `test_id,variant_id,round_id,order_id,action,state,order_type,qty,filled_qty,client_order_id,client_tracking_id,tags,memo,metadata,accepted_at,fill_id,fill_qty,fill_px,fee,fee_ccy,liquidity,fill_at,quote_source,published_at,`**`side,exit_plan,exit_trigger,exit_trigger_at,exit_trigger_px,close_reason`** — 요구한 6열이 **끝에 전부 있다**. JSON 내보내기의 `test.comparison` 은 상세 API 의 `comparison` 과 **완전히 같다**(딕셔너리 동등 비교 `True`). 외부 도구 연결 필드도 CSV 에 그대로 실렸다: `client_tracking_id=blog-a2-02` · `tags=a2-02\|evidence` · `metadata={"tool":"29B-review.py","external_run_id":"blog-a2-02-run-1"}` · `memo` 원문. 자동 청산 주문 줄의 `client_order_id` 는 서버가 만든 `exit-plan:<round_id>:1` 이고 `memo` 는 "등록된 청산 조건에 따른 자동 청산" 이다 |
| **T8** | 계좌는 안 움직였다 · **한도는 +2 (예상 +3)** | 계좌 스냅샷 9개 표(`account.accounts/portfolios/sleeves/ledger/direction_leases/reduce_reservations`·`trading.intents/fills/order_transitions`)를 T0 과 **전량 비교 → 완전 동일**(`account_unchanged_vs_T0 = true`). 현금 100000 USDT 그대로, 보유·손익·의도·체결 0행 그대로. 한도는 `used 0 → 2` (+2). **+3 이 아니다** — 아래 참조 |
| **T9** | PASS | `qty` 를 넣으면 **422**, pydantic `extra_forbidden` (`loc: ["body","qty"]`). 수량은 사용자가 정할 수 없다 |
| **T10** | PASS | 시간 조건 없이(`take_profit`·`stop_loss`만) → **422 `exit_plan_deadline_required`**, `detail`에 `max_close_at_hours 168` · `max_close_after_seconds 604800` |

## 예상과 달랐던 것 — 한도가 3이 아니라 2다

접수는 3건(T1·T3·T4b) 이 승인돼 `used` 가 3까지 올라갔고(T5 시점 확인), **T8 에서 2로 내려왔다.**
한도 원장이 이유를 그대로 말한다 (`forward.usage_events`):

```
1  fto_d596…  delta +1  entry_accepted                  22:22:31.444   (T1)
2  fto_3e59…  delta +1  entry_accepted                  22:22:31.454   (T3)
3  fto_b2b7…  delta +1  entry_accepted                  22:22:31.460   (T4b)
4  fto_b2b7…  delta -1  placement_rejected:market_closed 22:22:32.289  (T4b 보상)
```

즉 **접수는 됐지만 브로커 배치에서 거절된 진입은 1회를 되돌려준다** (`weltiq/runner/forward.py:630`
→ `_refund(o, f"placement_rejected:{r.code}")`, 원래 사용월에만·`UNIQUE (order_id, delta)` 로 1회).
계약 문서(`api-contract.md` §2 · `usage-guide.md` §6)는 "접수 전 검증 실패 = 0회 / 접수 뒤 미체결 취소·만료 =
1회 유지" 까지만 적고 **배치 거절 보상은 적지 않는다** — 구현이 문서보다 사용자에게 유리하게 동작한다.
기사에 쓸 때는 실측값(`used 2`)과 이 사유를 함께 쓰는 게 맞다.

## 그 밖에 남겨 둘 사실

- `binance-usdm BTCUSDT` 의 시세 구독 상태는 **`unlisted`** 로 나왔다 (`feed.subscribed = false`, 한국어·영어
  사유 동봉). 상장 행을 `price_venue='binance-spot'` + `alt_venues={binance-usdm}` 로만 넣었기 때문이다.
  **그래도 접수·체결·청산은 전부 정상 동작했다** — 우리가 엔진에 합성 호가를 직접 흘리기 때문이고, 실제
  운영에서는 이 상태면 수집이 안 붙어 체결이 안 난다. 화면·API 가 그 사유를 조용히 숨기지 않는 것은 확인됐다.
- T1 은 가격 조건(51000/49000)에 닿지 않아 **시간 조건으로 닫혔다**: `exit_trigger = close_reason = "close_after"`,
  `relative_from = 첫 체결의 공개 시각 22:22:32.285872`, `relative_close_at = 22:23:32.285872` (정확히 +60초), `exit_trigger_at` 도 같은 시각, `exit_trigger_px = null`
  (시한 발동에는 발동 호가가 없다 — 지어내지 않는다). 청산 체결은 `22:23:32.671`.
- 세 시험 모두 응답·상세에 `affects_account: false` · `paper_only: true` · `execution_purpose: "forward_test"`.
- 남은 프로세스·포트·디렉터리: **없다.** 자식에 SIGINT 를 보내 uvicorn graceful shutdown → `finally` 에서 커넥션
  정리 → `pg_ctl -m fast stop`(`stop.txt`) → 임시 디렉터리 삭제. 확인: `pgrep`(0건) · `lsof -iTCP:8891`(0건) ·
  `/tmp/weltiq-blog-trial-*`(없음). 스크립트 종료 코드 0.

---

## venue 별 「서버가 정한 기준 주문량」 (실측)

기준가는 이 런의 합성 호가 mid 다. 노셔널은 `qty × 기준가`.

| venue | 종목 | 프로필 | 서버가 정한 qty | lot | min_notional | 기준가 | **노셔널** | 판정 규칙 |
|---|---|---|---|---|---|---|---|---|
| `binance-spot` | BTCUSDT | binance-spot v1 | **0.00013** | 0.00001 | 5 USDT | 50000.5 | **6.500065 USDT** | `smallest_lot_multiple_meeting_min_notional_within_price_headroom` (headroom 20%) |
| `binance-usdm` | BTCUSDT | binance-usdm v1 | **0.002** | 0.001 | 50 USDT | 50000.5 | **100.001 USDT** | 같은 규칙 (headroom 20%) |
| `us-equity` | AAPL | us-equity **v4** | **1** (1주) | 1 | 1 USD | 230.01 | **230.01 USD** | `one_share_or_contract` |

코인 두 줄이 최소 주문금액보다 큰 이유는 **20% 가격 여유폭**이다: 기준가에서 20% 내린 값에서도
`min_notional` 을 넘는 최소 lot 배수를 고른다. 실측으로 확인 — binance-spot 은 0.00012×40000.4 = 4.80 (<5)
이라 탈락하고 0.00013×40000.4 = 5.20 이 통과, binance-usdm 은 0.001×40000.4 = 40.00 (<50) 이라 탈락하고
0.002×40000.4 = 80.00 이 통과한다. 몰래 늘리는 게 아니라 `qty_basis` 에 규칙·여유폭·기준가를 공개한다.

---

산출물: `run_evidence.py`(재현용) · `evidence.json`(케이스별 요청·응답 원문) · 이 파일.
부산물 로그: `initdb.txt` · `start.txt` · `role.txt` · `db.txt` · `schema.txt` · `migrate.txt` · `stop.txt`.
