# A축 10편 NautilusTrader — 실행 기록

- 일시: 2026-09-22 01:30~02:10 KST (일부 데이터 수집은 2026-09-21 16:40~ UTC)
- 환경: macOS 26.6.2 · arm64 · CPython 3.11.16 · 전용 venv `/private/tmp/tut-venvs/nautilus`
- 패키지: 본문 실행은 `nautilus_trader` **1.221.0** (py3.11) + `websockets` 17.1 (틱 수집용).
  시나리오 D 의 `liquidity_consumption` 칸만 **1.231.0** (py3.14, `/private/tmp/tut-venvs/nautilus-py314`)
- **거래소에는 공개 시세 조회만 했습니다.** 계정·API 키 없음, 주문 없음.
- **weltiq 슬롯을 쓰지 않았습니다.** 이 편의 실행은 전부 로컬 백테스트이고, weltiq 쪽은
  읽기 전용 `GET /v1/bars`·`GET /v1/capabilities` 만 호출했습니다.

---

## 1. 설치

```
/Users/unolee/.local/bin/python3.11 -m venv /private/tmp/tut-venvs/nautilus
/private/tmp/tut-venvs/nautilus/bin/pip install nautilus_trader
```

| 항목 | 실측 |
|---|---|
| 걸린 시간 | **29.3초** (`real`, 콜드 캐시) |
| 설치 패키지 | 15개 |
| 휠 크기 | `nautilus_trader-1.221.0-cp311-cp311-macosx_15_0_arm64.whl` **93.2 MB** |
| 설치본 총량 | **257 MB** · `.so` 파일 **108개** |
| 제일 큰 `.so` | `backtest/engine…so` 63.8 MB · `core/nautilus_pyo3…so` 62.3 MB |

전체 로그: `install.log`

### 정정 (2026-09-22 02:10) — "3.14 에서는 안 된다" 는 **틀렸습니다**

처음 이 로그에 "파이썬 3.14 에서는 안 됩니다. 휠이 cp311~cp313 까지만" 이라고 적었는데
**확인하지 않고 넘겨짚은 것**이었습니다. 실제로 해 보니 3.14 에서도 깔리고, **더 최신 릴리스**가 옵니다.

```
[python 3.11 venv]
nautilus_trader (1.221.0)
Available versions: 1.221.0, 1.220.0, 1.219.0, ... 1.202.0

[python 3.14 venv]
nautilus_trader (1.231.0)
Available versions: 1.231.0, 1.230.0, 1.229.0, ... 1.222.0
```

**두 목록이 한 칸도 안 겹칩니다** (`logs_version_split.txt`). 1.222.0 에서 3.11 지원이 끊겼습니다.
그래서 3.11 을 고른 것은 **열 버전 뒤처진 물건을 조용히 받은 것**이었습니다.

버전별 API 차이 (실측):

| | 1.221.0 (py3.11) | 1.231.0 (py3.14) |
|---|---|---|
| `FillModel(prob_fill_on_stop=…)` | 있음 | **없음** (`TypeError`) |
| `add_venue(liquidity_consumption=…)` | **없음** | **있음** |
| `add_venue(latency_model=…)` | 있음 | 있음 |
| `add_venue(bar_adaptive_high_low_ordering=…)` | 있음 | 있음 |
| `nautilus_trader.execution` 네임스페이스 | 비어 있음 | 비어 있음 (양쪽 다 `DefaultFillModel` 임포트 실패) |

**본문의 4-1~4-6 은 전부 1.221.0 결과이고 그대로 둡니다.** 4-9 만 1.231.0 입니다. 숫자를 섞지 않았습니다.

---

## 2. GitHub 메타 (2026-09-21 관측)

`github-meta.json` · `github-languages.json`

| 항목 | 값 |
|---|---|
| 별 | 29,234 · 포크 3,860 · watch 228 |
| 라이선스 | LGPL-3.0 |
| 개설 | 2018-06-25 · 마지막 푸시 2026-09-21 · 열린 이슈 128 |
| 기본 브랜치 | `develop` · 주 언어 Rust |
| 언어 비중 | **Rust 93.6%** (70.8 MB) · Python 5.0% (3.8 MB) · Shell 1.1% |
| 최근 릴리스 | v2.0.0rc5 (2026-09-15) · rc4 (2026-09-02) · rc3 (2026-08-21) |

**문서와 휠이 어긋납니다.** `docs/` 는 `develop`(2.x) 기준이고 `pip install nautilus_trader` 는
1.221.0 을 줍니다. 아래 §6 에 실제로 부딪힌 차이를 적었습니다.

---

## 3. 데이터 (전부 공개 API)

| 파일 | 출처 | 내용 |
|---|---|---|
| `data/btcusdt-1d-90.json` | Binance REST `/api/v3/klines` | BTCUSDT 일봉 90개 (2026-06-24 ~ 09-21). **마지막 1개는 진행 중**이라 코드에서 버림 → 89개 |
| `data/btcusdt-1h-90d.json` | 〃 (3회 페이지) | BTCUSDT 1시간봉 2,160개 → 진행 중 1개 제외 2,159개 |
| `data/ethusdt-1d-90.json` | 〃 | ETHUSDT 일봉 90개 |
| `data/bybit-xrpusdt-1d-90.json` | Bybit REST `/v5/market/kline` | XRPUSDT 무기한 일봉 90개 |
| `data/quotes.json` · `data/trades.json` | Binance WS `btcusdt@bookTicker` · `@aggTrade` | **90초분 호가틱 12,552 · 체결틱 1,098** (`capture_ticks.py`) |
| `data/weltiq-btcusdt-1m.json` · `-1h.json` | weltiq `GET /v1/bars` | 대조용 |

**틱의 타임스탬프는 우리 쪽 수신 시각(`time.time_ns()`)입니다.** Binance `bookTicker` 페이로드에는
이벤트 시각 필드가 없어 거래소 발행 시각을 쓸 수 없었습니다. 순서는 도착 순서입니다.

---

## 4. 실행한 것

| 스크립트 | 무엇 | `real` |
|---|---|---|
| `run01_first.py` | 일봉 89개 · SIM venue 1개 · 전략 1개 | 0.53s |
| `run02_ticks.py` | 나노초 호가틱·체결틱 13,650개 | 0.59s |
| `run03_multi.py` | 2 venue · 3 상품 · 3 전략 동시 | 0.54s |
| `run04_determinism.py` | 같은 입력 5회 해시 비교 | — |
| `run05_fillmodels.py` | 시나리오 D — 체결 가정 9가지 스윕 (1시간봉) | 1.36s |
| `run06_tick_sweep.py` | 시나리오 D(2) — 같은 손잡이를 틱 위에서 | 0.86s |
| `compare_bars.py` | 5-1 — weltiq 봉 ↔ Binance 공개 봉 대조 | — |
| `perf_check.py` | 엔진 순수 처리 시간 (`eng.run()` 만) | — |
| `run07_liquidity.py` | 시나리오 D(3) — `liquidity_consumption` (**1.231.0 전용**) | 0.9s |
| `run08_tearsheet.py` | 3절 — 표 리포트 + HTML 티어시트 생성 (**1.231.0 전용**) | — |
| `shoot_tearsheet.py` | 티어시트 PNG 캡처 (playwright 헤드리스) | — |

출력 원본: `logs_*.txt`

### 4-1. 첫 백테스트 (`run01_first.py`)

89 iterations · 체결 2건 · 왕복 1회.

```
BUY  MARKET 0.010000 @ 65043.98  2026-07-14 23:59:59.999999999+00:00
SELL MARKET 0.010000 @ 75644.48  2026-09-15 23:59:59.999999999+00:00
realized_pnl 104.59811540 USDT   realized_return 0.16297
```

경고가 하나 떴습니다 — `Cannot check MARKET order risk: no prices for BTCUSDT.BINANCE`.
봉만 넣고 호가를 안 넣어서 RiskEngine 이 사전 위험 점검을 건너뛴 것입니다.

**이 수익률 숫자는 어떤 전략도 추천하지 않습니다.** 엔진이 돌았다는 증거일 뿐입니다.

### 4-2. 나노초 틱 (`run02_ticks.py`)

```
틱 합계        : 13650  (호가 12552 · 체결 1098)
고유 나노초     : 13650
고유 밀리초     : 3788
밀리초로 뭉개면 : 9862개가 동률이 됩니다 (72.2%)
구간           : 89203726000 ns = 89.204 초
iterations     : 13650
제출 ts_event=1790009101015356000 bid=85857.20 limit=85857.19
체결 ts_event=1790009103313999000 px=85857.19 qty=0.002000 liquidity=<LiquiditySide.MAKER: 1> commission=0.17171438 USDT
```

지정가가 **메이커로** 2.298초 뒤에 채워졌고 `slippage 0.0` 입니다.

**처리 속도 실측** (별도 측정):

```
틱 객체 생성 13650개 : 0.011 초
eng.run()  : 0.0479 초  (13650틱 → 초당 285,114 틱)
90초치 시장을 48 ms 에 재생 = 실시간의 1,880 배
```

### 4-3. 다중 venue·상품·전략 (`run03_multi.py`)

```
venues     : ['BINANCE', 'BYBIT']
instruments: ['BTCUSDT.BINANCE', 'ETHUSDT.BINANCE', 'XRPUSDT-LINEAR.BYBIT']
strategies : ['Breakout-000', 'Breakout-001', 'Breakout-002']
iterations : 267
```

- BINANCE 는 CASH 계좌, BYBIT 은 USDT 기준 MARGIN 계좌 — **한 엔진 안에서 계좌 종류가 다릅니다.**
- ETHUSDT 전략(`Breakout-001`)은 이 구간에서 **포지션이 0건**입니다. 조건이 안 걸렸습니다.
- XRP 전략은 3회 왕복: `-9.16` · `-7.90` · `+24.96` USDT.

### 4-4. 결정론 (`run04_determinism.py`)

체결·주문 리포트에서 `init_id`(UUID4) · `ts_init`(실행 시각) · `account_id` 를 뺀 나머지를 SHA-256.

```
같은 프로세스 2회 → ac16db296daa2da6607af2e76cb3351e3990a98f2b7978e09f3e1dbb09f15256 (동일)
별도 프로세스 3회 → 같은 해시 3번
```

5회 전부 `iterations=13650`, `n_fills=1`, 같은 해시.

### 4-5. 시나리오 D — 체결 가정 스윕 (`run05_fillmodels.py`)

BTCUSDT 1시간봉 2,159개 · 20시간 고가 돌파 → 종가에 대기 지정가 → ±0.4% 브래킷.
자본 1,000,000 USDT. **전략 코드는 한 글자도 안 바꿨습니다.**

```
           label  지정가체결p  스톱체결p  경로adaptive  지연ms  진입 제출  체결 건수  청산 완료  이긴 청산   최종 USDT     순손익
기준 — 전부 채워진다고 가정    1.00   1.00       False     0     97    194     97     52 999415.20 -584.80
목표 지정가 체결확률 0.75    0.75   1.00       False     0     97    194     97     52 999415.20 -584.80
 목표 지정가 체결확률 0.5    0.50   1.00       False     0     97    194     97     52 999415.20 -584.80
목표 지정가 체결확률 0.25    0.25   1.00       False     0     97    194     97     52 999415.20 -584.80
 손절 스톱 체결확률 0.75    1.00   0.75       False     0     97    194     97     52 999415.20 -584.80
  손절 스톱 체결확률 0.5    1.00   0.50       False     0     97    194     97     52 999415.20 -584.80
 손절 스톱 체결확률 0.25    1.00   0.25       False     0     97    194     97     52 999415.20 -584.80
 봉 내 경로 adaptive    1.00   1.00        True     0     97    194     97     50 999359.55 -640.45
       셋 다 보수적으로    0.50   0.50        True     0     97    194     97     50 999359.55 -640.45
```

**발견 1 — 확률 손잡이가 봉 위에서는 하나도 안 움직였습니다.** `prob_fill_on_limit`·`prob_fill_on_stop`
은 "시장이 내 가격에 **닿기만** 하고 관통하지 않을 때" 에만 적용됩니다. 봉이 만드는 합성 호가는
O→H→L→C 네 점으로 **건너뛰므로** 정확히 닿는 일이 거의 없습니다.

**발견 2 — 봉 안의 경로는 움직였습니다.** `bar_adaptive_high_low_ordering=True` 로 바꾸자
이긴 청산이 52 → 50 으로, 순손익이 −584.80 → −640.45 USDT 로 갈렸습니다.
±0.4% 브래킷이라 손절선과 목표선이 **한 봉 안에 둘 다** 들어가고, 어느 쪽을 먼저 밟았다고
보느냐가 그대로 결과가 됩니다.

### 4-6. 시나리오 D(2) — 같은 손잡이를 틱 위에서 (`run06_tick_sweep.py`)

호가틱 200개마다 best bid 에 지정가를 하나씩 걸었습니다 (닿기만 하는 자리).

```
prob_fill_on_limit     제출     체결     체결률%
              1.00     62     68    109.7
              0.75     62     67    108.1
              0.50     62     67    108.1
              0.25     62     68    109.7
              0.00     62     49     79.0
```

- 체결률이 100% 를 넘는 것은 **부분체결 때문**입니다 (주문 1건에 체결 이벤트 여러 개).
- `0.0` (닿아도 절대 안 채움)에서도 **49건이 채워집니다** — 그건 시장이 지정가를 **관통**한 경우라
  이 확률의 대상이 아닙니다. 즉 68건 중 **19건(28%)만 '닿기' 규칙에 걸려 있었습니다.**
- 1.0 → 0.25 구간은 67~68 로 **단조롭지 않습니다.** 같은 시드라도 어느 주문에 뽑기가 걸리느냐로
  갈립니다. **N=62 는 이걸 말하기에 작은 표본입니다.**

### 4-7. 5-1 — weltiq 봉 ↔ Binance 공개 봉 (`compare_bars.py`)

```
=== BTCUSDT 1분봉 ===
weltiq n=348 delay_s=0  |  binance n=400
  라벨 그대로                 겹침  348  시가    0  종가    0  거래량    0
  마감→시작 보정 (-60s)        겹침  348  시가  348  종가  348  거래량  348

=== BTCUSDT 1시간봉 ===
weltiq n=711 delay_s=0  |  binance n=2160
  라벨 그대로                 겹침  711  시가    0  종가    0  거래량    0
  마감→시작 보정 (-3600s)      겹침  711  시가  711  종가  711  거래량  711
```

**weltiq 는 봉을 마감 시각으로, Binance klines 는 시작 시각으로 라벨링합니다.**
그 한 칸을 맞추면 **1분봉 348개·1시간봉 711개가 시가·종가·거래량까지 전부 일치**합니다.

### 4-10. 대시보드 — 없습니다 (`run08_tearsheet.py`, py3.14 · 1.231.0)

상주 대시보드·웹 UI·TUI 가 **없습니다.** 결과를 보는 길은 둘입니다.

1. `eng.trader.generate_account_report(venue)` · `generate_order_fills_report()` ·
   `generate_positions_report()` → **pandas 표.** 추가 설치 없음.
2. `nautilus_trader.analysis.create_tearsheet(engine=…, output_path=…)` → **자체 완결 HTML.**
   **1.231.0 에는 있고 1.221.0 에는 `ImportError` 입니다.** `visualization` 엑스트라 필요
   (Plotly·Kaleido, 설치 실측 **3.98초**).

BTCUSDT 1시간봉 2,159개 실행 결과:

```
왕복 31회 · 체결 61건 · iterations 2159
티어시트: tearsheet.html  (4,868,898 bytes)
```

티어시트 안의 수치 (`shots/07-tearsheet.png`):
`Iterations 2_159` · `Total orders 61` · `Total positions 31` ·
`Elapsed time 0 days 00:00:00.106934` · `Starting balance 100_000 USDT` ·
`Ending balance 96_457.49645600 USDT` · `Ending balance 0.05000000 BTC` ·
`PnL (total) -3542.5035` · `Win Rate 0.4194` · `Expectancy 17.3510`

**장표 안의 불일치 (`미확인`)**: `PnL (total USDT)` 는 −3,542.5035 인데 Equity Curve 는
1.0 → 1.24 로 끝납니다. 백테스트가 0.05 BTC 를 들고 끝났고(미청산) 지표마다 그걸 다르게
세는 것으로 보이지만 **끝까지 확인하지 않았습니다.** 그리고 `Rolling Sharpe Ratio (60-day)` 는
구간이 89일뿐이라 **비어 있습니다** — 정상입니다.

### 4-9. 시나리오 D(3) — `liquidity_consumption` (`run07_liquidity.py`, py3.14 · 1.231.0)

같은 호가틱 위에서 시장가 매수 0.5 BTC 를 연달아 5건씩 던졌습니다.

```
python 3.14.7 · nautilus_trader 1.231.0

liquidity_consumption :          False   |             True
             체결 건수 :             45   |               19
         서로 다른 체결가 :              9   |               11
            최저 체결가 :       85842.14   |         85842.14
            최고 체결가 :       85883.78   |         85883.78
              가중평균 :     85865.0224   |       85872.9493

같은 주문 다발의 평균 체결가 차이: +7.9269 USDT (+0.92 bp)
```

**잔량을 깎게 하면 45건 중 26건이 아예 안 채워집니다.** 채워진 것들도 평균 0.92 bp 불리합니다.

이 칸이 우리 `MODE_DIFFERENCES.md` §13 항목 1(「안 흉내 내는 것」 1위 — 내 주문의 시장 충격)과
정확히 같은 자리입니다. 우리 `binance-spot` 프로필의 `beta_bps=1` 과 크기가 비슷하게 나왔는데,
**한 조건에서 한 번 잰 것이라 분포가 아닙니다.** 주문 크기·호가창 깊이에 따라 달라질 숫자입니다.

**처리량 재측정** — 돌릴 때마다 다릅니다. 두 번 재서 초당 **285,114틱**(1,880배)과 **269,825틱**(1,779배).
글의 캡처는 두 번째 것입니다.

### 4-8. 부수 관측 — weltiq 상위 시간틀 봉의 신선도 (2026-09-22 01:52 KST)

`GET /v1/bars?venue=binance-spot&symbol=BTCUSDT&hours=24` 를 시간틀별로:

| tf | n | 마지막 봉 | 지금으로부터 |
|---|---|---|---|
| `1m` | 1,428 | 2026-09-22T01:52:00+09:00 | **0.4분 전** |
| `5m` | 0 | — | — |
| `1h` | 15 | 2026-09-21T16:00:00+09:00 | 592.4분 전 |
| `1d` | 1 | 2026-09-21T09:00:00+09:00 | 1,012.4분 전 |

**1분봉 수집은 실시간입니다.** 상위 시간틀만 이 시점에 뒤처져 있었습니다. 집계 주기 때문인지
다른 이유인지는 이 편에서 확인하지 않았습니다 — `미확인`. 이 관측은 운영자에게 보고합니다.

---

## 5. 문서(develop)와 휠(1.221.0)이 다른 자리 — 실제로 부딪힌 것

| 문서가 말하는 것 | 1.221.0 휠의 실제 |
|---|---|
| `from nautilus_trader.execution import DefaultFillModel` | **ImportError.** `nautilus_trader.execution` 는 빈 네임스페이스. 실제 위치는 `nautilus_trader.backtest.models.FillModel` |
| `DefaultFillModel`·`BestPriceFillModel`·`TwoTierFillModel` 등 11종 | **없음.** `FillModel` 하나 (`prob_fill_on_limit`·`prob_fill_on_stop`·`prob_slippage`·`random_seed`) |
| `BacktestVenueConfig(liquidity_consumption=True)` | **`add_venue()` 에 그 인자가 없음** |
| — | **문서에 없는데 휠에 있는 것: `latency_model=LatencyModel(base_latency_nanos=…)`.** 나노초 단위 제출/수정/취소 지연 모델 |

`add_venue()` 실제 시그니처 (1.221.0):

```
(venue, oms_type, account_type, starting_balances, base_currency=None, default_leverage=None,
 leverages=None, margin_model=None, modules=None, fill_model=None, fee_model=None,
 latency_model=None, book_type=BookType.L1_MBP, routing=False, reject_stop_orders=True,
 support_gtd_orders=True, support_contingent_orders=True, use_position_ids=True,
 use_random_ids=False, use_reduce_only=True, use_message_queue=True, bar_execution=True,
 bar_adaptive_high_low_ordering=False, trade_execution=False, allow_cash_borrowing=False,
 frozen_account=False)
```

---

## 6. 안 한 것

| | 왜 |
|---|---|
| S2 한국 종목 005930 | 공식 어댑터 15개에 한국 거래소가 없고, 이 시점 weltiq `krx` 봉도 `n=0` 이었습니다 |
| S3 내 실행 기록 복기 | 이 편에서 weltiq 슬롯을 돌리지 않았습니다 |
| S5~S8 (주문·고리·중지·재연결) | 이 편은 백테스트 엔진만 봤습니다. nautilus 의 sandbox/live 노드는 안 띄웠습니다 |
| S9 수수료 바꿔 재기 | `fee_model` 자리는 확인했으나 스윕하지 않았습니다 |
| 체결 모델 11종 (`BestPriceFillModel` 등) | 문서에 있으나 **1.x 휠 어느 쪽에도 없습니다.** 2.0 정식판 대기 |
| S10 나란히 대결 | 해당 없음 |
| L2/L3 호가창 백테스트 | `OrderBookDelta` 이력을 공개 REST 로 못 받습니다. L1 까지만 했습니다 |
| `tests/` 실행 | 저장소를 클론하지 않고 휠만 깔았습니다 |

---

## 7. 파일 목록

```
github-meta.json  github-languages.json  install.log
capture_ticks.py  compare_bars.py  perf_check.py
run01_first.py  run02_ticks.py  run03_multi.py  run04_determinism.py
run05_fillmodels.py  run06_tick_sweep.py  run07_liquidity.py
logs_run01_first.txt  logs_run02_ticks.txt  logs_run03_multi.txt
logs_run04_determinism.txt  logs_run05_fillmodels.txt  logs_run06_tick_sweep.txt
logs_run07_liquidity.txt  logs_compare_bars.txt  logs_version_split.txt
data/  ntdocs/   (ntdocs = nautilus 공식 문서 원문 사본, 인용 대조용)
shots/            (글에 실은 화면 캡처 6장)
```

---

## 8. 화면 캡처 (`shots/`)

실제 Terminal.app 창을 띄워 명령을 돌리고 **그 창만** 캡처했습니다 (`screencapture -R`, 창 좌표 기준).
캡처 직전에 Terminal 이 최전면인지 확인하고, 아니면 캡처하지 않고 중단하도록 했습니다 —
처음 시도에서 다른 앱 창이 잡혀 그 파일은 **즉시 삭제**했습니다.

| 파일 | 무엇 | 절 |
|---|---|---|
| `01-install-version-split.png` | 3.11·3.14 에서 보이는 릴리스 목록이 안 겹친다 | 2 |
| `02-first-backtest.png` | 첫 백테스트 — 체결 2건·`iterations = 89` | 3 |
| `03-ticks-throughput.png` | 나노초 틱 백테스트 + 처리 속도 | 4 |
| `04-determinism.png` | 5회 실행, 같은 해시 (별도 프로세스 3회 포함) | 4 |
| `05-scenario-d-sweep.png` | 체결 가정 9가지 — `adaptive` 행만 갈린다 | 4 |
| `06-liquidity-consumption.png` | 잔량 소진 ON/OFF — 체결 45 → 19 | 4 |
| `07-tearsheet.png` | HTML 티어시트 첫 화면 (실행 정보·성과 통계·자산곡선·낙폭) | 3 |
| `08-tearsheet-charts.png` | 티어시트 아래쪽 그래프 6개 | 3 |

07·08 은 터미널이 아니라 **브라우저 렌더**라 `playwright` 로 헤드리스 캡처했습니다
(`shoot_tearsheet.py`, viewport 1440x1000 · `device_scale_factor=2`). 창을 띄우지 않았습니다.

**키·토큰·계좌번호는 한 장에도 찍히지 않았습니다.** weltiq API 키를 쓰는 명령은 캡처 대상에서 뺐습니다
(이 편은 정보군이고 슬롯을 만들지 않아 weltiq 화면이 필요 없었습니다).
