한 시장을 거래하면서 다른 시장으로 필터를 거는 전략에는 그 두 번째 시장의 봉이 필요하지만, 내 차트의 CSV에는 그 봉이 없습니다. 이런 스크립트를 돌리려면 PineForge 엔진에 무엇이 필요한지, 봉이 없을 때 엔진은 어떻게 하는지, Docker 이미지와 MCP 서버는 무엇을 제공하는지 정리했습니다.
2026-10-04에 업데이트했습니다. 릴리스 1.1.0부터 pineforge-release 이미지는
PINEFORGE_SYMBOL_FEEDS가 가리키는 JSON 인덱스를 읽어request.security가 요청하는 다른 심볼의 봉을 설치합니다. 피드는 요청한 타임프레임마다 하나씩이고, 집계해서 만든 피드는 없습니다(엔진 #327, pineforge-release #23, 엔진의 1.1.0 변경 로그 참고). 이 글의 나머지 내용은 릴리스 1.0.1 기준으로 2026-10-02에 게시한 그대로입니다.
주식 한 종목을 거래하는 전략인데, 나스닥이 상승 추세일 때만 매수하고 싶다고 해 봅시다. TradingView에서는 한 줄이면 되고, 잘 돌아갑니다. TradingView는 모든 심볼의 봉을 갖고 있기 때문입니다. 내 CSV로 돌리는 백테스터에는 차트의 봉밖에 없습니다. 이 백테스터에 QQQ를 요청하면 선택지는 셋입니다. 거부하거나, 멈추거나, 엉뚱한 시장으로 필터를 조용히 계산하거나.
PineForge는 두 번째를 택하고, 그것도 꼭 필요할 때만 그렇게 합니다. 엔진에 QQQ 자체의 봉을 넣어 주면 스크립트는 그대로 돌아갑니다.
두 번째 시장이 필요한 한 줄
다음 스크립트는 QQQ가 50일 평균 위에 있을 때만 롱 진입합니다.
//@version=6
strategy("QQQ regime filter", overlay=true)
qqqUp = request.security("NASDAQ:QQQ", "D", close > ta.sma(close, 50))
fast = ta.sma(close, 20)
if ta.crossover(close, fast) and qqqUp
strategy.entry("L", strategy.long)
if ta.crossunder(close, fast)
strategy.close("L")
문제는 3번째 줄입니다. 이 줄의 식 close > ta.sma(close, 50)은 QQQ 자체의 일봉, 즉 QQQ의 종가와 QQQ의 50일 평균으로 계산됩니다. 차트의 봉을 아무리 집계해도 이 값은 나오지 않습니다. 얼마 전까지 PineForge는 이 스크립트를 아예 거부했습니다. 지금은 엔진을 실행하는 쪽이 QQQ의 봉을 넘겨주기만 하면 이 스크립트를 실행합니다.
엔진에 필요한 것
엔진의 공개 C 헤더에는 이를 위한 호출이 하나 있습니다(pineforge.h).
PF_API int strategy_set_symbol_feed(pf_strategy_t s, const char* key, const char* timeframe,
const pf_bar_t* bars, const int64_t* close_ms, int n);
요구 사항 세 가지로 읽으면 됩니다. key는 스크립트가 넘기는 문자열 그대로인 NASDAQ:QQQ여야 합니다. 그래야 봉이 그 봉을 요청한 호출과 이어집니다. timeframe은 요청한 타임프레임이어야 합니다. 일봉 요청에는 일봉이 필요하고, 같은 심볼이라도 다른 타임프레임의 피드는 인정되지 않습니다. 그리고 close_ms는 봉마다 마감 시각을 따로 줍니다. 엔진은 “심볼의 캘린더를 추측하지 않기” 때문에, 봉이 알려 주지 않으면 나스닥이 언제 마감하는지 모릅니다. 심볼의 세션, 통화, 틱 크기 같은 정보는 선택 사항이며, 짝을 이루는 다른 호출로 넘깁니다.
누구의 시계를 따르나
마감 시각이 중요한 이유는 두 시장이 서로 다른 시각에 마감하기 때문입니다. 엔진은 차트 봉마다, 그 차트 봉이 마감하는 시점까지 이미 마감한 상대 심볼의 마지막 봉을 요청에 넘깁니다. lookahead를 켜면 차트 봉이 시작하는 시점까지 시작한 마지막 봉을 넘깁니다(pineforge.h). 그래서 “차트의 세션 마감보다 늦게 마감하는 일봉은 TradingView에서와 마찬가지로 차트 기준 하루 늦게 보입니다”(MTF 가이드).
이것은 PineForge가 편의상 택한 방식이 아니라 TradingView의 규칙입니다. 엔진 테스트에는 달러 인덱스를 요청하는 금 일봉 차트의 TradingView 내보내기가 들어 있습니다. 일요일 저녁에 시작하는 금 일봉은 월요일 17:00 ET에 마감하고, 달러 인덱스의 월요일 봉은 그보다 두 시간 늦은 19:00 ET에 마감합니다. 그래서 TradingView에서 이 금 봉은 금요일의 달러 인덱스 봉을 읽고, 월요일 봉은 다음 금 봉에서야 들어옵니다(픽스처 노트). 상대 시장의 봉이 마감하기도 전에 그 봉을 차트 봉에 넘기는 백테스터는 미래를 읽고 있는 셈입니다.
봉이 없을 때
스크립트는 그래도 트랜스파일됩니다. 다만 이 요청이 “다른 심볼의 봉”을 읽으며 “설치된 봉이 없으면 값을 읽는 지점에서 실행이 오류로 멈춘다”는 경고가 함께 나옵니다(codegen). 처음으로 값을 읽는 순간, 실행은 해당 호출을 짚어 주는 메시지를 남기고 멈춥니다(소스).
request.security("NASDAQ:QQQ", "D", ...) at line 3: no data is pinned for this request, and its value was read
QQQ 대신 차트의 봉을 읽는 일은 절대 없습니다(변경 로그). 엉뚱한 시장으로 계산한 필터는 그럴듯하지만 아무 의미 없는 equity 곡선을 만들어 내므로, 멈추는 쪽이 더 안전한 실패입니다. 멈추지 않는 경우는 두 가지입니다. 값을 한 번도 읽지 않는 요청(예를 들어 꺼 둔 입력 뒤에 있는 요청), 그리고 값이 플롯, 알림, 테이블에만 쓰이는 요청입니다. 후자는 “트레이드에는 영향이 없다”는 경고와 함께 na가 됩니다(codegen).
Docker 이미지와 MCP 서버가 제공하는 것
이들은 모두 한 시장의 봉, 즉 차트의 봉만으로 백테스트합니다.
- Docker 이미지는
strategy.pine과ohlcv.csv하나를 받고(README), 이미지의 하네스는 다른 심볼의 봉을 설치하지 않습니다. QQQ 스크립트는 여기서 위의 메시지와 함께 멈추고, 이미지는 종료 코드 4로 끝납니다. - 로컬 MCP 서버는 이 이미지를 기반으로 만들었고 CSV 하나를 백테스트합니다. README에도 그렇게 적혀 있습니다. 이 서버는 “다른 심볼의 봉을 설치하지 않으며 … 예를 들어 다른 심볼에 대한
request.security는 엔진이 지원하지만, 여기서는 값이 트레이드에 닿을 수 있는 요청이 실행을 멈춥니다”(README). - 호스팅 MCP 서버는 심볼, 인터벌, 거래소를 받아 그 한 시장의 봉을 직접 가져옵니다. 크립토만 지원하므로 QQQ 예시 같은 주식 전략은 여기서 아예 돌릴 수 없습니다(같은 README).
여기서 많이들 걸리는 함정이 하나 있습니다. Docker 이미지와 로컬 서버는 차트가 어느 시장인지 엔진에 알려 주지 않습니다. 그래서 문자열로 적은 심볼은 차트 자신의 시장을 가리키더라도 다른 심볼로 취급됩니다(커버리지 노트). 차트의 심볼에는 syminfo.tickerid를 쓰세요. 이 심볼에 대한 상위 타임프레임 요청은 추가 데이터가 필요 없습니다. 엔진이 그 봉을 차트의 봉으로 만들기 때문입니다(MTF 가이드). 한계도 하나 있습니다. 미국 주식이나 CME 선물의 장중 차트에서 TradingView의 일봉은 거래소가 직접 낸 일봉이고 공식 종가를 담고 있어서, 어떤 집계로도 똑같이 만들 수 없습니다(pineforge.h).
실무에서 기억할 것
- 차트 자신의 시장으로 필터를 건다면?
syminfo.tickerid를 쓰세요. 스크립트가 내 CSV만으로 돌아갑니다. - 다른 시장으로 필터를 건다면? 엔진을 직접 실행하고, 실행 전에 그 시장의 봉을 마감 시각과 함께 설치하세요. 엔진 저장소의 테스트 하네스가 좋은 예시입니다. 스크립트별 요청 매니페스트에서 봉을 설치하며, 매니페스트의 필드는
scripts/run_strategy.py가 정의합니다. - TradingView와 비교한다면? 기본값대로
lookahead가 꺼져 있으면, 내 시장보다 늦게 마감하는 시장의 일봉은 하루 늦게 들어온다고 보세요. TradingView의 동작이고, 엔진도 이를 그대로 맞춥니다.
2026-10-02에 그날 릴리스된 PineForge 엔진과 codegen 1.0.1로 확인했습니다. 이 동작은 1.0.0에서 처음 나왔습니다.
다음으로 볼 곳
- Claude나 Cursor에서 백테스트하기: 두 MCP 서버를 단계별로 설명합니다.
- PineForge를 오프라인으로 돌리기: 내 데이터로 돌리는 Docker 이미지.
- 실전에서 보는 Pine v6 타입 시스템:
request.security의 lookahead 기본값이 Pine 버전마다 어떻게 바뀌었는지도 다룹니다.