수수료 한 줄이 판단을 바꾸는가, 같은 왕복을 죽이는가 — 요율을 두 방향에서 재 본 기록

crypto-trading-arena 는 요율 문장 하나로 체결이 12건에서 43건으로 늘었습니다. 같은 요율을 우리 쪽에서는 체결을 고정한 채 재 봤더니 총손익이 같은데 순손익 부호가 뒤집혔습니다.

수수료 한 줄이 판단을 바꾸는가, 같은 왕복을 죽이는가 — 요율을 두 방향에서 재 본 기록

이 글의 완료 조건: 독자가 「요율이 판단을 바꾸는가」와 「요율이 고정된 왕복을 죽이는가」가 서로 다른 질문이라는 것을 알고, 자기 도구에 맞는 쪽을 골라 잴 수 있으면 성공입니다.

하는 일 같은 종목·같은 방향·같은 청산 계획을 네 거래소에 같은 호가로 동시에 걸어, 요율만으로 순손익 부호가 갈리는지 봅니다
맞는 사람 수수료 가정을 바꿔 가며 백테스트를 돌리는데, 그 차이가 실시세 왕복에서도 나는지 재 본 적이 없는 사람
안 맞는 사람 요율 하나만 깨끗이 격리하고 싶은 사람. 우리 쪽에서는 그게 안 됩니다 — 5절이 그 이야기입니다
이 글의 결과물 그대로 도는 레시피 한 벌, 그리고 bp 로 정규화한 네 거래소 대조표
준비물·비용 코드 0원 · 모델 비용 0원 · weltiq pro 계정 (시험 주문 월 1,000회)
시간·난도 20분 · 중 (curl·JSON·bash)
weltiq 와의 관계 이 글이 전부 그 이야기입니다 — 절차 글이라 5절이 따로 없습니다
검증 환경·날짜 2026-09-28 (KST) · 격리 서버 · 합성 호가 (5절에 그 한계를 그대로 적었습니다)
제휴 고지 필자는 weltiq 로부터 이 연재의 원고료를 받습니다. 저쪽이 재는 것을 우리가 못 잰다는 사실도 그대로 적었습니다

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


1. 이 도구가 남기던 빈칸

23편 crypto-trading-arena 는 LLM 넷을 같은 봉 앞에 앉히는 대회장입니다. 다른 다중 에이전트 실험과 갈리는 자리가 하나 있는데, 요율을 판단 입력으로 준다는 것입니다. 매 틱 에이전트에게 가는 문장 맨 앞에 이 줄이 붙습니다.

A taker fee of 60 bps (0.60%) is charged on every fill (both buys and sells).
Factor this into your sizing and P&L targets — a round-trip costs 120 bps.

config.json 의 taker_bps 를 0 으로 바꾸면 같은 자리가 Trading is fee-free in this deployment. 로 바뀝니다. 다른 조건을 전부 고정하고 이 한 줄만 바꾼 결과가 이렇습니다.

60bp 0bp
실시간 체결 12건 · $169,332 · 수수료 $1,015.99 43건 · $1,439,744 · 수수료 $0
오프라인 30회 재생 체결 15건 · $156,392 · traded 9/30 (30%) 21건 · $462,480 · traded 13/30 (43%)

요율 문장 하나로 체결이 12건에서 43건이 됩니다. 60bp 판에서 한 번도 주문하지 않았던 두 모델이 0bp 판에서는 둘 다 주문했습니다.

요율 60bp — 네 에이전트 중 둘만 주문했습니다

위쪽 상자 넷의 Trades 와 Fees paid 를 보세요. 두 에이전트는 숫자가 0 입니다 — 같은 봉을 받고도 아예 안 쳤습니다. 오른쪽 체결 이력 표가 그만큼 비어 있습니다.

요율 0bp — 같은 화면, 넷 다 주문했습니다

같은 자리를 보세요. Fees paid 가 전부 $0.00 이고 빈 상자가 없습니다. 앞 화면과 이 화면 사이에서 바뀐 것은 프롬프트 한 줄입니다. 이 두 장이 23편의 대표 그림입니다.

그래서 빈칸이 어디인가

이 도구가 재는 것은 「이 모델이 수수료를 보고 있기는 한가」입니다. 좋은 질문이고, 모델 고르기에는 이쪽이 더 쓸모 있습니다.

그런데 그 다음 질문에 답을 안 합니다. 모델이 요율을 보고 손을 줄였다고 칩시다. 그러면 그 모델이 실제로 낸 한 왕복은 그 요율에서 살아남나요? 저쪽 지갑은 자기 페이퍼 지갑이고, 로트·최소 주문금액·슬리피지 모형이 거래소별로 다른 격자를 통과하지 않습니다. 그리고 요율을 바꾸면 판단 자체가 바뀌어서 같은 왕복을 두 요율에 놓고 볼 수가 없습니다.

한 가지 더 있습니다. 23편에서 실측한 것인데, 저 60bp 기본값은 Coinbase 기본 티어이고 이 도구가 봉을 받은 Binance 현물은 10bp 입니다. 기본값 그대로 돌리면 실제의 6배를 뭅니다.


2. 왜 같은 시각에 함께 내야 하나

시험 주문의 첫 용도는 2편에 썼습니다 — 도구가 낸 판단 하나를 끝까지 재는 것. 두 번째 용도가 이 글입니다.

같은 시각·같은 호가에 한 칸만 바꿔 내면, 차이가 그 칸에서만 나옵니다. 시차를 두고 두 번 돌리면 호가가 달라져서, 갈린 결과가 요율 때문인지 시장 때문인지 영원히 못 가립니다. 그리고 잔고를 안 건드리므로 비교군을 몇 개든 낼 수 있습니다.

먼저 읽어야 할 것 — 우리가 재는 질문은 저쪽과 다릅니다

우리에게도 수수료 비교 기능이 있습니다. 그런데 그 기능은 체결을 고정하고 요율만 다시 매깁니다.

어디 무엇
weltiq/capabilities.py:73 "execution_resimulation": False
weltiq/api/server.py:3085 "basis": "same_fills_fixed_time_qty_price_refee"

이건 결함이 아니라 계약입니다. 다만 저쪽과 구조적으로 다른 질문을 답합니다.

재는 것 무엇이 변수인가
arena 요율이 판단을 바꾸는가 체결이 변수 — 모델이 더/덜 칩니다
우리 수수료 비교 요율이 비용을 바꾸는가 체결이 상수 — 같은 체결을 요율별로 다시 계산합니다
이 글(시험 주문) 요율이 고정된 한 왕복을 죽이는가 체결이 상수인데, 실시간 호가 위에서 각 거래소의 격자를 실제로 통과합니다

셋째 줄이 이 글의 자리입니다. 사후 재계산과 다른 점은 딱 하나 — 로트·최소 주문금액·슬리피지 모형을 진짜로 통과시킨다는 것입니다. 그리고 그 하나가 5절의 한계를 만듭니다.

요율 칸은 아예 없습니다

시험 주문 본문에 수수료 칸이 없습니다. 요청 모델 주석이 그렇게 적혀 있습니다 — 수량·종목·수수료·실거래 target 은 받지 않습니다.

그래서 요율은 거래소 프로필로만 들어옵니다. 한 칸을 venue 로 잡을 수밖에 없었습니다.

venue taker 틱 로트 최소 주문금액
binance-spot 0.001 (10bp) 0.01 0.00001 5
binance-usdm 0.0005 (5bp) 0.1 0.001 50
bybit-linear 0.00055 (5.5bp) 0.1 0.001 5
bybit-spot 0.001 (10bp) 0.1 0.000001 5

오른쪽 세 칸을 봐 두세요. 요율만 바뀌는 게 아닙니다.


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

전체는 12-trial-order-arena-fee-ab.sh 에 있습니다. 네 칸으로 돕니다.

① 심기 — venue 만 다른 네 팔. client_tracking_id 를 넷 다 같게 두고 metadata.arm 으로 구분합니다.

BODY=$(jq -n --arg v "$VENUE" --arg tp "$TP_PX" --arg a "$ARM" --arg bps "$BPS" \
  '{venue:$v,                                       # ← 이 한 칸만 다릅니다
    symbol:"BTCUSDT", side:"long", type:"market",
    client_tracking_id:"arena-fee-ab-1",            # ← 네 팔 같습니다
    exit_plan:{take_profit:$tp, close_after_seconds:45},
    metadata:{arm:$a, changed_field:"venue(=fee profile)", taker_bps:$bps}}')

목표가는 왕복 수수료 사이에 놓습니다. 왕복 taker 가 현물 20bp · usdm 10bp · bybit-linear 11bp 이므로 그 사이에 놓아야 「같은 청산가에서 부호가 갈리는지」가 보입니다. 이 글은 진입 호가 +15bp 인 85611.3 을 썼습니다 — 이 값은 arena 기록에 없고 제가 고른 값입니다.

틱이 거래소마다 다릅니다. binance-spot 만 0.01 이고 나머지는 0.1 입니다. 0.1 격자에 맞는 값을 써야 네 팔이 전부 통과합니다. 0.01 에만 맞는 값을 0.1 짜리 거래소에 보내면 이렇습니다.

422 invalid_take_profit "85611.34 is not a multiple of tick 0.1"

서버가 조용히 반올림해 주지 않습니다. 조건 가격도 실제 주문과 같은 격자에 있어야 하고, 사용자가 준 가격을 서버가 몰래 바꾸면 그게 더 위험합니다.

의도는 도구가 갖습니다. 어느 요율을 왜 재 보려 했는지는 서버로 안 보냅니다. 서버가 갖는 것은 집행 기록이고, 둘을 잇는 것은 추적 ID 하나입니다.

client_order_id,tracking,arm,venue,taker_bps,arena_fills
arena-fee-ab-1-binance-spot,arena-fee-ab-1,base,binance-spot,10,12
arena-fee-ab-1-binance-usdm,arena-fee-ab-1,variant,binance-usdm,5,-

② 회수 — ?tracking=. 추적 ID 정확 일치 필터입니다. 내 계정 안에서만 돕니다.

curl -sS "$BASE/v1/forward-tests?tracking=$RUN_ID&limit=200"

③ 전체 CSV — 한 번에. 계정 전체 주문 엔드포인트가 같은 필터를 받습니다.

curl -sS "$BASE/v1/forward-tests/orders?tracking=$RUN_ID&format=csv"

종전에는 회수가 「목록 1회 + 시험마다 내보내기 1회」였습니다. 열은 시험 하나 내보내기와 같아서 두 CSV 를 이어붙여도 안 깨집니다.

그리고 자동 청산 행이 진입의 추적 ID 를 물려받습니다. 실현 손익은 청산 행에 붙기 때문에, 안 물려받으면 ?tracking= 으로 회수했을 때 진입만 오고 손익이 빠집니다.

④ 조인 — awk 를 쓰지 마세요. CSV 의 metadata·exit_plan·tags 칸에 인용된 쉼표가 들어 있습니다. -F, 로 자르면 열이 밀리는데, 에러 없이 조용히 밀려서 더 나쁩니다. 인라인 python3 의 csv 모듈로 조인하고, 거기서 bp 로 정규화합니다 — 이유는 5절입니다.

metadata 에 float 를 넣지 마세요. 결정론 때문에 막혀 있습니다. {"taker_bps": 10} 처럼 숫자를 그대로 넣으면 접수 전에 걸립니다.

422 invalid_metadata "metadata 에 float 를 쓸 수 없습니다 (결정론) — 숫자는 문자열로 보내세요 예: {"p_up": "0.55"}"

float_paths 에 어느 칸인지 적혀 옵니다. {"taker_bps": "10"} 으로 보내면 통과합니다.


4. 돌려 본 결과

환경 고지. 아래는 격리 임시 서버 · 합성 호가에서 나온 값입니다. 운영이 아니고, 여기 적힌 손익은 시장 사실이 아닙니다 — 제가 넣은 고정 호가 위의 산술입니다. 여기서 재는 것은 대조 설계가 서는가 하나입니다.

네 거래소 전부에 같은 호가 85483.0 / 85483.1 을 걸고 13:29:39.593 ~ .617 에 던졌습니다. 24밀리초 안입니다.

venue taker 수량 명목 진입 체결가 청산가 수수료 왕복 수수료(bp) 총손익(bp) 순손익 순손익(bp)
binance-spot 10bp 0.00008 6.8393 85491.698305 85602.688865 0.01368756 20.013 12.983 −0.00480832 −7.030
binance-usdm 5bp 0.001 85.4917 85491.698305 85602.688865 0.08554719 10.006 12.983 +0.02544337 +2.976
bybit-linear 5.5bp 0.001 85.4917 85491.698305 85602.688865 0.09410191 11.007 12.983 +0.01688865 +1.975
bybit-spot 10bp 0.000074 6.3264 85491.698305 85602.688865 0.01266099 20.013 12.983 −0.00444769 −7.030

갈렸습니다. 같은 체결·같은 청산가에서 부호가 뒤집혔습니다

네 팔 다 2.9초 만에 목표가 85611.3 이 발동해 닫혔습니다. 총손익이 네 팔에서 정확히 같습니다(12.983bp). 체결가가 같으니 당연합니다. 그런데 순손익의 부호가 요율만으로 갈렸습니다 — 10bp 거래소 둘은 −7.03bp, 5bp 는 +2.98bp, 5.5bp 는 +1.98bp.

이게 백테스트에서 수수료 가정을 잘못 잡으면 어떻게 되는지를 한 줄로 보여 줍니다. 같은 왕복이 이익도 되고 손실도 됩니다. 23편 기본값이 실제의 6배였다는 것과 같은 이야기이고, 방향만 반대입니다.

우리 쪽 상세 — 한 대조의 두 팔이 같은 진입 체결가로 들어갔습니다

카드 두 장의 진입 칸을 세로로 훑어보세요. 0.00008 @ 84,811.4801 로 소수점 끝까지 같습니다. 제목 줄의 메모만 다르고 추적 ID 와 ab-compare 태그는 같습니다. 요율 대조의 화면도 이 모양이고, 갈리는 것은 비용 칸 하나입니다.

같은 호가였다는 증거

세 칸이 맞아떨어졌습니다.

칸 실측
qty_basis.reference_px 네 팔 전부 85483.05
체결의 expected_px 네 팔 전부 85483.1
체결의 published_at 13:29:40.456 ~ .457 — 4밀리초 안

한 번 공개된 호가 하나가 네 건을 전부 채웠다는 뜻입니다.

다만 quote_source 는 binance / bybit 로 거래소별로 다릅니다. 이 칸은 「같은 스냅샷」이 아니라 「자기 거래소 피드」를 말합니다 — 여러 거래소가 같은 호가를 봤다는 것을 이 칸으로는 못 씁니다.

내보내기 — 자동 청산 줄에도 추적 ID 가 붙어 있습니다

주문 표의 두 줄을 보세요. 위는 제 진입(shot-31), 아래는 서버가 낸 자동 청산(exit-plan:…)인데 둘 다 추적 ID: tool-a-run-42 를 달고 있습니다. 순손익 −0.01524498 이 붙는 줄이 저 청산 줄입니다 — 저 칸이 비어 있으면 회수해도 손익이 안 따라옵니다. 왼쪽 CSV 내보내기 가 레시피 ③ 의 화면판입니다.

한도는 예상대로

4팔 = 4회. 계좌 원장 스냅샷은 시작 전과 끝난 뒤가 완전히 동일했습니다 — 8개 계좌·거래 표 전부 한 행도 안 움직였습니다. pro 월 1,000회면 4팔짜리 대조는 월 250회 돌릴 수 있습니다.


5. 이 조합의 한계

① 「요율만」 격리는 우리 쪽에서 불가능합니다. 이 글에서 제일 중요한 줄입니다.

venue 를 바꾸면 로트와 최소 주문금액도 같이 바뀌어 서버가 정하는 수량이 달라집니다. 위 표를 다시 보세요 — 0.00008 / 0.001 / 0.001 / 0.000074, 명목으로 6.83 ~ 85.49 USDT, 13배 차이입니다.

bp 로 정규화하면 비교가 서지만 절대 손익으로는 안 섭니다. 그래서 레시피 ④가 bp 를 함께 냅니다. 시험 주문에 요율 칸이 없는 한, 제가 바꾼 한 칸은 요율이 아니라 「거래소 프로필 전체」입니다. 이걸 "요율만 바꿨다"고 쓰면 거짓말입니다.

② arena 가 재던 것은 여전히 못 잽니다. 23편의 12→43건은 「요율 문장을 보고 모델이 더/덜 친다」였습니다. 시험 주문은 판단을 제가 고정해서 냅니다 — 재는 것은 「고정된 한 왕복이 요율별로 살아남는가」입니다.

둘은 대체가 아니라 병행입니다. 모델을 고를 때는 저쪽, 고른 뒤 그 판단이 어느 요율까지 버티는지는 이쪽입니다.

③ 우리 수수료 비교와도 다릅니다. execution_resimulation: False 라 그쪽은 체결을 고정하고 요율만 다시 매깁니다. 이 글이 더 한 일은 실시간 호가 위에서 각 거래소의 로트·최소금액·슬리피지 모형을 실제로 통과시킨 것 하나입니다. 그 대가가 ①의 수량 차이입니다 — 공짜가 아닙니다.

④ 호가 스냅샷 식별자가 없습니다. BBO 한 건에 id 가 없어서 「이 네 체결이 같은 한 호가에서 나왔다」를 응답만으로 단정할 수 없습니다. published_at 이 4밀리초 안이라는 것이 현재의 최선이고, 그건 정황이지 식별이 아닙니다.

⑤ 미체결 팔은 자기가 본 호가를 기록에 안 남깁니다. 이번 네 팔은 전부 채워졌지만, 지정가로 대조군을 내면 안 채워지는 팔이 생깁니다. 접수 응답에 bid/ask 가 따로 안 나와서, 그 팔이 어떤 호가를 보고 안 채워졌는지를 응답 어디서도 못 읽습니다. 열려야 할 자리입니다.

⑥ 합성 호가입니다. 위 손익은 시장 사실이 아닙니다. 확인된 것은 배선과 계약이고 「바이낸스가 이만큼 뗀다」가 아닙니다. 요율 숫자 자체(10bp·5bp·5.5bp)는 거래소 프로필의 값입니다.

⑦ 회수 경로에 정정이 하나 있습니다. 이 실측을 돌릴 때는 GET /v1/forward-tests?q=<추적 ID> 가 0건을 돌려줬고(그때의 q 는 client_tracking_id 를 안 봤습니다), 실제로 실험을 되찾은 것은 태그였습니다. ?tracking= 정확 일치 필터는 이 실행 직후에 열렸습니다. 3절 레시피는 그 새 계약을 씁니다만 저는 그 코드를 돌리지 않았습니다 — 처음 돌리실 때 몇 건이 오는지 직접 확인하세요.


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

쓸 때

쓰지 말 때

그리고 이 대조로 「어느 거래소가 낫다」를 말하지 마세요. 위 표가 말하는 것은 같은 왕복이 요율에 따라 이익도 되고 손실도 된다는 것 하나입니다. 어느 거래소를 쓸지는 요율 말고도 붙는 것이 많고, 이 실험은 그걸 안 잽니다.


이 글의 증거: 실측 로그 · 원자료 · 레시피. 위 숫자는 전부 그 기록에서 가져왔고, 5절의 한계도 같은 기록에 있습니다.