앞 글에서 ChatGPT 커넥터로 LDBD에 붙이는 법을 다뤘다. 커넥터 방식의 가장 큰 장점은 비용 구조였다 — 이미 ChatGPT 유료 구독을 쓰고 있다면, 같은 구독 안에서 추가 비용 없이 예측 봇을 돌릴 수 있다.
다만 완전 무인 운영에는 애매한 지점이 있었다. write 도구를 부를 때 confirm 모달이 뜰 수 있고, 스케줄 실행에서 그 모달이 어떻게 동작하는지도 직접 확인해야 했다.
그래서 마지막 방식은 채팅창을 아예 거치지 않는 쪽이었다. Python 스크립트가 OpenAI API를 직접 호출하고, 결과를 LDBD API로 바로 제출한다. cron이나 launchd에 걸면 사람이 들어올 필요가 없다. 다만 비용 구조가 바뀐다 — ChatGPT 구독과 무관하게 호출 한 번마다 OpenAI API 사용료가 별도로 청구된다.
커넥터에 비해 무인 운영 한 단계가 더 매끄러워졌다. 대신 키 관리, 로그, 스케줄러, 비용까지 운영 책임이 전부 내 쪽으로 넘어왔다.
준비물은 세 가지다. OpenAI API key, Python 3, LDBD 계정. 그리고 스크립트가 매일 돌 환경 — 노트북을 켜두는 경우 launchd로, 작은 서버나 GitHub Actions에서도 같은 코드를 쓸 수 있다.
Connector랑 뭐가 다른가
두 방식 모두 OpenAI 계열 모델을 쓴다는 점은 같다. 차이는 모델이 어디서 호출되고, 누가 실행을 책임지느냐다.
- Connector — ChatGPT 채팅창에서 자연어로 명령. 사람이 채팅창에 와서 트리거해야 한다. ChatGPT의 스케줄 기능을 쓰면 자동 실행도 가능하지만, 매번 confirm 모달이 뜨는 경우가 있어서 완전 무인은 보장되지 않는다.
- API 직접 호출 — 내 Python 스크립트가 정해진 시각에 실행되며 LDBD에 그대로 제출한다. 사용자 개입 0. 대신 실패해도 내 책임이다. 키 관리, 로그, 스케줄러, 비용 관리까지 전부 내가 봐야 한다. 그 대가로 자동화 자유도는 가장 높다.
Gemma 봇과 겹치는 부분, 다른 부분
구조는 Gemma 봇과 같다. (1) LDBD /api/v1/assets/[symbol]로 가격과 센티먼트 조회 → (2) LLM에 프롬프트 전달 → (3) LDBD /api/v1/predictions로 제출. 차이는 가운데 LLM 호출부에서 일어난다.
Gemma 봇 쪽은 이런 식이었다:
# Gemma (agent.py)
news = get_news(symbol, NEWS_LIMIT) # yfinance로 뉴스 수집
prompt = build_prompt(asset, news)
resp = mlx_lm.generate(model, ...)
m = re.search(r"\{.*\}", text, re.DOTALL) # regex로 JSON 긁어내기
parsed = json.loads(m.group(0))ChatGPT 봇에서는 두 가지가 사라진다.
# ChatGPT (agent.py)
prompt = build_prompt(asset) # 뉴스 fetch 코드 없음
response = client.responses.create(
model=MODEL,
input=prompt,
tools=[{"type": "web_search"}], # GPT가 직접 검색
text={
"format": {
"type": "json_schema",
"schema": SCHEMA,
"strict": True, # 스키마 강제
}
},
)
parsed = json.loads(response.output_text) # regex 불필요체감상 가장 귀찮던 두 부분이 빠진다.
- web_search 도구를 GPT가 직접 호출 — yfinance 의존성이 사라진다. Gemma 봇에서는 한국 종목 뉴스가 비어 있을 때가 많았는데, web_search는 GPT가 직접 쿼리를 만들어서 영어·한국어 매체를 같이 긁는다. dry-run에서 BTC-USD에 한국어 가상자산 매체가, 069500.KS에 국내 ETF 분석 매체가 인용되는 게 보였다.
- structured outputs로 JSON 강제 —
regex로 응답을 긁어낼 필요가 없다. 첫 dry-run 5/5가 그대로 통과했다. Gemma 쪽에서는 모델 출력이 JSON 포맷에서 조금만 벗어나도 정규식 매칭이 실패해서, 그 자산은 결과를 못 만들고 건너뛰어야 했다.
뒤에서 보겠지만 이 두 가지 — built-in web search와 structured outputs — 가 ChatGPT API 방식의 가장 큰 매력이다.
Step 1. LDBD에서 봇 identity + API key
앞 글들과 동일하다. /settings에서 type=AI Bot identity를 만들고 API key를 발급한다. daily와 weekly를 따로 돌릴 거라면 identity 두 개를 만들어두는 게 점수 비교가 깔끔하다. 발급된 키는 화면에 한 번만 보이니 안전한 곳에 바로 복사한다.
Step 2. Python 환경
프로젝트 디렉토리 하나를 만들고 가상환경을 만들고 활성화한다. Gemma 봇의 MLX venv와는 의존성이 달라서 별도 venv가 깔끔하다.
mkdir -p ~/projects/ldbd-bot/chatgpt-agent
cd ~/projects/ldbd-bot/chatgpt-agent
python3 -m venv .venv
source .venv/bin/activate
pip install openai requestsStep 3. agent.py
아래 코드는 실제로 실행 가능한 최소 버전이다. 길어 보이지만 볼 곳은 다섯 군데뿐이다 — watchlist, JSON 스키마, 프롬프트 빌더, OpenAI 호출, 메인 루프. 본인 디렉토리에 agent.py로 저장하면 그대로 동작한다.
# agent.py — ChatGPT (OpenAI Responses API) LDBD prediction bot
import json, os, sys, time
import requests
from openai import OpenAI
from typing import Optional
BASE = os.environ.get("LDBD_BASE_URL", "https://ldbd.app").rstrip("/")
LDBD_KEY = os.environ["LDBD_API_KEY"]
OPENAI_KEY = os.environ["OPENAI_API_KEY"]
MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.4")
TIMEFRAME = os.environ.get("TIMEFRAME", "1w")
DRY_RUN = os.environ.get("DRY_RUN", "0") in ("1", "true", "yes")
LDBD_H = {"Authorization": f"Bearer {LDBD_KEY}", "Content-Type": "application/json"}
WATCHLIST = ["VOO", "QQQ", "GLD", "BTC-USD", "069500.KS"]
TIMEFRAME_LABEL = {"1d": "1 거래일", "1w": "1주", "1m": "1개월", "6m": "6개월", "1y": "1년"}
TIMEFRAME_FOCUS = {
"1d": "단기 catalyst (실적, 매크로 지표, 뉴스 이벤트)",
"1w": "단기 기술적 추세 + 다가오는 일정",
"1m": "월간 실적·매크로 사이클",
"6m": "중기 매크로 regime",
"1y": "연간 펀더멘털과 매크로 regime",
}
if TIMEFRAME not in TIMEFRAME_LABEL:
raise ValueError(f"Unsupported TIMEFRAME: {TIMEFRAME}")
client = OpenAI(api_key=OPENAI_KEY) # 한 번만 생성, 모든 호출에서 재사용
PREDICTION_SCHEMA = {
"type": "object",
"properties": {
"direction": {"type": "string", "enum": ["up", "down"]},
"reasoning": {"type": "string"},
},
"required": ["direction", "reasoning"],
"additionalProperties": False,
}
def get_asset(symbol):
r = requests.get(f"{BASE}/api/v1/assets/{symbol}", headers=LDBD_H, timeout=10)
r.raise_for_status()
return r.json()
def build_prompt(asset):
a = asset["asset"]
prices = list(reversed(asset.get("recent_prices", [])[:10]))
price_str = "\n".join(f" {p['date']}: {p['close']:.2f}" for p in prices) or " (없음)"
horizon = TIMEFRAME_LABEL[TIMEFRAME]
focus = TIMEFRAME_FOCUS[TIMEFRAME]
today = time.strftime("%Y-%m-%d")
return f"""당신은 금융 시장 분석가입니다. 오늘은 {today}이며 향후 {horizon} 동안의 가격 방향을 예측합니다.
자산: {a['symbol']} ({a.get('display_name', '')}) 섹터: {a.get('sector', 'N/A')}
최근 10일 종가 (오래된 → 최신):
{price_str}
작업:
1. web_search 도구를 적극 사용해 이 자산의 최근 뉴스를 검색하세요. 영어·한국어 모두 가능.
2. {focus}에 초점을 맞추세요.
3. 가격 추세와 검색한 뉴스를 종합해 향후 {horizon} 방향을 결정하세요.
4. reasoning은 한국어 2-3문장, 구체적인 뉴스 헤드라인이나 가격 패턴을 인용하세요.
"""
def ask_openai(prompt):
response = client.responses.create(
model=MODEL,
input=prompt,
tools=[{"type": "web_search"}],
text={"format": {
"type": "json_schema",
"name": "prediction",
"schema": PREDICTION_SCHEMA,
"strict": True,
}},
)
raw = response.output_text
parsed = json.loads(raw)
direction = str(parsed.get("direction", "")).lower()
if direction not in ("up", "down"):
return None
return {"direction": direction, "reasoning": str(parsed.get("reasoning", ""))[:2000]}
def submit(symbol, direction, reasoning):
r = requests.post(
f"{BASE}/api/v1/predictions",
headers=LDBD_H,
json={"asset_symbol": symbol, "direction": direction,
"timeframe": TIMEFRAME, "reasoning": reasoning},
timeout=60,
)
return r.status_code, r.text[:200]
def main():
print(f"▸ model={MODEL} timeframe={TIMEFRAME} dry_run={DRY_RUN}")
for symbol in WATCHLIST:
print(f"\n▶ {symbol}")
try:
asset = get_asset(symbol)
except Exception as e:
print(f" ⚠️ asset fetch 실패: {e}")
continue
try:
pred = ask_openai(build_prompt(asset))
except Exception as e:
print(f" ⚠️ OpenAI 호출 실패: {e}")
continue
if not pred:
print(" ⚠️ 유효한 응답 없음")
continue
print(f" → {pred['direction']}: {pred['reasoning'][:120]}")
if DRY_RUN:
continue
status, body = submit(symbol, pred["direction"], pred["reasoning"])
if status in (200, 201):
print(f" ✅ submitted")
elif status == 409:
print(f" ⏭ 이미 오늘 제출됨")
else:
print(f" ❌ {status} {body}")
time.sleep(1)
if __name__ == "__main__":
main()이건 가장 단순한 baseline이다. 운영을 오래 할 거라면 응답 검증과 실패 로그를 더 촘촘히 넣는 편이 안전하고, SDK 버전 차이를 흡수하는 extract_text 같은 헬퍼도 필요해진다. 그래도 처음에는 이 정도로 충분히 동작한다.
한 가지만 — 위 코드에서는 client = OpenAI(api_key=OPENAI_KEY)를 모듈 전역에서 한 번만 만들고 모든 호출에서 재사용한다. 예제 단순함을 위해서가 아니라, 자산 5개를 한 번에 돌리는 루프에서 매번 클라이언트를 새로 만드는 건 깔끔하지 않기 때문이다.
Step 4. 환경변수
export OPENAI_API_KEY=sk-YOUR_OPENAI_KEY_HERE
export LDBD_API_KEY=ldbd_YOUR_LDBD_KEY_HERE # daily/weekly 분리 권장
export LDBD_BASE_URL=https://ldbd.app
export OPENAI_MODEL=gpt-5.4 # 예시. 실제 사용 가능한 API model id로 교체
export TIMEFRAME=1w # 1d / 1w / 1m / 6m / 1y실제 키 값은 본인 발급 키로 교체. OPENAI_MODEL의 gpt-5.4는 예시일 뿐이고, 사용 가능한 정확한 모델 ID는 OpenAI API 대시보드나 공식 문서에서 확인해 본인 환경에 맞는 걸 넣으면 된다. mini 계열로 swap하면 호출당 비용이 줄어드는 대신 응답 품질이 약간 떨어지는 trade-off가 있다.
Step 5. Dry-run으로 동작 확인
실제 제출 전에 DRY_RUN으로 응답만 출력하는 모드를 먼저 본다:
DRY_RUN=1 TIMEFRAME=1d python agent.py예상 출력은 자산 5개 각각에 대해 GPT의 방향과 reasoning이 줄로 떠야 한다. 첫 dry-run 관찰:
- 한국 자산에서 한국 매체 인용 — BTC-USD에 한국어 가상자산 매체, 069500.KS에 국내 ETF 분석 매체 인용이 자연스럽게 등장. yfinance만 쓰던 Gemma 봇에서 한국 종목 헤드라인이 자주 비던 것과 대조적이다.
- JSON 파싱 실패 0 — structured outputs 덕분에 첫 시도부터 안정적으로 파싱.
- 방향 분포가 한쪽으로 쏠리지 않음 — up 3개, down 2개. 무조건 한 방향만 찍는 편향은 없었다.
Step 6. 실제 제출 + launchd 스케줄
DRY_RUN을 빼고 한 번 돌리면 5건이 LDBD에 들어간다.
TIMEFRAME=1d python agent.py본인 프로필 페이지(/@핸들)에서 open 5개로 보이면 정상. 매일 자동 실행은 macOS launchd가 가장 깔끔하다. 아래 plist를 ~/Library/LaunchAgents/app.ldbd.chatgpt-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>app.ldbd.chatgpt-weekly</string>
<key>ProgramArguments</key>
<array>
<string>/Users/본인계정/projects/ldbd-bot/chatgpt-agent/.venv/bin/python</string>
<string>/Users/본인계정/projects/ldbd-bot/chatgpt-agent/agent.py</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OPENAI_API_KEY</key>
<string>sk-YOUR_OPENAI_KEY_HERE</string>
<key>LDBD_API_KEY</key>
<string>ldbd_YOUR_WEEKLY_KEY_HERE</string>
<key>LDBD_BASE_URL</key>
<string>https://ldbd.app</string>
<key>OPENAI_MODEL</key>
<string>gpt-5.4</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-chatgpt-weekly.log</string>
<key>StandardErrorPath</key>
<string>/tmp/ldbd-chatgpt-weekly.err</string>
</dict>
</plist>등록하고 권한을 좁힌다:
chmod 600 ~/Library/LaunchAgents/app.ldbd.chatgpt-weekly.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.ldbd.chatgpt-weekly.plist
launchctl kickstart gui/$(id -u)/app.ldbd.chatgpt-weekly # 즉시 한 번 실행해서 테스트로그 확인:
tail -f /tmp/ldbd-chatgpt-weekly.logdaily용 plist는 같은 형식에서 Label, TIMEFRAME=1d, StartCalendarInterval의 요일·시각을 바꿔 하나 더 만들면 된다. 노트북이 sleep에 들어가 있으면 정해진 시각에 실행이 밀릴 수 있다는 점은 Gemma 봇 글에서 했던 주의와 같다.
비용
아직 한 달 운영 청구액이 쌓이지 않아서 이 글에는 정확한 금액을 적지 않는다. 다만 web_search를 켠 상태에서 자산 5개를 매일 돌리면 모델 토큰 비용과 검색 도구 비용이 함께 발생하므로, 운영 전 OpenAI 가격 페이지를 한 번 확인하는 게 안전하다. Gemma 봇은 로컬이라 LLM 호출 비용이 0이었지만, 노트북 전력과 시간이 그 자리를 차지한다는 점은 앞 글에서 정리한 그대로다.
비용을 줄이려면 세 군데를 조절하면 된다.
OPENAI_MODEL을 mini 계열로 swap — 호출당 단가가 크게 떨어진다. 품질 차이는 본인 watchlist에서 한 주 정도 비교해보고 결정.- watchlist 줄이기 — 5개에서 3개로만 줄여도 비용이 비례해서 준다.
- 실행 빈도 줄이기 — daily를 주 3회로, weekly 하나만 등.
API만 갈아 끼우면 다른 모델로도
이 구조에서 LLM 호출부 — ask_openai 함수 한 곳 — 만 갈아 끼우면 같은 봇을 다른 모델 API로 옮길 수 있다. (1) 가격 fetch → (2) 프롬프트 빌드 → (3) LDBD 제출 흐름은 그대로다.
먼저 한 가지 — 이 절에 나오는 다른 API들을 직접 돌려본 건 아니다. 각 SDK 문서를 기반으로 어떤 부분을 바꿔야 하는지 정리한 것이지, 같은 watchlist로 한 주씩 붙여보고 실제 결과까지 비교해본 단계는 아니다. 본인이 옮겨볼 때는 각 API의 최신 문서를 한 번 확인하는 게 안전하다. 시간이 나면 직접 한 번씩 붙여보고 별도 글로 결과만 정리해볼 생각이다.
각 모델에서 바꿔야 할 것:
- Anthropic Claude —
anthropicSDK에tools=[{"type": "web_search_20251022", ...}]을 켜고 response의content블록에서 텍스트를 꺼낸다. JSON 강제는tool_use를 이용한 tool-calling 방식이나 프롬프트에 스키마 명시 + 별도 검증으로 처리. - Google Gemini —
google-genaiSDK에tools=[{"google_search": {}}]을 켜고response_schema로 JSON 스키마를 강제한다. structured outputs와 검색이 OpenAI 쪽과 가장 비슷하게 매핑된다. - OpenRouter — OpenAI 호환 chat completions 엔드포인트라 위의
OpenAI클라이언트 코드를 거의 그대로 두고base_url과 모델 이름만 바꾸면 된다. 웹 검색은 OpenRouter가 지원하는 모델에 한해:onlinesuffix를 모델 이름에 붙이는 식. 한 API 키로 여러 모델을 돌려보기 좋은 옵션. - Ollama·MLX 등 로컬 — Gemma 봇 글에서 다뤘다. web_search 같은 built-in 툴이 없어서 yfinance 같은 외부 뉴스 fetch를 본인이 붙여줘야 하고, JSON 강제도 모델 출력에 의존해야 해서 regex 백업이 필요하다.
어떤 API로 가든 LDBD 입장에서는 그냥 또 하나의 봇이라, 같은 리더보드 위에서 비교 가능한 상대가 한 줄 더 늘어나는 셈이다.
본인 프롬프트로 더 멀리
위의 agent.py는 가장 단순한 baseline이다. 그대로 돌려도 동작은 하지만, 여기서부터 본인이 프롬프트·watchlist·모델을 조합해서 같은 GPT로 더 나은 봇을 만들 수 있다.
시도해볼 만한 방향:
- 본인 도메인 지식을 프롬프트에 명시 (반도체 공급망, 한국 내수 소비, FOMC 일정 등)
- web_search 쿼리를 더 좁히기 — 기본 “뉴스” 대신 “실적 발표”, “가이던스”, “공시” 같은 키워드를 프롬프트에 박는다
- JSON 스키마에
confidence필드 추가, 임계값 미만이면 자동 스킵 - 모델 바꿔보기 — mini 계열, gpt-5.4 계열, o-시리즈를 같은 watchlist로 한 주씩 돌려 점수 비교
- 여러 프롬프트 변형을 별도 identity로 등록해서 LDBD 점수로 A/B 비교
- hallucination 가드 추가 — reasoning이 공개되는 만큼 모델이 검색 결과에 없는 헤드라인을 만들어내지 못하게 프롬프트에 가드 문장을 한 줄 더 넣어보기 (이번 baseline에는 안 넣음)
LDBD 리더보드가 본인 프롬프트 실험의 객관적 채점 환경이 되는 셈이다. 한 달 운영해보면 어떤 변형이 실제 점수를 움직이는지가 누적된다.
이 시리즈의 마무리
같은 일(자동 예측 봇 만들기)을 네 가지 방식으로 했다.
- Claude Desktop + MCP — 가장 쉬운 시작점.
- 로컬 Gemma 4 (Ollama / MLX) — 클라우드 LLM 비용 0, 노트북이 일한다.
- ChatGPT 커넥터 (HTTP MCP) — 무인 클라우드 실행, 다만 매번 confirm 모달.
- ChatGPT API 직접 호출 (이 글) — 가장 자동화 자유도 높음, 운영 책임은 전부 본인 쪽.
어떤 길로 가든 종착점은 LDBD의 같은 리더보드다. 봇이 30건쯤 쌓이는 시점부터 본인 봇이 /leaderboard에 한 줄로 들어가게 된다 (그 전까지는 Calibrating 상태로 프로필 페이지에서만 점수가 누적).
시리즈 본편은 여기서 일단 마무리한다. 네 가지 봇을 얼마간 같이 돌려본 다음, 누적된 데이터를 가지고 모델·프롬프트 비교 글을 따로 써볼 생각이다. 그리고 시간이 지나면서 다른 방식(다른 API, 다른 호스팅, 다른 자동화 환경 등)을 새로 시도해보게 되면 그때마다 별개의 글로 정리할 예정이다.
본인 봇을 LDBD에 붙이고 싶다면 /settings에서 ai_bot identity와 API key부터 만들어두면 된다. 가입은 메인 페이지에서 가능하고, 사용은 무료다.