카테고리 없음

터틀트레이딩 파이썬 코드 공개 — 55일 신고가 돌파 자동매매 직접 구현

우베르 2026. 5. 11. 20:45
반응형

 

직장인 자동매매 시리즈 · 2편

터틀트레이딩 파이썬 코드 공개 —
55일 신고가 돌파 자동매매 직접 구현

1편 읽기: 주식 자동매매 실전 후기 — 백테스트 수익률을 믿었다가 배운 것들

지난 글에서 이런 로그를 보여드렸습니다.

OPEN_LONG 028050 · 진입가 52,300원 · 수량 9주 · 손절가 45,091원 · 다음 피라미딩 55,503원

당시에는 "봇이 알아서 계산했다"고 넘어갔는데, 댓글과 메시지로 같은 질문을 여러 번 받았어요. "왜 하필 9주예요?", "45,091이라는 손절가는 어떻게 나온 거예요?" 이 글은 그 질문에 대한 답입니다.

숫자 하나하나에는 공식이 있습니다. 그리고 그 공식은 1970년대 월스트리트에서 만들어진 터틀트레이딩에서 왔어요. 리처드 데니스라는 트레이더가 내기를 하나 했습니다. "트레이더는 타고나는 게 아니라 만들어진다." 그는 신문 광고로 사람을 뽑아 단 2주간 규칙을 가르쳤고, 그들은 수백만 달러를 벌었습니다. 그 규칙이 터틀트레이딩이에요. 저는 그 규칙을 파이썬 코드로 옮겼습니다.

규칙을 종이에 쓸 수 있으면, 코드로도 쓸 수 있다. 코드로 쓸 수 있으면, 감정 없이 실행할 수 있다.

이 글에서는 제가 직접 짠 코드를 기반으로 전략의 작동 원리를 처음부터 끝까지 풀어드립니다. 투자를 전혀 모르는 분도 읽을 수 있도록 개념부터 설명하고, 개발자 분들을 위한 실제 파이썬 코드 스니펫도 함께 담았어요. 읽고 나면 저 숫자들이 왜 저렇게 나왔는지 전부 이해하실 수 있을 거예요.

참고로 이 전략은 S2(55일/20일) 중심으로 운용했습니다. S1(20일)과 무엇이 다른지, 왜 직장인에게 S2가 더 맞는지도 함께 설명할게요.

S1 vs S2 — 두 전략의 차이, 왜 S2만 썼나

개념 설명

터틀트레이딩에는 두 가지 채널이 있습니다. S1은 20일 신고가 돌파 시 진입하고 10일 신저가에서 청산합니다. S2는 55일 신고가 돌파 시 진입하고 20일 신저가에서 청산해요.

S1은 빠릅니다. 20일이면 한 달이 채 안 되니 신호가 자주 납니다. 그만큼 잦은 진입과 손절이 반복돼요. 직장인으로서 하루에도 여러 번 알림이 울리는 건 부담이었습니다. S2는 55일, 약 두 달 반의 최고가를 봅니다. 신호가 드문 대신 추세가 확실히 형성된 뒤에야 움직여요. 하루 한 번 체크해도 되는 구조라 직장인에게 훨씬 적합합니다. 이 때문에 제 코드는 S2 중심으로 운용하고, S1 진입은 비활성화해 뒀어요.

# 코드 스니펫 ① — 진입 신호 계산 df["s2_hi"] = df["high"].rolling(55).max().shift(1) # 전일 기준 55일 최고가 df["s2_long"] = df["close"] > df["s2_hi"] # 오늘 종가가 55일 고점 돌파 # S1은 비활성화 (allow_s1_entries: False) # S2 중심 운용 — 하루 1회 체크로 충분

N(ATR)이 뭔지 모르면 아무것도 모르는 것

핵심 개념
ATR: 캔들 아래 붉은선

터틀트레이딩의 모든 숫자는 N이라는 값 하나에서 나옵니다. 손절가도, 피라미딩 가격도, 매수 수량도 전부 N의 배수예요. N이 뭔지 모르면 나머지 공식이 전부 의미없어집니다.

N은 ATR(Average True Range)의 별명입니다. 한 마디로 "이 종목이 하루에 평균 얼마나 움직이는가"를 나타내는 수치예요. 52,300원짜리 삼성E&A의 ATR이 4,005원이었다는 건, 하루 평균 4,005원씩 오르내린다는 뜻입니다.

# 코드 스니펫 ② — ATR 계산 (지수이동평균, 기간 20일) h, l, pc = df["high"], df["low"], df["close"].shift(1) tr = pd.concat([ h - l, # 당일 고저 차이 (h - pc).abs(), # |고가 - 전일 종가| (갭 반영) (l - pc).abs() # |저가 - 전일 종가| (갭 반영) ], axis=1).max(axis=1) atr = tr.ewm(span=20, adjust=False).mean() # N = 20일 지수평균 진폭

True Range를 쓰는 이유는 갭이 있는 날을 놓치지 않기 위해서예요. 전날 4만 원에 마감했는데 오늘 3만 5천 원으로 열렸다면, 단순 고저 차이만 보면 그 5천 원 갭이 사라집니다. 세 가지 중 최댓값을 쓰면 이런 갭도 빠짐없이 반영돼요. 삼성E&A 진입 당일 ATR이 4,005원, 이 숫자 하나로 아래 모든 계산이 시작됩니다.

수량 계산 — "몇 주 살까"를 공식으로 정하다

핵심 공식

수동 매매에서 가장 주관적인 순간이 언제인지 아세요? 바로 "이거 얼마치 살까"를 결정할 때입니다. 기분 좋으면 많이 사고, 불안하면 조금 사고. 매번 달라집니다. 터틀트레이딩은 이 결정을 공식으로 대체합니다. 봇은 세 가지 상한을 계산하고, 그 중 가장 작은 값을 씁니다.

수량 A
계좌 × 1% ÷ (ATR × 2) ← 원래 터틀 규칙
손절됐을 때 계좌 손실이 딱 1%가 되도록 수량을 역산합니다.
삼성E&A: 10,000,000 × 0.01 ÷ (4,005 × 2) = 12주
수량 B
계좌 × 10% ÷ 진입가
한 종목에 계좌의 10% 이상 투입하지 않도록 막는 상한선.
삼성E&A: 10,000,000 × 0.10 ÷ 52,300 = 19주
수량 C
계좌 × 15% ÷ 진입가
피라미딩 포함 총 포지션의 절대 상한.
삼성E&A: 10,000,000 × 0.15 ÷ 52,300 = 28주
최종
min(A, B, C) = min(12, 19, 28) = 12주
# 코드 스니펫 ③ — 수량 계산 (원래 터틀 규칙: stop_loss_n = 2.0) def calc_qty(price, atr, capital): qa = int((capital * 0.010) / (atr * 2.0)) # 리스크 1% (2N 손절 기준) qb = int((capital * 0.10) / price) # 자본 10% 상한 qc = int((capital * 0.15) / price) # 포지션 15% 절대 상한 return max(min(qa, qb, qc), 0)
삼성E&A 수량 계산 결과 (원래 터틀 규칙 기준)
12주
수량A (리스크 1%)
19주
수량B (자본 10%)
28주
수량C (상한 15%)
min(12, 19, 28) = 12주
원래 터틀 규칙 기준 최종 수량

손절가와 피라미딩 — 원래 규칙과 내 코드의 차이

핵심 공식

수량이 정해지면 나머지 두 숫자도 자동으로 계산됩니다. 여기서 솔직하게 고백할 것이 있어요. 원래 터틀트레이딩 규칙은 손절 2N, 피라미딩 0.5N입니다. 그런데 제 코드에는 손절 1.8N, 피라미딩 0.8N으로 적혀 있어요. 의도적으로 수정한 값입니다.

항목 원래 터틀 규칙 내 코드 차이
손절가 배수 2N 1.8N 0.2N 더 좁게
피라미딩 배수 0.5N 0.8N 0.3N 더 신중하게
삼성E&A 손절가 44,291원 45,091원 (실제 일치) 800원 높음
삼성E&A 피라미딩가 54,302원 55,503원 (실제 일치) 1,201원 높음
왜 수정했나

손절을 1.8N으로 줄인 건 조금 더 빠르게 손절해서 손실을 제한하기 위해서예요. 원래 2N 손절은 허용 폭이 넓어서 한 번 잘못되면 타격이 큽니다. 피라미딩을 0.8N으로 늘린 건 추세가 더 확실히 확인된 뒤에 추가 매수하겠다는 의도예요. 0.5N이면 아직 추세가 채 형성되기 전에 너무 일찍 물량을 늘리는 느낌이 있었거든요. 수치 변경이 맞는지 틀린지는 더 긴 기간 운용해봐야 알 수 있어요. 어디까지나 저의 판단이고, 원래 규칙 그대로 써도 됩니다.

# 코드 스니펫 ④ — 손절가 & 피라미딩 가격 계산 (실제 코드 기준) stop = entry_price - atr * 1.8 # 원래 터틀: atr * 2.0 next_pyr_price = entry_price + atr * 0.8 # 원래 터틀: atr * 0.5 # 삼성E&A 실제 계산 # 손절: 52,300 - (4,005 × 1.8) = 45,091원 ✓ # 피라미딩: 52,300 + (4,005 × 0.8) = 55,503원 ✓
그런데 실제 기록은 왜 9주였을까?

원래 터틀 규칙(2N) 기준 12주인데, 실제 로그에는 9주로 찍혀 있어요. 이유는 진입 순서에 있습니다. 4월 8일 GS건설이 먼저 체결되면서 현금이 약 486,000원 차감됐고, 지정가 주문 슬리피지(호가 2틱)까지 더해지면서 실시간 현금 잔고 기준으로 재계산한 결과 9주가 나왔어요. 백테스트와 실전이 미묘하게 다른 이유 중 하나가 바로 이것입니다.

규칙을 그대로 따르는 것도 전략이고, 이유를 알고 수정하는 것도 전략이다. 모르고 바꾸는 것만 아니면 된다.

봇은 어떻게 삼성E&A를 골랐나 — 100점 만점 종목 선정

종목 선정

ATR로 수량을 계산하고 N 배수로 손절가를 잡는 것까지는 이해했을 거예요. 그런데 한 가지 질문이 남습니다. 봇은 애초에 어떤 종목을 감시 대상으로 올리는 걸까요? 전체 코스피·코스닥에는 수천 개 종목이 있습니다. 그 중에서 삼성E&A와 GS건설이 진입 후보로 올라온 데는 이유가 있어요. 봇은 매일 장 시작 전 오전 8시 50분에 두 단계로 종목을 걸러냅니다.

1단계 — 거래대금 1,000억 이상만 통과

가장 먼저 하는 건 거래대금 필터입니다. 전일 거래대금이 1,000억 원 이상인 종목만 대상으로 삼아요. 코스피 상위 150개, 코스닥 상위 150개, 총 300개 종목을 추립니다.

거래대금 필터가 있는 이유는 간단합니다. 하루 거래대금이 수십억 원 수준인 종목은 내가 100주만 팔려 해도 주가가 크게 흔들릴 수 있어요. 실제 봇이 주문을 넣었을 때 원하는 가격에 체결이 안 되거나, 청산 시 손실이 커질 위험이 있습니다. 유동성이 충분한 종목만 다루는 거예요.

2단계 — 100점 만점 점수화, 70점 이상만 감시 리스트에

거래대금을 통과한 종목들을 100점 만점으로 채점합니다. 70점 이상인 종목만 감시 리스트(최대 50개)에 올라가고, 그 안에서만 신규 진입이 발생해요.

항목 조건 배점
추세 강도
가장 비중 높음
20일 수익률 × 100 (0~30 클램프) 최대 30점
55일 수익률 × 100 (0~20 클램프) 최대 20점
이동평균 정배열 MA20 > MA55 > MA120 완전 정배열 +15점
MA20 > MA55 부분 정배열 +8점
55일 신고가 돌파
핵심 진입 신호
최근 3일 내 55일 고점 돌파 +20점
55일 고점의 1% 이내 근접 +10점
거래대금 일 거래대금 1,000억 이상 +5점
일 거래대금 2,000억 이상 (추가) +5점
ATR 적정성 ATR / 주가 = 1~3% 범위 +5점
과열 페널티 당일 갭 5% 이상 (갭 급등) −10점
3일 수익률 15% 이상 (단기 과열) −10점
합계 상한 100점 캡
# 코드 스니펫 ⑤ — score_ticker 핵심 로직 s += min(max(ret20 * 100, 0), 30) # 추세 강도: 20일 수익률 (최대 30점) s += min(max(ret55 * 100, 0), 20) # 추세 강도: 55일 수익률 (최대 20점) if ma20 > ma55 > ma120: s += 15 # 완전 정배열 elif ma20 > ma55: s += 8 # 부분 정배열 if df["s2_long"].tail(3).any(): s += 20 # 3일 내 55일 신고가 돌파 elif close >= s2_hi * 0.99: s += 10 # 신고가 1% 이내 근접 if tv >= 1_000_000_000_00: s += 5 # 거래대금 1000억+ if tv >= 2_000_000_000_00: s += 5 # 거래대금 2000억+ if 0.01 <= atr_ratio <= 0.03: s += 5 # ATR 적정성 1~3% if gap >= 0.05: s -= 10 # 갭 급등 페널티 if ret3 >= 0.15: s -= 10 # 단기 과열 페널티 return max(min(s, 100.0), 0.0)

삼성E&A는 왜 진입 후보가 됐나

4월 8일 삼성E&A가 진입 후보로 올라온 건 운이 아니었어요. 당시 건설 섹터 전반이 추세를 타고 있었고, 삼성E&A는 그 중에서도 점수가 높게 나온 종목이었습니다.

추세 강도
+38점
20일 수익률 약 18% → +18점
55일 수익률 약 20% → +20점
이평 정배열
+15점
MA20 > MA55 > MA120
완전 정배열 충족
55일 신고가 돌파
+20점
3일 내 55일 최고가 돌파
핵심 진입 신호 발생
거래대금 + ATR
+15점
거래대금 2,000억+ (+10점)
ATR 비율 1~3% 범위 (+5점)
삼성E&A 추정 합계

38 + 15 + 20 + 15 = 88점 → 기준 70점 통과 ✓
과열 페널티 없음 (당일 갭 5% 미만, 3일 수익률 15% 미만)

같은 날 건설주가 여러 개 진입된 이유

4월 8~10일에 삼성E&A, GS건설, 대우건설, DL이앤씨, 휴스틸이 연달아 진입된 건 당시 건설·소재 섹터 전반이 추세를 타고 있었기 때문이에요. 점수화 로직이 섹터를 직접 구분하지 않으니, 같은 섹터 종목 여러 개가 동시에 70점을 넘으면 한꺼번에 진입 후보로 올라옵니다. 이것이 5월 4일 세 종목 동시 손절로 이어진 섹터 집중 리스크의 원인이기도 해요. 다음 편에서 이 문제를 코드로 어떻게 해결했는지 다룰 예정입니다.

봇이 종목을 고른 게 아니다. 점수가 높은 종목이 스스로 올라온 것이다. 그 기준을 내가 설계했을 뿐이고, 그 기준이 맞는지는 계속 검증해야 한다.

반응형