많은 사람이 한 번쯤 생각해 보는 주제가 바로 'AI에게 주식의 매매를 맡기면 어떻게 될까'입니다. 이에 이번에는 TypeSafe의 범용 판단 모델 Jev에 미국 주식 ETF인 SPY를 1초 간격으로 판단하게 하고, 그 판단대로 모의 매매(Shadow Trade) 하는 시스템을 Python으로 만들어 테스트해 보았습니다.
3줄 요약
- 약 4시간 동안
14,285회 판단을 진행했고, 모의 매매는 총 11왕복이었습니다. 결과는 승리 1, 무승부 1, 패배 9, 손실 −1.68달러였습니다 (주식당 거래 기준이며, 실제 주문은 나가지 않았습니다). 하룻밤 사이에 이 정도의 손실을 봤다고 이해하시면 됩니다.
- 손실액의 약 60% (0.97달러)는 매수 가격과 매도 가격의 차이(스프레드)였습니다. 나머지 변동 부분 역시 '같은 시간 동안 주식을 보유했을 경우'와 거의 동일하여, 이 설정에서는 예측력의 증거를 찾을 수 없었습니다.
- 가장 큰 수확은 AI에게 수치를 전달할 때의 표기 방식에 따라 판단이 편향된다는 것을 알게 된 것입니다.
7.8e-05를 +0.78 bp로 수정하는 것만으로 하락 쪽으로의 편향도가 −12.7 → −1.7 포인트로 줄어들었습니다.
먼저 고지합니다
- 투자 조언이 아닙니다. 특정 종목이나 매매를 권유하는 내용도 아닙니다.
- 이번 실험은 단 2세션 분량입니다. 1초 모드에서는 실제 주문을 전혀 내지 않았습니다. 모의 체결은 IEX라는 단일 거래소의 호가(Quote)를 기준으로 계산했기 때문에, 실제 매매보다 유리한 결과가 나오기 쉬운 조건입니다.
- Jev는 시장 예측을 위해 만들어진 모델이 아니며, 이번에는 일부러 본래 용도를 벗어나 사용했습니다. 'Jev는 쓸모없다'라는 이야기가 아니라, **'이런 사용법과 이 설정으로는 실력의 증거를 찾지 못했다'**라는 내용으로 읽어주시기 바랍니다.
[영상: 대시보드 — 1초마다 Jev의 판단/확률/모의 손익이 업데이트되는 모습]
| 항목 | 내용 |
|---|
| 종목 | SPY (S&P 500에 연동하는 ETF. 미국 주식 전체 변동의 대표격) |
| 데이터 | Alpaca 무료 실시간 데이터 (IEX 거래소 호가 및 체결만 사용) |
| 판단 | TypeSafe의 Jev에게 매초 '앞으로 30120초 동안 오를 것 같은지, 내릴 것 같은지, 둘 다 아닌지'를 확률로 답변받음 (두 번째 설정. 첫 번째는 1060초) |
| 매매 | Jev의 답변을 코드의 고정 규칙에 통과시켜 모의 체결(그 순간의 호가로 매매한 것으로 장부에 기록) |
| 자금 | 전혀 움직이지 않음. 주문도 내지 않음 |
실제로 진행한 세션은 다음 두 번입니다.
| 1회차 (v1) | 2회차 (v2) | |
| :--- | :--- |
| 일시(미 동부 시간) | 2026/10/20 13:0015:50 | 2026/10/23 11:5015:50 |
| ... | 11왕복 |
| 모의 손익 | 0달러 | −1.68달러 |
1회차에서는 1만 회 판단에 거래는 없었습니다. 그 원인을 분석하여 재구축한 것이 2회차입니다. 아래에 그 경위를 순서대로 작성하겠습니다.
금융 용어가 몇 가지 나오므로, 먼저 정리해 두겠습니다.
| 용어 | 쉽게 말해 |
|---|
| bp (베이시스 포인트) | 0.01%를 의미합니다. SPY가 약 760달러라면, 1bp는 약 7.6센트입니다 |
| bid / ask | bid는 매수자가 제시하는 최고 가격이며, ask는 매도자가 제시하는 최저 가격입니다. 즉시 사고 싶다면 ask로, 즉시 팔고 싶다면 bid로 체결합니다 |
| 스프레드 | ask와 bid의 차이입니다. 바로 사서 바로 판다고 해도, 이 차이만큼 손해를 보게 됩니다 |
| midmid | bid와 ask의 정확히 중간값입니다 |
| IEX | 미국에 많은 주식 거래소 중 하나입니다. 무료 데이터에서는 IEX의 정보만 볼 수 있기 때문에, 시장 전체 기회(호가)와 체결된 건수 중 극히 일부만을 보고 있는 셈입니다 |
| 섀도트레이드 (Shadow Trade) | 실제로 주문을 내리지 않고, 그 순간의 호가를 기준으로 매매한 것처럼 장부에 기록하는 방식입니다. 이번 1초 모드는 이 방식을 사용합니다 |
| 페이퍼트레이드 (Paper Trade) | 증권사의 모의 계좌에 실제와 같은 형식으로 주문을 내는 방식입니다(실제 돈은 움직이지 않습니다). 이번에는 다른 1분 봉 봇에서 사용했습니다 |
| VWAP | 거래량 가중 평균 체결 가격입니다 |
| IC (정보 계수, Information Coefficient) | '신호가 강할수록 그 이후의 값 움직임도 큰지'를 순위 상관관계로 나타낸 수치입니다. 예측대로라면 플러스, 무관하다면 0 근처, 반대라면 마이너스입니다 |
| 캘리브레이션 (Calibration) | '70%라고 했을 때, 실제로 70%의 비율로 그렇게 되는가'라는 확률의 정확성입니다 |
| 미러 테스트 (Mirror Test) | 입력된 값의 상하를 뒤집어서 같은 AI에게 다시 질문하고 판단에 '선호도(편향)'가 있는지 확인하는 방법입니다(6장에서 설명). |
| 룩어헤드 (Look-ahead) | 판단 시점에서는 아직 알 수 없는 미래 데이터가 실수로 입력값에 섞이는 경우입니다. 백테스트 성적이 너무 좋게 보이는 전형적인 원인입니다 |
전체 구조는 네 개의 계층으로 나뉘어 있습니다.
③의 정책(Policy)과 리스크 관리(Risk Management)는 다음 고정 규칙을 따릅니다.
| 규칙 | 내용 |
|---|
| 매수 (Buy) | 상승 확률 68% 이상 이고, 매수장 스코어 2 이상 이고, 불안정성 35% 이하일 때 |
| ... | |
| Jev는 상태(JSON)와 '형식이 정해진 질문'을 전달하면, 선택지별 확률이나 점수를 돌려주는 범용 판단 모델입니다. 매초 다음 네 가지를 질문합니다 (실제 질문은 물론 영어입니다. Jev가 일본어에 약했던 기억이 납니다). | |
| 질문 | 답변 형식 |
|---|
| 이 상태에 가장 적합한, 다음 30~120초의 값 움직임 방향은? | 상승 / 하락 / 중립 각각의 확률 |
| ... | |
| 방향 질문의 원문은 다음과 같습니다. 6장의 실험에서 이 문구가 효과를 발휘합니다. | |
Which next-30-to-120-second price direction is most consistent with this compact state?
Do not infer certain profits.
up: "Up bias" / down: "Down bias" / neutral: "No directional edge"
여기서가 설계의 핵심입니다. 몇 주를 살지, 언제 멈출지, 몇 번까지 매매할지, 손실이 얼마가 되었을 때 멈출지는 모두 코드의 고정 규칙이 결정합니다. Jev가 담당하는 것은 '상승할 것 같은가, 하락할 것 같은가'라는 좁은 판단에만 국한되며, 주문 크기를 결정하거나 긴급 정지를 무효화할 수는 없습니다.
자금 상한선을 1,000달러로 설정했기 때문에, 매매는 매번 1주씩 진행됩니다. 이 기사의 손익 금액이 작은 이유가 바로 이것 때문입니다.
'상태 계산 → Jev → 정책(Policy) → 실행(Execution)'으로 나누는 방식은 Roan 님이 공개 석상에서 소개했던 구성[링크: Roan님의 게시물]을 참고했습니다. 또한, Jarrod Watts 님의 jev-trader로부터는 모의(규칙만 있는 가짜 AI)와 Jev를 전환하는 것, 지연되어 도착한 판단을 버리는 메커니즘, 드라이런(Dry Run), 이벤트 로그 같은 설계 아이디어를 얻었습니다.
참고로, 이번 코드는 jev-trader의 포크 버전도 아니고 이식된 것도 아닙니다. jev-trader는 TypeScript/Bun으로 작성되었으며 Monad 기반의 Kuru의 MON-USDC를 대상으로 합니다. 본 코드는 Alpaca와 미국 주식에 맞춰 Python으로 독자적으로 작성되었습니다.
AI에게 매매를 시키는 실험에서 가장 무서운 것은 '버그로 인해 실제 돈이 움직이는 것'입니다. 그래서 전략을 생각하기보다 먼저 다음 메커니즘을 만들었습니다.
돈이 움직이지 않음
⚠️ [IMG:N]
주문 전송처는 Alpaca의 페이퍼(모의) 계좌 URL만 코드에 고정했습니다. 실제 계좌로 주문을 내보내는 경로는 코드 어디에도 존재하지 않습니다. -
기본값은 관찰 모드(주문 없음)입니다. 페이퍼 주문 모드는 명시적인 플래그를 붙이고, 게다가 내용물이 비어있는 전용 페이퍼 계좌가 아니면 시작할 수 없습니다. -
이번 1초 모드는 더욱 엄격합니다. 주문의 전송/취소/조회 기능 자체를 가지지 않고, 모의 체결만 수행합니다.
미래 데이터 혼합 방지 (Lookahead Bias 대책)
- 판단 시점보다 앞선 타임스탬프를 가진 데이터만을 사용하여 상태를 만듭니다.
- '현재' 시간 역시 PC의 시계가 아닌 데이터의 타임스탬프로 결정합니다. Mac의 시계가 틀어져도 영향을 받지 않습니다.
- 모의 체결에는 판단이 끝난 후의 호가를 사용합니다.
늦거나 오류 발생 시, 거래하지 않는다 (Fail Closed)
- Jev의 답변이 600밀리초 이내에 도착하지 않으면 폐기합니다. 데이터가 2초 이상 오래되었다면 판단하지 않습니다.
- 오류 시에는 멈추는 쪽으로 기울입니다(fail closed). 1분봉 페이퍼 주문 모드에서는, 주문 전에 식별자가 붙은 '주문 의도'를 기록하고, 결과가 명확하지 않은 주문은 재전송하지 않고 조회한 후 해당 세션의 신규 주문을 중단합니다.
기타
- API 키나 계좌 정보는 로그에 남기지 않습니다.
- 전략은 세션 중에 변경하지 않습니다. 변경하는 것은 다음에 실행하기 직전에만 하며, 동일한 데이터로 규칙상의 기준과 비교한 후에 변경합니다.
사소한 이야기지만, 실제로 유용했던 상황도 있었습니다. 10/20에 다른 1분봉 봇을 페이퍼 계좌에서 구동했을 때, 네트워크 오류가 간헐적으로 발생했습니다. 봇은 오류 동안 판단을 멈추고 기다리다가, 단 한 건의 주문도 내보내지 않은 채 세션을 종료했습니다. 모르는 것이 있을 때는 아무것도 하지 않는다는 원칙이 설계대로 작동한 예입니다.
초기 버전에서는 IEX의 **호가(bid/ask)**를 중심으로 상태를 구성했습니다. 1초/5초/30초/60초의 mid 리턴, 호가의 편향도(매수 주문과 매도 주문 수량 차이), 스프레드, 체결 건수 등이 포함되었습니다. 수치는 원시 실수(raw float) 그대로 전달했으며, 시간이나 주가 자체도 상태에 포함했습니다.
- 판단은 10,146회였습니다. 응답 시간은 중앙값 0.15초, 90퍼센타일에서 0.20초로, 1초 간격의 루프에는 충분히 빠른 속도였습니다.
- 판단 내역은 **하락 39%, 중립 55%, 상승 6%**였습니다. - '상승 확률 68% 이상'은 약 2시간 50분 동안 단지 3회였고('하락 확률 68% 이상'은 1,138회) 매수 조건을 한 번도 충족하지 못해 거래는 제로였습니다.
이 시간대의 SPY는 거의 횡보(IEX의 mid 기준 -0.04%)했습니다. 그런데도 판단의 대부분이 하락 쪽으로 기울어져 있었습니다. 이 시점에서 '뭔가 이상하다'고 생각하기 시작합니다.
사후 분석 결과, 판단과 10초 후의 움직임 간의 관계(IC)는 -0.18 (95% 구간 -0.24~-0.12)였습니다. 이것만 보면 'Jev의 반대를 하면 맞는다'고 생각할 수 있습니다.
하지만, Jev를 사용하지 않은 단순한 '직전 5초 리턴' 역시 마찬가지로 -0.18이었고, 호가 편향도 또한 -0.17이었습니다. 원인은 IEX의 호가 깜빡임(flickering) 때문이었습니다. 1초 간격의 mid 변화에 대해 '이전 1초의 변화'와 '다음 1초의 변화'의 상관관계(1차 자기상관)를 측정했더니 -0.32였습니다. 오르면 다음은 하락하는 반복 패턴을 보이고 있었습니다. 정답 레이블 자체가 되돌아오기 때문에, 직전 움직임에 따르는 시그널은 무엇이든 '반대로 맞는다'고 보이게 됩니다.
실제로, 정답 레이블을 mid가 아닌 실제 체결 가격으로 사용하자, Jev의 10초 IC는 +0.08 (+0.01~+0.14)로 부호까지 바뀌었습니다. 기준점 선택에 따라 결론이 역전되는 만큼, 1초 단위의 IEX 데이터는 노이즈가 매우 컸습니다. v1에서는 아무런 결론도 내릴 수 없습니다.
횡보장에서 하락 판단이 많은 현상. 이것은 시장을 읽은 결과인지, 아니면 Jev의 '선호' 때문인지. 이를 분리하기 위해 사용한 것이 **미러 테스트(Mirror Test)**입니다.
- 실제로 기록된 상태 200개를 추출합니다.
- 방향성을 가지는 입력값(리턴, VWAP 괴리율, 매매 흐름 등)의 부호를 모두 반전시킨 '거울상'을 만듭니다. - 원본 상태와 거울상 둘 다 Jev에게 질문합니다.
편향성이 없는 판단자라면, 원래 상태에서 '상승 70%, 하락 10%'라고 답한 것은 거울 이미지에서는 '상승 10%, 하락 70%'가 되어야 합니다. 그래서 '상승 확률 − 하락 확률'을 점수로 하고, 원래 점수와 거울 이미지의 점수를 더해서 2로 나눈 값을 편향으로 했습니다. 선호도가 없다면 0이 됩니다. 단위는 확률 차이를 100배한 '포인트'입니다.
생각을 코드로 구현하면 다음과 같습니다 (실제 코드가 아닌, 설명을 위한 간소화된 것입니다).
# 방향의 의미를 가진 특징량 (부호를 반전시킬 대상)
DIRECTIONAL = {
"ret_tr_30s", "ret_tr_60s", "ret_tr_300s",
...
}
추가로, 방향 입력값을 모두 0으로 설정한 상태도 20가지 경우를 테스트했습니다. 이 경우는 어떤 조건에서도 **모두 '중립'**이었으며, 자료가 없을 때 하락을 선택하는 '사전의 선호도'는 없었습니다. 편향은 수치가 들어갔을 때 발생하고 있었습니다.
처음 기록한 데이터에서 v2에 사용할 특징량 일부를 다시 계산하여 상태를 만들고, 조건별로 200쌍씩 측정했습니다. 표는 방향 질문에 한정하여 조건을 비교한 것입니다.
| 조건 (바꾼 곳은 단 한 곳) | 편향 (포인트) | 편향이 마이너스였던 쌍의 비율 | 중립 외를 답한 비율 |
|---|
| 원래 설정 (순수 소수, 원래 문구) | −12.8 | 97.5% | 25% |
| A: 문구에서 “Do not infer certain profits.” 삭제 | −9.0 | 90.5% | 35% |
B: 수치를 부호가 있는 bp로 표기 (예: +0.78 bp ) | −1.7 | 62% | 69% |
| C: 선택지를 '높아질 것 같음/낮아질 것 같음'으로 변경 | −0.8 | 58% | 0.5% |
| A+B+C 모두 적용 | +5.9 | 18.5% | 59% |
실제 환경과 동일한 4가지 질문 구성으로 측정했을 때도, 순수 소수로는 −12.7이었고, 부호가 있는 bp로는 −1.7이었습니다. v1의 기회 기반 상태(순수 소수)에서는 −19.2였으며, 200쌍 모두 마이너스였습니다.
첫 번째 기록부터 10초 간격으로 추출한 961개의 상태를 표기 방식만 바꿔서 다시 질문했을 때도 차이가 명확하게 나타납니다.
| 표기 | 상승 | 중립 | 하락 |
|---|
| 순수 소수 | 0 | 729 | 232 |
| 부호가 있는 bp | 304 | 324 | 333 |
순수 소수의 상태로는, 961번 질문하는 동안 '상승'이 단 한 번도 나오지 않았습니다.
확인한 것은 아니지만, 하나의 유력 가설이 있습니다.
Python의 float은 절대값이 1e-4 (즉, 1bp) 미만일 경우, repr()이나 json.dumps()에서 2.6e-05와 같은 지수 표기법으로 나타납니다. 즉 1bp 미만의 작은 양수는 문자열 안에 마이너스 기호를 포함하게 됩니다. 반면, 음수는 -0.0001366처럼 앞에 마이너스가 붙습니다. 결과적으로, 순수 소수에서는 양수든 음수든 대부분의 수치에 '−'가 포함되어 있던 것입니다.
실제 v1 입력값 일부는 다음과 같았습니다.
ret_1s: 2.6e-05 ret_5s: -0.0001366 ret_30s: -0.0002277 ret_60s: -0.0002537
부호가 있는 bp로 변환하면 다음과 같습니다.
ret_1s: +0.26 bp ret_5s: -1.37 bp ret_30s: -2.28 bp ret_60s: -2.54 bp
변환은 이것뿐입니다 (이 역시 설명용 간소화 버전입니다).
def to_signed_bp(x: float) -> str:
""리턴(소수)을 부호가 있는 bp 문자열로 변환. 예: 7.8e-05 -> '+0.78 bp'""
return f"{x * 1e4:+.2f} bp"
C안의 편향은 −0.8로, 언뜻 가장 좋은 결과입니다. 그런데 중립 외를 답한 비율이 겨우 0.5%에 불과했습니다. 거의 항상 '어느 쪽도 아님'이라고 대답하게 되었을 뿐, 판단자로서의 역할을 수행하지 못하고 있었습니다. 편향만 보고 있으면 이 함정에 빠지게 됩니다.
또한 A, B, C를 모두 넣었더니 이번에는 상승 방향으로 +5.9까지 지나치게 갔습니다. 변경은 하나씩 시도해야 한다는 교훈을 얻었습니다. 실제 운영(본판)에서는 A, B, C 중 B(부호가 지정된 bp)만 채택했습니다. 2차 세션 당일 상태로 다시 측정한 편향도는 −2.8 (95% 구간 −3.3〜−2.3)입니다. 0은 아니지만, −12.7과 비교하면 상당히 작아졌습니다.
같은 공식을 1분봉 봇의 상태(주가 자체나 이동평균선 수준 포함)에 적용했을 때(총 77쌍), 편향도는 순수 소수로 −15.2, 부호가 지정된 bp로 −15.4로 전혀 개선되지 않았습니다. 주가 수준을 제거하고 상대적인 값만 사용하자 −10.8까지 줄었지만, 여전히 남아있습니다. 상태 설계가 다르면 효과적인 공식도 다르기 때문에, 매번 측정해야 합니다.
이 장의 교훈은 시장에 국한되지 않고, LLM이나 판단 모델에 수치를 전달하는 모든 상황에 적용될 것이라고 생각합니다(별도의 글로 자세히 작성할 예정입니다).
미러 테스트를 통해 편향도를 측정한다. 200쌍이라면, 0 상태 20건을 더해도 420회 호출로 충분한, 저렴한 건강 검진입니다. -
부호를 명시하고 단위를 통일한다. 지수 표기법의 소수를 그대로 전달하지 않는다. -
편향도와 동시에 '제대로 답변하는지'도 본다. 중립적인 값만 나오는 것은 아닌지(C안의 함정).
- 변경은 하나씩. 한꺼번에 바꾸면, 반대 방향으로 지나치게 갈 수 있습니다.
- 상태 설계를 변경했다면 다시 측정한다. 이전에 효과가 있었던 공식이 다음에도 효과를 보장하지는 않습니다.
| 항목 | v1 |
|---|
| 상태 재료 | IEX의 호가(mid, 호가 편향) |
| 실제 체결(Execution) 데이터 | 실제 체결 가격 리턴 30초/1분/5분, VWAP 괴리율, 매매 흐름(Buy/Sell Flow), 체결 건수, 5분 값 범위 + 스프레드 |
| Jev에 보여주는 수치 | 순수 소수(2.6e-05) |
| 부호가 지정된 bp | (+0.26 bp) |
| 시간/주가 자체 포함 여부 | 포함 |
| 포함하지 않음 (날짜나 가격 수준으로부터 무언가를 '기억'할 여지를 없앰) | |
| 질문의 시간 축 | 다음 10~60초 |
| 기록 | 판단 로그 |
| Jev 버전 | 1.13.0 |
'매매 흐름(Buy/Sell Flow)'은 각 체결이 매수 주도(ask에서 체결)인지 매도 주도(bid에서 체결)인지를 분류하고, 거래량으로 가중치를 부여한 −1부터 +1 사이의 값입니다. +1에 가까울수록 매수자들이 적극적으로 사고 있는 상태를 나타냅니다.
2차 세션에서 처음 '매수'가 발생했을 때, Jev는 다음과 같이 보였습니다. 방향성을 가진 값에는 부호를 붙이고, 값의 폭이나 스프레드처럼 방향성이 없는 값은 그대로 두었습니다.
ret_tr_30s: +0.66 bp ret_tr_60s: +2.62 bp ret_tr_300s: +4.98 bp
vwap_dev_30s_bps: -0.13 bp vwap_dev_300s_bps: +3.16 bp
flow_60s: +0.889 flow_300s: +0.044
...
Jev의 답변은 '상승 79%, 중립 17%, 하락 4%', 시장 상태는 '추세(Trend)', 매수 강도 점수는 2.0, 불안정성은 26%였습니다. 모든 조건을 충족했기 때문에 모의 거래로 1주를 매수했습니다(이 거래가 다음 장 표의 #1이며, 결과는 −0.27달러였습니다).
| 항목 | 값 |
|---|
| 시간 | 미국 동부 시간 10/23 11:5015:50 (한국 시간 10/24 0:504:50), 약 240분 |
| ... | |
| [스크린샷: 대시보드 — 2차 세션의 가격 차트 및 매매 마커] | |
시간은 미국 동부 시간입니다(한국 시간은 +13시간). 매수는 ask, 매도는 bid를 기준으로 한 모의 체결입니다. 'mid 기준'이란 스프레드를 지불하지 않고 mid에서 거래할 수 있었다고 가정한 손익을 의미합니다.
| # | 매수 | 매도 | 보유 시간 | 매수가 | 매도가 | 손익 | 중간값 기준 | 매도 이유 |
|---|
| 1 | 11:57:38 | 12:01:28 | 3.8분 | 763.02 | 762.75 | −0.27 | −0.235 | Jev가 하락 판단 |
| 2 | 12:30:37 | 12:39:22 | 8.7분 | 761.79 | 761.91 | +0.12 | | |
| +0.165 | Jev가 하락 판단 | | | | | | | |
| 3 | 12:45:51 | 12:48:35 | 2.7분 | 762.63 | 762.43 | −0.20 | −0.105 | Jev가 하락 판단 |
| 4 | 13:30:11 | 13:35:19 | 5.1분 | 760.39 | 760.36 | −0.03 | +0.075 | Jev가 하락 판단 |
| ... | 합계 | | | | | | | |
| 61.3분 | −1.68 | | | | | | | |
| −0.715 | | | | | | | | |
(단위: 달러, 모두 1주)
승리 1회, 무승부 1회, 패배 9회, -1.68달러. 중간값 기준으로는 −0.715달러입니다. 차액인 0.965달러(손실의 약 57%)가 스프레드 비용입니다. 왕복당 손익은 −2.0bp이며, 이 중 스프레드가 약 1.15bp를 차지합니다. 중간값 기준으로는 플러스 기록이 5회 있었으나, 스프레드를 지불하면 그중 4회가 마이너스 또는 ±0이었습니다( #4・#5・#6・#9).
- 평균 보유 시간은 5.6분이었습니다. 주식을 보유했던 시간은 세션 전체의 약 26%였습니다.
단 한 번의 손익만으로는 좋고 나쁨을 판단할 수 없습니다. 그래서 같은 날, 같은 데이터로 기준과 비교했습니다.
| 방식 | 왕복 횟수 | 손익(스프레드 포함) | 중간값 기준 | 보유 시간 비율 |
|---|
| Jev (실제 샤도우 운용) | 11 | −1.68 | | |
| −0.715 | 26% | | | |
| Jev (기록에서 재생한 오프라인 재현) | 11 | −1.56 | −0.94 | 26% |
| 규칙만 기준 (60초의 체결 모멘텀 + 매매 흐름, Jev 없음) | 30 | −7.31 | −2.38 | 5% |
| ... | | | | |
(단위: 달러, 모두 1주)
- 규칙만 기준은 30왕복을 하면서 스프레드로 크게 손해를 보았습니다. Jev 버전은 매매 횟수가 적었기 때문에 손실도 작게 끝났습니다. -
**매수 보유(Buy hold)**는 이날 −2.42달러였습니다. SPY가 −0.31% 하락한 날이었으므로, 매수만 하는 전략에는 본래 역풍이었습니다. - 마지막 행은 매수 보유의 손익을 Jev의 보유 시간 비율(26%)로 단순하게 할인한 근사치입니다.
하락한 날에 26%의 시간 동안 1주를 가지고 있었다면, 그것만으로도 약 0.6달러는 손해를 보는 계산이 됩니다. Jev의 중간값 기준 손익(−0.715달러)은 이와 거의 같으며, 값의 움직임을 예측하여 벌었다는 흔적이나 크게 벗어났다는 흔적도 없습니다. - 오프라인 재현(−1.56달러)이 실제 운용(−1.68달러)과 거의 일치한 것은 시스템 검증으로서 좋은 소식입니다. 차이는 체결에 사용된 호가가 몇 밀리초 정도 어긋난 데서 기인합니다.
Jev의 점수(상승 확률 − 하락 확률)와 입력한 각 특징량과의 순위 상관관계를 측정했습니다.
| 특징량 | Jev 스코어와의 상관관계 |
|---|
| 직전 60초의 체결 가격 리턴 | +0.81 |
| ... | |
| 즉, Jev는 **“최근 상승했다면 상승, 하락했다면 하락”**이라는 순수한 모멘텀(추세) 기반의 판단을 하고 있었습니다. 판단 자체로 부자연스러운 것은 아닙니다. | |
판단과 그 이후의 값의 움직임과의 IC(Information Coefficient)는 다음과 같습니다(겹치지 않는 구간별로 센 샘플. [ ]는 95% 구간).
| 몇 초 후 예측 | IC (체결가로 평가) | IC (IEX의 중간값으로 평가) |
|---|
| 10초 | −0.10[−0.16, −0.05] | −0.05[−0.11, +0.00] |
| ... | | |
어떤 시간 축에서도 마이너스였으며, 세션을 전반과 후반으로 나누어도 같은 경향이었습니다. Jev를 사용하지 않은 단순한 “60초 모멘텀”도 체결가로 평가하면 −0.07~−0.12로 마이너스입니다. 반대로 “30초 VWAP보다 낮으면 매수(평균 회귀)”는 30120초에서 +0.07+0.12로 플러스에 가까웠습니다(다만 대부분 구간이 0을 가로지르고 있습니다).
가장 솔직한 해석은, 이 날은 '상승하면 되돌아오는' 평균 회귀(Mean Reversion) 경향의 시장이었기 때문에, 추세에 올라타는 판단과는 궁합이 좋지 않았다는 것입니다. 그렇다고 해도 하루치 데이터만으로는 Jev의 특성 때문인지, 아니면 그날 시장 자체의 성질 때문인지는 구별할 수 없습니다.
Jev가 '상승할 것 같다'고 예측했을 때 실제 상황은 어땠을까요? 상승과 하락의 확률만을 비교한 값(상승 ÷ (상승 + 하락))을 5단계로 나누어, 10초 후에 mid 값이 실제로 상승했던 비율을 확인했습니다.
| Jev의 상승 확률 (상승 ÷ (상승 + 하락), 각 단계 평균) | 실제 상승 비율 | 건수 |
|---|
| 11% | 53% | 350 |
| ... | | |
| 90%라고 했을 때도, 실제로 상승한 것은 절반 이하였습니다. 예측의 좋고 나쁨을 측정하는 Briar Score(낮을수록 좋음)는 0.35로, 매번 '50%'라고 답했을 경우의 0.25보다 나쁜 결과입니다. Jev의 확률을 그대로 매매의 확신도로 사용하는 것은 위험하며, 사용하려면 다른 날짜의 데이터로 교정(Calibration)할 필요가 있습니다. | | |
정보 계수(IC)가 마이너스라면, Jev와 반대로 포지션을 잡는 것이 승리할 수 있을까요? 'Jev가 하락 68% 이상이라고 하면 매수한다'라는 규칙도 검증했지만, 스프레드를 지불하면 30초 만에 거래당 −0.78bp로, 어떤 시간 축에서도 마이너스였습니다. 사실 시도했던 모든 규칙(Jev를 따르는 것, 반대 포지션을 잡는 것, Jev를 사용하지 않는 단순한 것 모두 포함)은 스프레드를 지불하면 마이너스였습니다. 1초~2분 시간 축에서 매번 약 1bp의 스프레드를 초과하는 것은 그만큼 어려운 일이라는 것을 의미합니다.
스프레드 손실. 손실액 1.68달러 중 0.97달러는 매수 가격과 매도 가격의 차이를 지불한 비용입니다. 몇 분 간격으로 사고파는 설계에서는, 값 움직임 예측이 맞았더라도 이 비용을 초과하는 것은 쉽지 않습니다.
-
하락했던 날에 매수만으로 승부했습니다. SPY는 −0.31%였습니다. 보유 시간이 길수록 불리한 날이었습니다.
-
판단 방향과 시장 성질이 반대였습니다. Jev는 추세에 올라타는 판단을 했지만, 이 날은 평균 회귀 경향이 강했습니다.
-
확률에 대한 과신이 있었습니다. '90%'가 실제로는 47%였습니다. 확률의 임계치(68%)에는 보이는 것만큼 의미가 없었습니다.
-
애초에 샘플 자체가 작습니다. 11회 왕복은 운의 범위에 속하는 적은 수입니다. 좋든 나쁘든, 여기서 실력을 논할 수는 없습니다.
데이터 노이즈를 먼저 조사하지 않았습니다. v1에서 IEX의 mid 값을 1초 단위로 사용하며 깜빡임(자기상관 −0.32)에 휘둘렸습니다. 세션 전에 기록 데이터로 자기상관을 확인했어야 했습니다.
- AI의 편향성을 실전 전에 검사하지 않았습니다. v1은 '하락 편향적인 AI' 상태 그대로 약 3시간 동안 구동했습니다. 미러 테스트는 수백 번 호출만으로 충분하므로, 실전 전 필수 점검 항목에 포함했어야 했습니다.
- 평가 기준을 사전에 정하지 않았습니다. mid 값과 체결 가격 중 무엇으로 평가할지에 따라 IC의 부호까지 바뀌었습니다.
- v1과 v2 사이에서 Jev의 버전도 변경되었습니다(1.13.0 → 1.14.0). 따라서, v1과 v2의 차이를 모두 설계 변경의 효과라고 말할 수는 없습니다.
- 모의 체결은 낙관적입니다. 주문 대기열(Queue), 통신 지연, 시장 전체 가격 등을 고려하지 않았습니다. 실제로 매매했다면 결과는 더 나빴을 가능성이 있습니다.
- 단 2세션에 불과하며, 게다가 같은 데이터로 사후 진단을 했습니다. 9~10장의 비교는 모두 인샘플(In-sample, 정답을 알고 있는 데이터에서의 검증)입니다.
- 1분봉 봇은 네트워크 오류로 실험이 불완전하게 끝났습니다. 안전한 쪽으로 멈춘 것은 좋았지만, 데이터로는 거의 사용할 수 없습니다.