QuantDinger 격자 최적점 둘을 같은 시각에 나란히 걸어 봤습니다 — 그리고 대조를 못 세웠습니다
실측 비용을 넣으면 격자 최적점이 60/180 에서 120/360 으로 밀립니다. 그 둘을 같은 호가에 함께 걸어 보려다 막힌 기록과, 무엇이 있어야 그 대조가 서는지입니다.
QuantDinger 격자 최적점 둘을 같은 시각에 나란히 걸어 봤습니다 — 그리고 대조를 못 세웠습니다이 글의 완료 조건: 독자가 자기 도구의 설정 두 개를 같은 시각·같은 호가에 한 칸만 바꿔 걸어 보고, 그 대조가 「설 수 있는 것인지 아닌지」를 먼저 판정할 수 있으면 성공입니다.
| 하는 일 | 도구가 고른 설정 둘을 같은 시각·같은 호가에 한 칸만 바꿔 함께 발주하고, 차이가 그 칸에서만 나오게 만듭니다 |
| 맞는 사람 | 파라미터를 바꿔 가며 격자를 돌리는데, 그 답이 실시세에서도 갈리는지 재 본 적이 없는 사람 |
| 안 맞는 사람 | 도구가 거래별 진입·청산 레벨을 안 내놓는 경우. 이 글이 바로 그 벽에 부딪힌 기록입니다 |
| 이 글의 결과물 | 그대로 도는 레시피 한 벌, 그리고 대조가 안 선 이유와 무엇이 있어야 서는지 |
| 준비물·비용 | 코드 0원 · 모델 비용 0원 · weltiq pro 계정 (시험 주문 월 1,000회) |
| 시간·난도 | 20분 · 중 (curl·JSON·bash) |
| weltiq 와의 관계 | 이 글이 전부 그 이야기입니다 — 절차 글이라 5절이 따로 없습니다 |
| 검증 환경·날짜 | 2026-09-28 (KST) · 격리 서버 · 합성 호가 (5절에 그 한계를 그대로 적었습니다) |
| 제휴 고지 | 필자는 weltiq 로부터 이 연재의 원고료를 받습니다. 이번 편은 실패한 대조가 본론이고, 그 사실을 성공담으로 바꾸지 않았습니다 |
사람 쪽 문장은 실제로 친 명령을 대화로 옮긴 것이고, 출력은 실측이며 숫자는 손대지 않았습니다.
1. 이 도구가 남기던 빈칸
14편에서 QuantDinger 의 백테스트 격자를 돌리고, 우리 쪽 체결 기록에서 뽑은 실측 비용(수수료 0.0010 · 슬리피지 0.0001)을 그 격자에 되먹였습니다. 그랬더니 답이 바뀌었습니다.
| 격자 | 비용 가정 | 수익률 | 거래 | PF |
|---|---|---|---|---|
| 60 / 180 | 가정 비용 (수수료 0.0005 · 슬리피지 0.0005) | -5.344065708001111 |
68 | 0.5123854981235674 |
| 120 / 360 | 실측 비용 (수수료 0.0010 · 슬리피지 0.0001) | -1.9871885445930992 |
33 | 0.678789397136766 |
비용이 오르면 최적점이 더 느린 쪽으로 밀립니다. 이게 14편에서 제일 값어치 있던 한 줄이었습니다.
그래서 이번에 하려던 일은 간단했습니다. 그 두 최적점을 같은 시각·같은 호가에 함께 걸어서, 실시세에서도 정말 갈리는지 보는 것.
못 했습니다. 두 군데서 막혔습니다
① 격자 행에 거래가 없습니다. 격자 결과(40-qd-grid-cp0.json · 40-qd-grid-cp1.json)의 rows 에 들어 있는 것은 이게 전부입니다.
fast · slow · totalReturn · totalTrades · totalCommission · profitFactor · maxDrawdown · finalEquity
거래별 진입·청산 레벨이 한 줄도 없습니다. 그래서 "두 최적점이 같은 봉에서 어떻게 갈리는가"를 이 기록으로 답할 수 없습니다. 68거래와 33거래가 있었다는 것만 알고, 그 68과 33이 어디서 났는지를 모릅니다.
② 기간쌍은 시험 주문에 들어가는 칸이 아닙니다. 시험 주문 요청 본문(TrialBuyReq)에 fast·slow 같은 칸이 없습니다. 있어도 안 됩니다 — 우리 규칙 문법의 기간 상한이 200 이라 360 을 못 받습니다. 14편에서 이미 받아 둔 거절이 있습니다.
422 {"code":"invalid_rules","detail":"period_limit"}
그래서 무엇이 있어야 이 대조가 서나 — 행마다 trades[]
격자 한 줄에 거래 목록이 붙어 있으면 됩니다. 이런 모양입니다.
{"fast": 60, "slow": 180, "totalReturn": -5.344…,
"trades": [{"entry_time": "…", "entry_price": "…", "exit_time": "…",
"exit_price": "…", "direction": "long", "exit_reason": "…"}, …]}
이게 있으면 두 최적점의 진입 레벨을 같은 시각에 나란히 낼 수 있습니다. 어려운 요구가 아닙니다 — 15편 Moss는 자기 백테스트 결과에 trades[] 를 실제로 내놓습니다(21-moss-own-backtest-30d.json). QuantDinger 의 /backtest 결과는 안 내놓습니다. 그 한 칸 차이가 「이 도구로 반사실 대조를 세울 수 있는가」를 가릅니다.
백테스트 센터 — 격자 여섯 판이 UI 에 그대로 쌓여 있습니다
오른쪽 Recent backtest runs 를 보세요. 한 줄이 격자 한 칸이고, 보이는 것은 수익률 하나입니다. 이 목록 어디를 눌러도 「이 설정이 몇 시 몇 분에 얼마에 들어갔나」는 나오지 않습니다. 이 글이 막힌 자리가 이 화면입니다.
2. 왜 같은 시각에 함께 내야 하나
시험 주문의 첫 용도는 2편에 썼습니다 — 도구가 낸 판단 하나를 끝까지 재는 것. 두 번째 용도가 이 글입니다.
같은 시각·같은 호가에 한 칸만 바꿔 내면, 차이가 그 칸에서만 나옵니다. 그게 전부입니다. 시차를 두고 두 번 돌리면 호가가 달라져서, 갈린 결과가 설정 때문인지 시장 때문인지 영원히 못 가립니다.
이게 가능한 이유는 잔고를 안 건드리기 때문입니다. 계좌가 움직이면 비교군 두 번째 팔을 낼 자본이 없고, 낸다 해도 첫 팔이 자본을 묶어 두 번째 팔의 조건이 달라집니다. 재는 행위가 재려는 대상을 망가뜨립니다. 시험 원장은 거기서 빠져 있으니 비교군을 몇 개든 낼 수 있습니다.
실제로 몇 개까지 되나
한 종목·한 호가에 변형 6개를 동시 발주해 봤습니다.
| 결과 | 6/6 전부 201. 실패·거절 0 |
| 소요 | 0.02초 (6건 전체) |
| 접수 시각 분포 | 6.5 밀리초 안에 전부 |
| 막힌 것 | 없습니다. 멱등 충돌·한도·중복 어느 코드도 안 났습니다 |
상한을 정하는 것은 월 한도뿐입니다. 그리고 그 한도는 그냥 나눗셈입니다.
| 소모 | |
|---|---|
| 대조군 N팔 | N회 |
같은 client_order_id · 같은 본문 재전송 |
0회 (200 멱등 재생) |
같은 client_order_id · 다른 본문 |
0회 (409 client_order_id_conflict) |
| 청산 체결 | 0회 (체결 기록의 counted: false) |
재시도가 공짜라서 실수해도 예산이 안 샙니다. pro 월 1,000회면 2팔짜리 대조는 월 500회, 3팔은 333회, 4팔은 250회입니다.
3. 레시피 — 그대로 도는 것
전체는 11-trial-order-quantdinger-grid-ab.sh 에 있습니다. 네 칸으로 돕니다.
① 심기 — 한 칸만 다른 두 팔. 두 팔의 client_tracking_id 를 같게 두고, metadata.arm 으로 구분합니다.
BODY=$(jq -n --arg px "$PX" --arg sl "$STOP_PX" --arg a "$ARM" \
'{venue:"binance-spot", symbol:"BTCUSDT", side:"long",
type:"limit", limit_price:$px, # ← 이 한 칸만 다릅니다
client_tracking_id:"qd-grid-ab-1", # ← 두 팔 같습니다
exit_plan:{stop_loss:$sl, close_after_seconds:45},
metadata:{arm:$a, changed_field:"limit_price"}}') # ← base / variant
손절은 두 팔을 같은 값으로 묶었습니다. 보호 손절 5% 는 두 설정이 같은데 진입가가 달라서 손절선이 갈립니다. 한 칸만 남기려고 국면 호가 기준 85900 × 0.95 = 81605.00 하나로 고정했습니다.
의도는 도구가 갖습니다. 격자 순위·PF·거래 수는 서버로 안 보냅니다. 서버가 갖는 것은 집행 기록이고, 둘을 잇는 것은 추적 ID 하나입니다.
client_order_id,tracking,arm,grid,profit_factor,total_trades,cost_basis
qd-grid-ab-1-60-180,qd-grid-ab-1,base,60/180,0.5123854981235674,68,assumed
qd-grid-ab-1-120-360,qd-grid-ab-1,variant,120/360,0.678789397136766,33,measured
② 회수 — ?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회」였습니다. 한 달 한도를 다 쓴 달이면 1,001번입니다. 열은 시험 하나 내보내기와 같아서 두 CSV 를 이어붙여도 안 깨집니다.
④ 조인 — awk 를 쓰지 마세요. CSV 의 metadata·exit_plan·tags 칸에 인용된 쉼표가 들어 있습니다. -F, 로 자르면 열이 밀리는데, 에러 없이 조용히 밀려서 더 나쁩니다. 인라인 python3 의 csv 모듈로 조인합니다.
레시피 안에 주석으로 박아 둔 것 셋
틱 반올림. 우리 기록의 실제 진입 체결가는 85979.70711050 과 85795.14865650 입니다. 그대로 limit_price 에 넣으면 이렇습니다.
422 invalid_limit_price "px 85979.70711050 is not a multiple of tick 0.01"
서버가 조용히 반올림해 주지 않습니다. 조건 가격도 실제 주문과 같은 격자에 있어야 하고, 사용자가 준 가격을 서버가 몰래 바꾸면 그게 더 위험합니다. 그래서 호출하는 쪽에서 버립니다 — 롱 지정가는 내리는 쪽이 보수적이라 올림이 아니라 버림입니다. 85979.70 / 85795.14.
metadata 에 float 를 넣지 마세요. 결정론 때문에 막혀 있습니다. 확률을 싣는 것은 흔한 쓰임이라 안내가 같이 옵니다.
422 invalid_metadata "metadata 에 float 를 쓸 수 없습니다 (결정론) — 숫자는 문자열로 보내세요 예: {"p_up": "0.55"}"
float_paths 에 어느 칸인지 적혀 옵니다. 문자열로 보내면 통과합니다.
자동 청산이 추적 ID 를 물려받습니다. 실현 손익은 청산 행에 붙습니다. 종전에는 그 행의 추적 ID 가 비어 있어서, ?tracking= 으로 회수하면 진입만 오고 손익이 빠졌습니다. 지금은 진입의 추적 ID·태그를 청산이 물려받습니다.
4. 돌려 본 결과
환경 고지. 아래는 격리 임시 서버 · 합성 호가에서 나온 값입니다. 운영이 아니고, 여기 적힌 손익은 시장 사실이 아닙니다 — 제가 넣은 고정 호가 위의 산술입니다. 여기서 재는 것은 대조 설계가 서는가 하나입니다.
두 팔을 국면 호가 85900.00 / 85900.01 위에 13:26:45.882 와 .889 에 던졌습니다. 7밀리초 차이입니다.
| 팔 | 격자 | 지정가 | 상태 | 진입 체결가 | 청산가 | close_reason |
수수료 | 순손익 | 소요 |
|---|---|---|---|---|---|---|---|---|---|
| base | 60/180 | 85979.70 | closed |
85908.6050005 | 85891.4049995 | close_after |
0.01374400 | −0.015120000080 | 46.0초 |
| variant | 120/360 | 85795.14 | entering |
미체결 | — | 없음 | 0 | null | — |
갈렸습니다. 체결 여부에서
같은 호가(ask 85900.01)에서 85979.70 은 즉시 체결되고 85795.14 는 호가에 걸린 채 남았습니다.
그리고 여기가 이 글에서 제일 값어치 있는 대목입니다.
미체결 팔은 close_after_seconds 시계가 시작조차 하지 않습니다.
상대 시한의 기준이 「첫 진입 체결의 공개 시각」이라, 안 채워지면 시계가 안 돕니다. max_duration_hours 가 될 때까지 그대로 남습니다. 관측 창 45초 안에서 비교군은 아무 결과도 안 냈습니다 — 손익도, 수수료도, 청산 사유도, 소요 시간도 없습니다.
안 채워진 팔은 비교에서 빠집니다. 이건 우리 쪽 버그가 아니라 계약대로입니다(있지도 않은 체결을 지어내는 쪽이 훨씬 나쁩니다). 다만 대조 설계를 짤 때 이걸 모르면 표가 한 칸 비어 있는 이유를 영원히 못 찾습니다. 지정가로 대조군을 낼 거면 두 팔이 전부 체결될 호가대인지 먼저 확인하세요. 아니면 둘 다 시장가로 내고 다른 칸을 바꾸는 편이 낫습니다.
우리 쪽 상세 — 한 대조의 두 팔이 같은 추적 ID 아래 쌓여 있습니다
카드 두 장의 제목 줄을 비교해 보세요. 메모만 「기준안 손절 −0.05%」와 「비교군 손절 −0.20%」로 다르고, 그 아래 추적 ID: tool-a-run-42 와 ab-compare 태그가 두 팔에 똑같이 붙어 있습니다. 한 칸만 바꾼 대조가 화면에서는 이렇게 보입니다. (이 화면의 두 팔은 둘 다 체결됐습니다. 위 표의 비교군처럼 안 채워지면 진입·청산·순손익 칸이 통째로 빕니다.)
같은 호가였다는 증거
접수 시점의 기준가가 두 팔 모두 85900.005 로 같았습니다(qty_basis.reference_px). 다른 대조에서는 더 센 증거가 나왔습니다 — 세 팔짜리 대조에서 진입 체결의 published_at 이 13:25:57.507 로 밀리초까지 같았고, 네 팔짜리에서도 13:29:40.456~.457 로 4밀리초 안에 들어왔습니다. 한 번 공개된 호가 하나가 여러 건을 전부 채웠다는 뜻입니다.
이번 편에서는 그 증거를 못 씁니다. 한 팔이 안 채워졌으니 짝지을 published_at 이 없습니다.
시험 매수 화면 — 한 실험의 팔들이 목록에 나란히 섭니다
맨 위 8 / 1000 건 이 이번 달 소모입니다 — 팔 하나가 1건입니다. 아래 「내 비교」에 카드가 줄줄이 쌓여 있고, 카드마다 ab-compare 태그와 같은 추적 ID 가 붙어 있습니다. 여기서 test_id 를 뽑아 CSV 로 내리는 것이 레시피 ②③ 입니다.
한도는 예상대로
2팔 = 2회. 계좌 원장 스냅샷은 시작 전과 끝난 뒤가 완전히 동일했습니다 — 8개 계좌·거래 표 전부 한 행도 안 움직였습니다.
5. 이 조합의 한계
① 격자 파라미터 대조는 안 섰습니다. 이 글의 결론입니다. 제가 대조한 것은 「두 설정이 우리 쪽에서 실제로 낸 진입가」이지 격자 그 자체가 아닙니다. 60/180 과 120/360 이 실시세에서 어떻게 갈리는지는 이 방법으로 답이 안 나옵니다. 도구가 거래별 레벨을 안 내놓고, 우리 규칙 문법 상한이 200 이라서입니다.
② 미체결 팔은 자기가 본 호가를 기록에 안 남깁니다. 접수 응답에 bid/ask 가 따로 안 나옵니다. reference_px 는 중간값 하나입니다. 그래서 「이 팔은 어떤 호가를 보고 안 채워졌나」를 응답 어디서도 못 읽습니다. 열려야 할 자리입니다.
③ 호가 스냅샷 식별자가 없습니다. BBO 한 건에 id 가 없어서 「이 두 체결이 같은 한 호가에서 나왔다」를 응답만으로 단정할 수 없습니다. published_at 이 밀리초까지 같다는 것이 현재의 최선이고, 그건 정황이지 식별이 아닙니다.
④ 합성 호가입니다. 위 손익은 시장 사실이 아닙니다. 확인된 것은 배선과 계약이고 거래소 성능이나 실제 비용 실증이 아닙니다.
⑤ 회수 경로에 정정이 하나 있습니다. 이 실측을 돌릴 때는 GET /v1/forward-tests?q=<추적 ID> 가 0건을 돌려줬습니다 — 그때의 q 는 test_id·symbol·memo·tags 만 봤고 client_tracking_id 를 안 봤습니다. 그래서 실제로 실험을 되찾은 것은 태그였습니다(?tag=a2-04-04 → 정확히 3건).
그 칸이 이 실행 직후에 열렸습니다 — ?tracking= 정확 일치 필터와 q 의 추적 ID 확장. 3절 레시피는 그 새 계약을 씁니다. 다만 저는 그 코드를 돌리지 않았습니다. 위 「0건」은 열리기 전의 사실이고, 열린 뒤가 어떤지는 이 글이 실측으로 주장하지 않습니다. 레시피를 처음 돌리실 때 ?tracking= 이 몇 건을 돌려주는지 직접 확인하세요.
6. 언제 쓰고 언제 쓰지 마라
쓸 때
- 도구가 절대 가격 레벨을 내놓을 때. 진입가·목표가·손절가 중 하나를 한 칸으로 삼을 수 있으면 대조가 섭니다.
- 바꾸려는 칸이 시험 주문 본문에 실제로 있는 칸일 때 —
limit_price·exit_plan.take_profit·exit_plan.stop_loss·side·venue. - 두 설정이 전부 체결될 만한 호가대일 때. 아니면 시장가로 내고 청산 조건 쪽을 바꾸세요.
쓰지 말 때
- 도구의 산출이 파라미터·비율·기간일 때. 시험 주문에 그 칸이 없습니다. 절대 레벨로 번역하는 순간 그건 도구의 답이 아니라 제 번역이 됩니다.
- 격자 행에 거래 목록이 없을 때. 이 글이 그 경우입니다 — 무엇을 대조하는지 말할 수 없게 됩니다.
- 손익의 절대 크기를 비교하고 싶을 때. 시험 주문의 수량은 서버가 정하고 최소 주문금액을 겨우 넘는 크기입니다.
그리고 억지로 성공담을 만들지 마세요. 이번 편에서 제가 낸 두 팔은 실제로 갈렸지만, 갈린 이유는 격자가 아니라 지정가입니다. 그걸 "격자 최적점이 갈렸다"고 쓰면 거짓말입니다. 대조가 안 선다는 판정도 결과이고, 무엇이 있어야 서는지를 아는 게 독자에게 더 값어치 있습니다. 그 한 칸은 격자 행마다 붙은 trades[] 입니다.
이 글의 증거: 실측 로그 · 원자료 · 레시피. 위 숫자는 전부 그 기록에서 가져왔고, 못 세운 대조와 5절의 한계도 같은 기록에 있습니다.