엔지니어링

252개 전략, 그 과정에서 찾은 브로커 버그 세 가지

패리티 코퍼스가 167개에서 252개로 늘어나는 동안 TradingView 브로커 동작 중 모델링하지 않았던 세 가지를 발견했다 — 강제 청산, FX 환산, trailing stop arming.
약 9분 읽기#parity#corpus#engine#tradingview#broker-emulation

패리티 코퍼스에 167개 기준 전략이 들어 있었고, 그중 165개가 TradingView 대비 거래 단위 strict 패리티였다. 그게 5월이었다. 지금은 252개 전략 — 251개가 "excellent" 패리티(99.6%), anomaly 1건, fail 0건으로 늘었다.

헤드라인 숫자는 사실 재미없는 부분이다. 재미있는 부분은 그 과정에서 발견한 것들이다. 우리가 모델링하지 않고 있던 TradingView 브로커 동작 세 가지를, 기존 167개로는 건드리지 못했던 코드 패스를 실제 또는 합성 전략이 건드리면서 찾아냈다. 이 글은 그중 셋을 다룬다.

코퍼스 현황

corpus/validation_report.md (2026-06-29)
  Total probes:    252
  Excellent:       251  (99.6%)
  Anomaly:           1
  Strong/Moderate/Weak/Minimal/Fail: 0
 
  TV trades:      389,590
  Engine trades:  389,688
  Matched:        389,468

엔진은 코퍼스 전체에서 TradingView보다 98건 더 많은 거래를 낸다 — 기준 거래 389,590건 대비 0.025% 차이다. 이건 합산 숫자고, 전략별로 보면 거의 전부가 아래 anomaly 1건에서 나온다. 나머지 251개에 노이즈로 흩어져 있는 게 아니다.

가장 최근에 추가된 6개 프로브는 drawing 카테고리다. Pine 드로잉 객체 (line.get_price, label·box geometry)를 차트 주석이 아니라 백테스트 시점 데이터로 읽는 런타임을 추가했을 때 들어갔다. 그려진 라인을 연산 상태로 쓰는 전략 — 예를 들어 수동으로 고정한 트렌드라인을 동적 지지선으로 쓰는 경우 — 은 스크립트가 이미 삭제한 객체에 대해서도 엔진이 geometry를 계속 제공해야 한다. TradingView가 하는 방식 그대로. 6개 전부 통과했다.

솔직하게 밝히는 anomaly 1건

맞지 않는 전략 1건을 걸러내고 숨기지 않는다. anomaly-equity-mirror-strategy-equity-01은 마진 admission의 1× equity 경계 정확히 그 지점에 있다. TradingView의 브로커 에뮬레이터는 그 경계에서 non-monotonic하다 — over-equity 진입을 받아주는 경우도 있고 under-equity 진입을 거부하는 경우도 있는데, 바마다 일관성이 없다. 우리 엔진은 deterministic하다. 같은 equity면 같은 admission 결정이 매번 나온다. 이건 Pine이 문서화한 마진 시맨틱스 기준으로는 맞는 동작이다. 동시에, 그 경계 지점에서 TradingView 브로커가 실제로 하는 동작과는 다르다는 것도 증명 가능하다. 맞추려면 우리 쪽에 non-determinism을 직접 집어넣어야 하는데, 그렇게는 안 한다. 이 갭은 숨기지 않고 문서화했고, 252개 중 유일한 1건이다.

버그 1: TradingView라면 강제 청산했을 포지션

TradingView의 브로커는 마진 요건을 넘긴 포지션을 전략 로직이 알아서 정리하길 기다려주지 않는다 — 그냥 청산한다. 레버리지 포지션이나 숏 포지션의 equity가 충분히 떨어져서 required margin이 account equity를 넘으면, TradingView는 strategy.exit() 조건 충족 여부와 무관하게 다음 바에서 그 포지션을 강제로 닫아버린다.

엔진은 이걸 모델링하지 않고 있었다. 포지션을 그대로 들고 가다가 전략 자체 로직이 언젠가 닫게 내버려뒀다 — 타이트한 스탑이 없는 전략이면, TradingView가 이미 청산한 포지션을 수십 바 동안 더 들고 있는 셈이 될 수 있다는 뜻이다. 레버리지가 큰 코퍼스 전략들에서는 entry·exit 로직이 완벽히 일치해도 스트레스 구간에서 equity curve가 완전히 다르게 나왔다.

수정은 account equity가 마진 요건을 침범하면 강제 청산을 추가하는 것이다. 청산가는 TradingView 브로커와 동일한 방식으로 계산하고, 청산 수량은 심볼의 lot step에 맞춰 quantize한다 — TradingView는 거래소가 허용하는 단위보다 작은 fractional lot을 청산하지 않는다. 우리도 이제 그렇게 한다.

레버리지나 숏 포지션을 perpetual에서 백테스트하는 사람한테 가장 크게 영향을 준다. drawdown 구간에서 equity curve가 너무 매끄러워 보였다면, 이게 그 원인일 가능성이 있다.

버그 2: FX를 모르던 affordability 체크

Pine 전략은 심볼의 quote currency와 다른 account currency를 선언할 수 있다 — 예를 들면 USDT 페어를 거래하면서 currency.INR을 쓰는 경우. 주문을 감당할 수 있는지 체크하기 전에, TradingView 브로커는 주문의 notional value를 그 시점 FX rate로 account currency로 환산한 다음 같은 통화 기준 equity와 비교한다.

엔진의 affordability gate는 이 환산을 건너뛰고 있었다. USDT notional을 마치 FX rate가 1:1인 것처럼 INR 기준 equity와 그대로 비교했다 — 즉 currency.INR을 선언한 전략은 실제보다 약 83배 더 감당 가능한 것처럼 보였다(1 USDT가 대략 그만큼의 루피 가치를 가지므로). 영향을 받은 스크립트에서는 TradingView 브로커가 unaffordable로 거부했을 거래까지 엔진이 받아주면서 거래 수가 대략 두 배로 늘었다.

수정은 affordability 체크가 돌기 전에 required margin에 account-currency FX rate를 적용하는 것이다. TradingView가 적용하는 바로 그 시점, 주문 파이프라인 안에서. non-USD 계열 account currency로 USD 또는 USDT 표시 심볼을 백테스트하고 있다면, 이게 거래 수를 조용히 부풀리고 있던 버그다.

버그 3: 한 번도 armed되지 않은 trailing stop

이건 우리보다 사용자가 먼저 잡아냈으면 가장 아쉬웠을 버그다. 조용히 실패하기 때문이다. strategy.exit()은 trailing stop을 지정하는 방법을 두 가지 지원한다. entry 기준 상대 거리인 trail_points, 그리고 절대 활성화 가격인 trail_price다.

// armed correctly today; trail_price-only calls used to never arm at all
strategy.exit("TS", from_entry = "Long", trail_price = close * 1.05, trail_offset = 10)

엔진의 arming 조건은 trail_points만 체크하고 있었다. trail_price만으로 지정한 trailing exit은 컴파일도 에러 없이 되고 실행도 에러 없이 됐지만, 한 번도 armed되지 않았다 — 포지션은 그냥 trailing stop 없이 백테스트 끝까지 조용히 앉아 있었다. exception도, 로그 한 줄도, trade도 없었다. 전략은 보호받고 있는 것처럼 보였다. 아니었다.

이건 코퍼스 프로브 하나가 trail_price-only activation만 따로 격리해서 테스트하고 있었기 때문에 찾았다 — 코퍼스 분류 글에서 설명한 validation 카테고리가 존재하는 정확한 이유다. 수익성도, 현실성도 따지지 않고, Pine 기능 하나만 단독으로 건드리도록 만들어서, 이런 회귀가 "전체 숫자는 괜찮아 보이는데"에 숨을 수 없게 만든다.

수정은 trail_pointstrail_price든 둘 중 하나만 있어도 trailing stop을 arm하는 것이다. trail_price로 Pine 전략을 작성해봤고 equity curve가 trailing stop이 전혀 없는 것처럼 의심스러워 보였다면 — 실제로 없었다, 이번 엔진 버전 이전까지는.

코퍼스의 진짜 의미

이 세 버그 중 코드 리뷰로 찾은 건 하나도 없다. 모양이 맞는 전략이 엔진을 통과하면서 TradingView와 맞지 않는 숫자를 냈기 때문에 찾았다. 167개에서 252개, 그리고 계속 늘려가는 held-out, 버전 관리되는 코퍼스를 몇 개짜리 smoke test 대신 유지하는 이유가 바로 이거다. 거래 수나 entry price를 바꾸지 않는 버그도 가장 중요한 숫자 — 전략이 벌었는지 잃었는지 — 는 바꿀 수 있고, 이걸 잡는 유일한 방법은 더 많은 모양의 전략을 같은 ground truth에 대고 계속 diff하는 것뿐이다.

갭을 발견하는 대로 프로브를 계속 추가하고, 숫자도 계속 공개한다 — 좋은 숫자도, 유일한 anomaly도 있는 그대로.

다음으로 가볼 곳