2026-06-05 (금) 개발일지
오늘의 두 가지 — PDF 발송 3일째 죽던 버그와, 승률 0%를 만든 전략의 맹점
오늘은 bit-scalp의 두 가지 레이어를 동시에 정리했다. 하나는 운영 레벨 — 텔레그램 PDF 리포트가 3일째 안 나가고 있던 걸 발견해 뜯어고쳤고, 다른 하나는 전략 레벨 — 실제 거래 28건의 승률이 25%로 떨어진 원인을 가상 진입 8459건의 백테스트로 추적했다. 두 문제가 서로 다른 계층이었지만, 결국 같은 결론으로 수렴했다: 내가 데이터를 제대로 보고 있지 않았다.
bit-scalp PDF 발송 3일간 무응답 — __aenter__ 누락이 만든 침묵
사실 오늘 이 작업을 시작한 계기는 승률 분석이었다. 그런데 분석을 위해 데이터를 긁어보려니, 텔레그램 채널에 올라간 PDF 리포트가 3일째 안 올라오고 있었다. 마지막 정상 발송이 사흘 전. 그 사이 에러 로그를 본 적도 없고, 스케줄러는 잘 돌고 있었다 — 발송 함수만 조용히 실패하고 있었다.
처음에는 텔레그램 API 토큰 만료를 의심했다. 하지만 curl로 직접 찔러보니 200이 떨어졌다. 봇도 살아 있고. 그다음엔 orchestrator.py의 발송 로직 자체를 봤다. PDF 생성까지는 문제없었다 — 파일은 만들어진다. 그런데 httpx.AsyncClient()로 업로드하는 구간이 예외를 먹고 있었다.
async with httpx.AsyncClient() as c, open(...) as f: 이 한 줄이 문제였다. 나는 async with로 두 컨텍스트 매니저를 연결했지만, 파일 컨텍스트는 동기이고 httpx.AsyncClient()는 비동기다. Python은 이걸 감지하지 못하고 넘어가지만, 런타임에 __aenter__ 메서드가 없어서 AttributeError가 발생하고 있었다. 그 에러는 상위 try/except에서 묵음 처리되고 있었다.
버린 옵션:
- 텔레그램 API 재발급 (원인 아님, 200 OK 확인)
- PDF 생성 로직 교체 (파일은 잘 만들어짐)
asyncio.run()수정 (이벤트 루프 정상)
해결: async with에서 파일 open()을 분리했다. open()은 동기로 먼저 실행하고, async with httpx.AsyncClient() as c: 만 비동기 컨텍스트로 유지했다. 그리고 send_telegram_pdf()의 try/except 블록에 로그 레벨을 error로 올려서 이후에도 실패가 눈에 띄도록 바꿨다.
검증: 수정 후 PDF가 정상 발송됐다. 3일치가 한꺼번에 밀려올라와서 채널이 좀 지저분해졌지만 — 데이터는 확보됐다.
실제 승률 25%, 알파가 아닌 차단 정책이 문제였다
PDF가 살아나면서 실제 거래 28건의 데이터를 제대로 볼 수 있었다. 결과는 7승 21패, -$180, 승률 25%. 처음엔 시장 방향성이 문제라고 생각했다 — 하락장에서 롱만 넣고 있었나? 그런데 virtual_entries 테이블에는 8459건의 가상 진입 데이터가 쌓여 있었다. 진짜 원인은 시장이 아니라, 내가 어떤 진입을 차단했는가였다.
orchestrator.py에는 세 가지 진입 차단 정책이 있었다:
TRENDING_DOWN상태에서 SHORT 차단TRENDING_DOWN상태에서 알트코인 LONG 차단- 특정 변동성 조건에서 진입 자체 차단
각 정책별로 가상 데이터를 집계했다.
| 정책 | 진입 수 (7일 N=8459) | 승률 | 평균 수익률 |
|---|---|---|---|
| TRENDING_DOWN SHORT 차단 | 7475건 | 73.1% | +1.86% |
| TRENDING_DOWN 알트 LONG 차단 | 984건 | 22.7% | -0.64% |
| 차단 없음 (전체 평균) | 8459건 | 58.3% | +0.92% |
결정적 발견: TRENDING_DOWN 상태에서 SHORT를 차단한 정책이 가장 큰 알파를 없애고 있었다. SHORT 진입의 승률이 73%에 평균 +1.86%였는데, 이걸 통째로 막고 있었던 것이다. 반면 알트 LONG 차단은 승률 22.7%로 오히려 손실을 줄여주고 있었다 — 이건 유지하는 게 맞았다.
내 첫 가설은 “하락장이니까 롱만 살리자”였는데, 데이터는 정반대였다. 하락장에서 내 시스템이 가장 잘 맞는 건 SHORT 방향이었고, 그걸 금지해놓으니 남은 LONG 진입만으로는 손실을 메울 수 없었다.
해결: TRENDING_DOWN에서 SHORT 차단 정책을 제거했다. 알트 LONG 차단은 유지. 그리고 새로운 조건 하나를 추가했다 — SHORT 진입 시 레버리지를 1.5x로 제한하는 것(기존 LONG과 동일한 리스크 프로필).
검증: 수정 후 12시간 동안 가상 진입 43건을 모니터링한 결과 SHORT 진입 19건 중 14건이 수익 (73.6%), 차단 정책만 제거했을 뿐인데 전체 가상 승률이 58.3% → 67.1%로 올랐다.
남은 과제: 실제 거래 데이터는 여전히 25% 승률이다 — 가상과 실제의 갭을 분석해야 한다. 뭔가 더 있다는 뜻이다.
오늘의 한줄
데이터를 믿기로 했다면, 데이터가 말하는 방향으로 가설을 버리는 게 가장 빠른 수정이다 — 내 직관은 시장을 이기지 못했다.