# 2편 ai-hedge-fund — 검증 기록

**대상**: `virattt/ai-hedge-fund` (PyPI `aihf` 2.3.0, MIT, ★63,653 / 포크 11,159, 관측일 2026-09-22, 마지막 푸시 2026-09-18, 개설 2024-11-29, 열린 이슈 166)
**검증 일시**: 2026-09-21 ~ 2026-09-22
**환경**: macOS · Python 3.12 (uv venv `/tmp/aihf-venv`) · 저장소 `/tmp/ai-hedge-fund`
**작성일**: 2026-09-24 (독자판 v2 재편 시 신설 — 기사에서 내린 전체표·원문·ID·제품 과제를 여기에 옮겨 적었다)

독자판: [`docs/tutorials/a/02-ai-hedge-fund.md`](../../../docs/tutorials/a/02-ai-hedge-fund.md) · 영어판 [`.en.md`](../../../docs/tutorials/a/02-ai-hedge-fund.en.md)

---

## 0. 이 디렉터리 색인

| 파일 | 무엇 |
|---|---|
| `github-repo.json` | GitHub API 응답 원문 (별·포크·푸시·이슈·라이선스) |
| `install-time.txt` | 설치 실측 (가상환경 0.024초 + 패키지 4초) |
| `cycle-quant-20260918.json` / `.stdout.txt` / `.stderr.txt` | 계량 모델만으로 돌린 한 사이클 (2026-09-18, 3 signals · 0 orders · NAV $100,000.00) |
| `cycle-quant-20260702.json` / `.stderr.txt` | 같은 만다테의 2026-07-02 단독 한 사이클 (해시 대조용 (가)) |
| `backtest-quant-20260701-20260918.json` / `backtest-quant.stderr.txt` | Financial Datasets 백테스트 원문 (12 사이클, 주문 6건) |
| `d-check-identical.txt` | 해시 대조 결과 — `identical=True` |
| `mandate-quant-only.yaml` · `mandate-hybrid.yaml` | 기사에 쓴 만다테 두 개 원문 |
| `cycle-hybrid-20260731.json` / `.stderr.txt` | 재량+계량 혼합 사이클 (5절 목표 비중의 출처) |
| `cycle-hybrid-tui-20260921.json` | 2026-09-21 TUI 실행 기록 (4절 캡처의 출처) |
| `llm-cache/` | 유료로 나간 LLM 호출 4건의 캐시 파일 |
| `openrouter-key-before.json` · `openrouter-spend.txt` · `openrouter-models-check.txt` | OpenRouter 잔액 전후, 모델 440개 조회 결과 |
| `yf_client.py` | yfinance 데이터 클라이언트 전체 (주석 포함 254줄) |
| `backtest_on_yfinance.py` · `backtest-quant-yfinance.json` · `.stdout.txt` | yfinance 로 갈아 끼운 같은 백테스트 |
| `compare_fd_vs_yf.py` · `fd-vs-yf.json` · `fd-vs-yf.stdout.txt` · `side_by_side.py` | 엔드포인트별 대조 스크립트와 결과 |
| `fd-calls.jsonl` · `fd-usage-summary.txt` | Financial Datasets 호출 로그 전체 (합계 80 요청) |
| `nvda-gap-diagnosis.txt` | NVDA 가 빠진 이유 추적 원문 |
| `weltiq-capabilities.json` | `GET /v1/capabilities` 응답 |
| `slot-create-r1~r5.json` / `.resp.json` · `slot-status-*.json` · `slot-export-r1~r5.json` · `slot-versions-r1.json` · `slot-block-trace-r1.json` | 전략 컨테이너 5회차 생성·상태·내보내기 원문 |
| `instantiate-tpl002.json` · `instantiate-tpl002-params.json` | 템플릿 인스턴스화 두 경로 (params 무시 확인) |
| `backtest-weltiq-msft.json` | `qty_below_lot:20` 백테스트 응답 |
| `oneoff-estimate.json` · `oneoff-buy.json` · `oneoff-buy.resp.json` | 1회 주문 견적·요청·422 응답 |
| `envelopes.json` · `envelope-403.json` · `envelope-403.txt` | 주문 한도 목록과 403 응답 |
| `shots/` | 캡처 15장 (기사에 11장 사용) |

---

## 1. 설치·환경

```bash
git clone https://github.com/virattt/ai-hedge-fund /tmp/ai-hedge-fund
uv venv --python 3.12 /tmp/aihf-venv          # 0.024초
uv pip install --python /tmp/aihf-venv/bin/python /tmp/ai-hedge-fund   # 4초
```

`clone` 시간은 포함하지 않았다(저장소를 미리 받아 둔 상태). README 는 `pipx install aihf` 또는 `poetry install` 을 안내하며 검증 기계에는 둘 다 없었다.

키는 사전 주입 없이 시작했고, 앱이 처음 필요한 시점에 물어 `~/.hedge-fund/.env` 에 0600 권한으로 저장했다. 셸 `export` 값이 항상 우선한다.

---

## 2. 기사에서 내린 것 — OpenRouter 모델 이름 치환 스크립트

3-3 에서 "고치는 건 한 줄"이라고 쓴 부분의 실제 스크립트 전문이다.

```bash
export OPENAI_API_KEY="<OpenRouter 키>"
export OPENAI_API_BASE="https://openrouter.ai/api/v1"

python - <<'PY'
import json, pathlib, hedge_fund.llm.registry as r
p = pathlib.Path(r.API_MODELS_PATH)
d = json.loads(p.read_text())
for e in d:
    if e["model_name"] == "gpt-6-astra":
        e["model_name"] = "openai/gpt-6-astra"
p.write_text(json.dumps(d, indent=2))
PY
export HEDGE_FUND_LLM_MODEL="openai/gpt-6-astra"
```

근거가 된 소스:

```python
# hedge_fund/llm/client.py:259
elif provider == "OpenAI":
    from langchain_openai import ChatOpenAI
    chat = ChatOpenAI(model=model, api_key=api_key, timeout=timeout,
                      max_retries=1, base_url=os.getenv("OPENAI_API_BASE"))
```

OpenAI 분기만 `base_url` 을 환경변수에서 읽는다. Anthropic 분기에는 그 인자가 아예 없다. 그리고 `provider_for()` 가 `None` 을 돌려주면 기본 전송로가 Anthropic 이라, 목록에 없는 이름을 그대로 export 하면 Anthropic 키를 찾다 죽는다.

OpenRouter 모델 목록 조회 결과 (관측일 2026-09-22, 총 440개):

| 이름 | OpenRouter 에 |
|---|---|
| `openai/gpt-6-astra` | **있음** |
| `openai/gpt-5.6` | 없음 |
| `gpt-5.6` | 없음 |

**비용 실측**: 유료 호출 4건(`~/.hedge-fund/cache/llm/` 파일 정확히 4개). OpenRouter 계정 사용량 **16.311928679 → 17.148309638 = $0.836381**, 호출당 약 $0.21. 실행 직후 조회값은 **$0.706238** 이었고 몇 분 뒤 $0.836381 로 확정됐다 — 집계 지연이 있다.

---

## 3. 기사에서 내린 것 — CLI 직접 실행 요령

기사 3-2 의 `<details>` 블록 원문.

```bash
aihf ~/.hedge-fund/mandates/quant-only.yaml \
  --tickers AAPL,MSFT,NVDA --date 2026-09-18 --out cycle.json
```

전체 `CycleRecord` 가 stdout 에 JSON 으로 나오고 사람용 요약은 stderr 로 간다. 그래서 `| jq` 가 바로 된다.

실행 요약 전문:

```
  earnings-drift (100% of capital): 3 signals (0 abstained)
quant-only @ 2026-09-18  ·  1 strategies  ·  3 signals  ·  0 risk clamps  ·
0 orders  ·  NAV $100,000.00
```

13.2초 · Financial Datasets 요청 6개(종목 3 × 시세 1 + 실적 이력 1). 주문 0건의 원인은 PEAD 의 "실적 발표 4일 이내" 조건 미충족이며, 확신도 0.0 → 목표 비중 0 → 주문 없음이 기록에 그대로 남는다.

---

## 4. 기사에서 내린 것 — 엔드포인트별 전체 비교표 (FD vs yfinance)

독자판 4-2 에는 요약 한 문장만 남겼다. 원표는 이것이다.

| 호출 | Financial Datasets | yfinance | 판정 |
|---|---|---|---|
| `get_prices` (56일) | 56행 | 56행 | **동등** |
| `get_earnings_history` (limit=8) | 6행 · `8-K`·`10-Q`·`10-K` 구분 있음 | **24행** · 전부 미상(필자가 `8-K` 로 채움) · **2020년까지** | 무료가 더 길고, 유료가 더 정밀 |
| `get_financial_metrics` | 4행 · **제출일 있음** | 5행 · **제출일 없음** | **유료만 가능** |
| `get_company_facts` | 1건 | 1건 (최신값만 — 백테스트엔 미래 참조) | 동등, 단 주의 |
| `get_insider_trades` | 131건 | 81건 | 유료가 더 많음 |
| `get_news` | **400 Invalid limit** (도구 쪽 기본값 버그) | 0건 | 둘 다 실패 |

실적 발표일 자체는 일치한다. AAPL 의 2026-04-30 · 2026-07-30 은 양쪽이 같은 날을 가리켰다.

시세 전체 대조 (2026-07-01~09-18, 56 거래일):

| | AAPL | MSFT | NVDA |
|---|---|---|---|
| 봉 개수 | 56 / 56 | 56 / 56 | 56 / 56 |
| 빠진 날짜 | 없음 | 없음 | 없음 |
| 종가 최대 절대차 | 0.000015 | 0.000029 | 0.000007 |
| 종가 최대 상대차 | 0.000005% | 0.000006% | 0.000004% |
| 응답 시간 (FD / yf) | 975ms / 553ms | 328ms / 188ms | 330ms / 179ms |

거래량은 다르다 — 9/18 AAPL 에서 FD 86,588,203 vs yfinance 86,433,100.

NVDA 가 빠진 이유 추적 원문 (기사에는 2줄만 실었다):

```
=== yfinance NVDA (필자 클라이언트가 매핑한 대로)
  filed=2026-08-26 report_period=2026-06-30 lag=57d -> DROPPED by PEAD 45-day filter
  filed=2026-05-20 report_period=2026-03-31 lag=50d -> DROPPED
=== Financial Datasets NVDA
  filed=2026-08-26 report_period=2026-07-26 src=8-K lag=31d -> kept
  filed=2026-05-20 report_period=2026-04-26 src=8-K lag=24d -> kept
```

`yf_client.py` 최소 형태 (전체는 같은 디렉터리의 파일):

```python
import yfinance as yf
from hedge_fund.data.models import Price

class YFinanceClient:
    def get_prices(self, ticker, start_date, end_date, interval="day",
                   interval_multiplier=1, **_):
        end_excl = (pd.Timestamp(end_date) + pd.Timedelta(days=1)).date().isoformat()
        frame = yf.Ticker(ticker).history(start=start_date, end=end_excl,
                                          interval="1d", auto_adjust=False)
        return [Price(open=r["Open"], close=r["Close"], high=r["High"],
                      low=r["Low"], volume=int(r["Volume"]),
                      time=t.date().isoformat())
                for t, r in frame.iterrows()]
    # … get_earnings_history / get_financial_metrics / get_company_facts /
    #    get_news / get_insider_trades / get_market_cap 도 같은 요령
```

상속이 필요 없다 — 메서드 이름만 맞추면 파이프라인이 그대로 받는다(`hedge_fund/data/protocol.py` 독스트링에 교체 예시가 직접 들어 있다).

---

## 5. 기사에서 내린 것 — 2026-07-31 사이클 전체 판정

| 칸 | 값 |
|---|---|
| Deep Value 팟 (60%) | 버핏 MSFT **+0.80** · 그레이엄 MSFT **−0.88** · 그레이엄 AAPL **−0.94** · 버핏 AAPL 0.00(중립) |
| Earnings Drift 팟 (40%) | PEAD AAPL **+1.0** · PEAD MSFT **+1.0** (둘 다 `BEAT on … (filed 2026-07-30, 8-K)`) |
| 합쳐진 목표 비중 | AAPL **−0.353** · MSFT **+0.153** |
| 리스크 클램프 | `max_position_pct` 가 AAPL 을 **−0.353 → −0.25** 로 깎음 |
| 최종 주문 | AAPL 공매도 80주 @ $308.91 · MSFT 매수 32주 @ $464.72 |

옮김 대조표 (도구 출력 → 전략 컨테이너):

| 이 도구가 주는 것 | 컨테이너로 |
|---|---|
| 종목 (MSFT) | `symbols: ["MSFT"]` |
| 방향·크기 (+0.153 × $100,000) | `sizing.qty` — $15,294 어치, 주가 기준 30주 |
| 한도 (`max_position_pct: 0.25`) | `balance` 로 상한을 물리적으로 제한 |
| **진입 시점 규칙** | **없음 ← 빈 칸** (템플릿이 댐) |
| **중지 조건** | **없음 ← 빈 칸** (사람이 $300 / 3시간을 넣음) |

`GET /v1/capabilities` 응답 요약:

```
plan=pro   live_orders=False
binance-spot open · binance-usdm open · bybit-linear open · bybit-spot open
cme license_pending · krx open · us-equity open
slots: max_symbols=3 · stop_conditions_required=True · min_balance=100
```

---

## 6. 기사에서 내린 것 — 전략 컨테이너 5회차 전체 기록

미국장 개장 중 2026-09-21 13:14~14:15 ET. 매 회차 `GET /v1/containers/{id}/export` 의 `counts.intents` 와 신호등을 읽고 규칙을 고쳤다.

| 회차 | 컨테이너 ID | 무엇을 바꿨나 | 기록이 말한 것 | 그래서 다음에 |
|---|---|---|---|---|
| **r1** | `ct_94fd0904…` | 이 도구의 비중 그대로 — 20봉 고가 돌파 · 30주 | `intents: 0`, 중간에 신호등 `entry_blocked`/`DENY` | 신호가 더 자주 나게, 주문을 더 작게 |
| **r2** | `ct_d9c4fe43…` | 20봉 → **5봉**, 30주 → **20주** | `intents: 0` | 돌파형이 이 종목·이 시간대엔 안 잡힌다 → 규칙 교체 |
| **r3** | `ct_3c697e5e…` | **이동평균 5/15 교차**로 교체 | 신호등 `entry_blocked`/`DENY` — 그런데 `intents: 0` | 신호는 났는데 주문 전에 막혔다 → 크기를 확 줄인다 |
| **r4** | `ct_9d34c823…` | 20주 → **5주** ($2,480 어치) | `intents: 0` | 종목 문제인지 본다 |
| **r5** | `ct_2e18fde0…` | 종목을 **NVDA** 로 (이 도구가 8/28 에 실제로 산 종목) | `intents: 0` | 컨테이너 말고 **1회 주문**으로 경계를 직접 두드린다 |

다섯 회차 모두 중지 조건을 걸었다(`stop_max_loss: 300`, `stop_after_hours: 3`). 화면 우측 `Auto-stop −300` 으로 표시됨. **손실이 날 체결이 없어 발동은 미확인.**

`doc_hash`: r1 = `3d9deb89065d4087cf118e78ea249a69d7b45de9906c2abb1f7c6e4d85233e60`. r2 에서 수량과 봉 수를 고치자 `9a451c9ff4…` 로 갈렸다 — 파라미터별로 문서 해시가 달라진다.

템플릿 인스턴스화 경로 (막힌 것):

```
POST /v1/templates/TPL-002/instantiate        # params 를 무시함
→ 기본값을 넣든 이 도구의 숫자를 넣든 같은 해시: 22c512f3…b8e3

POST /v1/backtest {"doc_hash":"22c512f3…","venue":"us-equity","symbol":"MSFT","tf":"1m","hours":24}
→ {"run_id":"bt_df8e50c0b61e1913","n_bars":224,"n_fills":0,
   "warning":"execution:qty_below_lot:20 — 모든 진입이 최소 수량 미만으로 건너뜀 —
              수량 설정 확인 (주식은 1주 단위)"}
```

24시간에 진입 의도 20건, 전부 수량 미달로 건너뜀. 기본 수량 0.01(암호자산 기준)은 주식에서 1주가 안 된다.

1회 주문 원문:

```
GET /v1/orders/estimate?venue=us-equity&symbol=MSFT&side=buy&size_unit=base&size_value=30
→ {"qty":"30","ref_px":"499.31","est_notional":"14979.30","fee_estimate":"0E-8"}

POST /v1/orders/oneoff {"venue":"us-equity","symbol":"MSFT","side":"buy","type":"market","qty":"30",
   "thesis":{"why":"… Earnings Drift(PEAD) BEAT +1.0 와 Deep Value(Buffett +0.80 / Graham −0.88) 를
                    합쳐 MSFT 목표비중 +0.153 …"},
   "invalidation":{"px_below":"489.32"}}
→ 422 {"code":"envelope_required","detail":"이 심볼을 허용하는 주문 한도 승인이 없습니다 — 먼저 승인하세요"}

POST /v1/envelopes {"symbols":["MSFT"],"max_order_notional":"20000", …}   # API 키로
→ HTTP 403 {"code":"session_required","detail":"/v1/envelopes 은(는) 로그인 세션에서만
             호출할 수 있습니다 (API 키 불가)"}
```

승인돼 있던 한도는 `BTCUSDT`(주문당 $500) 하나뿐. 미국 주식은 애초에 열려 있지 않았다.

---

## 7. 기사에서 내린 것 — 돌리지 않은 시나리오 전체

| | 왜 |
|---|---|
| **S1** 글로벌 종목 하나 (BTC-USD) | **이 도구로는 안 된다.** 미국 주식 전용 — 데이터 클라이언트에 암호자산 엔드포인트가 없고 알파 모델이 전부 SEC 공시 기반 |
| **S2** 한국 종목 (005930) | 〃 같은 이유 |
| **S3** 내 실행 기록 복기 | 기록을 분석하는 물건이 아니다. 자기 백테스트 기록만 다룬다 |
| **S4** 여러 종목 훑어 후보 좁히기 | **안 된다.** 종목을 골라 주는 게 아니라 준 종목에 의견을 낸다. 스크리너 없음 |
| **S5** 주문 한 번 내고 결과 받기 | 5-2 에서 수행 — 견적 통과, 체결은 `envelope_required` 로 막힘 |
| **S6** 고리 한 바퀴 | 5-2 에서 수행 — 다섯 바퀴. 테두리·주문 시도·결과 읽기·고치기 네 칸은 돌았고 체결 없음 |
| **S7** 중지 조건 발동 | 한도는 다섯 번 다 걸었으나 **발동 미확인** — 손실이 날 체결이 없었다 |
| **S8** 끊고 다시 붙이기 | 상주하지 않는다. 한 사이클 돌고 끝나 붙일 세션이 없다 |
| **S9** 수수료 바꿔 다시 재기 | `SimBroker` 에 수수료 모델이 없다(소스에 "future addition") |
| **S10** 둘·셋 나란히 돌리기 | 대결 트랙의 일 |

---

## 8. 기사에서 내린 것 — 이번 편에서 나온 제품 과제 (weltiq 쪽)

독자용이 아니라 내부 QA 항목이므로 독자판에서 제거하고 여기로 옮겼다.

1. **템플릿 파라미터가 `doc_hash` 를 못 따라간다.** `POST /v1/templates/{id}/instantiate` 가 `params` 를 무시해서, 외부 도구가 정한 수량으로 백테스트를 돌릴 경로가 없다. `POST /v1/containers` 는 파라미터별 해시를 제대로 만드는데 두 경로가 어긋나 있다.
2. **여러 종목을 한 장부로 netting 하는 자리가 없다.** 이 도구의 핵심 기능(합쳐진 장부에 마스터 리스크)이 옮겨지는 도중에 사라진다. 컨테이너 하나는 거래소 하나·종목 3개까지이고, 합산은 `GET /v1/containers/compare` 로 따로 봐야 한다.
3. **신호등은 `entry_blocked` 인데 실행 기록에는 아무것도 안 남는다.** r3 에서 `DENY` 가 떴는데 `counts.intents` 가 0이었다. 거절이 화면에만 있고 export 묶음에 없으면 에이전트가 자기 기록을 읽고 고치는 고리가 거기서 끊긴다. **이 편에서 제일 아픈 칸.**
4. **일봉 전략을 위한 리밸런스 개념이 없다.** "주에 한 번 목표 비중을 맞춘다"를 표현할 칸이 지금 문법에 없다.
5. **`us-equity` 컨테이너에 공매도가 없다.** 이 도구는 숏을 일상적으로 낸다.

부기: 일봉 펀더멘털 전략을 1분봉 컨테이너에 태운 것은 필자 쪽 설계 실수다. 이 도구의 리밸런스 주기는 주간이라 같은 시간틀로 돌리면 하루에 한 번만 평가되고, 그러면 글 한 편 안에 고리가 닫히지 않는다. 궁합 문제이지 어느 쪽의 결함이 아니다.

---

## 9. 기사에서 내린 것 — 모드 동일성 대조

독자판 5절에서는 도구 쪽 `run_cycle` 독스트링 인용만 남겼다. weltiq 쪽 실측은 이것이다.

같은 문서 해시의 `mode=backtest` · `mode=paper` 두 실험에서 갈리는 칸은 `data.data_mode` 하나뿐이었고, 전략·블록·시간틀을 바꿔 다시 해도 같았다. 다만 **실전 구간은 열려 있지 않다**(`live_orders: false`). "같은 문서가 그대로 간다"까지가 관측된 사실이고 그 뒤는 예정이다. 전환 자체는 커넥터 교체 수준이나 승격 통계 사다리가 관문이다.

도구 쪽 `run_cycle` 독스트링 전문:

> A backtest is run_cycle in a loop over history with a SimBroker; paper trading is the same loop on a live clock with a PaperBroker; live is the same loop with a real broker. **Only the clock and the broker change — the pipeline never does.**

이 도구의 `PaperBroker` 와 `RealBroker` 는 모듈 표에 `◐`("planned")로만 있다.

---

## 10. 캡처 목록

| 파일 | 기사 사용 | 무엇 |
|---|---|---|
| `shots/01-home.png` | 3-1 | 키 0개 상태의 TUI 홈 |
| `shots/02-model-picker.png` | 3-1 | `m` 모델 목록 (일곱 공급자·아홉 모델) |
| `shots/03-no-key-prompt.png` | 3-1 | 데이터 키를 먼저 묻는 화면 |
| `shots/05-run-today-fail.png` | 2절 | 시간대 때문에 죽은 실행 |
| `shots/05-run-report.png` | 4-1 | 버핏 `— 68%` vs 그레이엄 `▼ 94%` |
| `shots/06-run-report-2.png` | 4-1 | 그레이엄 근거 원문 |
| `shots/07-run-report-3.png` | — | 같은 보고서의 세 번째 화면 |
| `shots/08-backtest-window.png` | — | 백테스트 구간 설정 화면 |
| `shots/09-backtest-curve.png` | 4-1 | 자산 곡선 + 체결 테이프 |
| `shots/10-builder.png` | 4-1 | 펀드 만들기 마법사 1단계 |
| `shots/11-builder.png` | 4-1 | 전략 고르기 |
| `shots/12-builder.png` | — | 마법사 마지막 단계 |
| `shots/13-weltiq-slot.png` | 5절 | 전략 컨테이너 r2 상세 (`Auto-stop −300`) |
| `shots/14-fd-vs-yf.png` | 4-2 | 양쪽 종가 대조 |
| `shots/15-narrow-80x24.png` | 3-1 (링크) | 80×24 에서도 안 깨지는 홈 화면 |
