# 33편 swarm-trader — 검증 기록

2026-09-24 04:31~04:52 KST. 모든 숫자는 실측.
트리 `/Users/unolee/dev/weltiq-launch` · 도구 `/Users/unolee/dev/weltiq-tools/swarm-trader`.
LLM 은 OpenRouter 경유 `openai/gpt-5.6-luna` 고정. 주문은 전부 페이퍼.

## 0. 고정된 것

| | |
|---|---|
| 저장소 | `zhound420/swarm-trader` |
| 커밋 | **`3470b8291c438dc36b22ed37b7e56ba2bbf249b5`** (2026-03-22 15:28:41 -0700) |
| 라이선스 | **없음** — GitHub API `license: null`, `LICENSE*` 파일 없음 |
| 별·포크 | ★58 · 13 (관측 2026-09-24) · 생성 2026-03-08 · 마지막 푸시 2026-03-22 |
| 코드 | `src/` + `app/` 20,135줄 |
| 파이썬 | 3.11.16 (`python3.11 -m venv`) |
| 설치 | `.venv/bin/pip install -e .` — **36.083초** (경고: `fastapi 0.104.1 does not provide the extra 'standard'`) |
| 브로커 | `paper-api.alpaca.markets` 하드코딩 (`src/alpaca_integration.py:23`) — 실계좌 URL 이 코드에 없음 |

## 1. 기획서의 두 주장 — 판정

### 주장 ①「자료원이 정말 무료(yfinance·SEC EDGAR·Alpaca)」→ **사실이다. 실측으로 확인**

`requests.Session` · `urllib` · **`curl_cffi`**(yfinance 1.7.0 이 쓰는 전송층) 셋을 후킹해
나가는 호스트를 셌다. `probe_data_layer.py` · `net_probe.py`.

**자료 층만 (`src/tools/api` 함수 여섯, 전부 성공)** — `raw/net-hosts-datalayer.json`

| 호스트 | 호출 수 |
|---|---:|
| `www.sec.gov` | 20 |
| `query2.finance.yahoo.com` | 6 |
| `query1.finance.yahoo.com` | 4 |
| `data.sec.gov` | 1 |
| `finance.yahoo.com` | 1 |
| **`with_auth_header`** | **{} — 0건** |

**전체 회차(애널리스트 20명 포함)** — `raw/net-hosts.json`

| 호스트 | 호출 수 | 인증 |
|---|---:|---|
| `www.sec.gov` | 129 | 없음 |
| `data.sec.gov` | 16 | 없음 |
| `paper-api.alpaca.markets` | 2 | **있음** (알파카 무료 키) |

**유료 호스트 0건.** `api.financialdatasets.ai` 는 `src/tools/api_original.py` 에 남아 있으나
**임포트하는 곳이 없다**(`grep -rn api_original` → `api.py` 주석 2행뿐). `src/tools/api.py` 는
`api_free` 순수 재수출 9줄.

> ⚠️ **정정 필요 — README:666 의 "유료→무료 폴백 하이브리드 디스패처"는 사실이 아니다.**
> 폴백이 아니라 통째 교체다. 코드에 유료 경로가 없다.

**비용 실측**: OpenRouter 사용량 전후 스냅샷 (`raw/or-usage-delta.txt`)
`53.476170927 → 53.480154677` = **3종목 1회차 $0.003984**. 자료 구독료 0원.

### 주장 ②「"스케줄 자율 운용"은 마크다운에만 있다」→ **사실이다. 그리고 더 나아간다**

| 찾은 것 | 결과 |
|---|---|
| APScheduler · `schedule` · crontab · systemd · launchd (`*.py *.toml *.yml *.yaml`) | **0건** |
| 회차를 도는 상주 루프 | **0건** (`time.sleep` 은 `app/backend/services/ollama_service.py` 폴링뿐) |
| 알파카 `/clock` · `is_open` · `market_open` | **0건** — 장중 게이트가 코드에 없다 |
| 도커 | `docker/docker-compose.yml` 은 **일회성** 실행 |
| `cron` 이 적힌 마크다운 | README 28줄 · PLAYBOOK 23줄 · `autoresearch/CRON-ARCHITECTURE.md` 15줄 · `autoresearch/BRIDGE-DESIGN.md` 3줄 |

**그리고 마크다운에 적힌 것도 크론이 아니다.** `openclaw cron add`(저장소 의존성에 없는 별도 제품)로
**또 다른 LLM 에이전트**를 시각에 깨우고, 그 에이전트에게 **영어 산문 지시문**을 넘긴다.

```
--message 'Midday analysis.
  1. Gather data: poetry run python gather_data.py
  2. Analyze universe stocks for swing mode          ← 실행되는 줄이 아니라 사람 말
  3. Execute: echo trades | poetry run python execute_trades.py --mode swing'
```

→ 「자율 운용」 = **타이머에 걸린 LLM 이 할 일 목록을 읽고 대신 타이핑해 주는 것.**
24시간 회차에 세우려면 **우리가 launchd/cron 을 건다.** 장중 게이트가 없으므로 시작 시각을
우리가 정해야 하고, 장외에 돌리면 주문이 큐에 쌓인다.

## 2. 새로 찾은 것 — 안전장치 세 층이 새 포지션에서 빈다

### 2.1 원인: 프로세스 경계에서 가격이 떨어져 나간다

`PortfolioDecision`(`src/agents/portfolio_manager.py:13-24`)에 **가격 칸이 없다** —
`action`·`quantity`·`confidence`·`reasoning` 넷이 필수이고 `stop_price`·`take_profit`·`limit_price`
셋은 LLM 선택. `run_hedge_fund.py:_decisions_to_trades`(`:56-79`)가 그 dict 를 JSON 으로
`execute_trades.py` **별도 프로세스**에 넘긴다. 받는 쪽:

```python
# execute_trades.py:392-394
pos = positions.get(ticker, {})                  # 새 종목이면 빈 dict
ref_price = entry_price or limit_price or float(pos.get("current_price", 0))   # → 0
```

### 2.2 그 0 이 퍼지는 곳 넷

| 규칙 | 스윙 값 | 새 포지션에서 | 위치 |
|---|---|---|---|
| Rule 9 1종목 한도 | 8% | `trade_value = qty×0 = 0` → **항상 통과** | `risk_manager.py:176, 351-356` |
| Rule 10 섹터 한도 | 30% | 같은 `trade_value` → **항상 통과** | `risk_manager.py:365-374` |
| Rule 8 최소 현금 | 20% | 같음 → **항상 통과** | `risk_manager.py:338-348` |
| 의무 브래킷 −7%/3R | 필수 | `ref > 0` 이라야 계산 → **안 붙는다** | `execute_trades.py:416-430` · `:167-181` |

`place_order` 는 폴백조차 없다 — `ref_price = entry_price or limit_price or 0` (`:169`).

**실측 (`run5-execute.txt`)**

```
APPROVED: SHORT 75 AMZN @ $0.00 ($0 = 0.0% of equity)
```

| 실제 | |
|---|---|
| 체결 | 75주 @ **249.25** · order `7da9d0f3-f7e2-44e3-aa7f-00e90af314df` · 2026-09-23T19:43:36.523944264Z |
| 평가액 | **−18,691.5 USD** / 자산 98,911.92 = **18.9%** (한도 8%) |
| `order_class` | `""` · `legs` `null` · `stop_price` `null` |
| 미체결 주문 | **`[]`** — 보호 손절 0건 |

→ **README 두 주장이 동시에 무너진다**: *"code-enforced risk management layer that no agent
can override"* · *"Alpaca bracket orders with mandatory stops"*.

### 2.3 감시기가 숏을 못 본다

```python
# portfolio_monitor.py:216
long_positions = [p for p in positions if float(p.get("qty", 0)) > 0]
# :252
for pos in long_positions:            # 숏(qty<0)은 손절 고리에 안 들어온다
```

AMZN −75 주를 들고 돌린 결과(`run6-monitor.txt`): **`Positions: 1`**, 표에는 0주짜리 BTCUSD 하나,
그리고 **`All positions within stop thresholds.`**
README 의 *"Full short selling support"* 와 *"portfolio_monitor.py — auto-sells stops"* 가 맞물리지 않는다.

### 2.4 들어간 크기로 나갈 수 없다 (= S7 이 출구에서 발동)

`cover` 시에는 포지션이 존재하므로 `current_price` 가 있다 → **같은 8% 규칙이 이번엔 작동한다.**

```
Legacy validator BLOCKED cover AMZN:
  Trade value $18,716 exceeds max $7,911 (8% of portfolio)
```

**검사가 들어올 때는 눈이 멀고 나갈 때만 눈을 뜬다.** 도구만으로는 이 포지션을 닫을 수 없다
(수량을 쪼개면 가능하나 사람이 알아채야 한다). 필자는 `DELETE /v2/positions/AMZN` 로 직접 청산.

| | |
|---|---|
| 청산 | 75주 @ **249.450667** · order `545c59fa-0a02-4fc6-b44e-c192e0bd35c6` · 2026-09-23T19:44:50.545625032Z |
| 보유 시간 | **74초** |
| 실현 손익 | **−15.050025 USD** ((249.25 − 249.450667) × 75) |
| 자산 | 98,911.92 → **98,894.90** |
| 계좌 정리 | AMZN 0. 남은 것은 다른 회차의 잔존 `BTCUSD 0.001143519` 하나 |

### 2.5 설치 벽 — 「선택」이라던 day 계좌

`src/alpaca_integration.py:33` 이 **임포트 시점에** `_get_headers("day")` 를 부른다.
`src/accounts.py:95-100` 의 폴백은 **swing→day 한 방향뿐**이라 day→swing 이 없다.
→ 스윙 키만 넣으면 어떤 모드든 `ValueError` (`run0-install-wall.txt`).
README 의 *"optional — falls back to primary"* · *"backward compatible"* 은 **사실이 아니다.**
우회: `.env` 에 `ALPACA_DAY_API_KEY`/`_SECRET` 을 같은 값으로 추가.

### 2.6 잠복 버그 — 스윙 모드 장중 자료

`gather_data.py:43-45` 가 `ALPACA_SWING_API_KEY`/`_SECRET` 을 읽는데 이 이름은 `.env.example`
에도, 다른 코드에도 없다(저장소 전체 3곳: 이 둘 + `accounts.py:104` 오류 메시지).

```
swing headers empty? {'APCA-API-KEY-ID': True, 'APCA-API-SECRET-KEY': True}
swing-mode portfolio call -> 401 {"message": "unauthorized."}
```

## 3. 판단 21건 — 회차 기록

**바구니와 N 을 먼저 정하고 시작했다.** 같은 날 04:34~04:52 KST, SPY 당일 −0.77%.

| 회차 | 파일 | 바구니 | 종목 | 결과 | 소요 |
|---|---|---|---|---|---|
| 1 | `run1-dryrun.txt` | 대형 기술·지수 | NVDA·AAPL·SPY | SHORT 3 (82·84·72%) | 1:44.60 |
| 2 | `run2-dryrun.txt` | 안전자산·배당 | GLD·TLT·KO | SHORT 3 (65·60·65%) | 55.716s |
| 3 | `run3-netprobe.txt` | 금융·필수소비·결제 | MSFT·JPM·JNJ·PG·V·COIN | SHORT 4 · HOLD 2 | 2:29.39 |
| 4 | `run4-dryrun.txt` | 반도체·플랫폼 | AVGO·TSM·GOOGL·META·AMZN·AMD | SHORT 4 · HOLD 2 | 2:31.87 |
| 5 | `run8-cost.txt` | 성장주 | PLTR·CRM·NET | SHORT 3 (72·84·82%) | 1:45.23 |
| | | | **21종목** | **SHORT 17 · HOLD 4 · 매수 0** | |

확신도 폭 58~94%. 최고는 COIN 94% — *"Near-unanimous bearish signals support a full-size short."*

**주의**: 하루 한 국면에서만 쟀다. **이것이 도구의 성질인지 그날 장의 성질인지는 이 회차로 못 가른다.**
다만 금·미 국채·필수소비재까지 같은 방향이 나온 것은 기록해 둔다.

> `buy` 가 메뉴에서 빠진 것은 **아니다.** `compute_allowed_actions`(`portfolio_manager.py:127-135`)는
> 그래프 안의 실제 현재가로 `max_buy = min(max_qty, cash//price)` 를 계산하며, 현금 98,813 이
> 있었으므로 buy 는 내내 허용 상태였다. **LLM 이 고른 결과다.**

## 4. 시나리오 판정

| | 결과 |
|---|---|
| **S1** BTC-USD | **이 도구로는 안 됨** — 회차마다 `Quote not found for symbol: BTCUSD`. 유니버스 하드코딩 미국 티커, 정수 주수, 알파카 주식 페이퍼 |
| **S2** 한국 종목 | **이 도구로는 안 됨** — 같은 이유 |
| **S3** 우리 기록 복기 | 입구 없음. 자기 스냅샷만 읽는다 |
| **S4** (= D-2) 여러 종목 훑기 | **했다** — 21종목 5회차 (§3) |
| **S5** 주문 한 번 | **했다 — 단 도구 쪽 브로커에서.** 우리 쪽은 422 로 거절 (§5) |
| **S6** 고리 한 바퀴 | **닫지 못했다.** 판단→주문→체결까지 갔으나 청산이 도구 안에서 막혀(§2.4) 규칙 수정·재주문으로 못 돌아왔다 |
| **S7** 중지 조건 발동 | **발동했다 — 출구에서**(§2.4). 진입 쪽에서는 발동 불가(§2.2) |
| **S8** 끊고 다시 붙이기 | 안 돌림. **상주 프로세스가 없어 끊길 것이 없다** |
| **S9 · S10** | 안 돌림. B축 몫 |
| **D** | 「자료가 정말 공짜인가」를 호스트 계수로 재기 (§1) |

## 5. weltiq 연결 — 422 거절

옮긴 것: 도구 최고 확신도 판단 **COIN 숏 52주 (94%)**.
레벨은 도구 코드 식 그대로 재현 — 손절 = 기준가×1.07, 목표 = 기준가 − 손절거리×3.0
(`src/config.py:47` `stop_loss_pct=0.07`, `:286` `DEFAULT_TARGET_MULTIPLIER=3.0`).
기준가 198.21499633789062 (도구 자신의 `api.get_prices` 2026-09-23 종가) → stop 212.09 · target 156.59.

```
POST /v1/strategy-rules
← 422 {"code":"invalid_rules","detail":"unsupported_direction"}
```

원인: `weltiq/pine.py:28` `SHORT_VENUES = ("binance-usdm","bybit-linear","cme")`.
`slot_rules.py:163` 이 그 밖의 venue 에서 `side:"short"` 를 거절한다. **us-equity 는 롱 전용.**

`raw/70-rules-doc.json` · `raw/71-rules-submit.json`.

**슬롯을 만들지 않았다** — 규칙 문서가 거절돼 `/v1/containers` 까지 가지 못했다.
따라서 정지·삭제할 슬롯이 없고 `FIX_BACKLOG` 75번(삭제 불가) 상황도 발생하지 않았다.

### 붙는 방향이 반대인 편

| | 이 도구 | weltiq us-equity |
|---|---|---|
| 미국 주식 숏 | **된다** | 안 된다 |
| 새 포지션 1종목 한도 | **안 걸린다** | 필수 |
| 새 포지션 손절 | **안 붙는다** | 규칙 문서에 레벨 없으면 거절 |
| 숏 손절 감시 | **못 본다** | 방향 무관 |
| 들어간 크기로 나가기 | **막힌다** | 진입 한도를 청산에 다시 걸지 않는다 |

**우리 쪽 숙제 1건**: 도구가 원했는데 우리에게 없는 칸 = **us-equity 숏**.

## 6. B축 판정

**참가 가능 — 2026-09-22 판정 유지. 단 세 칸을 고쳐 적는다.**

| | 2026-09-22 판정 | 이번 실행 뒤 |
|---|---|---|
| 참가 자격 | 참가 가능 | **유지** |
| B / A-2 | **B축 단독** | **유지** — 단 ③(포지션 관리)을 **격하해야 한다** (아래) |
| 시장 태그 | `stock` | **유지.** BTCUSD 404 로 실측 확인 |
| 판단 형태 | 배분(주수) + 스윙모드 **자동 브래킷** | **정정: 배분(주수)뿐.** 자동 브래킷은 **새 포지션에 안 붙는다**(§2.2 실측) |
| ③ 포지션을 관리하나 | **예** (하드 손절 + 트레일링) | **롱만.** 숏은 감시 대상에서 빠진다(§2.3) |
| ⑥ confidence | 낸다. 아무 데도 안 쓴다 | **유지** — 21건 실측에서도 크기·게이트에 영향 0 |
| ⑦ 계정 벽 | 없다 | **유지.** 유료 자료 0건 실측(§1) |

### 회차에 세우기 전에 반드시 정할 것

1. **`--execute` 를 켜면 한도 없는 포지션이 열린다**(§2.2). 회차 자본이 곧 노출 상한이 아니다.
   → **우리 슬롯 쪽 중지 조건이 유일한 방어선**이라는 전제로 회차를 짜야 한다
2. **숏이 나오면 우리 us-equity 로 못 옮긴다**(§5). 21종목 중 17건이 숏이었으므로
   **이 도구는 미션에 따라 회차 전체가 공전할 수 있다**
3. **스케줄러가 없다**(§1 주장②). launchd/cron 은 우리가 건다. 장중 게이트가 없으므로 시작 시각 고정 필수
4. 진입점은 **`run_hedge_fund.py` 로 못 박는다.** `run_analysis.py` 는 거의 복제본이나
   같은 프로세스에서 실행하고 스윙 의무 브래킷을 적용하지 않으며 기본 모델도 다르다

### 어느 회차에 맞나

| 미션 | 적합 | 이유 |
|---|---|---|
| **M2 종목 선택** | ◎ | 이 도구의 본업. 21종목 5회차가 그대로 재료 |
| **M3 −2% 한도** | ○ | 일일 서킷브레이커는 **가격과 무관**해 실제로 작동한다(`daily_pnl` 기반) |
| **M5 수수료** | ○ | 미국주식 수수료 구조 그대로 |
| **M7 미국장 세션** | ◎ | 유일한 시장 |
| **M11 뉴스 쇼크** | ○ | SEC Form 4 · 뉴스 에이전트 보유 |
| **M12 확신도 보정** | ✕ | 값은 나오나 **실행기에 전달조차 안 된다**(⑥). 34편(orallexa) 몫 |

## 7. 비용

| | |
|---|---|
| 자료 | **0원** — 유료 호스트 0건, 인증 헤더 0건(§1) |
| LLM (실측) | **$0.003984** — 3종목 1회차, `openai/gpt-5.6-luna`, 애널리스트 7명 |
| LLM (이 글 전체) | 5회차 21종목. 실측 1회차에서 환산하면 **약 $0.028** — **환산값이지 실측이 아니다** |
| 브로커 | 알파카 페이퍼. 실현 손익 **−15.050025 USD**(모의 자금) |
| **실지출** | **약 $0.03 (OpenRouter 토큰). 그 밖의 유료 자원 사용 0** |

## 8. 산출물

```
docs/tutorials/a/33-swarm-trader.md        한국어
docs/tutorials/a/33-swarm-trader.en.md     영어
docs/tutorials/a/33-swarm-trader.pdf       · .en.pdf

reports/a/33-swarm-trader/
├── RUN-LOG.md             이 파일
├── api.py                 weltiq API helper (키는 인쇄하지 않는다)
├── to_weltiq.py           도구 판단 → /v1/strategy-rules 규칙 문서
├── net_probe.py           requests·urllib·curl_cffi 후킹 호스트 계수기
├── probe_data_layer.py    자료 함수 여섯 개만 돌려 호스트 세기
├── shots.py · shots_base.py
├── run0-install-wall.txt  day 계좌 임포트 벽 (재현)
├── run1~run4, run8        판단 5회차
├── run5-execute.txt       AMZN 숏 75주 실제 주문
├── run6-monitor.txt       감시기가 그 숏을 못 보는 화면
├── run7-cover.txt         커버가 8% 규칙에 막힌 기록
├── raw/  net-hosts.json · net-hosts-datalayer.json · or-usage-*.txt
│         70-rules-doc.json · 71-rules-submit.json
└── shots/ 01~06.png       도구 화면 5 · 우리 화면 1
```

## 9. 안전

- 키는 기사·로그·캡처 어디에도 평문으로 없다. `.env` 는 도구 디렉터리(저장소 밖)에만.
- 전부 페이퍼. `paper-api.alpaca.markets` 는 코드에 하드코딩이고 실계좌 URL 이 없다.
- 도구 소스는 **수정하지 않았다**(라이선스 없음 — 인용만). 우회는 전부 `.env` 로 했다.
- `weltiq/`·`web/`·`db/`·`tools/`·`docs/design/` 미수정. `docs/tutorials/a/33-*` ·
  `reports/a/33-*` · `~/dev/weltiq-tools/` 만 건드렸다.
