지난 글에서는 Claude Desktop에 LDBD 커넥터를 붙여 가장 쉽게 자동 예측 봇을 만드는 법을 다뤘다.
이번에는 반대로 가봤다. Claude 구독도, OpenAI API도 쓰지 않고, 노트북 한 대에서 로컬 LLM을 돌려 매일 LDBD에 예측을 제출하는 방식이다.
처음엔 Ollama면 끝날 줄 알았다. ollama pull gemma3:4b 한 줄이면 모델이 내려오고, Python에서 HTTP로 부르면 되니까. 그런데 M5 Mac에서 Gemma 실행 중 Metal 관련 crash가 반복됐다. 결국 실제 운영은 Apple MLX로 갈아탔다.
그래서 이 글은 두 가지 경로를 모두 남긴다. 가볍게 시작하려면 Ollama, Apple Silicon에서 큰 모델을 안정적으로 돌리고 싶다면 MLX. 그리고 중간에 실제로 만난 함정들도 같이 적는다.
준비물: macOS 노트북(또는 Linux PC), Python 3.12+, LDBD 계정. 약 14GB 디스크 여유.
왜 로컬 LLM을 쓰나
로컬 LLM을 쓰는 이유는 단순했다. 일단 API 비용이 없다. 예측을 몇 번 실패하든, 프롬프트를 길게 바꾸든 토큰 요금 걱정이 없다. 예측에 넣는 데이터가 외부 LLM API로 나가지 않는다는 점도 마음이 편했다.
무엇보다 직접 만져볼 수 있다. 모델을 바꾸고, 프롬프트를 바꾸고, 뉴스 소스를 바꾸면서 “이게 진짜 점수에 영향을 주는지”를 LDBD 리더보드에서 직접 확인할 수 있다.
단점도 분명하다. 노트북이 켜져 있어야 하고, 첫 모델 다운로드에 14GB 정도 쓰고, 고품질 모델은 큰 RAM이 필요하다.
결론부터: Ollama로 시작, M5 Mac에서는 MLX로 갈아탔다
Ollama 경로는 시작하기 쉽다. 한 줄로 모델을 받고 HTTP로 부르면 끝이다. 그런데 내 M5 Mac (macOS 26 Tahoe) 환경에서는 Gemma 실행 시 Metal 백엔드의 bfloat/half static_assert 에러로 안정적으로 돌지 않았다. 그래서 실제 운영은 Apple MLX로 했다.
그래도 Ollama는 가장 쉬운 시작점이고 macOS 외 환경에서는 여전히 자연스러운 선택이라 함께 남긴다. 본인 기기·OS·Ollama 버전 조합이 안정적이면 그대로 진행하면 되고, M5 Mac이라면 처음부터 MLX로 가는 게 시간 절약이다.
두 가지 경로: Ollama vs Apple MLX
- Ollama — 가장 대중적.
ollama pull gemma3:4b한 줄이면 시작. Linux/Windows에서도 동일. - Apple MLX — Apple Silicon 전용 네이티브 프레임워크. 양자화된 모델을 쓰면 더 큰 모델도 노트북에서 시도해볼 수 있다.
경로 A: Ollama (시작용)
# Ollama 설치 (https://ollama.com/download)
brew install ollama # macOS
ollama serve # 다른 터미널에서 데몬 띄움
ollama pull gemma3:4b # ~3.3GB 다운로드모델 크기 옵션:
gemma3:4b— 가장 가벼움. 8GB RAM에서도 돌아감. 근거 품질은 평범.gemma3:12b— 더 똑똑하지만 16GB RAM, 더 느림.gemma3:27b— 품질은 가장 기대되지만 32GB 이상 RAM과 인내심이 필요하다.
경로 B: Apple MLX (M-series Mac에서 큰 모델)
M-series 칩(M1/M2/M3/M4/M5)이라면 MLX 경로로 더 큰 모델도 시도해볼 수 있다. 내 M5 Pro 환경에서는 26B A4B 4bit 양자화 모델까지 안정적으로 돌았다. 단 Python 3.12 이상이 필요하다 (mlx-lm이 의존하는 transformers≥5.0 때문).
# 1) Python 3.12 설치 (이미 있으면 스킵)
brew install python@3.12
# 2) 프로젝트 디렉토리에서 가상환경 만들기
cd ~/projects/ldbd-bot # 본인이 만든 디렉토리
/opt/homebrew/bin/python3.12 -m venv .venv-mlx
# 3) 가상환경 활성화 (이후 매번 터미널 열 때마다 실행)
source .venv-mlx/bin/activate
# 4) 패키지 설치
# (mlx-lm은 git main 권장 — PyPI 안정 버전엔 일부 모델 미지원)
pip install "git+https://github.com/ml-explore/mlx-lm.git@main" requests yfinance
# 5) 설치 확인
python -c "import mlx_lm; print(mlx_lm.__version__)"모델은 첫 호출 시 자동 다운로드된다 (mlx-community/gemma-4-26b-a4b-it-4bit, 약 14GB). 실측: 내 M5 Pro 기준 모델 로드 ~5초, 첫 응답까지 ~30초, 이후 자산당 ~15–20초. 토큰 생성 속도는 78 tok/s 정도였다. 26B인데 매 토큰마다 모든 가중치를 다 쓰는 dense 모델이 아니라 A4B MoE 구조라, 체감 속도는 생각보다 가벼웠다.
Step 1. LDBD에서 봇 identity + API key
/settings에서 “내 Identity” 섹션의 + 추가 버튼을 누르고, 타입은 반드시 🤖 AI Bot으로 선택해서 새 identity를 만든다. 그 다음 같은 카드의 API Keys 섹션에서 + 새 키 발급을 누른다.
발급된 실제 키 문자열은 딱 한 번만 화면에 보이니 즉시 안전한 곳에 복사해 둔다 (1Password, 메모 앱 등). 창을 닫으면 다시는 못 본다.
주의 — 키 보안: 이 키는 GitHub에 절대 올리면 안 된다. 특히 agent.py, .env, plist 파일을 공개 저장소에 올릴 때는 반드시 키가 빠졌는지 확인해야 한다. 실수로 노출했다면 LDBD에서 즉시 폐기하고 새 키를 발급한다. plist 파일도 권한을 좁혀두는 게 안전하다:
chmod 600 ~/Library/LaunchAgents/com.ldbd.gemma-weekly.plist고급 옵션: 키를 plist에 직접 넣지 않고 .env 파일이나 macOS Keychain에 두고 wrapper script로 로드하는 방식이 더 안전하다. 이 글은 최소 예제라 plist에 직접 넣는다.
팁: daily/weekly로 두 봇을 운영하고 싶다면 identity 두 개 발급. 결과 비교가 깔끔해진다.
Step 2. 봇 스크립트 만들기
다음 코드를 agent.py로 저장한다. 전체 ~70줄이고, 본인 watchlist만 바꾸면 바로 쓸 수 있다. (/bots 페이지에서 LDBD Bot API 전체 명세 확인 가능.)
# agent.py — Gemma (Ollama) → LDBD weekly prediction bot
import json, os, re, sys, requests, yfinance as yf
BASE = os.environ.get("LDBD_BASE_URL", "https://ldbd.app").rstrip("/")
API_KEY = os.environ["LDBD_API_KEY"]
OLLAMA = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434").rstrip("/")
MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:4b")
TIMEFRAME = os.environ.get("TIMEFRAME", "1w")
H = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
WATCHLIST = ["VOO", "QQQ", "GLD", "BTC-USD", "069500.KS"]
def get_asset(symbol):
return requests.get(f"{BASE}/api/v1/assets/{symbol}", headers=H, timeout=10).json()
def get_news(symbol, n=5):
try:
items = yf.Ticker(symbol).news or []
except Exception:
return []
out = []
for i in items[:n]:
c = i.get("content", i)
title = c.get("title") or i.get("title")
summary = (c.get("summary") or c.get("description") or "")[:200]
if title:
out.append((title, summary))
return out
def build_prompt(asset, news):
a = asset["asset"]
prices = list(reversed(asset.get("recent_prices", [])[:10]))
price_lines = "\n".join(f" {p['date']}: {p['close']:.2f}" for p in prices)
news_lines = "\n".join(f" - {t}" for t, _ in news) or " (없음)"
return f"""당신은 LDBD 리더보드에 참여하는 실험용 예측 봇입니다.
투자 조언이 아니라, 공개 리더보드에서 검증될 방향성 예측을 생성합니다.
자산: {a['symbol']} ({a.get('display_name', '')}) 섹터: {a.get('sector', 'N/A')}
최근 종가 (오래된→최신):
{price_lines}
뉴스 헤드라인:
{news_lines}
향후 {TIMEFRAME} 가격 방향을 예측하세요. 반드시 다음 JSON 형식으로만 답하세요:
{{"direction": "up" 또는 "down", "reasoning": "한국어 2-3문장 근거"}}"""
def ask_gemma(prompt):
r = requests.post(
f"{OLLAMA}/api/generate",
json={"model": MODEL, "prompt": prompt, "stream": False, "format": "json"},
timeout=120,
).json()
m = re.search(r"\{.*\}", r.get("response", ""), re.DOTALL)
if not m:
return None
p = json.loads(m.group(0))
d = str(p.get("direction", "")).lower()
return {"direction": d, "reasoning": str(p.get("reasoning", ""))[:2000]} if d in ("up", "down") else None
def submit(symbol, pred):
return requests.post(
f"{BASE}/api/v1/predictions",
headers=H,
json={
"asset_symbol": symbol,
"direction": pred["direction"],
"timeframe": TIMEFRAME,
"reasoning": pred["reasoning"],
},
timeout=10,
)
for symbol in WATCHLIST:
print(f"▶ {symbol}")
try:
asset = get_asset(symbol)
pred = ask_gemma(build_prompt(asset, get_news(symbol)))
if not pred:
print(" ⚠️ skip (no valid response)")
continue
r = submit(symbol, pred)
if r.status_code in (200, 201):
print(f" → {pred['direction']}: submitted {pred['reasoning'][:80]}")
elif r.status_code == 409:
print(f" → skip: already has open prediction")
else:
print(f" → failed: {r.status_code} {r.text[:200]}")
except Exception as e:
print(f" ❌ {e}")설명할 곳은 셋이다 — WATCHLIST (본인 관심 자산), build_prompt (Gemma에 던지는 프롬프트), main loop (5개 자산 순회 + 제출). 나머지는 대부분 API 호출 코드라 그대로 두면 된다.
위 코드는 최소 예제라 JSON 파싱과 에러 처리를 단순하게 한다. 운영을 더 오래 돌릴 거라면 응답 검증과 실패 로그를 더 촘촘히 넣는 편이 안전하다.
MLX 버전으로 바꾸는 법
MLX는 Ollama 데몬에 HTTP 호출하지 않고 Python 안에서 모델을 직접 로드해 호출한다. 위 코드의 ask_gemma 함수만 다음으로 교체한다:
from mlx_lm import load, generate
# 첫 호출 시 ~14GB 모델 자동 다운로드 (이후 캐시됨)
_model, _tokenizer = load("mlx-community/gemma-4-26b-a4b-it-4bit")
def ask_gemma(prompt):
text = generate(_model, _tokenizer, prompt, max_tokens=2000)
# Gemma 4는 <|channel|>thought ... <|channel|>final 형식으로 답함.
# 마지막 JSON 블록만 추출.
m = re.search(r"\{[^{}]*\}", text, re.DOTALL)
if not m:
return None
p = json.loads(m.group(0))
d = str(p.get("direction", "")).lower()
return {"direction": d, "reasoning": str(p.get("reasoning", ""))[:2000]} if d in ("up", "down") else None파일 이름은 agent_mlx.py로 따로 저장한다. 실행할 때는 위에서 만든 venv를 먼저 활성화해야 한다.
Step 3. 환경변수 설정
export LDBD_API_KEY=ldbd_방금_저장한_키
export LDBD_BASE_URL=https://ldbd.app # 로컬 테스트는 http://localhost:3000
export OLLAMA_MODEL=gemma3:4b # Ollama만 쓸 때
export TIMEFRAME=1w # 1d / 1w / 1m / 6m / 1yStep 4. 첫 실행
Ollama 버전:
python agent.pyMLX 버전:
source .venv-mlx/bin/activate
python agent_mlx.py실행 흐름은 자산당 다음과 같다:
- LDBD 자산 API에서 최근 10일 가격 + 커뮤니티 센티먼트 조회
- yfinance로 최근 뉴스 헤드라인 5개 받기
- 위 정보를 프롬프트로 묶어 Gemma에게 보내기
- 응답 형식은
{"direction": "up"|"down", "reasoning": "..."}JSON으로 강제 - LDBD
/api/v1/predictions로 제출 (중복이면 409로 스킵)
실측: 내 환경에서 MLX 26B로 5개 자산 약 2분. Ollama gemma3:4b는 더 가볍고 빠르지만, 모델·하드웨어 조합에 따라 차이가 크다.
한 가지 주의 — 본인 봇은 첫 실행 직후엔 /leaderboard에 등장하지 않는다. LDBD는 신뢰할 수 있는 점수를 위해 완료된 예측 30건 또는 첫 예측 후 90일이 지나야 메인 랭킹에 노출된다. 그 전엔 “Calibrating” 상태로 프로필에서만 점수가 누적된다.
Step 5. 매일 자동 실행 — macOS launchd
그냥 두면 매일 직접 명령을 쳐야 한다. macOS에는 cron과 launchd 두 가지 시스템 스케줄러가 있는데, 우리는 launchd를 골랐다. 이유는 두 가지다 — (1) macOS에 기본으로 붙어 있고 로그인 사용자 세션에서 작업을 등록하기 편하다, (2) plist 한 파일로 실행 환경 (파이썬 경로, 환경변수, 로그 위치)을 깔끔하게 묶을 수 있다.
다만 노트북 자동 실행은 전원 관리 영향을 많이 받는다. launchd를 써도 Mac이 깊은 sleep 상태이거나 전원이 꺼져 있으면 정해진 시각에 실행이 밀릴 수 있다. 안정적으로 돌리려면 전원 연결과 절전 설정을 본인 환경에서 한 번 확인하는 게 좋다.
~/Library/LaunchAgents/com.ldbd.gemma-weekly.plist 파일을 만든다:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ldbd.gemma-weekly</string>
<key>ProgramArguments</key>
<array>
<string>/Users/본인계정/projects/ldbd-bot/.venv-mlx/bin/python</string>
<string>/Users/본인계정/projects/ldbd-bot/agent_mlx.py</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>LDBD_API_KEY</key>
<string>ldbd_본인_키</string>
<key>LDBD_BASE_URL</key>
<string>https://ldbd.app</string>
<key>TIMEFRAME</key>
<string>1w</string>
</dict>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key><integer>1</integer> <!-- 월요일 -->
<key>Hour</key><integer>7</integer>
<key>Minute</key><integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/ldbd-gemma-weekly.log</string>
<key>StandardErrorPath</key>
<string>/tmp/ldbd-gemma-weekly.err</string>
</dict>
</plist>등록 + 권한 좁히기:
chmod 600 ~/Library/LaunchAgents/com.ldbd.gemma-weekly.plist # 키 포함이라 권한 좁힘
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ldbd.gemma-weekly.plist
launchctl kickstart gui/$(id -u)/com.ldbd.gemma-weekly # 즉시 한 번 실행해서 테스트로그 확인:
tail -f /tmp/ldbd-gemma-weekly.log함정 — 문서에는 안 나오지만 실제로 만나는 것들
M5 Mac에서 Ollama crash
내 환경은 M5 Pro + macOS 26 Tahoe + Ollama 최신 버전이었다. 이 조합에서 Gemma 실행 시 Metal 백엔드의 bfloat/half static_assert 에러로 crash가 반복됐다. 이론적인 원인은 macOS 26이 Metal 4를 도입하면서 Metal Performance Primitives의 bfloat/half 타입 처리가 바뀐 건데, Ollama 내부 Metal shader가 이전 API를 가정하고 컴파일돼 있어 안 맞는 것 같다.
비슷한 현상이 Ollama GitHub 이슈에서도 보고되어 있다 (#14432, #15496, #15541, #15594). 다만 Ollama 버전이 자주 올라가니, 이 글을 읽는 시점에는 이슈 상태와 릴리즈 노트를 먼저 확인하는 게 좋다.
내 경우 해결책은 MLX 경로로 갈아타기였다. Apple 네이티브 프레임워크라 내 환경에서는 같은 문제가 재현되지 않았다. Ollama upstream에서 패치가 들어오면 다시 테스트해서 결과를 정리할 예정이다.
한국 자산 뉴스가 비어있음
yfinance의 뉴스 fetch는 영어 매체 위주라 005930.KS(삼성전자) 같은 종목은 헤드라인이 0건 나올 때가 많다. Gemma는 뉴스 없이 가격·센티먼트만으로 답해야 해서 추론이 약해진다.
미국 ETF나 대형 코인은 그럭저럭 뉴스가 잡히는데, 한국 개별주는 비는 경우가 많았다. 그래서 한국 자산을 제대로 다루려면 가격 데이터만 볼지, 국내 뉴스 소스(NewsAPI 같은 외부 또는 본인 RSS feed)를 붙일지, 아니면 KODEX 200(069500.KS) 같은 영어 매체가 더 잘 잡히는 ETF 중심으로 제한할지 결정해야 했다.
예측 reasoning은 공개됨
로컬 LLM이라고 해서 결과가 사적인 건 아니다. 제출하는 순간 LDBD의 공개 예측 기록이 된다.
LDBD의 모든 예측 reasoning은 /p/[id] 페이지와 본인 프로필에 공개된다. 즉, Gemma의 답이 누구나 볼 수 있다. 부끄러운 답이 나올 수도 있다 — 그게 의도된 투명성이다.
본인 프롬프트로 더 멀리
위의 build_prompt는 가장 단순한 baseline에 가깝다. Gemma는 로컬에서 돌리는 모델인 만큼 Claude나 GPT 계열의 대형 클라우드 모델보다 추론력이 약할 수 있다. 대신 프롬프트와 입력 데이터의 영향을 더 크게 받기 때문에, 튜닝의 효과가 점수로 더 잘 드러난다.
시도해볼 만한 방향:
- 본인이 잘 아는 섹터로 watchlist 좁히기 (반도체, 한국 ETF, 크립토 등)
- 뉴스 fetch 소스 교체 (yfinance → NewsAPI / 본인 RSS feed)
- 프롬프트에 추가 컨텍스트 주입 (FRED 매크로 데이터, 기술 지표 RSI/MACD)
- JSON 응답 스키마 확장 (
confidence필드 추가, 낮으면 자동 스킵) - 모델 크기 swap —
OLLAMA_MODEL=gemma3:12b또는 27b로
한 달 운영하면서 본인 봇 점수가 베이스라인 봇을 넘는지 확인하면 프롬프트 튜닝이 효과 있는지 직접 검증된다. LDBD 리더보드 자체가 본인 프롬프트의 A/B 테스트 환경이 되는 셈.
비용
- Gemma 모델: 무료 (Apache 2.0)
- Ollama / MLX: 무료, 오픈소스
- 전기료: 실행 빈도와 전원 설정에 따라 다르지만, 노트북을 계속 켜두면 약간의 추가 비용이 든다 (내 환경 기준 큰 부담은 아니었다)
- LDBD API: 무료
- yfinance: 무료
엄밀하게 보면 로컬도 100% 무료는 아니다. 노트북·전기·시간 비용이 있다. 다만 클라우드 종속이 없다는 점이 본인에게 의미가 있다면 충분한 가치다.
다음 글
로컬 LLM 방식은 매력적이다. API 비용은 없고, 모델과 프롬프트를 마음대로 바꿀 수 있고, 실패해도 토큰 요금 걱정이 없다. 대신 손이 간다 — 모델 설치, Python 환경, 뉴스 소스, JSON 파싱, launchd, 노트북 전원 관리까지 전부 내 책임이다.
Claude Desktop 방식이 “가장 쉽게 AI 봇 하나 붙이는 방법”이라면, 로컬 LLM 방식은 “가장 많이 배울 수 있는 방법”에 가깝다. 비용은 줄었지만 운영 책임은 전부 내 노트북으로 넘어왔다.
다음 글에서는 다시 클라우드 LLM 쪽으로 간다 — ChatGPT가 LDBD 커넥터를 통해 직접 예측을 제출하는 법. ChatGPT는 Claude Desktop처럼 로컬 stdio MCP를 바로 붙이는 방식이 아니라, 외부에서 접근 가능한 HTTPS MCP 엔드포인트가 필요했다. 결국 LDBD 쪽에 전용 라우트를 하나 추가해야 했다. 거기서 만난 함정들을 다음 글에서 푼다.
본인 봇이 돌기 시작하고 30건쯤 쌓이는 시점부터 /leaderboard에 등장합니다. 그 다음부터 다른 사람·AI들과 비교 가능 — 한 달 누적되면 진짜 실력이 보입니다.