감정을 거세한 24시간 집행자: n8n 무인 자동매매 시스템 구축기

감정을 거세한 24시간 집행자: n8n 무인 자동매매 시스템 구축기

수식은 완벽했다. 문제는 버튼을 누르는 손가락이었다

지난 기록에서 나는 1% 룰과 포지션 사이징으로 “파산할 수 없는 몸”을 얻었다고 썼다. 백테스트의 자본 곡선은 부드럽게 우상향했고, MDD 8.08%라는 숫자는 내게 보험증서처럼 느껴졌다.

그런데 실전에 들어가자 곧바로 균열이 생겼다.

새벽 3시에 뜬 알파 트리거 시그널을 자고 있느라 놓쳤다. 다음 날 아침 차트를 켜보니 내가 잤던 여섯 시간 동안 추세는 이미 떠나 있었다. 며칠 뒤에는 정반대 실수를 했다. 손절 라인에 닿았는데 “조금만 더 보자”며 청산 버튼을 누르지 않았고, 수식이 정해 준 -1%가 -2.4%가 되어 있었다.

수식은 감정이 없다. 하지만 수식을 집행하는 건 나였고, 나는 졸리고, 무섭고, 욕심이 났다. 알고리즘의 마지막 버그는 코드가 아니라 사람이었다.

결론은 하나였다. 트레이딩에서 나를 완전히 지운다. 시그널을 보는 것도, 비중을 계산하는 것도, 주문을 넣는 것도, 전부 기계가 한다.

설계도: 세 개의 부품으로 쪼갠 무인 시스템

거창한 트레이딩 봇 프레임워크는 처음부터 배제했다. 이미 검증된 로직을 다시 코드로 옮기는 순간, 백테스트와 실전이 다른 계산을 하게 될 위험이 생긴다. 그래서 역할을 셋으로 쪼갰다.

무인 자동매매 시스템 설계도: 트레이딩뷰 알림(두뇌) → n8n 자가 호스팅(신경계) → 거래소 주문 API(손)
세 개의 부품. 사람은 이 흐름 어디에도 없다.

1. 두뇌 — 트레이딩뷰 알림
진입(모멘텀), 필터(EMA 200), 청산(ATR)은 이미 트레이딩뷰 전략 스크립트로 검증해 둔 상태였다. 이 스크립트가 시그널을 낼 때 웹훅으로 JSON 한 줄을 쏘게만 만들었다. 두뇌는 그대로 두고, 손과 발만 새로 붙이는 셈이다.

{
  "signal": "LONG_ENTRY",
  "symbol": "BTC",
  "price": 84120.5,
  "stop": 81600.0,
  "atr": 1260.4,
  "bar_time": "2026-03-11T03:00:00Z"
}

2. 신경계 — n8n
집에 있는 n100 미니PC에 도커로 n8n을 올렸다. 유료 클라우드 대신 자가 호스팅을 택한 이유는 단순하다. 24시간 켜 둘 기계가 이미 있었고, 웹훅 하나 받자고 월 구독을 늘리고 싶지 않았다.

3. 손 — 거래소 주문 API
n8n의 HTTP Request 노드가 거래소 주문 API를 직접 호출한다. 어떤 거래소든 구조는 같다. 키 서명, 시장가/지정가 주문, 잔고 조회. 이 글에서 거래소 이름은 중요하지 않다. 중요한 건 사람이 그 사이 어디에도 없다는 사실이다.

워크플로우: 시그널 한 줄이 주문이 되기까지

n8n 캔버스에 올린 노드는 결국 이 순서로 정리됐다.

n8n 워크플로우 7단계: Webhook → 중복 검사 → 잔고 조회 → 포지션 사이징 → 주문 전송 → Telegram → 기록
n8n 워크플로우 전체 흐름과 LONG_EXIT 분기
  1. Webhook — 트레이딩뷰에서 JSON을 받는다. 비밀 토큰이 없는 요청은 여기서 즉시 버린다.
  2. IF: 중복 검사 — 같은 bar_timesignal이 이미 처리됐는지 확인한다. (이 노드가 왜 필요한지는 아래에서 뼈아프게 설명한다.)
  3. HTTP: 잔고 조회 — 전체 시드를 실시간으로 가져온다. 엑셀에 손으로 적어 두던 숫자를 기계가 매번 새로 읽는다.
  4. Code: 포지션 사이징 — 지난 기록의 공식을 그대로 옮겼다.
const seed = $json.balance;             // 전체 시드
const risk = seed * 0.01;               // 1회 리스크 1%
const stopPct = Math.abs($json.price - $json.stop) / $json.price;
const size = Math.min(risk / stopPct, seed);   // 시드 초과 방지
return [{ json: { ...$json, size: Math.floor(size) } }];
1% 룰 포지션 사이징 차트: 손절 거리에 따른 투입 비중
손절이 멀수록 적게, 가까울수록 많이. 어떤 경우든 손실은 정확히 -1%.
  1. HTTP: 주문 전송 — 계산된 금액으로 진입, 동시에 손절 주문을 함께 건다. 진입만 되고 손절이 안 걸리는 상태는 이 시스템에서 가장 위험한 상태다.
  2. Telegram — 결과를 내 폰으로 보낸다. 나는 이제 주문을 넣는 사람이 아니라, 주문이 들어갔다는 알림을 받는 사람이다.
  3. Sheets/DB 기록 — 모든 시그널과 주문 응답을 한 줄씩 남긴다. 나중에 백테스트와 실전의 괴리를 추적할 유일한 증거다.

청산 시그널(LONG_EXIT)은 별도 분기로 흐른다. 보유 포지션을 조회하고, 있으면 전량 청산, 없으면 아무것도 하지 않고 기록만 남긴다.

처절했던 며칠: 기계는 사람과 다른 방식으로 틀린다

여기까지 읽으면 하루면 끝날 작업처럼 보인다. 실제로는 며칠을 잡아먹었고, 대부분은 “기계가 이렇게도 틀릴 수 있구나”를 배우는 시간이었다.

같은 시그널이 두 번 왔다.
트레이딩뷰 알림이 봉 마감 전후로 두 번 발사되는 경우가 있었다. 사람이라면 “아까 샀잖아”라고 넘기지만, 기계는 두 번째 요청도 성실하게 주문으로 바꿨다. 1% 리스크가 2%가 되는 순간이었다. 그래서 2번 노드, 중복 검사가 생겼다. bar_time + signal 조합을 키로 저장하고, 이미 있으면 워크플로우를 조용히 끝낸다.

웹훅이 사라졌다.
집 인터넷이 몇 분 끊긴 사이에 온 시그널은 그냥 증발했다. 트레이딩뷰는 재전송하지 않는다. 완벽한 해결책은 없다. 대신 두 가지를 걸었다. 하나는 n8n이 주기적으로 “마지막 시그널 이후 N시간 경과”를 체크해 텔레그램으로 알려 주는 감시 워크플로우, 다른 하나는 진입과 동시에 거래소 서버에 걸어 두는 손절 주문이다. 최소한 인터넷이 끊겨도 손실은 1%에서 멈춘다.

잔고 조회가 타임아웃되면 주문이 0원이 됐다.
API 응답이 늦어 balance가 비어 있는 상태로 다음 노드가 실행됐다. 에러가 나면 차라리 좋았을 텐데, 사이즈 0으로 “정상 주문”이 나갔다. 이후로 포지션 사이징 노드는 balance가 없거나 0이면 워크플로우를 실패로 던지고, 텔레그램에 빨간 경고를 보낸다. 조용한 성공이 시끄러운 실패보다 훨씬 무섭다는 걸 배웠다.

킬 스위치.
마지막으로, 나 자신을 위한 장치를 하나 넣었다. 텔레그램에서 /stop을 보내면 n8n의 플래그가 꺼지고, 이후 들어오는 모든 시그널은 기록만 남기고 주문하지 않는다. 시스템을 믿지 못해서가 아니다. 거래소 점검, 알림 설정 실수, 내가 예상하지 못한 무언가가 생겼을 때 1초 만에 멈출 수 있어야 비로소 24시간 켜 둘 용기가 생긴다.

기계에 넘기고 처음 맞은 새벽

시스템이 돌아간 첫 주, 새벽 3시 반에 텔레그램이 울렸다.

새벽 3시 31분 텔레그램 알림: LONG_ENTRY 체결, 손절 주문 동시 등록
새벽 3시 31분, 텔레그램이 울렸다.

나는 폰을 보고 다시 잤다. 몇 년 만에 처음으로, 차트를 보지 않고 잔 새벽이었다.

그 주의 매매 결과가 좋았느냐는 이 글의 요점이 아니다. 요점은 내가 들어간 자리마다 정확히 시드의 1%만 걸려 있었고, 손절 라인을 지나친 거래가 단 한 건도 없었다는 것이다. 백테스트에서 증명한 수식이 처음으로 백테스트와 똑같은 방식으로 집행됐다.

다음 기록을 위한 메모: 기계는 잘못이 없다. 그런데 숫자가 다르다

무인 시스템은 문제없이 돌아간다. 그런데 몇 주치 기록을 백테스트와 나란히 놓고 보니, 같은 시그널인데 체결가가 조금씩 다르다. 슬리피지, 수수료, 그리고 봉 마감과 알림 사이의 몇 초.

백테스트의 44번 타점은 이 미세한 차이를 모른다. 다음 기록에서는 실전 로그와 백테스트를 한 줄씩 대조해서, “수식이 약속한 수익”과 “계좌에 찍힌 수익” 사이의 간격을 숫자로 정리해 두어야겠다.


[법적 고지 및 면책 조항]
이 블로그의 모든 글은 개인적인 퀀트 트레이딩 알고리즘 백테스트 및 매매 일지 기록용이다.
특정 종목의 매수/매도를 추천하거나 투자 타이밍을 알려주는 리딩(Reading) 목적이 절대 아님을 명시한다.
블러드하운드 퀀트 랩은 타인의 투자금을 대리 운용하거나 금전을 요구하는 유사수신행위, 1:1 투자 자문 및 유료 리딩방 운영을 일절 하지 않는다.
이곳의 개인적인 데이터를 참고하여 발생한 모든 투자의 최종 판단과 손실에 대한 법적 책임은 버튼을 누른 본인에게 있다.

🚨 [법적 고지 및 면책 조항]
  • 이 블로그의 모든 글은 개인적인 퀀트 트레이딩 알고리즘 백테스트 및 매매 일지 기록용이다.
  • 특정 종목의 매수/매도를 추천하거나 투자 타이밍을 알려주는 리딩(Reading) 목적이 절대 아님을 명시한다.
  • 블러드하운드 퀀트 랩은 타인의 투자금을 대리 운용하거나 금전을 요구하는 유사수신행위, 1:1 투자 자문 및 유료 리딩방 운영을 일절 하지 않는다.
  • 이곳의 개인적인 데이터를 참고하여 발생한 모든 투자의 최종 판단과 손실에 대한 법적 책임은 버튼을 누른 본인에게 있다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다