freqtrade 손절 폭 대조 — 같은 시각·같은 호가에 −0.05%·−0.10%·−0.20% 를 함께 내 봤습니다

freqtrade 는 손절 폭을 바꾸려면 봇을 재시작해야 해서 두 폭을 나란히 못 봅니다. 같은 밀리초에 세 팔을 던져 봤더니 그 한 칸이 「왜·언제 닫혔나」를 갈랐고 손익은 안 갈랐습니다.

freqtrade 손절 폭 대조 — 같은 시각·같은 호가에 −0.05%·−0.10%·−0.20% 를 함께 내 봤습니다

이 글의 완료 조건: 독자가 자기 설정에서 칸 하나만 다른 N개 팔을 같은 밀리초에 던지고, 그 차이가 정말 그 칸에서만 났는지를 기록으로 확인할 수 있으면 성공입니다.

하는 일 설정 한 칸만 바꾼 여러 팔을 같은 시각·같은 호가에 함께 내고, 진입이 한 점으로 고정된 상태에서 그 칸의 효과만 읽습니다
맞는 사람 파라미터를 바꿔 가며 돌려 봤는데 회차가 갈려서 무엇이 원인인지 못 가린 경험이 있는 사람
안 맞는 사람 손익 차이를 보고 싶은 사람. 고정 호가에서는 손익이 안 갈립니다 — 갈리는 것은 발동 조건과 시각입니다
이 글의 결과물 팔마다 exit_trigger·exit_trigger_px·close_reason·소요 시간이 따로 찍힌 대조표, 그리고 그것을 뽑는 레시피
준비물·비용 코드 0원 · 모델 비용 0원 · weltiq pro 계정. 대조군 N개 = 한도 N회
시간·난도 20분 · 중 (curl·JSON)
weltiq 와의 관계 이 글이 전부 그 이야기입니다 — 절차 글이라 5절이 따로 없습니다
검증 환경·날짜 2026-09-28 (KST) · 격리 서버 · 합성 호가 (5절에 한계를 그대로 적었습니다)
제휴 고지 필자는 weltiq 로부터 이 연재의 원고료를 받습니다. 손익이 안 갈렸다는 것까지 그대로 적었습니다

사람 쪽 문장은 실제로 친 명령을 대화로 옮긴 것이고, 출력은 실측이며 숫자는 손대지 않았습니다.


1. 이 도구가 남기던 빈칸

4편에서 freqtrade 를 돌렸습니다. 손절을 실제로 발동시켜 보는 대목이 있었는데, 거기서 걸린 것이 이 글의 출발점입니다.

먼저 −0.2% 로 걸었습니다. 10분 동안 안 걸렸습니다.

값
stoploss -0.002 (−0.2%)
진입 85037.82
손절선 84867.75
결과 10분간 미발동

이유가 재미있습니다 — 화면 손익률에는 왕복 수수료 0.2% 가 이미 들어가 있어서, 손절이 걸리려면 표시 손익이 −0.4% 근처까지 가야 합니다. 10분 동안 제일 나빴던 게 −0.24% 였습니다.

FreqUI 의 열린 포지션 화면입니다. Open rate 85868.2 로 들어가서 Current rate 85884.98 인데 **Current profit % −0.18%** 로 빨간색입니다 — 가격은 올랐는데 손익은 마이너스입니다. 보실 곳은 이 빨간 숫자 하나 — **왕복 수수료가 이미 물려 있다**는 뜻이고, 손절선 계산이 여기서 어긋납니다

그래서 −0.05% 로 고쳐 다시 돌렸습니다. 이번엔 걸렸습니다.

INFO - Override strategy 'stoploss' with value from the configuration: -0.0005
21:23:50  BTC/USDT 진입 @ 85198.18  stop = 85155.59 (-0.05%)
21:25:21  Exit for BTC/USDT detected. Reason: stop_loss

거래 레코드도 그대로 남았습니다: close_rate 85145.81 · min_rate 85145.81 · exit_reason stop_loss · profit_abs -0.26057537 USDT.

같은 봇의 차트 화면입니다. 1분봉 위에 보유 종목과 손익이 같이 떠 있습니다. 앞 그림과 **나란히 놓고 보실 것** — 같은 화면인데 손절 폭이 다르면 여기 찍히는 결말이 달라집니다. 그런데 freqtrade 안에서는 그 둘을 나란히 못 봅니다

그런데 두 폭을 나란히 못 봅니다

여기가 빈칸입니다. 4편에서 이렇게 적었습니다 — 손절폭을 바꾸려면 봇을 재시작해야 합니다. reload_config 는 설정 파일을 다시 읽지만 이미 열린 포지션의 손절선은 그대로입니다. 돌고 있는 포지션의 규칙을 무중단으로 갈아 끼우는 통로가 없습니다.

그래서 두 폭의 기록이 다른 회차에 있습니다. 진입가부터 다릅니다 — 85037.82 와 85198.18. 이 둘을 놓고 "어느 손절이 나은가"를 물으면, 답이 손절 폭에서 나오는지 진입가에서 나오는지 가릴 수가 없습니다.

봇 기동 로그입니다. Dry run is enabled 와 HTTP 서버 주소가 보입니다. 보실 곳은 **기동 로그라는 사실 자체** — 손절 폭을 바꾸려면 매번 이 화면을 다시 봐야 하고, 그 사이 시장은 다른 자리에 가 있습니다


2. 왜 지금까지 못 쟀나

파라미터 하나를 바꿔 비교하려면 나머지가 전부 같아야 합니다. 그런데 실행하는 순간 나머지가 같기 어렵습니다.

백테스트로 하면 되지 않느냐 — 됩니다. 다만 백테스트는 과거 재생이라 실물 비용 모형·틱 격자·최소 주문금액·슬리피지가 그 엔진의 가정대로 돕니다. 4편에서 확인한 "왕복 수수료 0.2% 가 손익률에 이미 들어가 있어서 손절이 안 걸린다" 같은 일은 그 가정이 실제 집행 규칙과 맞물릴 때 드러납니다.

시험 주문은 이 셋을 한꺼번에 없앱니다. 잔고를 안 쓰니 포지션이 안 섞이고, 수량을 서버가 정하니 자본과 무관하고, 같은 밀리초에 던지면 같은 호가를 봅니다.

같은 시각·같은 호가에 한 칸만 바꿔 내면, 차이가 그 칸에서만 납니다.


3. 레시피 — 그대로 도는 것

레시피 한 장을 올려 뒀습니다: 09-trial-order-freqtrade-stoploss-ab.sh.

칸 무엇
⓪ 틱 비율을 절대 레벨로 바꾸고 틱 격자에 맞춥니다. 방향까지 적습니다
① 심기 세 팔을 동시에 던집니다. client_tracking_id 는 셋 다 같고, metadata.arm 이 팔을 가릅니다
② 회수 GET /v1/forward-tests?tracking=<런 ID>
③ 집행 기록 GET /v1/forward-tests/orders?format=csv&tracking=… — 계정 전체를 한 번에
④ 조인 추적 ID + client_order_id 로 도구 로그와 잇고, published_at 이 팔마다 같은지 셉니다

의도는 도구가 갖습니다. 서버로 가는 것은 client_order_id·client_tracking_id·문자열 metadata 뿐이고, 전략 이름이나 stoploss 비율을 왜 그 값으로 골랐는지는 tool_log.csv 에만 씁니다. metadata 에 float 을 넣으면 canonical_json 이 결정론 규칙으로 거부해서 500 이라, 비율도 문자열로 실었습니다.

한 칸만 바꿨다는 것을 어떻게 보이나 — client_tracking_id 를 팔마다 같게 두고 metadata.arm 으로만 가릅니다. 그리고 changed_field 를 적습니다.

{"client_tracking_id": "cf-04-stoploss-width",     // 세 팔 모두 같다
 "exit_plan": {"stop_loss": "85155.59", "close_after_seconds": 45},   // ← 이 한 칸만 다름
 "metadata": {"arm": "variant", "changed_field": "exit_plan.stop_loss"}}

비율은 그대로 안 들어갑니다

freqtrade 가 내는 것은 절대 가격이 아니라 비율입니다: {"stoploss": -0.0005}. 비율 × 기록된 진입가 85198.18 = 85155.590909 인데, 이 값은 틱 0.01 의 배수가 아닙니다.

422 invalid_stop_loss  "85155.590909 is not a multiple of tick 0.01"

조용히 반올림해 주지 않습니다. 부르는 쪽에서 맞춰야 하고, 어느 방향으로 굴렸는지 적어야 합니다 — 롱 손절은 버림이면 손절선이 내려가 더 늦게 걸리고, 올림이면 더 빨리 걸립니다. 그 선택이 결과를 만들 수 있습니다.

부수로 하나 더 나왔습니다. 시한 검증이 가격 검증보다 앞섭니다 — 시한 없이 비틱 레벨을 보내면 invalid_stop_loss 가 아니라 exit_plan_deadline_required 가 먼저 옵니다. 레벨 오류를 보려면 시한을 먼저 채워야 합니다.

세 팔의 출처

팔 stoploss 출처
기록 −0.0005 (−0.05%) reports/a/04-freqtrade/tight-stop.json · RUN-LOG.md:348-366
합성 −0.0010 (−0.10%) 하네스가 고른 값입니다. 두 기록 사이를 채웠습니다
기록 −0.0020 (−0.20%) docs/tutorials/a/04-freqtrade.md:197 (10분간 미발동)

원 기록에서는 두 회차의 진입가가 달랐으므로(85037.82 vs 85198.18), 폭만 가져와 같은 진입가 85198.18 에 붙였습니다.


아래 숫자의 환경: 격리 임시 서버 · 합성 호가 · 운영이 아닙니다 · 손익은 시장 사실이 아닙니다.

4. 돌려 본 결과

세 팔을 13:25:56.663 ~ .678 에 던졌습니다. 15밀리초 안에 전부입니다.

팔 손절선 접수 (KST) 진입 체결가 진입 호가 공개 시각 청산가 exit_trigger exit_trigger_px close_reason 수수료 순손익 소요
04-stop-0005 (기록) 85155.59 13:25:56.663 85206.7048175 13:25:57.507 85137.2904185 stop_loss 85145.81 stop_loss 0.01362752 −0.019180671920 2.9초
04-stop-0010 (합성) 85112.98 13:25:56.671 85206.7048175 13:25:57.507 85137.2904185 — — close_after 0.01362752 −0.019180671920 46.0초
04-stop-0020 (기록 폭) 85027.78 13:25:56.678 85206.7048175 13:25:57.507 85137.2904185 — — close_after 0.01362752 −0.019180671920 46.0초

세 팔의 진입 체결가가 소수점 끝까지 같습니다. 그리고 그 체결이 참조한 호가의 공개 시각(published_at)이 밀리초까지 같습니다 — 한 번 공개된 호가 하나가 세 건을 전부 채웠습니다.

우리 쪽 시험 상세 — 한 칸만 다른 팔들이 나란히 붙은 화면입니다. 보실 곳은 **exit_trigger 와 exit_trigger_px 열** — 손절이 걸린 팔에만 발동 호가 85145.81 이 찍혀 있고, 시한으로 닫힌 팔에는 null 입니다. 없는 것을 0 으로 채우지 않습니다

그 칸이 결과를 갈랐나 — 갈랐습니다. 「왜·언제」 를요

기록된 실제 최저가 85145.81 이 −0.05% 선만 뚫고 −0.10%·−0.20% 선은 못 뚫었습니다. 원 기록이 말하던 그대로입니다. 그 결과 한 팔은 2.9초에 stop_loss 로 닫혔고 두 팔은 46.0초에 close_after 로 닫혔습니다.

그런데 세 팔의 순손익이 같습니다. 소수점 끝까지 −0.019180671920 입니다.

이유는 설계에 있습니다. 합성 호가가 청산 뒤로 움직이지 않아 시한 청산도 같은 가격에 났습니다. 손절이 손익까지 갈라 놓으려면 호가가 그 뒤로 더 움직여야 하는데, 이번 설계는 「한 칸이 조건을 발동시키는가」만 재도록 호가를 고정했습니다.

이걸 실패로 읽으면 안 됩니다. 이 대조가 세운 것은 「왜·언제 닫혔나」가 팔마다 따로 기록된다는 것입니다 — exit_trigger·exit_trigger_px·close_reason·소요 시간 네 칸이 갈렸고, 그 넷이 복기의 재료입니다. 손익만 보는 표에서는 이 정보가 통째로 사라집니다.

내보내기 화면 — 팔마다의 기록이 CSV 로 나오는 자리입니다. 보실 곳은 열 이름 — exit_trigger·exit_trigger_at·exit_trigger_px·close_reason 이 끝에 붙어 있습니다. **이 네 열이 없으면 스프레드시트로 읽는 도구가 「왜 닫혔는지」를 모릅니다**

몇 팔까지 되나, 얼마나 드나

같은 회차에서 두 가지를 더 쟀습니다.

실측
한 호가에 6팔 동시 발주 6/6 전부 201, 실패·거절 0, 전체 0.02초, 접수 시각이 6.5밀리초 안에 분포. 막히는 코드 없음
한도 소모 N팔 = N회. 04편 3팔에 used 가 0 → 3
재전송·거절·청산 0회 (같은 본문 재전송 200 · 다른 본문 409 · 자동 청산은 counted: false)

그래서 예산은 그냥 나눗셈입니다. pro 월 1,000회 기준으로 3팔짜리 대조는 월 333회, 2팔은 500회. 재시도 비용이 0이라 실수해도 손해가 없습니다.


5. 이 조합의 한계

① 표본 하나로는 아무 말도 못 합니다. 위는 한 번의 3팔입니다. −0.05% 가 −0.20% 보다 낫다는 뜻이 전혀 아닙니다 — 이 한 창에서 최저가가 그 선만 뚫었다는 사실 하나입니다. 분포가 필요합니다.

② 반사실은 한쪽만 잽니다. 이 편에서 우리가 재는 것은 "그 손절선을 걸었으면 어떻게 닫혔나"입니다. "그 손절선 때문에 무엇을 피했나"는 여전히 판단입니다 — 일찍 닫혀서 피한 손실은 기록에 안 남습니다.

③ 우리는 어느 설정이 옳다/틀리다를 말하지 않습니다. 재고 기록할 뿐입니다. 위 표는 어느 손절이 나은지 말하지 않습니다.

④ 손익은 시장 사실이 아닙니다. 합성 호가 위의 산술입니다. 여기서 선 것은 손절 폭 대조 설계이고, 바이낸스에서 이렇게 된다는 뜻이 아닙니다.

⑤ 미체결 팔은 자기가 본 호가가 기록에 안 남습니다. 이번 3팔은 전부 체결됐지만, 지정가로 대조하는 다른 회차에서 미체결 팔이 나왔습니다. reference_px 는 접수 시점의 기준가일 뿐이고 published_at 은 체결 뒤에만 생깁니다. 그래서 안 채워진 팔은 "어떤 호가를 보고 안 채워졌나"를 응답 어디에서도 못 읽습니다. 그리고 미체결 팔은 close_after_seconds 시계가 시작조차 안 합니다.

⑥ 「같은 호가였다」는 정황이지 식별이 아닙니다. 호가 스냅샷에 id 가 없습니다. 지금의 최선은 published_at 이 밀리초까지 같다는 것이고, 그건 강한 정황이지만 "같은 한 호가"의 증명은 아닙니다. 우리가 열어야 할 칸입니다.

⑦ 세 팔 중 하나는 도구 기록이 아닙니다. −0.10% 는 하네스가 고른 값입니다. 도구가 그 폭을 낸 적이 없습니다.

⑧ 추적 ID 로 실험을 되찾는 길은 이 실행 직후에 열렸습니다. 이 회차를 돌릴 때는 ?tracking= 교차 조회가 없어서 실제로 실험을 되찾은 것은 태그였습니다(?tag=a2-04-04 → 3건). 그 필터가 열린 것은 그 뒤이고, 필자는 새 코드를 돌리지 않았습니다. 레시피는 열린 쪽을 씁니다 — 위 실측이 그것을 증명한 것은 아닙니다.


6. 언제 쓰고 언제 쓰지 마라

쓰세요.

쓰지 마세요.


이 글의 증거: B축 대조 측정 기록 · 케이스별 요청·응답 원문 · 이관 판정 · 레시피. 원 도구 회차는 4편 freqtrade에 있습니다.