c4e6aab7b2
- backend/ — FastAPI + JWT + 모든 REST 엔드포인트 - frontend/ — Next.js 14 + Tailwind + 7페이지 (대시보드/트레이드/거래소/자동매매/설정/내정보/로그인) - core_logic.py — 신호계산/알림 로직 분리 (기존 app_streamlit.py 에서 추출) - users_db.py + bcrypt 인증, exchange_keys.py + Fernet 암호화 - trades_db.py — 진입/청산 lifecycle 추적, signal_events raw 로그 - settings_db.py — 모든 운영 파라미터 DB 영속 저장 (RSI/거래량/펀딩비 임계값 포함) - docker-compose: frontend / backend / postgres + Traefik 라우팅 - assets/logo.svg — JUNGGOMOA 그라디언트 로고 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
231 lines
13 KiB
Markdown
231 lines
13 KiB
Markdown
# 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분 캔들이라 충분하지만, 순간적 네트워크 지연 시 초과 시간축에서는 신호 누락 가능.
|