# BTC/ETH Futures Dashboard — 소스 분석 Streamlit 기반 Binance Futures 실시간 모니터링 대시보드. 차트 시각화 + 다중 시간축 자동 알림 (Telegram) + 일일 신호 통계 리포트를 한 프로세스에서 처리한다. --- ## 1. 파일 구성 | 파일 | 역할 | |---|---| | [app_streamlit.py](app_streamlit.py) | 메인 앱 — 데이터 수집 / 지표 계산 / 신호 생성 / 차트 빌드 / 알림 스레드 / 사이드바 메뉴 (대시보드 / 트레이드 이력 / 설정) | | [alert_state.py](alert_state.py) | 멀티 rerun 환경에서 살아남는 mutable 상태 (`sys.modules` 캐싱 활용) | | [settings_db.py](settings_db.py) | SQLite key-value 설정 영속 저장 (텔레그램 토큰, 심볼, 시간축, 쿨다운, 손절가 비율, 알림 ON/OFF) | | [trades_db.py](trades_db.py) | PostgreSQL 트레이드 lifecycle — `trades` (진입→청산), `signal_events` (raw 신호 로그). DATABASE_URL 미설정 시 silent no-op | | [Dockerfile](Dockerfile) / [docker-compose.yml](docker-compose.yml) | python:3.11-slim 컨테이너 + Postgres 16 + Traefik labels (junggomoa.com) | | [DEPLOY.md](DEPLOY.md) | 서버 배포 절차 | | [requirements.txt](requirements.txt) | streamlit, pandas, numpy, plotly, ta, requests, python-dotenv, urllib3, psycopg2-binary | | [assets/override.css](assets/override.css) | 라이트모드 강제 CSS | | [.env.example](.env.example) | 환경변수 템플릿 (`TELEGRAM_TOKEN`, `TELEGRAM_CHAT_ID`) | --- ## 2. 아키텍처 전체 흐름 ``` ┌─────────────── Streamlit 메인 프로세스 ───────────────┐ │ │ │ ┌── UI Thread (main()) ──┐ ┌── Background Threads ─┐ │ │ 매 rerun 마다 실행: │ │ daemon thread × 2 │ │ │ - build_chart() │ │ │ │ │ - st.plotly_chart │ │ _alert_loop │ │ │ - time.sleep + rerun │ │ (30초 주기) │ │ └────────────────────────┘ │ │ │ │ │ _daily_report_loop │ │ ▼ │ (60초 주기, KST 자정)│ │ ┌─────────────┐ └───────┬───────────────┘ │ │ alert_state │ ◀───────────────────┘ │ │ (sys.modules│ │ │ 캐싱 보존) │ │ └─────────────┘ │ │ │ 외부 호출: Binance Futures API + Telegram Bot API │ └─────────────────────────────────────────────────────────┘ ``` **핵심 설계 결정**: Streamlit 의 매 rerun 은 메인 스크립트를 새 namespace 에서 재실행해 모듈 최상단 globals 가 모두 초기화된다. mutable 상태 ( dedup, 진입 추적, 스레드 가드 ) 는 [alert_state.py](alert_state.py) 라는 별도 모듈에 두어 `sys.modules` 캐싱으로 process lifetime 동안 보존되도록 분리. --- ## 3. 모듈 상세 ### 3.1 `alert_state.py` — 보존 상태 | 변수 | 타입 | 용도 | |---|---|---| | `last_alert` | `dict[(interval, key), float]` | 알림 cooldown (default 600s) | | `last_fired_candle` | `dict[(interval, key), Timestamp]` | per-candle dedup | | `long_entry` / `short_entry` | `dict[interval, record]` | 진입 추적 (손절/청산 권고용) | | `pending_groups` | `list[dict]` | forming candle 발사 후 신호 재검증 큐 | | `synced_intervals` | `set[str]` | 재시작 직후 역사적 신호 burst 차단용 sync 플래그 | | `signal_seen_count` | `dict[(interval, sig), {candle_time, count}]` | forming candle 의 연속 True polling 카운트 (false alert 차단) | | `alert_lock` | `threading.Lock` | UI ↔ alert thread 공유 변수 보호 | | `alert_started` / `daily_report_started` | `bool` | 스레드 중복 기동 가드 | ### 3.2 `app_streamlit.py` 섹션 분해 #### (1) 환경 설정 & 페이지 셋업 — [L1–L70](app_streamlit.py#L1-L70) - `st.set_page_config` (반드시 import 직후) - 라이트모드 강제 CSS inline - 텔레그램 토큰 / 채팅 ID 는 `os.getenv` 로 로드 - 핵심 상수: - `STOP_LOSS_PCT = 0.0075` — 10x 레버리지 기준 ROI -7.5% - `LONG_SIGNALS` / `SHORT_SIGNALS` — 신호 분류 set - `TF_LABEL_MAP` — 11개 시간축 한글 라벨 #### (2) 텔레그램 송신 + 알림 코어 — [L74–L263](app_streamlit.py#L74-L263) `SIG_DEFS` (9종 신호): - `strong_long_signal` / `strong_short_signal` — 다중 필터 통과 - `long_signal` / `short_signal` — 일반 진입 - `vol_long_signal` / `vol_short_signal` — 볼륨 급등 - `reversal_long_signal` / `reversal_short_signal` — 추세 꺾임 감지 - `short_caution_signal` — 극단 펀딩비 + OI 하락 (숏 주의) `check_and_alert(df, symbol, interval)` — 알림 코어 로직: 1. **Phase 0 — silent sync** (재시작 후 첫 polling): `synced_intervals` 미포함 시 모든 `last_fired_candle` 만 채우고 알림 skip → 역사적 신호 burst 방지 2. **Phase 1 — pending 검증**: forming candle 에서 발사된 알림은 매 polling 마다 신호 잔존 여부 확인. 사라지면 즉시 `[취소 알림]` 발사 (캔들 마감까지 기다리지 않음) 3. **Phase 2 — 신호 검사**: - `tail(3)` 만 검사 - cooldown (10분) + per-candle dedup - **forming candle 안정성**: 연속 2 polls True 만 발사 (`signal_seen_count`) — 깜빡임 false alert 차단 - 닫힌 캔들은 즉시 발사 (data 확정) 4. **Phase 3 — group 발사**: long / short / caution 그룹별로 한번에 묶어서 송신. 진입가 / 손절가 표시 5. **Phase 4 — 청산 권고**: 30m / 1h 의 새 진입 신호만 반대 방향 진입에 대한 `[반대 신호 감지 - 청산 권장]` 트리거 (5m / 15m 은 노이즈 多 → 폭주 방지) 6. **Phase 5 — 손절가 알림**: 현재가가 추적 중 진입의 stop 을 침범하면 `[손절가알림]` 송신 #### (3) Binance 데이터 수집 — [L267–L311](app_streamlit.py#L267-L311) | 함수 | 엔드포인트 | 반환 | |---|---|---| | `get_klines` | `/fapi/v1/klines` | OHLCV + taker buy/sell volume | | `get_funding_rate` | `/fapi/v1/fundingRate` | FR (%, 100배 환산) | | `get_open_interest_history` | `/futures/data/openInterestHist` | OI | | `get_long_short_ratio` | `/futures/data/topLongShortPositionRatio` | 탑트레이더 L/S ratio | | `get_taker_buy_sell_ratio` | `/futures/data/takerlongshortRatio` | (코드상 정의되나 차트엔 미사용) | 모든 시각은 KST (`+9h`). #### (4) 지표 + 신호 계산 — [L315–L450](app_streamlit.py#L315-L450) `compute_indicators(df, interval)` — 표준 TA: - MA 7 / 25 / 99 / 200 - BB (20, 2σ) — mid / upper / lower - RSI(14), StochRSI(14, 3, 3) - MACD(12, 26, 9) — line / signal / histogram - ATR(14) `compute_signals(df, interval)` — 신호 정의: | 신호 | 조건 | |---|---| | `long_signal` | `bull_ma_2` (close > MA7 & MA25) & RSI<75 & MACD_hist↑ & close > BB_mid & body% ≥ +0.2% | | `short_signal` | `bear_ma_2` & RSI>25 & MACD_hist↓ & close < BB_mid & body% ≤ -0.2% | | `strong_long_signal` | `bull_ma_2` & RSI<65 & MACD_hist↑ & oi_up_2 & taker_buy_2 & fr_long_favor & 양봉 | | `strong_short_signal` | `bear_ma_2` & RSI>35 & MACD_hist↓ & oi_down_2 & taker_sell_2 & fr_short_favor & 음봉 | | `vol_long_signal` | buy_net > avg×2 & taker_buy_vol > min × oi_active (interval 별 min 가변) | | `vol_short_signal` | sell_net 동일 미러 | | `short_caution_signal` | `oi_down_2` & FR ≤ -0.007% (극단 음수) | | `reversal_long/short_signal` | 직전 3봉 추세 반대 + |body|≥0.3% + 거래량 1.3× | | `exhaustion_long/short` | 직전봉 거래량 spike (vol > avg×3) + 매도/매수 우세 | 쿨다운: 신호별로 `rolling(N, sum).shift(1)==0` 패턴으로 N봉 내 중복 차단. > **MA 정렬 요구 완화**: 추세 반전 직후엔 MA7>MA25 정렬이 늦게 형성되어 `bull_ma` (3중 정렬) 대신 `bull_ma_2` (2중 정렬) 만 요구. (커밋 [d49ac84](app_streamlit.py)) #### (5) 차트 빌드 — [L455–L741](app_streamlit.py#L455-L741) `build_chart(symbol, interval, candle_limit)`: - 7-row Plotly subplot: 1. **메인** — Candlestick + BB + MA7/25/99/200 + 모든 신호 마커 + Taker buy/sell 점 + L/S / FR / OI 오버레이 2. Taker Buy/Sell Volume (Net Bar) 3. Open Interest 4. Funding Rate Bar (±0.5% / -0.7% 가이드 라인) 5. Long/Short Ratio (탑트레이더) 6. RSI / StochRSI (20/50/80 가이드) 7. MACD (Line + Signal + Histogram) 데이터 머지: OI / FR / L/S 는 시간 정렬 후 `floor()` + `merge` + `ffill`. FR 은 1h 기준, 나머지는 interval (혹은 5m fallback) 기준. **1시간 추세 컨텍스트**: 비-1h 시간축에서 별도로 1h 캔들 가져와 `h1_bull/bear` 컬럼을 생성, 같은 일자 안에서 ffill 로 채움 (현재 코드에선 차트 표시 X — 보존만). 신호 마커: - 롱 → `low × 0.9998` 위치, 위 화살표 - 숏 → `high × 1.0002` 위치, 아래 화살표 - caution → diamond, exhaustion → star 축 자동 스케일: `tight()` + 분위수 기반. #### (6) 알림 백그라운드 스레드 — [L774–L786](app_streamlit.py#L774-L786) ```python ALERT_TIMEFRAMES = ["5m", "15m", "30m", "1h"] def _alert_loop(): while True: for interval in ALERT_TIMEFRAMES: df = _build_signal_df(symbol, interval, 200) check_and_alert(df, symbol, interval) time.sleep(30) ``` UI 에서 선택한 symbol 만 추적 (`alert_state.alert_symbol`, lock 으로 보호). 4개 시간축 모두 동시 모니터링. #### (7) 일일 리포트 — [L791–L1005](app_streamlit.py#L791-L1005) KST 자정 통과 감지 시 4종 텔레그램 메시지 발송: 1. **24h 신호 통계 (1배 시간)** — 다음 봉에 반대 신호 떴는지 `T/F` 카운트 + 승률 2. **24h 신호 통계 (2배 시간)** — 2번째 봉 검증 3. **손절가 터치 횟수** — 진입 시 `±STOP_LOSS_PCT` 가격이 이후 3봉 안에 터치됐는지 4. **추세 꺾임 감지 통계** — 3봉 후 close 가 의도한 방향으로 갔는지 리포트 시간축: `["5m", "15m", "30m", "1h", "4h"]`. 각 시간축 별 `DAILY_REPORT_KLINES_LIMIT` 만큼 가져와 24h 윈도우 분석. #### (8) 메인 UI — [L1010–L1085](app_streamlit.py#L1010-L1085) - 5-column 헤더: 심볼 / 시간축 / 갱신주기(초) / 새로고침·자동갱신·범례·모바일 토글 - **Candle 수**: 데스크톱 53, 모바일 14 (`mobile_mode` 토글) - 펀딩비 경고 배너 (≤-0.7% 위험 / ≤-0.5% 경보 / ≥+0.5% 롱 과열) - Plotly 차트: scrollZoom, doubleClick reset - `auto` 활성화 시 `time.sleep(refresh_sec)` + `st.rerun()` --- ## 4. 동시성 / 안전성 설계 | 위험 요소 | 대응 | |---|---| | Streamlit rerun 마다 globals 초기화 | `alert_state` 모듈 분리 (`sys.modules` 캐싱) | | 스레드 중복 기동 | `alert_started` / `daily_report_started` bool 가드 | | UI ↔ alert thread 변수 충돌 | `alert_state.alert_lock` | | 재시작 직후 역사적 신호 burst | `synced_intervals` silent sync (첫 polling 은 dedup 만 채우고 alert 미발사) | | forming candle 깜빡임 false alert | `signal_seen_count` 연속 2 polls True 요구 | | forming candle 발사 후 신호 사라짐 | `pending_groups` Phase 1 검증 → `[취소 알림]` 즉시 발사 | | 청산 권고 폭주 (변동성 큰 날) | 30m / 1h 만 트리거 | | Binance API 일시 오류 | `try/except` 로 모든 보조 API 감싸고 `pass` (메인 klines 만 필수) | --- ## 5. 환경 / 실행 ```bash pip install -r requirements.txt cp .env.example .env # TELEGRAM_TOKEN, TELEGRAM_CHAT_ID 채우기 streamlit run app_streamlit.py ``` 기본 포트 `8501`. 환경변수 미설정 시 알림은 silent fail (콘솔에만 에러 출력). --- ## 6. 개선 가능 지점 (관찰만) - **신호 컬럼 마커 분기 로직 (L604–L641)** — `pd.Series([False]*len(df))` fallback 패턴 반복. helper 추출 가능. - **OI / FR / L/S 머지 로직 (L479–L510, L750–L770)** — 4번 비슷하게 반복. `merge_external_metric()` 함수로 추출 가능. - **`get_taker_buy_sell_ratio`** — 정의돼 있으나 호출처 없음 (dead code). - **`exhaustion_long/short`** — `compute_signals` 에서 계산되나 SIG_DEFS / 알림 대상엔 미포함 (차트 마커로만 표시). - **`h1_bull/bear` 컨텍스트** — 계산되나 차트/신호에서 사용 X. - **`requests.get(verify=False)` + `urllib3.disable_warnings`** — TLS 검증 비활성. 운영 환경에선 검토 필요. - **`recent = df.tail(3)`** — 30초 폴링 + N분 캔들이라 충분하지만, 순간적 네트워크 지연 시 초과 시간축에서는 신호 누락 가능.