이것은 무엇인가
Claude Code를 1시간마다 claude -p로 가동하여, 연구의 다음 단계를 인간의 승인 없이 진행시키고 있습니다. 대상은 암호자산의 무기한 선물(無期限先物)을 소액으로 거래하는 퀀트 연구 리포지토리입니다. 저는 이것을 자율 루프(自律ループ)라고 부릅니다.
루프가 1시간마다 수행하는 작업은 다음 5가지입니다.
- 시작 여부는 셸 스크립트(shell script)가 결정합니다. 이유가 없으면 LLM은 가동하지 않습니다.
- 다음에 할 행동을 코드가 하나 선택합니다. '아이디어 제시', '검증 등록', '페이퍼 트레이드에 올리기' 등 총 10가지 종류가 있습니다.
- Claude Code가 그 행동 중 단 하나를 실행하고 PR(Pull Request)을 1개 만듭니다.
- 병합용 스크립트(merge script)가 PR을 검사하여 통과된 것만 main 브랜치에 병합합니다.
- main으로의 병합 시, 거래를 수행하는 워커(worker)가 자동으로 배포됩니다.
결과적으로 나오는 것은 행동 1개 분량의 PR, 연구 기록, 작업 로그, 페이퍼 트레이드나 실자금으로 움직이는 전략입니다. 다음 절에서 하나씩 살펴보겠습니다.
거래 성적에 대한 이야기는 나오지 않습니다. 이 글이 다루는 것은 무엇이 작동하고, 무엇이 결과물로 나오는가 하는 것입니다.
무엇이 결과물인가
| 결과물 | 저장 위치 | 내용 |
|---|
| PR | GitHub | 행동 1개 분량의 변경 사항. 사이클당 1건 |
| 연구 기록 | 리포지토리의 research/ | 아이디어, 가설과 반증 조건, 실험 정의, 파악한 내용 |
| 작업 로그 | research/autonomy/journal.md | 사이클마다 무엇을 확인했고, 무엇을 했으며, 다음은 무엇인지 |
| 페이퍼 트레이드 | Cloudflare Workers와 D1 | 등록된 규칙을 가상 자금으로 움직인 포지션과 손익 |
| 실자금 전략 | 다른 worker | 기준을 통과한 규칙을, 중단 조건과 함께 소액의 실자금으로 움직임 |
| 진행 상황 화면 | worker의 대시보드 | 각 아이디어와 실험이 현재 어느 단계에 있는지 |
| 알림 | Slack | 사이클 종료, 병합 및 배포, 이상 발생 시 |
진행 상황
각 아이디어와 실험이 어떤 단계에 있는지는 명령어 1개로 확인할 수 있습니다. 다음은 실제 출력을 발췌하고 ID를 가린 것입니다.
$ uv run quant pipeline status
id kind stage blocked_by
IDEA-0xx idea registered -
...
blind until은 해당 날짜까지 채점할 수 없음을 의미합니다. 검증에 사용하는 데이터가 아직 존재하지 않는 미래 기간을 지정하여 등록했기 때문에, 기간이 끝날 때까지 실행 자체가 거부됩니다. funnel은 단계별 건수입니다. 마지막의 next는 루프가 다음에 할 행동입니다. IDEATE와 같은 행동 이름은 뒤의 '다음 행동 결정 방법'에서 설명하겠습니다.
작업 로그
사이클이 끝날 때마다, 루프는 작업 로그에 1개 항목을 추가합니다. 다음은 실제 항목을 간략화하고 ID와 종목을 가린 것입니다.
## 2026-10-04 daily (cycle 92)
- **요약:** table의 action은 DATA IDEA-0xx. 검증에 사용할 2개 종목의 4시간봉을 가져왔다.
기존 캐시가 오래되어, 처음에는 2개의 시리즈 기간이 겹치지 않아 실패했다. 다시 가져와 해결했다.
...
인간이 주로 읽는 것은 이 로그와 Slack 알림입니다. PR을 하나하나 읽어야 하는 경우는 병합용 스크립트가 거부하여 인간에게 넘겼을 때뿐입니다.
1시간의 흐름
1. 스케줄러와 가동 판정
스케줄러는 매시 0분에 precheck.sh를 실행하고, 종료 코드가 0일 때만 사이클을 가동합니다. 저는 Orca의 automation을 사용하고 있지만, cron이나 launchd로도 같은 방식으로 구현할 수 있습니다.
precheck.sh가 확인하는 것은 다음 사항들입니다. 이 중 하나라도 충족하지 못하면 LLM은 가동되지 않으며 비용도 발생하지 않습니다.
| 확인 사항 | 수단 |
|---|
| 다른 사이클이 작동하고 있지는 않은지 | 잠금용 디렉토리 유무 |
| ... | research/ 내용의 해시를 이전 사이클 종료 시점과 비교 |
| 결정표가 행동을 반환하는지 | quant pipeline next 결과가 WAIT 외인지 |
| 같은 행동이 직전에 공회전하지 않았는지 | 이전 행동과 결과 기록. 공회전일 경우 24시간 휴식 |
시작 판정은 진행 상황 화면에 표시되는 데이터도 매시간 작성합니다. 사이클이 시작되지 않는 시간에도, 화면은 최신 상태를 유지합니다.
2. 사이클 시작
run-cycle.sh
은 잠금을 걸고, 루프 전용 작업 트리를 main에서 브랜치로 분기하며, 프롬프트를 구성하여 Claude Code를 시작합니다. 시작 명령어는 다음과 같은 형태입니다. 허용하는 도구 목록은 발췌입니다.
claude -p "$prompt" \
--model opus \
--permission-mode acceptEdits \
...
여기서 결정되는 것은 3가지입니다.
- 비용 상한.
--max-budget-usd에 설정 파일의 상한 이하 값을 전달합니다. - 사용 가능한 명령어. 연구용 CLI, 테스트, git의 기본 조작만 서브커맨드 단위로 허용합니다.
- 사용할 수 없는 명령어. 머지(merge), 푸시(push), 배포(deploy), 비밀 정보 조작은 직접적으로 허용하지 않습니다. 푸시와 머지는 전용 스크립트 2개를 거쳤을 때만 실행할 수 있습니다.
--output-format stream-json의 출력은, 도구 호출 1회당 1줄로 정리하여 로그에 스트리밍합니다. tail -f로 사이클이 현재 무엇을 하고 있는지 추적할 수 있습니다.
3. 사이클 내용
프롬프트는 사이클 절차를 4단계로 지시하고 있습니다.
- 상황 파악. 직전 작업 로그와, 결정표가 반환한 행동을 읽습니다.
- 자금 및 가동 중인 전략 확인. 실제 데이터베이스를 조회하여 주문 오류, 정지 조건 발화, 정기 처리 실패가 없는지 확인합니다. 행동보다 반드시 먼저 수행됩니다.
- 행동 1개를 실행합니다. 행동별 절차는 프롬프트에 작성되어 있습니다.
- 변경 사항이 있으면, 검사를 거쳐 커밋(commit)하고, 푸시(push)하며, PR을 만들고, 머지용 스크립트를 호출합니다. 마지막으로 작업 로그를 작성합니다.
1 사이클은 5분에서 8분 정도 소요되며, 비용은 2달러에서 3달러입니다.
4. 머지와 배포
automerge.sh는 다음 순서로 확인한 후 머지합니다.
- PR이 보호 대상 파일에 손대지 않았는지. 만약 손댔다면 사람에게 넘깁니다.
- 하루의 머지 횟수가 상한 이내인지.
- main을 가져온 상태에서, 테스트(test), 타입 검사(type check), lint가 모두 통과하는지.
- 실제 정기 처리가 돌아가는 몇 분을 피했는지.
- 머지 후에, 새로운 버전의 worker가 배포되었는지.
배포는 Cloudflare Workers Builds의 Git 연동에 맡기고 있습니다. main으로 머지된 후 약 30초 만에, 새 버전으로 교체됩니다.
거부된 PR에는, 이유를 댓글로 달고, 사람 대기(human wait) 레이블을 붙이며, Slack에 통지합니다.
다음 행동 결정 방법
다음 행동은 LLM이 선택하지 않습니다. 커밋된 기록만을 입력으로 사용하는 함수가 행동 1개를 반환합니다.
$ uv run quant pipeline next --json
{"action": "IDEATE", "target": null,
"reason": "0 open idea(s) < min_open_ideas 3, 2 created in 7 days",
...
행동은 10가지가 있으며, 위에서부터 순서대로 조건을 확인하여, 가장 먼저 일치하는 것을 반환합니다.
| 행동 | 조건 | 수행할 것 | 결과물 |
|---|
| RETIRE | 실자금 전략이 정지 조건에 도달했거나 기한이 지났을 때 | 전략을 제외하고 경위를 기록한다 | 전략 상태 변경, 파악된 내용 기록 |
| ... | | | |
1 사이클을 1 행동으로 하는 이유는 비용 때문이 아닙니다. 머지할 때마다 worker가 교체되기 때문에, 두 가지 변경 사항을 동시에 내면, 고장 났을 때 어느 것이 원인인지 알 수 없기 때문입니다.
GATE의 계산을 보충합니다. 같은 데이터에 여러 전략을 테스트하면 우연히 성적이 좋은 것이 나오기 쉽습니다. 따라서, 시도한 본수와 기간의 길이로부터 '우연으로는 설명하기 어려운 성적'의 하한선을 계산하고, 아이디어에 기대하는 성적(예상치)이 이를 상회할 때만 다음 단계로 진행합니다. 예상치의 상한에는 같은 규칙을 다른 기간에서 측정한 실측값을 사용합니다.
실행하지 못한 행동은 그 이유를 대상 기록에 남깁니다. 기록이 바뀌기 때문에, 다음 사이클에서는 결정표가 다른 행동을 반환합니다.
기록의 형태
결정표의 입력은 리포지토리 내 파일로만 이루어집니다.
research/
ideas/ 아이디어와 심사 결과
hypotheses/ 가설과 반증 조건
...
기록의 일관성은 테스트가 실제 파일을 읽고 검사합니다. 예를 들어, 반증 조건이 없는 가설, 실행되지 않은 실험을 인용한 기록, 상한을 초과하는 기간 사용 등은 검사에서 실패합니다.
외부 툴에도 진행 상황을 반영하지만, 동기화는 단방향입니다. 판단의 근거로 삼는 것은 리포지토리 내 기록뿐입니다. 검사의 손길이 닿지 않는 곳에 있는 숫자는 검증된 것으로 취급하지 않습니다.
루프가 맡는 범위
범위는 문장이 아니라 설정과 코드로 결정됩니다.
상한
상한은 policy.yaml에 수치로 정의되어 있습니다. 다음은 실제 값입니다.
caps:
max_concurrent_arms: 3 # 동시에 실자금으로 구동하는 전략의 수
max_new_arms_per_30d: 4 # 30일 동안 실자금에 올릴 수 있는 수
...
키가 누락된 경우 0으로 읽습니다. 무제한이 되는 것이 아니라, 해당 단계에서 멈춥니다.
보호 대상
루프의 PR이 다음 파일에 접근하면, 머지용 스크립트는 사람에게 전달됩니다.
policy.yaml
와 검증 기간 목록 - 루프 자체의 스크립트 및 경계를 검사하는 테스트
- 본(本) 작업 worker의 설정, 주문을 내는 부분, 인증
이 때문에, 루프는 자신의 상한과 채점 기준을 수정할 수 없습니다. 루프의 스크립트를 고치는 것은 루프 외부의 다른 세션과 사람입니다.
검증 기간 등록
검증 기간 목록만은 예외로, 루프가 1건 추가할 수 있습니다. 추가 PR은 머지용 스크립트가 기계적으로 검사합니다. 그 조건은 다음과 같습니다: 해당 기간의 데이터가 등록 시점에 아직 존재하지 않을 것, 해당 기간에 해당하는 실험이 같은 PR에서 추가된 단 하나일 것, 심사의 계산을 다시 수행한 결과가 기록과 일치할 것. 이를 통해 과거 데이터를 보고 '미확인 데이터'라고 선언하는 경로를 차단하고 있습니다.
손실의 정지 조건은 어디에 있는가
루프가 만든 전략은 페이퍼 트레이드용과 실자금용 2개의 worker로 구동됩니다. 페이퍼 트레이드의 손익은 성적 증거로는 취급하지 않습니다. 규칙이 본 환경에서 의도대로 작동하는지 확인하기 위한 것입니다.
실자금 손실의 정지 조건은 루프가 아니라 실자금용 worker가 가지고 있습니다. 루프가 멈추거나 LLM이 무엇을 쓰더라도, 정지 조건은 작동합니다.
- 매분 정시 처리가 계좌 값을 읽어, 정지 임계치를 넘었는지 판정합니다. 계좌 값은 입출금을 제외하고 측정됩니다.
- 정지 확정을 위해서는 체결 손익, 수수료, 자금 조달료, 미실현 손익에서 별도로 계산한 값과의 일치 여부가 조건입니다. 일치하지 않으면 포지션을 닫지 않고 보류하며 Slack에 통지합니다.
- 동일한 대조표를 아무 일도 없을 때에도 1시간마다 기록합니다.
운용을 통해 알게 된 것
구동하기 시작하면서, 루프는 여러 번 예상치 못한 움직임을 보였습니다. 그때마다 원인을 재현하고, 실패하는 테스트를 작성하여 수정하며, 구동 판정부터 머지까지 실제로 한 번 거쳐 확인했습니다. 주요 내용은 다음과 같습니다.
| 발생한 일 | 현재의 대처 |
|---|
| 아무것도 하지 않는 PR을 하루에 34건 머지함 | 구동 판정 자료에서 자신의 로그를 제외하고, 결정표가 행동을 반환할 때만 구동되도록 함 |
| ... | |
또 하나의 검사 변경 수정은 작성한 사람과는 다른 에이전트에게 읽게 하고 있습니다. 요청 내용은 '이 변경으로 선언되지 않은 시도를 사용할 수 있는 경로를 찾아라'입니다. 어떤 수정 사항은 테스트가 모두 통과했음에도 불구하고, 이 검토자(읽는 사람)에 의해 병합 불가로 판정되었습니다. 이는 숫자 세기 오류를 고치는 과정에서 두 가지 경우의 검사가 느슨해져 있었기 때문입니다. 재현 가능한 테스트와 함께 돌아왔기에, 그것을 받아들여 수정했습니다.
알려진 한계점
- 병합용 스크립트가 실행하는 검사 코드는 아직 보호 대상으로 지정하지 않았습니다. 루프의 PR이 검사 코드를 덮어쓰고, 그 다음 PR을 그 검사를 통과시키는 것이 논리적으로는 가능합니다.
- 손실의 정지 조건이 확정되어 전체 포지션을 청산하는 동작은 실제 운영 환경에서 한 번도 발생하지 않았습니다. 확인한 것은 테스트와 평상시 데이터 대조까지입니다.
- IDEATE는 현재 중간에 멈춥니다. 후보를 선택하기 위한 집계를 LLM이 그 자리에서 작성하려고 시도하다가, 제한된 허용 설정에 의해 거부되기 때문입니다. 집계 수단을 커맨드로 준비할 때까지 진행되지 않습니다.
- 사전 등록한 검증은 기간이 끝나는 2027년 3월부터 10월까지 채점할 수 없습니다. 그전까지 이 시스템이 '좋은 전략을 찾았다'고 말할 수는 없습니다.
요약
- 루프는 1시간에 한 번 행동을 하나 실행하고, PR을 하나 생성합니다. 나오는 것은 연구 기록, 작업 로그, 페이퍼 트레이드, 실자금의 전략입니다.
- 시작 여부는 셸 스크립트가, 다음 행동은 코드가, 병합할지 여부는 검사 스크립트가 결정합니다. Claude Code가 담당하는 것은 정해진 행동의 실행입니다.
- 위임 범위는 상한 수치, 허용하는 커맨드 목록, 보호 대상 파일로 결정됩니다.
- 손실의 정지 조건은 루프 외부의 본(本) 운영 워커가 가지고 있습니다.
논의