DeepFund — 애널리스트 여섯이 신호를 내고 매니저가 주수를 정하는 연구실 코드
HKUST 연구실이 NeurIPS 에 낸 LLM 펀드 벤치마크를 깔고, 미국 주식 한 종목에 같은 질문을 여섯 번 던져 '몇 주를 사라'가 어디서 정해지는지 뜯어봅니다.
30편. DeepFund — 애널리스트 여섯이 신호를 내고 매니저가 주수를 정하는 연구실 코드이 글의 완료 조건: 독자는 한 종목에 대해 "몇 주를 사고 왜"를 담은 판단을 받으면 성공입니다. 그리고 이 편의 절반은 그 판단 중 무엇이 모델이 정한 것이고 무엇이 나눗셈인지 가르는 데 씁니다.
판정 카드
| 하는 일 한 줄 | 미국 주식 한 종목에 애널리스트 여섯이 강세/약세/중립 신호를 내고, 리스크 담당이 비중을 정하고, 매니저가 매수·매도·보유 + 주수 + 이유 를 냅니다 |
| 맞는 사람 | 판단의 근거 체인(누가 무슨 신호를 냈고 비중이 어떻게 정해졌는지)을 통째로 기록으로 남기고 싶은 사람. 날짜를 하루씩 옮겨 가며 과거를 재생해 보고 싶은 사람 |
| 안 맞는 사람 | 진입가·손절·목표가를 받고 싶은 사람(안 나옵니다). 크립토·한국 주식을 보려는 사람(미국 주식 전용). 키 없이 시작하고 싶은 사람 |
| 이 글의 결과물 | Decision for NVDA: Buy \| Shares: 43 \| Price: 227.38 \| Justification: … 한 줄과, 그 줄을 만든 신호 두 개가 로컬 데이터베이스에 남은 상태 |
| 준비물·비용 | 코드 0원(MIT) · 시세/재무 자료 Alpha Vantage 키 필요(무료 등급 하루 25요청, 가입 필요) · 모델 한 판 약 $0.04(추정) · 저장소는 Supabase 계정 또는 로컬 SQLite · weltiq 는 별도 |
| 시간·난도 | 설치 10분, 한 판 15~20초. 난도 중 (파이썬 가상환경과 YAML 편집) |
| weltiq 와의 관계 | 붙습니다 — 다만 이번엔 절반만. 방향과 주수는 규칙으로 옮겨져 엔진에 붙었고, 체결은 장이 닫혀 있어 못 봤습니다 |
| 검증 환경·날짜 | macOS 15 · Python 3.13 · 2026-09-22 새벽(한국시간) · 대상 종목 NVDA · 기준일 2026-09-21 종가 |
| 제휴 고지 | 필자는 weltiq 와 제휴 관계이며 이 연재의 원고료를 받습니다. 도구 평가는 실측만 싣고, weltiq 가 지는 대목도 그대로 적습니다 |
이 글의 명령과 출력은 전부 실제로 돌려서 얻었습니다. 숫자는 손대지 않았습니다. 도구가 내놓는
Buy/Sell/Hold는 그 도구의 출력이고, 맞는지에 대해 필자는 아무 말도 하지 않습니다. 주문은 전부 모의입니다. 원문 기록은reports/a/30-deepfund/에 있습니다.
1. 이게 뭔가요
HKUSTDial/DeepFund 는 "LLM 이 펀드 매니저를 할 수 있는가"를 재려고 만든 실험 장치입니다. 홍콩과기대 연구실 산출물이고, 논문이 NeurIPS 2025 데이터셋·벤치마크 트랙에 포스터로 붙었습니다. 논문 제목이 이 도구의 성격을 그대로 말해 줍니다 — "Time Travel is Cheating"(시간 여행은 반칙이다). 과거 데이터로 백테스트하면서 그 시점에 몰랐을 정보를 쓰는 것을 막겠다는 뜻입니다.
구조는 두 층입니다.
| 층 | 누가 | 무엇을 내는가 |
|---|---|---|
| 애널리스트 | 기술적·뉴스·내부자·펀더멘털·거시경제·정책 6명 | 강세 / 약세 / 중립 + 짧은 근거 |
| 매니저 | 리스크 담당 1명 → 포트폴리오 매니저 1명 | 비중(0~상한, 0.05 단위) → 매수·매도·보유 + 주수 + 이유 |
READme 에 적힌 것 중 실제와 다른 게 하나 있습니다. 자료원이 "Alpha Vantage 또는 yfinance"라고 적혀 있지만, 코드가 실제로 부르는 것은 전부 Alpha Vantage 입니다. yfinance 쪽 클래스에는 뉴스 함수 하나만 있고 시세 함수가 비어 있어서, 그 경로를 고르면 가격 조회에서 그대로 멈춥니다. 그래서 이 도구는 "키 없이 도는 도구"가 아닙니다. 이 사실을 카드 맨 위에 적어 둔 이유입니다. (필자는 이 글을 쓰려고 그 빈 함수 둘을 직접 메워 키 없이 돌렸습니다 — 2절 끝에 밝혀 둡니다.)
| 항목 | 값 |
|---|---|
| 저장소 | HKUSTDial/DeepFund (★296) |
| 마지막 커밋 | 2026-03-18 |
| 라이선스 | MIT |
| 필요 파이썬 | 3.11 이상 |
| 지원 시장 | 미국 주식만 (중국 A주용 어댑터 파일이 있으나 라우터에 연결돼 있지 않습니다) |
2. 설치
검증된 경로 하나만 적습니다. uv 를 씁니다.
git clone https://github.com/HKUSTDial/DeepFund.git
cd DeepFund
uv sync # 가상환경 생성 + 의존성 설치, 1~2분
자료·모델 키를 넣습니다. 제공되는 예시 파일을 복사해 편집하면 됩니다.
cp .env.example .env
# ALPHA_VANTAGE_API_KEY=... (https://www.alphavantage.co 에서 무료 발급, 가입 필요)
# OPENAI_API_KEY=... (또는 DeepSeek·Anthropic·Ollama 중 택일)
# DB_PATH=src/assets/deepfund.db
저장소는 Supabase 가 기본값입니다. 클라우드 계정을 만들고 싶지 않다면 로컬 SQLite 로 갑니다. 이쪽이 계정이 하나 덜 듭니다.
cd src
uv run python database/sqlite_setup.py # 표 4개 생성
막히는 자리
| 증상 | 원인 | 어떻게 |
|---|---|---|
TypeError: makedirs … NoneType |
DB_PATH 를 안 넣었습니다 |
.env 에 DB_PATH 추가 |
| 시세 조회에서 멈춤 | Alpha Vantage 키 없음 또는 그날 요청 한도 초과 | 키 확인. 한 종목 한 판에 요청 13건이 나갑니다(아래 참고) |
| 기본 설정 그대로 돌렸는데 하루치 한도가 끝남 | 기본 설정 파일이 종목 2개 × 애널리스트 6명 = 26건 | 종목을 하나로, 애널리스트를 둘셋으로 줄여서 시작 |
Supabase … connection |
--local-db 를 빼먹었습니다 |
명령 끝에 --local-db |
무료 등급 한도가 하루 25요청인데 기본 설정 파일 한 번이 26요청입니다. 처음 돌릴 설정은 종목 하나에 애널리스트 두셋으로 깎아 두는 게 맞습니다.
설치가 끝난 화면
uv sync 와 로컬 SQLite 초기화까지가 설치입니다. SQLite database initialized 와 표 네 개(config·portfolio·decision·signal)가 보이면 끝난 것입니다. 맨 아래 빨간 줄이 제일 많이 걸리는 자리입니다 — .env 에 DB_PATH 를 안 넣으면 여기서 죽습니다.
밝혀 둘 것 — 이 글의 여섯 판은 Alpha Vantage 로 돈 것이 아닙니다. 필자는 계정을 만들지 않는 규칙을 지키느라 키를 발급받지 않았고, 대신 1절에서 말한 비어 있는 yfinance 경로에 시세 함수 둘(
get_daily_candles_df·get_last_close_price)을 직접 써 넣어 돌렸습니다. 뉴스는 저장소에 이미 있던 yfinance 함수를 그대로 썼습니다. 고친 원문은raw/author_patch.diff에 있습니다.독자에게 무슨 뜻인가: 위 설치대로 Alpha Vantage 키를 받아 as-shipped 로 돌리면 자료원이 달라서 숫자가 이 글과 같지 않습니다. 갈리는 것은 자료이고, 3절과 4절이 말하는 구조 — 가격 칸이 종가로 덮어써진다는 것, 프롬프트에 종목 이름이 없다는 것 — 은 자료원과 무관하게 같습니다.
3. 첫 결과
설정 파일 하나를 만듭니다.
# src/config/my.yaml
exp_name: "my-first-run" # 실험마다 다른 이름 — 같은 이름은 같은 장부를 이어씁니다
cashflow: 100000
tickers:
- NVDA
planner_mode: false
workflow_analysts:
- technical
- company_news
llm:
provider: "OpenAI"
model: "gpt-4o-mini"
돌립니다.
cd src
uv run python main.py --config config/my.yaml --trading-date 2026-09-21 --local-db
이렇게 나오면 성공입니다.
INFO - Active analysts for NVDA: ['technical', 'company_news']
INFO - Agent: technical | Ticker: NVDA | Signal: Neutral | Justification: Bullish trend-following is
offset by a bearish volume trend … At $227.38, NVDA is only 0.26% below resistance at $227.98
versus approximately 0.69% above support at $225.83 …
INFO - Agent: company_news | Ticker: NVDA | Signal: Bullish | Justification: Rackspace joining
NVIDIA's Cloud Partner Program supports ecosystem expansion … Alibaba's proprietary AI chip
poses longer-term competitive risk.
INFO - Risk Control for NVDA | Optimal Position Ratio: 0.1 | Justification: Recommend a cautious 10%
NVDA allocation … allocate $10,000 and retain $90,000.
INFO - Decision for NVDA: Buy | Shares: 43 | Price: 227.3800048828125 | Justification: Buy 43 shares
based on the supplied positive tradable-share amount and zero current holdings, for
approximately $9,777.34 before fees.
INFO - DeepFund run completed in 18.32 seconds
한 판이 도는 화면
이 도구가 일하는 화면입니다. 위에서 아래로 애널리스트 둘(노랑=중립, 파랑=강세) → 리스크 담당의 비중 0.1 → 매니저의 최종 줄(초록) 순서로 내려옵니다. 초록 줄의 Shares: 43 과 Price: 227.38 을 보십시오 — 바로 아래 문단이 이 둘의 출처가 서로 다르다는 이야기입니다.
18초, 종목 하나, 판단 한 줄. 같은 내용이 로컬 데이터베이스의 decision·signal 표에 프롬프트 원문까지 함께 남습니다. 이 도구의 진짜 산출물은 화면이 아니라 이 표입니다.
대시보드는 없습니다. 웹 UI 도 TUI 도 차트도 없고 띄울 주소가 없습니다 — 터미널 로그와 위 표 넷이 전부입니다. 저장소 기본값인 Supabase 로 가면 Supabase 자신의 표 화면이 붙지만 그건 이 도구가 만든 화면이 아니고 계정이 필요합니다. --local-db 로 갔다면 sqlite3 로 표를 직접 들여다보는 것이 이 도구를 보는 방법입니다.
Price: 227.38 이 무엇인지 — 이 편에서 제일 중요한 문단
판단 줄에 가격이 함께 나오니 "모델이 227.38 에 사라고 했다"로 읽기 쉽습니다. 그게 아닙니다.
모델이 가격 칸을 채워 돌려준 뒤, 코드가 그 값을 직전 종가로 덮어씁니다. 프롬프트에 적힌 지시부터가 "현재가를 적어라"이고, 모델에게 진입가를 고르라고 물은 적이 없습니다. 위 여섯 번의 실행에서 가격 칸은 전부 227.3800048828125 — 2026-09-21 NVDA 종가와 소수점 끝까지 같습니다.
주수도 비슷합니다. 리스크 담당이 정한 비중 10% 에 자본 $100,000 을 곱하고 종가로 나누면 100000 × 0.10 ÷ 227.38 = 43.98 → 43주. 15% 였던 회차는 15000 ÷ 227.38 = 65.97 → 65주. 매니저의 이유 문장에도 "주어진 거래 가능 주수를 따랐다"고 적혀 있습니다.
정리하면 이렇습니다.
| 칸 | 누가 정하나 |
|---|---|
| 방향 (매수/매도/보유) | 모델 |
| 비중 (0.05 단위) | 모델 (리스크 담당) |
| 주수 | 나눗셈 — 비중 × 자본 ÷ 종가 |
| 가격 | 코드가 직전 종가로 덮어씀 |
| 손절·목표 | 없음 |
이 도구가 내는 판단은 "얼마에"가 아니라 "얼마만큼" 입니다. 진입가로 쓰면 안 되는 숫자를 진입가처럼 생긴 칸에 담아 내보내는 것이 이 도구의 가장 큰 함정이고, 이걸 모르고 자동화에 물리면 조용히 틀립니다.
4. 제일 잘 쓰는 법 — 프롬프트에 종목 이름 한 단어 넣기
처음 세 번은 세 번 다 보유(0주) 였습니다. 판단 이유를 읽다가 같은 문장을 발견했습니다.
"No target ticker was specified, and the headlines span multiple companies, preventing a reliable company-specific assessment." (대상 종목이 지정되지 않았고 헤드라인이 여러 회사에 걸쳐 있어 회사별 판단이 어렵다)
프롬프트를 열어 봤더니 정말 그랬습니다. 여섯 애널리스트와 두 매니저의 프롬프트 어디에도 종목 이름이 들어가지 않습니다. 영어 단어 "ticker" 가 자리표시자처럼 그냥 문장 안에 박혀 있습니다. 기술적 분석 쪽은 더 분명합니다 — 코드가 종목 이름을 프롬프트에 넘기고 있는데, 받는 프롬프트 문장에 그걸 받을 자리가 없습니다. 만들다 만 자리입니다.
프롬프트 네 곳의 ticker 를 {ticker} 로 바꾸고, 안 넘기던 두 곳에서 종목 이름을 넘기도록 했습니다. 문장 구조는 그대로입니다. 그리고 똑같은 조건으로 다시 세 번 돌렸습니다.
| 회차 | 고친 뒤/전 | 뉴스 신호 | 비중 | 결정 |
|---|---|---|---|---|
| 1 | 전 | 중립 ("종목이 지정되지 않음") | 0.00 | 보유 0주 |
| 2 | 전 | 중립 ("종목이 지정되지 않음") | 0.00 | 보유 0주 |
| 3 | 전 | 중립 ("종목이 지정되지 않음") | 0.00 | 보유 0주 |
| 4 | 후 | 강세 (NVIDIA 파트너 프로그램·IPO) | 0.10 | 매수 43주 |
| 5 | 후 | 강세 | 0.10 | 매수 43주 |
| 6 | 후 | 강세 | 0.15 | 매수 65주 |
여섯 판이 로컬 DB 에 남은 모습
표로 보면 이 절의 이야기가 한눈에 들어옵니다. 위쪽 signal 표에서 company_news 칸만 run4 부터 초록(강세)으로 바뀝니다 — technical 은 여섯 번 다 중립입니다. 아래쪽 decision 표가 그 결과이고, 맨 오른쪽 price 칸이 여섯 줄 전부 227.3800048828125 로 똑같은 것이 3절에서 말한 덮어쓰기입니다.
기술적 분석은 여섯 번 모두 중립이었습니다(그쪽은 숫자를 받으니 종목 이름이 없어도 계산이 됩니다). 갈린 것은 뉴스 담당입니다. 종목 이름이 없으면 같은 헤드라인 묶음을 놓고 "어느 회사 얘기인지 모르겠다"고 판단을 접었고, 이름을 주자 같은 헤드라인에서 NVIDIA 관련 항목을 골라 강세를 냈습니다.
이 한 단어가 "아무것도 안 함"과 "9,777달러어치 매수"를 갈랐습니다. 이 도구를 진지하게 쓸 생각이면 이 자리를 먼저 손보는 게 맞습니다. 다만 아래 5절로 옮긴 판단은 필자가 고친 코드에서 나온 것임을 분명히 적어 둡니다.
연구 코드다운 장치도 확인했습니다. 시세·내부자 거래는 기준일 이후 데이터를 잘라내고, 같은 실험 이름으로 기준일을 거꾸로 돌리면 실행을 거부합니다. 논문 제목값을 합니다. 다만 재무 지표와 거시경제 지표에는 그 필터가 없습니다 — 기준일이 언제든 최신 값을 가져옵니다. 도구 자신의 기술 문서도 그렇게 적어 놨습니다. 과거를 재생할 때 이 둘을 켜면 그 판단은 "그날 몰랐을 정보"를 봅니다.
5. weltiq 로 옮기기 — 통합 실험
weltiq 는 전략을 규칙 문서로 굳혀 놓고 모의 체결·비용·저널까지 붙여 24시간 돌리는 쪽입니다. DeepFund 가 "무엇을 살까"를 정한다면 이쪽은 "그게 실제로 얼마에 채워지고 얼마가 새는가"를 잽니다. 4회차 판단(NVDA 매수 43주)을 옮겨 봤습니다.
옮긴 절차는 다섯 단계입니다.
- 판단에서 옮길 수 있는 칸을 고릅니다. 방향(매수)과 주수(43)뿐입니다. 진입가·손절·목표는 도구가 내지 않습니다
- 진입 조건을 적습니다. 조건이 없으니 "보유 수량이 0이면 진입"으로 뒀습니다 — 도구가 무포지션일 때 매수 판단을 내는 동작에 가장 가깝습니다
- 보호 조건을 붙입니다. 손절 3% 는 필자가 정한 값입니다. 상주 실행에는 보호 조건이 필수인데 도구는 손절 개념 자체가 없습니다
- 근거를 제 말로 적습니다. 무엇이 도구 판단이고 무엇이 제가 채운 칸인지 여기에 적습니다. 이게 없으면 등록이 거부됩니다
- 전략 컨테이너를 만들고 시작합니다. 최대 손실과 종료 시각을 함께 겁니다
POST /v1/strategy-rules → 규칙 문서 확정 ("(position.size = 0) → 진입 long · 보호 손절 3% · 수량 1")
POST /v1/ai/thesis → 근거 기록
POST /v1/containers → status = running, 엔진에 붙음(attached)
여기까지가 확인한 것이고, 그 다음이 확인 못 한 것입니다. 돌린 시각이 한국시간 새벽이라 미국장이 닫혀 있었습니다. 컨테이너는 엔진에 붙어 "봉 대기" 상태로 있다가 정한 시각에 스스로 멈췄습니다. 체결 0건. 그래서 이 절의 제목이 "통합 실험"입니다.
옮기면서 두 칸을 잃었습니다. 둘 다 적어 둡니다.
| 잃은 칸 | 어느 쪽 문제인가 |
|---|---|
| 43주 → 1주 (당시) | weltiq 쪽이었습니다. 그때는 외부 키로 올린 규칙의 주문 수량 상한이 1주로 고정돼 있어 43주가 전량 거절됐고, 판단의 비중 정보가 여기서 사라졌습니다. 2026-09-23 에 고쳤습니다 — 지금은 규칙 문서의 sizing.qty 가 곧 주문 상한이라 43주가 그대로 들어갑니다. 아래 실측은 1주로 줄인 그날의 기록입니다 |
| 진입 조건이 없음 | DeepFund 쪽입니다. "언제 사는가"가 판단에 없으니 필자가 "포지션 없으면"으로 채웠습니다. 이건 도구의 판단이 아닙니다 |
이 절에서 남는 문장은 이겁니다. DeepFund 의 판단은 규칙으로 옮길 때 절반이 필자 손을 탑니다. 방향과 주수는 그대로 가고(주수 상한은 그 뒤 없어졌습니다), 진입가와 손절은 애초에 없어서 새로 써야 합니다. 판단을 그대로 집행하고 싶다면 이 도구가 아니라 진입선과 무효 조건을 함께 내는 도구를 찾는 게 맞습니다.
6. 써 보니
좋았던 것
- 근거 체인이 통째로 남습니다. 신호 6개, 비중, 최종 결정이 프롬프트 원문까지 표에 들어갑니다. 나중에 "왜 그때 샀지"를 되짚을 수 있는 도구는 이 연재에서도 흔하지 않습니다
- 판단이 흔들리지 않습니다. 같은 조건 세 번에 방향이 세 번 다 같았습니다(고치기 전 보유 3회, 고친 뒤 매수 3회). 비중만 0.10·0.10·0.15 로 한 칸 움직였습니다
- 과거 재생이 설계에 들어 있습니다. 기준일을 하루씩 옮기며 장부를 이어 가는 구조이고, 거꾸로 돌리는 것을 막습니다
- 15~20초, 종목당 한 판. 이 연재에서 가장 빠른 축입니다
- 모델을 갈아 끼우기 쉽습니다. 제공사 여덟 곳이 이미 들어 있고 로컬 모델도 그중 하나입니다
아쉬웠던 것
- 가격 칸이 판단이 아닙니다. 진입가처럼 생겼지만 직전 종가를 덮어쓴 값입니다. 3절 문단을 안 읽고 쓰면 조용히 틀립니다
- 손절도 목표도 없습니다. 위험 관리는 "비중"이 전부입니다
- 프롬프트에 종목 이름이 없습니다. 4절이 그 얘기입니다. 고치기 전에는 뉴스 담당이 제 역할을 못 합니다
- 키 없이 안 돕니다. README 가 말하는 무료 경로는 코드에 구현돼 있지 않고, 무료 등급 한도(하루 25요청)는 기본 설정 한 판(26요청)도 못 받습니다
- 재현이 안 됩니다. 모델 온도가 0.5 로 고정이고 난수 고정 장치가 없습니다. 논문 제목은 시간 여행을 막는데, 같은 입력에 같은 답이 나오는 쪽은 안 막습니다
- 재무·거시 지표에는 시점 필터가 없습니다. 과거 재생에 이 둘을 켜면 그날 몰랐을 값이 섞입니다
비용 표
| 칸 | 실측/확인 |
|---|---|
| 코드 | 0원 (MIT) |
| 자료 | Alpha Vantage 키 — 무료 등급 하루 25요청. 한 종목 한 판에 최대 13요청 |
| 모델 | 한 판 약 $0.04 (추정 — 도구가 사용량을 보고하지 않아 저장된 프롬프트·응답 길이로 계산) |
| 저장소 | Supabase 계정(기본) 또는 로컬 SQLite(0원) |
| 시간 | 설치 10분 · 한 판 15~20초 |
7. 다음 행동
지금 바로 할 수 있는 것 세 가지입니다.
- 종목 하나, 애널리스트 둘로 시작하세요. 기본 설정 파일을 그대로 돌리면 무료 자료 한도가 한 판에 끝납니다
- 첫 판이 "보유 0주"로 나오면 4절을 먼저 보세요. 도구가 신중한 게 아니라 종목 이름을 못 본 것일 수 있습니다
- 가격 칸을 진입가로 쓰지 마세요. 이 도구가 주는 것은 방향과 비중입니다. 언제 들어갈지는 여러분이 정해야 합니다
그리고 그 "언제·얼마에"를 정하고 나면, 그 규칙이 실제 체결과 비용 위에서 어떻게 움직이는지 재 볼 차례입니다.
weltiq 준비
- 가입 — 모의 전용, 실주문 없음
- 마이 → 연결 관리에서 키 발급
- 터미널에서
claude mcp add weltiq --header "Authorization: Bearer <키>" …판단을 규칙으로 굳히고, 체결·수수료·미끄러짐까지 붙은 상태로 돌려 볼 수 있습니다.