- 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>
13 KiB
BTC/ETH Futures Dashboard — 소스 분석
Streamlit 기반 Binance Futures 실시간 모니터링 대시보드. 차트 시각화 + 다중 시간축 자동 알림 (Telegram) + 일일 신호 통계 리포트를 한 프로세스에서 처리한다.
1. 파일 구성
| 파일 | 역할 |
|---|---|
| app_streamlit.py | 메인 앱 — 데이터 수집 / 지표 계산 / 신호 생성 / 차트 빌드 / 알림 스레드 / 사이드바 메뉴 (대시보드 / 트레이드 이력 / 설정) |
| alert_state.py | 멀티 rerun 환경에서 살아남는 mutable 상태 (sys.modules 캐싱 활용) |
| settings_db.py | SQLite key-value 설정 영속 저장 (텔레그램 토큰, 심볼, 시간축, 쿨다운, 손절가 비율, 알림 ON/OFF) |
| trades_db.py | PostgreSQL 트레이드 lifecycle — trades (진입→청산), signal_events (raw 신호 로그). DATABASE_URL 미설정 시 silent no-op |
| Dockerfile / docker-compose.yml | python:3.11-slim 컨테이너 + Postgres 16 + Traefik labels (junggomoa.com) |
| DEPLOY.md | 서버 배포 절차 |
| requirements.txt | streamlit, pandas, numpy, plotly, ta, requests, python-dotenv, urllib3, psycopg2-binary |
| assets/override.css | 라이트모드 강제 CSS |
| .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 라는 별도 모듈에 두어 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
st.set_page_config(반드시 import 직후)- 라이트모드 강제 CSS inline
- 텔레그램 토큰 / 채팅 ID 는
os.getenv로 로드 - 핵심 상수:
STOP_LOSS_PCT = 0.0075— 10x 레버리지 기준 ROI -7.5%LONG_SIGNALS/SHORT_SIGNALS— 신호 분류 setTF_LABEL_MAP— 11개 시간축 한글 라벨
(2) 텔레그램 송신 + 알림 코어 — 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) — 알림 코어 로직:
- Phase 0 — silent sync (재시작 후 첫 polling):
synced_intervals미포함 시 모든last_fired_candle만 채우고 알림 skip → 역사적 신호 burst 방지 - Phase 1 — pending 검증: forming candle 에서 발사된 알림은 매 polling 마다 신호 잔존 여부 확인. 사라지면 즉시
[취소 알림]발사 (캔들 마감까지 기다리지 않음) - Phase 2 — 신호 검사:
tail(3)만 검사- cooldown (10분) + per-candle dedup
- forming candle 안정성: 연속 2 polls True 만 발사 (
signal_seen_count) — 깜빡임 false alert 차단 - 닫힌 캔들은 즉시 발사 (data 확정)
- Phase 3 — group 발사: long / short / caution 그룹별로 한번에 묶어서 송신. 진입가 / 손절가 표시
- Phase 4 — 청산 권고: 30m / 1h 의 새 진입 신호만 반대 방향 진입에 대한
[반대 신호 감지 - 청산 권장]트리거 (5m / 15m 은 노이즈 多 → 폭주 방지) - Phase 5 — 손절가 알림: 현재가가 추적 중 진입의 stop 을 침범하면
[손절가알림]송신
(3) Binance 데이터 수집 — 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
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봉 추세 반대 + |
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)
(5) 차트 빌드 — L455–L741
build_chart(symbol, interval, candle_limit):
- 7-row Plotly subplot:
- 메인 — Candlestick + BB + MA7/25/99/200 + 모든 신호 마커 + Taker buy/sell 점 + L/S / FR / OI 오버레이
- Taker Buy/Sell Volume (Net Bar)
- Open Interest
- Funding Rate Bar (±0.5% / -0.7% 가이드 라인)
- Long/Short Ratio (탑트레이더)
- RSI / StochRSI (20/50/80 가이드)
- 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
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
KST 자정 통과 감지 시 4종 텔레그램 메시지 발송:
- 24h 신호 통계 (1배 시간) — 다음 봉에 반대 신호 떴는지
T/F카운트 + 승률 - 24h 신호 통계 (2배 시간) — 2번째 봉 검증
- 손절가 터치 횟수 — 진입 시
±STOP_LOSS_PCT가격이 이후 3봉 안에 터치됐는지 - 추세 꺾임 감지 통계 — 3봉 후 close 가 의도한 방향으로 갔는지
리포트 시간축: ["5m", "15m", "30m", "1h", "4h"]. 각 시간축 별 DAILY_REPORT_KLINES_LIMIT 만큼 가져와 24h 윈도우 분석.
(8) 메인 UI — 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. 환경 / 실행
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분 캔들이라 충분하지만, 순간적 네트워크 지연 시 초과 시간축에서는 신호 누락 가능.