# A-28 검증 기록 — ardhanurfan/trading-bot-agent (TradingAgents Crypto Edition)

- 작성 2026-09-22 · macOS 26.6.2 / arm64 / Python 3.12
- 설치 위치 `/private/tmp/tut-src/trading-bot-agent` · 가상환경 `/private/tmp/tut-src/.venv-tba`
- 고정 커밋 `e478951d308c5a7e606ff25e92c0c2c7e2e3c08e` (2026-05-24, `master`)
- 모델 `openai/gpt-5.6-luna` (OpenRouter) — **12편과 같은 모델**로 고정해 비교 가능성을 확보
- 독자판: [`docs/tutorials/a/28-trading-bot-agent.md`](../../../docs/tutorials/a/28-trading-bot-agent.md)

이 편의 질문은 "별 0 개짜리 파생판이 쓸 만한가"가 아니라 **"미국 주식용 다중 에이전트 프레임워크를 크립토로 옮기면 무엇이 살아남고 무엇이 깨지는가"** 다. 12편(원본 TradingAgents)에서 실측한 13판 결과와 **같은 방식**으로 재는 것이 목적이다.

---

## 0. 저장소 사실관계 — 포크인가

| 항목 | 값 | 확인 방법 |
|---|---|---|
| GitHub API `fork` | **false** · `parent` 없음 | `api.github.com/repos/ardhanurfan/trading-bot-agent` |
| 별 / 포크 / 이슈 | **0 / 0 / 0** | 같음 |
| 개설 → 마지막 푸시 | 2026-05-12 → **2026-05-24** (12일) · 커밋 156 | 같음 (`commits?per_page=1` Link 헤더 `page=156`) |
| 라이선스 | Apache-2.0 (원본과 동일) | 같음 |
| `pyproject.toml` `version` | **`0.2.4`** | 저장소 |
| README 자기 서술 | "This project is a heavily modified and upgraded version of the original TauricResearch/TradingAgents" | `README.md` |
| README star-history 이미지 | `star-history.com/#TauricResearch/TradingAgents` — **원본의 별 곡선** | `README.md` |
| `assets/` | `TauricResearch.png` 등 원본 자산 그대로 | 저장소 |
| CLI 환영 화면 | "Built by [Tauric Research]" | 실행 캡처 `shots/01-cli-welcome.png` |

**판정: 깃허브 포크 플래그는 없으나 실질은 파생판이다.** 소스를 새 저장소로 다시 밀어 넣은 형태이고, 본인도 README 에서 그렇게 밝힌다. 라이선스는 Apache-2.0 을 유지해 승계 자체는 적법하다.

**따라서 원본(12편)과 같은 회차에 세우면 "독립 설계 둘"이 아니다.** 프롬프트·그래프·에이전트 구성이 원본 v0.2.4 와 거의 같고(§1), 갈리는 것은 자료원과 실행층뿐이다.

### 갈린 지점 — `pyproject` 가 `0.2.4` 라는 사실의 뜻

원본은 현재 `0.5.0` 이다(12편 §1 실측). 파생판은 **원본의 2026-04-25 판(v0.2.4)에서 갈라져 나왔고 그 뒤 원본을 따라오지 않았다.** 그래서 소스 비교에서 나오는 차이의 상당 부분은 "파생판이 고친 것"이 아니라 **"원본이 그 뒤에 더한 것"** 이다. 이 구분을 하지 않으면 판정이 통째로 틀린다.

---

## 1. 원본 대비 무엇이 다른가 — 파일 근거

### 1.1 파생판이 **더한** 것 (크립토 이식)

| 파일 | 무엇 |
|---|---|
| `tradingagents/dataflows/binance_client.py` | Binance REST 클라이언트. SDK 의존 없음. **testnet 기본** |
| `tradingagents/dataflows/binance_spot_data.py` | yfinance 출력 스키마를 그대로 흉내 낸 봉·지표 함수 |
| `tradingagents/agents/utils/crypto_on_chain_tools.py` | 온체인·파생 지표 도구 |
| `tradingagents/integrations/ticker_scanner.py` | 24h 거래대금·공포탐욕 지수로 종목을 스스로 고름 |
| `tradingagents/integrations/autonomous_loop.py` | 상주 데몬 — 스캔 → 분석 → 주문 큐 |
| `tradingagents/integrations/trade_validator.py` | 판정문 → 실행 주문 변환 + 검증 |
| `tradingagents/integrations/binance_executor.py` | Redis 큐를 소비해 Binance 현물에 주문 |
| `tradingagents/integrations/redis_queue.py` · `telegram_notifier.py` · `pinecone_memory.py` · `lumibot_executor.py` | 큐·알림·RAG 기억·주식 실행기 |
| `docker-compose.yml` | `ollama` · `redis` · `tradingagents` · `executor` 4서비스 |

### 1.2 파생판에 **없는** 것 (원본이 그 뒤에 더한 것 포함)

| 원본에 있음 | 파생판 | 비고 |
|---|---|---|
| `agents/analysts/sentiment_analyst.py` | 없음 (애널리스트 4종) | 원본은 5종 |
| `agents/utils/macro_data_tools.py` · `market_data_validation_tools.py` · `prediction_markets_tools.py` | 없음 | |
| `dataflows/fred.py` · `reddit.py` · `sec_edgar.py` · `stocktwits.py` · `polymarket.py` | 없음 | 12편에서 답을 갈랐던 **StockTwits 가 파생판에는 아예 없다** |
| `dataflows/market_data_validator.py` + `get_verified_market_snapshot` 도구 | 없음 | 원본은 "수치 주장은 검증 스냅숏을 진실로 삼아라"를 시장 애널리스트 프롬프트에 박아 뒀다 |
| `cli/utils.py` 의 `filter_analysts_for_asset_type` | **없음** | 원본은 크립토면 펀더멘털 애널리스트를 자동으로 뺀다. **§3 의 크래시 원인** |
| `.py` 파일 수 | 174 vs 241 | |

### 1.3 프롬프트 차이 — 매수/보류 편향에 직접 닿는 것

두 파일이 결정적이다. **아래 문장들은 전부 원본에만 있고 파생판에는 없다.**

`agents/managers/portfolio_manager.py` (원본):

> "The risk debate always contains conflicting stances; deciding which is stronger is the job, so **conflict alone is not a reason to Hold.** Commit to the stronger case, sized by how decisively it wins. Choose Hold only when the evidence is still balanced after that weighing, or too thin to support a call"

파생판의 같은 자리는 한 줄이다: `"Be decisive and ground every conclusion in specific evidence from the analysts."`

`agents/trader/trader.py` (원본):

> "Ground concrete price levels (entry, stop-loss, position sizing) in the technical market report's price structure -- current price, support/resistance, ATR, and volatility"
> "State entry price and stop-loss as **absolute price levels** in the instrument's quote currency (for example 189.5), never a percentage or a range"

원본은 트레이더에게 **시장 보고서 원문을 따로 넘겨 주고** 레벨을 숫자로 못 박게 한다(`market_report` 주입, `#1167`·`#1288`). 파생판의 트레이더는 리서치 매니저의 계획만 받는다.

`agents/utils/rating.py` (원본):

> "a decision nobody can read is not a Hold, and a Hold recorded in its place is quoted back to the next run as a call that was never made (#1170)."

원본은 판독 불가 판정을 `REVIEW` 로 따로 뺀다(`RATING_REVIEW`). 파생판에는 `RATING_REVIEW` 상수 자체가 없다.

`agents/utils/agent_utils.py`: 원본은 `get_portfolio_context_from_state`(장부 주입)를 트레이더·포트폴리오 매니저 양쪽에 넘긴다. 파생판에는 **`--portfolio` 자리 자체가 없다** — 12편 §5-2 에서 "이 도구라서 되는 것"으로 꼽았던 칸이 파생판에는 없다.

**결론**: 12편이 측정한 "Hold 0/13"의 원인 중 하나로 지목할 만한 프롬프트 장치들(Hold 금지 유도, 레벨 강제)은 **원본 쪽 자산**이다. 파생판은 그 이전 상태다. 그러면 파생판은 Hold 를 더 낼 것이고, 레벨은 덜 낼 것이다 — §4 에서 실제로 그렇게 나왔다.

---

## 2. 시장 — 현물인가 선물인가 (자격 ③)

| 검사 | 결과 |
|---|---|
| `grep -rn "fapi\|UMFutures\|futures_" --include="*.py"` | **2건.** 둘 다 `crypto_on_chain_tools.py` (`fapi/v1/fundingRate`, `fapi/v1/premiumIndex`) |
| 봉·시세·주문 경로 | `binance_client.py` 전량 **`/api/v3/*`** — `klines` · `ticker/24hr` · `ticker/price` · `exchangeInfo` · `account` · `order` · `openOrders` |
| 주문 함수 | `place_market_order` · `place_limit_order` → `POST /api/v3/order` (현물) |
| 슬롯·실행 | 현물 계정 잔고 기반 |

**판정: 실행과 시세는 100% Binance 현물이다.** 선물은 **펀딩비·프리미엄 지수 두 개의 참고 지표로만** 들어온다(CryptoTradingAgents 가 `UMFutures()` 로 자료 전체를 선물에서 받는 것과 다르다). **자격 ③ 충족.**

단, 다음은 현물 적용에 실제로 걸린다.

- `binance_executor.py` 에 **주문 전 잔고 확인이 없다** (`get_account`·`free` 호출 0건). 판정이 `Sell` 이면 보유가 없어도 현물 매도 주문이 큐에 들어간다. 현물에서 이것은 체결되지 않는다.
- §4 의 유효 판정 9건 중 포트폴리오 매니저 등급이 **매도 쪽(Underweight/Sell) 8건**, 트레이더 행동이 **Sell 6건**. 현금으로 시작한 현물 계정에서는 **실행 가능한 판정이 사실상 없다.**

---

## 3. 설치와 막힌 자리 — 우회 전 과정

```
git clone --depth 1 https://github.com/ardhanurfan/trading-bot-agent.git
uv venv -p 3.12 /private/tmp/tut-src/.venv-tba
uv pip install --python /private/tmp/tut-src/.venv-tba/bin/python -r requirements.txt
```

설치 자체는 막힘 없음(exit 0).

### H1. 펀더멘털 애널리스트가 BTC 의 재무제표를 요구하며 **크래시**

첫 실행이 통째로 죽었다.

```
File "tradingagents/agents/utils/fundamental_data_tools.py", line 39, in get_balance_sheet
File "tradingagents/dataflows/interface.py", line 170, in route_to_vendor
File "tradingagents/dataflows/alpha_vantage_common.py", line 14, in get_api_key
ValueError: ALPHA_VANTAGE_API_KEY environment variable is not set.
During task with name 'tools_fundamentals'
```

원인 사슬:

1. `default_config.py` 는 `"fundamental_data": "binance"` 로 설정돼 있다
2. `interface.py` 의 `VENDOR_METHODS` 에서 `get_fundamentals` 에만 `binance` 구현(`get_crypto_fundamentals`, CoinGecko)이 있다. **`get_balance_sheet`·`get_cashflow`·`get_income_statement` 에는 `binance` 항목이 없다**
3. 펀더멘털 애널리스트는 그 네 도구를 전부 바인딩받으므로 LLM 이 `get_balance_sheet("BTCUSDT")` 를 부른다
4. `route_to_vendor` 는 지원되지 않는 벤더를 건너뛰고 `alpha_vantage` 로 넘어간다
5. 폴백 `except` 절이 `AlphaVantageRateLimitError` **만** 잡는다. 키 없음은 `ValueError` 라 그대로 위로 던져져 그래프가 죽는다

원본은 `filter_analysts_for_asset_type` 로 크립토일 때 펀더멘털 애널리스트를 아예 뺀다(12편 §3 에서 `Detected asset type: crypto` 로 확인된 동작). **파생판은 그 가드를 가져오지 않았다.**

**우회**: 애널리스트를 `["market","social","news"]` 3종으로 지정. 이 편의 모든 판정은 이 구성이다. (원본이 크립토에서 자동으로 만드는 구성과 동일하므로 비교 가능성은 유지된다.)

### H2. `BINANCE_TESTNET` 기본값 `true` — 판단이 **14일치 모의 시장** 위에서 난다

`binance_client.py`:

```python
if testnet is None:
    testnet_env = os.getenv("BINANCE_TESTNET", "true").lower()
    testnet = testnet_env not in ("false", "0", "no")
```

testnet(`testnet.binance.vision`)은 공개 시세 엔드포인트가 열려 있어 **오류 없이 돈다.** 그래서 잘못 쓰고 있다는 신호가 안 난다. 실측:

| | 일봉 개수(`limit=1000`) | 가장 오래된 봉 |
|---|---:|---|
| `api.binance.com` | **1,000** | 2023-12-28 |
| `testnet.binance.vision` | **14** | 2026-09-09 |

200일 이동평균이 성립하지 않는다. 도구 자신이 그것을 알아챘다 — testnet 판(rT1)의 시장 보고서 원문:

> "The reported **close_200_sma** was not emphasized in the signal interpretation because it is **identical to the 50 SMA** in the supplied output, indicating that the **data window is insufficient** or the vendor has substituted the available average."

**우회**: `export BINANCE_TESTNET=false`. 공개 시세만 읽을 때는 키가 필요 없다.

### H3. testnet 캐시가 이후의 실시장 판을 **오염**시킨다 (우리 쪽 실험 사고이자 저쪽 결함)

`binance_spot_data.py:109` `_load_ohlcv_binance` 의 캐시 파일명이 **testnet/live 를 구분하지 않는다.**

```python
start_date = (today - pd.DateOffset(years=5)).strftime("%Y-%m-%d")
cache_file = f"{safe_symbol}-Binance-data-{start_date}-{end_date}.csv"
```

rT1(testnet)이 먼저 돌면서 `~/.tradingagents/cache/BTCUSDT-Binance-data-2021-09-22-2026-09-22.csv` 에 **14행**을 써 놓았다. 이름은 5년 범위인데 내용은 14일이다. 그 뒤 `BINANCE_TESTNET=false` 로 돌린 판들이 그 캐시를 그대로 읽었다.

- 오염 사본: [`raw/testnet-poisoned-cache.csv`](raw/testnet-poisoned-cache.csv) (15행 = 헤더 + 14봉)
- 오염된 판 3개는 **판정에서 제외**하고 원문만 남긴다: `raw/rA1_poisonedcache.json` · `rA2_poisonedcache.json` · `rA3_poisonedcache.json`
- 주의: `get_stock_data`(`get_binance_ohlcv`)는 캐시를 안 쓰고 직접 받는다. 즉 **OHLCV CSV 는 실시장, 지표는 testnet** 이라는 섞인 상태였다. 가장 나쁜 종류의 오염이다
- **우회**: 스위치를 바꾼 뒤 `rm ~/.tradingagents/cache/*Binance*.csv`

### H4. 판정이 판마다 독립이 아니다 — 디스크에 남는 기억

`default_config.py` 의 `memory_log_path` 기본값은 `~/.tradingagents/memory/trading_memory.md` 다. `propagate()` 는 끝에 판정을 여기 적고, 다음 같은 종목 판에서 이전 판정을 되읽는다.

- 이 기계의 그 파일은 **6,741줄**이었고, 2026-04-30 자 `NQ=F`·`BTCUSDT`·`ETHUSDT`·`YM=F` 판정(전부 Overweight/Underweight)이 들어 있었다. `~/.tradingagents/` 는 **원본과 파생판이 공유하는 경로**다 — 파생판이 원본 시절의 판정을 물려받는다
- 실제로 되먹임이 일어났다. rA1 의 포트폴리오 매니저 원문:

> "Prior BTCUSDT outcomes reinforce this balance: **the recent Underweight calls correctly warned against chasing extended momentum**, while the earlier Overweight and Hold calls underperformed"

- **B축 D1("독립 판단 N회")에 직결된다.** 같은 미션을 N번 던져도 기본 설정에서는 독립이 아니다
- **우회**: `config["memory_log_path"]` 를 판마다 새 파일로. 이 편은 그 구성을 **C계열**로 따로 돌려 대조했다(§4)

### H5. CoinGecko 뉴스 404 / 429

모든 판에서 `CoinGecko news fetch failed for BTC: HTTP Error 404` 또는 `429`. 뉴스 칸이 비거나 얇아진다. 판이 죽지는 않는다.

### H6. CLI 는 주식 시절 그대로다

`python -m cli.main` 첫 화면(캡처 `shots/01-cli-welcome.png`):

- "Built by [Tauric Research]" · Announcements 가 `api.tauric.ai` 를 가리킴
- `Step 1: Ticker Symbol` 기본값 **`SPY`**, 예시 `SPY, CNC.TO, 7203.T, 0700.HK` — **크립토 예시 0개**
- 원본의 `Detected asset type: crypto`·환경변수 건너뛰기(`TRADINGAGENTS_*`)가 **없다**. `cli/config.py` 는 공지 URL 3줄이 전부

`main.py` 도 작성자 개인 설정 그대로다 — `llm_provider="ollama"`, `qwen3:8b`, `data_vendors` 전부 `yfinance`, `ticker="NVDA"`, `trade_date="2026-05-10"`. **크립토 에디션의 진입점은 CLI 가 아니라 `run_autonomous.py` 와 파이썬 API 다.**

---

## 4. B축 자격 판정 — 미션 실행 검증

### 4.1 미션 원문과 조건

크립토 가능하므로 브리프 원문을 쓴다.

> 비트코인을 거래해서 수익을 내줘. 그리고 왜 그렇게 했는지 설명해줘

이 도구에는 자연어 미션 입력구가 없다. **종목을 받는 방식**이고, 이는 기획 §4.1 이 "종목을 입력받는 방식(TradingAgents)은 인정한다"로 명시한 형태다. 미션은 `propagate("BTCUSDT", "2026-09-22")` 로 던졌다 — 12편이 원본에 던진 것과 같은 형태.

고정: 모델 `openai/gpt-5.6-luna` · 애널리스트 `market,social,news` · 토론 1라운드 · 리스크 논의 1라운드 · 커밋 `e478951`.

### 4.2 판정 전체표 (12판)

| 판 | 날짜 | 시세 | 기억 | PM 등급 | 트레이더 | Entry | Stop | 초 |
|---|---|---|---|---|---|---|---|---:|
| rT1 | 2026-09-22 | **testnet** | 공유 | Underweight | Sell | 86625.0 | 87383.0 | 197.7 |
| rA1 | 2026-09-22 | 실시장 | 공유 | **Hold** | **Hold** | — | — | 274.4 |
| rA2 | 2026-09-22 | 실시장 | 공유 | Underweight | Sell | — | — | 171.7 |
| rA3 | 2026-09-22 | 실시장 | 공유 | Underweight | Sell | — | — | 189.4 |
| rB1 | **2026-06-30** | 실시장 | 공유 | Underweight | Sell | — | — | 177.3 |
| rC1 | 2026-09-22 | 실시장 | **격리** | Underweight | Hold | — | 74000.0 | 165.7 |
| rC2 | 2026-09-22 | 실시장 | **격리** | **Sell** | Sell | — | — | 178.6 |
| rC3 | 2026-09-22 | 실시장 | **격리** | Underweight | Sell | — | — | 177.5 |
| rD1 | 2026-09-22 | 실시장 | **격리** | Underweight | Hold | — | — | 179.3 |
| rA1p | 2026-09-22 | 오염(H3) | 공유 | Underweight | Hold | — | — | 199.9 |
| rA2p | 2026-09-22 | 오염(H3) | 공유 | Underweight | Hold | — | 79150.0 | 188.6 |
| rA3p | 2026-09-22 | 오염(H3) | 공유 | Underweight | Sell | — | 79150.0 | 171.1 |

rD1 은 독자판 §3 의 캡처(`shots/03-run-decision.png`)를 찍은 실행이기도 하다. 원문 전부: [`raw/`](raw/) (`r*.json` — `final_trade_decision` · `trader_investment_plan` · `investment_plan` · 애널리스트 보고서 전문 포함). 격리 기억 파일은 `raw/rC*-memory.md` · `raw/rD1-memory.md`.

캡처 3장: `shots/01-cli-welcome.png`(명령줄 첫 기동 — 기본 티커 `SPY`) · `shots/02-testnet-vs-live.png`(같은 요청의 일봉 개수 1,000 vs 14) · `shots/03-run-decision.png`(rD1 판정). weltiq 캡처는 **체결이 0이라 넣지 않았다**(가이드 R6).

### 4.3 집계

| | 12판 전체 | 유효 9판 (오염 제외) |
|---|---:|---:|
| PM **Buy** | **0** | **0** |
| PM **Overweight** | **0** | **0** |
| PM Hold | 1 | 1 |
| PM Underweight | 10 | 7 |
| PM Sell | 1 | 1 |
| 트레이더 **Buy** | **0** | **0** |
| 트레이더 Hold | 5 | 3 |
| 트레이더 Sell | 7 | 6 |
| `Entry Price` 가 나온 판 | 1 / 12 | 1 / 9 |
| `Stop Loss` 가 나온 판 | 3 / 12 | 2 / 9 |
| `Position Sizing` 이 나온 판 | **12 / 12** | 9 / 9 |

### 4.4 원본(12편)과 나란히 — 이 편의 핵심

| | 원본 TradingAgents (12편, 13판) | 파생판 (이 편, 유효 9판) |
|---|---|---|
| 모델 | `openai/gpt-5.6-luna` | **같음** |
| 매수 판정 (Buy/Overweight) | **0 / 13** | **0 / 9** |
| Hold | **0 / 13** | **1 / 9** (PM) · **3 / 9** (트레이더) |
| 트레이더 최종 제안 | SELL **13 / 13** | SELL **6 / 9** |
| 과매도 국면(−53%, RSI 30)을 물었을 때 | Underweight (매수 거절) | **Underweight (매수 거절)** — rB1, 2026-06-30 동일 날짜 |
| `Entry Price` 를 낸 비율 | 다수 (원본은 프롬프트로 강제) | **1 / 9** |

**매수 편향은 그대로 물려받았다.** 자료원을 야후 파이낸스에서 Binance 현물로 바꾸고, 감성 자료원(StockTwits·Reddit)을 CoinGecko 로 갈아 끼우고, 프롬프트도 한 세대 뒤진 판인데 **매수는 여전히 0이다.** 12편이 "종목·날짜·국면을 바꿔도 부호가 안 바뀐다"고 적은 것이 파생판에서도 재현됐다. 12편과 **같은 날짜(2026-06-30, 고점 대비 −53%)** 를 물었을 때 역시 매수를 거절했다.

**달라진 것은 결단성이다.** 원본이 13판에서 한 번도 안 낸 Hold 를 파생판은 낸다. §1.3 의 프롬프트 차이와 방향이 맞는다 — "conflict alone is not a reason to Hold" 와 레벨 강제 지시가 **원본에만** 있고, 그 두 문장이 없는 파생판은 더 자주 판단을 미루고 레벨을 덜 적는다. 다만 9판은 인과를 증명하기에 작다. **"프롬프트 차이와 관측 방향이 일치한다"까지가 이 실측이 말할 수 있는 전부다.**

### 4.5 자격 7문항

**① 판단 형태 — 레벨 / 배분 / 방향만**

**배분(크기)이 본체이고, 레벨은 간헐적이다.** 기획 §4.3b 의 사전 판정 "레벨+방향·크기"는 **과대평가였다.**

- `Position Sizing` 9/9 — 항상 나온다. 예: rA3 "Target roughly 75–85% of a normal BTCUSDT allocation"
- `Entry Price` 1/9 · `Stop Loss` 2/9 — 스키마에 `Optional` 로 잡혀 있고(`agents/schemas.py` `TraderProposal`) 프롬프트가 레벨을 요구하지 않는다(§1.3)
- 레벨이 나온 유일한 판(rT1)의 값은 **Entry 86625 / Stop 87383** — 매도 판정인데 손절이 진입 **위**다. 본문에서 저항 구간으로 인용한 `86,625–87,383` 두 숫자를 그대로 두 칸에 나눠 넣은 것에 가깝다. **레벨로 신뢰할 수 없다**
- 단, 본문(prose)에는 레벨이 풍부하다. rA1: "additions only after a controlled pullback toward **83,500–84,500** or a confirmed close above **87,400**, and reassess defensively below **81,270**". 구조화 칸이 아니라 글 속에 있다

**3회 일관성**: 같은 조건 3회를 두 계열로 돌렸다.

- A계열(공유 기억): Hold · Underweight · Underweight → **부호는 일관(비매수), 등급은 흔들림**
- C계열(격리 기억): Underweight · Sell · Underweight → **부호 일관, 등급 흔들림**. 같은 구성의 네 번째 판(rD1)도 Underweight
- 두 계열 합쳐 7판 모두 **매수 아님**. 방향은 일관되고 **강도는 일관되지 않다**

**② 근거를 말하는가 — 그렇다.** 원문 인용(rA1 포트폴리오 매니저):

> "BTCUSDT is above rising short-, medium-, and long-term references—the 10-day EMA at 81,270, 50-day SMA at 74,140, and 200-day SMA at 70,706—and MACD accelerated from roughly 880 to 2,274; the September 21 advance also had meaningful volume of about 31,963 BTC versus 10,994 BTC previously."

rC2(격리·Sell):

> 근거 원문은 `raw/rC2.json` 의 `investment_thesis` 전문에 있다. 강세/약세 토론의 어느 쪽이 이겼는지를 매 판 적는다.

**근거의 질은 원본과 같은 수준이다.** 이 부분은 이식에서 손상되지 않았다.

**③ 시장 — 현물** (§2). 자격 ③ 충족. 단 **판정 8건 중 6건이 매도**라 현금 시작 현물 계정에서 실행 가능한 것이 거의 없다.

**④ 반복 호출 — 가능하다.** CLI 는 대화형이지만 파이썬 진입점이 안정적이다.

```bash
PYTHONPATH=. python -c "
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph.trading_graph import TradingAgentsGraph
c=dict(DEFAULT_CONFIG); c.update({'llm_provider':'openrouter','backend_url':'https://openrouter.ai/api/v1',
 'deep_think_llm':'openai/gpt-5.6-luna','quick_think_llm':'openai/gpt-5.6-luna',
 'memory_log_path':'/tmp/run7.md'})
g=TradingAgentsGraph(debug=False,config=c,selected_analysts=['market','social','news'])
print(g.propagate('BTCUSDT','2026-09-22')[1])"
```

**`memory_log_path` 를 판마다 갈아 주지 않으면 독립 판단이 아니다**(H4). 이 한 줄이 B축 D1 의 전제다. 상주형으로는 `python run_autonomous.py`(`scan_interval_minutes` 기본 60).

**⑤ `confidence` 를 내는가 — 낸다. 단 에이전트가 내는 것이 아니다.**

이 편에서 제일 중요한 소스 발견이다. 11명의 토론 어디에도 `confidence` 가 없다. 그런데 `integrations/trade_validator.py` 의 `ExecutableTradeOrder` 에는 있고, **0.60 미만이면 주문을 거절**한다.

그 값은 `parse_decision_to_order` 가 **별도의 LLM 1회 호출**로 판정문을 다시 읽어 뽑는다(`_EXTRACTION_PROMPT`):

```
"quantity": <float — base asset amount>,
"stop_loss": <float>, "take_profit": <float>,
"confidence": <float between 0 and 1>,
...
- If values are not mentioned, make reasonable estimates based on the analysis
```

실측했다 — 세 판정문을 이 경로에 통과시킨 결과(`raw/extracted-orders.json`):

| 원 판정 | side | quantity | limit | stop_loss | take_profit | confidence |
|---|---|---|---|---|---|---|
| rA2 (Underweight) | sell | 0.05 | 87400.0 | 92000.0 | 81270.0 | **0.82** |
| rB1 (Underweight) | sell | 0.01 | 60700.0 | 65000.0 | 54000.0 | **0.84** |
| rT1 (Underweight) | sell | 0.01 | 87000.0 | 90000.0 | 81400.0 | **0.86** |

**레벨은 판정문 본문의 숫자를 다시 가져온 것이고, `quantity` 와 `confidence` 는 "reasonable estimates"로 지어낸 것이다.** BTC 0.05 개라는 숫자는 어떤 에이전트도 말한 적이 없다.

LLM 없이(`llm=None`) 돌리면 정규식 폴백이 도는데, 이쪽은 더 나쁘다.

```python
# _extract_via_regex
entry = prices[0]                                    # 본문에 나온 첫 번째 숫자
stop_loss  = round(entry * (0.95 if BUY else 1.05), 2)
take_profit= round(entry * (1.10 if BUY else 0.90), 2)
confidence = 0.7                                     # 기본값 — 0.60 게이트를 그냥 통과
```

게다가 **실제 판정문에 이 폴백을 먹이면 터진다**:

```
Regex extraction failed for BTCUSDT: could not convert string to float: ''
Could not extract trade parameters for BTCUSDT from decision text. No order generated.
```

정규식 `[\d,]+\.?\d*` 이 빈 문자열에 매치돼 `float('')` 에서 죽는다. 결과적으로 **키 없이 돌리면 주문이 한 건도 안 나온다.**

**M12(확신도 보정) 대상 판정**: **부적격.** 이 `confidence` 는 판단자의 확신도가 아니라 **판정문을 요약한 별도 모델의 자평**이다. 보정 곡선을 그려도 무엇을 재는지 알 수 없다.

**⑥ 내장 주문기와 testnet** — `binance_executor.py` 는 Redis 큐를 소비해 `POST /api/v3/order` 를 친다. `BINANCE_TESTNET` 이 여기서도 기본 `true` 이고, `execution_enabled` 는 기본 `False` 다. **공개 실시장 자료를 읽는 설정(`BINANCE_TESTNET=false` + 키 없음)과 내장 주문기 사용(키 필요)은 갈린다** — 시세만 쓸 거면 Binance 키가 아예 필요 없다. 이 편은 **내장 주문기를 쓰지 않았다.**

**⑦ 결론**

> **조건부 참가.**
>
> 자격 ①②③ 은 충족한다 — 시장을 읽고, 방향과 배분을 내고, 근거를 말하며, 현물이다. 그러나 **원본(12편)과 같은 회차에 세우면 안 된다.** 프롬프트·그래프가 원본 v0.2.4 와 거의 같고 매수 편향도 동일하게 나타났다(0/9 vs 0/13) — 독립 설계 둘이 아니라 **같은 설계의 두 세대**다.
>
> 세울 자리가 있다면 **"같은 프레임워크를 세대 차이로 붙이는 회차"** 다 — 원본 0.5.0 vs 파생판 0.2.4 를 같은 미션에 붙여 **프롬프트 한 문장("conflict alone is not a reason to Hold")이 판정 분포를 얼마나 바꾸는지** 재는 것. 이것은 다른 도구끼리는 못 하는 실험이다.
>
> 참가시킬 경우 전제 셋: (a) `BINANCE_TESTNET=false` 고정 + 캐시 삭제, (b) `memory_log_path` 판마다 격리, (c) `confidence` 를 M12 지표에 넣지 않는다.

**기획 §4.3b 사전 판정과 다른 점 둘**

| | 사전 판정 (§4.3, Codex) | 실행 검증 |
|---|---|---|
| 판단 형태 | "레벨+방향·크기" | **방향+크기.** 레벨은 9판 중 1판(Entry)·2판(Stop). 구조화 레벨은 신뢰 불가 |
| 반복 호출 | "Python 루프" | 맞다. 단 **기본 설정에서는 반복이 독립이 아니다**(H4) — 사전 조사에 없던 조건 |

사전 판정에 없었으나 확인된 것: 현물 100%(자격 ③ 확정) · `confidence` 존재하나 후처리 산물 · 원본과 매수 편향 동일.

---

## 5. weltiq 로 옮기기 — 통합 실험

### 5.1 옮길 수 있는 것과 없는 것

| 도구가 낸 것 | weltiq 로 | 결과 |
|---|---|---|
| rA2 의 완결 주문 (sell 0.05 BTC, limit 87400, stop 92000, tp 81270) | **못 옮긴다** | 현물 슬롯은 현금에서 시작한다. 보유가 없으면 매도가 성립하지 않는다. **12편 §5-3 이 부딪힌 벽과 같은 벽** |
| 배분 지시 ("정상 배분의 75~85%") | **못 옮긴다** | 배분은 규칙이 아니다 (12편이 이미 적은 도구 성질) |
| rA1 이 글로 적은 **조건부 롱 분기** ("confirmed close above 87,400", "reassess defensively below 81,270") | **옮겼다** | 아래 |
| `confidence` | 옮기지 않았다 | 후처리 산물이라 근거로 쓸 수 없다 (§4.5 ⑤) |
| 수량 | **필자가 정했다** | rA1 은 Hold 라 신규 진입 수량을 말하지 않았다. 0.02 BTC 는 필자의 값이다 |

**R8 표시**: rA1 의 최종 행동은 **Hold** 다. 옮긴 것은 그 판정문이 명시한 **추가매수 조건 분기**이며, 방향(롱)과 레벨(87,400 / 81,270)은 도구 원문 그대로다. 수량만 필자가 정했다. 12편 §5-3 처럼 매도를 매수로 뒤집은 것이 아니다.

### 5.2 실행 기록

| 순서 | 호출 | 결과 |
|---:|---|---|
| 1 | `POST /v1/strategy-rules` | `doc_hash = 8984a776d238855ea8cc0d6fb75022cfde84397fc98ef74f217e86bd5604e832` · 요약 `((close > 87400) AND (position.size = 0)) → 진입 long L → 청산: 보호 손절 7%` |
| 2 | `POST /v1/ai/thesis` | 1차 `422 Field required: text` (`why`/`invalidation_text` 로 보냈다) → 필드명 `text` 로 재송신 → `ok: true` |
| 3 | `POST /v1/containers` | 1차 `422` — `stop` 을 중첩 객체로 보냄. `stop_max_loss`/`stop_after_hours` 평면 필드여야 함. 2차 `422` — `stop_on_worker_failure` 는 받지 않음. 3차 성공 |
| 4 | 결과 | `ct_b5b7d05505980f20` / `dep_pa_acad32a517b3fa8a` / `sl_pa_acad32a517b3fa8a` · `status: running` · `stop.max_loss 300` · `stop.until 2026-09-22T05:39:46Z` |

손절 7% 는 도구가 준 두 숫자에서 나왔다 — (87,400 − 81,270) / 87,400 = 7.01%.

**변환에서 잃은 칸**

| 칸 | 왜 |
|---|---|
| 배분 지시("정상 배분의 75~85%") | 규칙 문법에 배분 개념이 없다 (도구 성질) |
| 이익실현 81,270 | rA2 의 매도 시나리오 값이고 rA1 의 롱 분기에는 대응이 없다 |
| "controlled pullback toward 83,500–84,500" 분기 | 한 슬롯에 진입 조건 하나만 옮겼다 |
| `confidence` 0.82 | 후처리 산물 |

### 5.3 관측 — 체결 없음

| 시각(UTC) | `stage` | 조건 평가 | 보유 |
|---|---|---|---|
| 03:43 | `conditions` | `85474.22 > 87400` → **false** · `position.size = 0` → true | 없음 |
| 03:50 | `conditions` | `85548.00 > 87400` → **false** | 없음 |
| 03:53 | `conditions` | `85494.74 > 87400` → **false** | 없음 |

- `data_mode: realtime` · `attached: true` · 1분봉을 끊김 없이 받았다. 규칙 평가기는 **정상 동작했다** — 두 비교 노드의 피연산자와 결과가 매 봉 기록된다
- **체결 0.** 도구가 지목한 돌파선(87,400)이 당시 가격보다 2.2% 위였고 약 14분의 관측 창 안에 닿지 않았다
- `GET /v1/containers/{id}/export` → `fills: 0`. 원문 [`raw/export.json`](raw/export.json) · [`raw/container_detail.json`](raw/container_detail.json)
- 관측 뒤 `POST .../stop` → `status: stopping` ("엔진 동기화 후 다음 봉에서 보유분을 청산합니다") → 청산 완료 후 `DELETE` 로 삭제. 정지 직후 `DELETE` 는 `409 still_stopping` 을 돌려준다

**이 관측이 말해 주는 것**: 규칙 전달과 슬롯 동작까지는 확인됐고, **체결은 미확인이다.** 도구 판정이 조건부 분기라 관측 창 안에 트리거가 안 왔다. R3 기준으로 "직접 확인"은 규칙 생성·슬롯 기동·조건 평가까지이고, 체결·손익은 **미확인**이다.

---

## 6. 안 돌린 것 — 사유

| | 왜 |
|---|---|
| `run_autonomous.py` 상주 데몬 | Redis·Telegram·Pinecone 이 전제이고 `docker compose` 4서비스가 필요하다. 이 편의 질문(판정 분포)에 필요하지 않다 |
| `ticker_scanner.py` 로 종목 자동 선정 | 미션이 BTC 고정이라 불필요. 공포탐욕 지수 API 의존 |
| `binance_executor.py` 실주문 | **페이퍼 전용 원칙.** 내장 주문기는 한 번도 켜지 않았다 (`execution_enabled=False` 기본값 유지) |
| Alpha Vantage 키로 펀더멘털 애널리스트 복구 | 키가 없다. BTC 의 재무제표를 받을 방법도 없다 |
| 미국 주식 판정 | 기본 `data_vendors` 가 전부 `binance` 라 NVDA 를 넣으면 Binance 에 `NVDA` 심볼을 묻는다. `main.py` 는 `yfinance` 로 덮어쓰지만 그건 작성자 개인 설정이고 이 편의 질문 밖 |
| 확신도 보정(M12) | §4.5 ⑤ 에서 대상 아님으로 판정 |

---

## 7. 결함 목록

### 저쪽 (trading-bot-agent)

| # | 결함 | 영향 | 근거 |
|---:|---|---|---|
| 1 | 크립토인데 펀더멘털 애널리스트가 재무제표를 요구 → **그래프 크래시** | 기본 설정으로 첫 실행이 죽는다 | §3 H1 |
| 2 | `BINANCE_TESTNET` 기본 `true` + testnet 일봉 **14개** | 200SMA 가 성립하지 않는데 오류가 안 난다 | §3 H2 |
| 3 | 캐시 파일명이 testnet/live 를 구분 안 함 | 한 번의 testnet 실행이 이후 실시장 판을 오염 | §3 H3 |
| 4 | 기억 로그가 기본 공유 + 원본과 경로 공유 | 반복 호출이 독립이 아니다 | §3 H4 |
| 5 | `_extract_via_regex` 가 실제 판정문에서 `float('')` 로 예외 | LLM 없이는 주문이 안 나온다 | §4.5 ⑤ |
| 6 | 정규식 폴백이 손절·목표를 진입가 ±5%/±10% 고정으로 지어냄 · `confidence` 기본 0.7 이 0.60 게이트를 자동 통과 | 검증 게이트가 형식뿐 | `trade_validator.py` |
| 7 | 현물 주문 전 **잔고 확인 없음** | 보유 없는 매도 주문이 큐에 들어간다 | §2 |
| 8 | CoinGecko 뉴스 404/429 | 뉴스 칸이 빈다 | §3 H5 |
| 9 | CLI·`main.py` 가 주식 시절 그대로 | 크립토 에디션인데 기본 티커가 `SPY`/`NVDA` | §3 H6 |
| 10 | 원본 v0.2.4 에 고정, 4개월간 미갱신 | 원본의 수정(레벨 강제·REVIEW·검증 스냅숏)을 못 받는다 | §1 |

### 우리 쪽 (weltiq)

| # | 결함/마찰 | 내용 |
|---:|---|---|
| 1 | `POST /v1/ai/thesis` 의 필수 필드명이 `text` | 422 가 `why`/`invalidation_text` 를 안 받는다고만 말한다. 오류 본문에 입력 전문이 그대로 되돌아온다 |
| 2 | `POST /v1/containers` 의 중지 조건이 **평면 필드** | 슬롯 상세 조회 응답은 `stop: {max_loss, after_hours, on_worker_failure}` 중첩 객체로 준다. **읽는 모양과 쓰는 모양이 다르다.** 422 두 번을 이걸로 썼다 |
| 3 | `stop_on_worker_failure` 를 생성에서 안 받음 | 응답에는 `on_worker_failure: liquidate` 가 들어 있다 |

---

## 8. 비용

| | 값 |
|---|---|
| 측정된 판 9건 | 입력 678,151 토큰 · 출력 129,221 토큰 |
| 모델 단가 (`openai/gpt-5.6-luna`, OpenRouter 조회값) | $0.20 / 1M 입력 · $1.20 / 1M 출력 |
| **9판 합계** | **$0.2907** (판당 $0.0323) |
| 오염된 3판 (토큰 미계측) | 추정 $0.09 — 계측 코드를 넣기 전에 돌았다 |
| 주문 추출 3회 | 추정 $0.01 |
| **총 추정** | **약 $0.39** (상한 $1.5) |
| Binance · CoinGecko | 무료, 키 불필요 |
| weltiq | 슬롯 1개 |

판당 $0.0323 은 12편 원본의 $0.058 보다 싸다. 애널리스트가 3명(원본 크립토 판과 동일 구성)이고 파생판에 검증 스냅숏·거시 도구가 없어 도구 호출이 적기 때문이다.

---

## 9. 이 검증이 말해 주지 않는 것

- 9판은 인과를 증명하기에 작다. "프롬프트 차이와 관측 방향이 일치한다"까지가 전부다
- 물어본 것은 **BTCUSDT 두 날짜**뿐이다. "이 도구는 절대 안 산다"고 말할 수 없다
- 원본과의 대조는 **12편의 13판(4애널리스트·야후 자료)** 과 **이 편의 9판(3애널리스트·Binance 자료)** 을 비교한 것이다. 모델과 토론 라운드는 같지만 자료원과 애널리스트 수가 다르다. 완전한 통제 실험이 아니다
- 상주 데몬·스캐너·내장 주문기는 안 돌렸다. 이 도구가 파는 물건의 절반은 미검증이다
- weltiq 슬롯은 한 개, 짧은 관측이다
