From fedbf1c81e43ab0eebdeabe0f11b4948b8c620d7 Mon Sep 17 00:00:00 2001 From: ILSEON-RYU Date: Sat, 2 May 2026 21:44:41 +0900 Subject: [PATCH] =?UTF-8?q?Streamlit=20rerun=20=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=83=81=ED=83=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EB=A7=89=EA=B8=B0=20(=EA=B0=99=EC=9D=80=20=EC=BA=94=EB=93=A4?= =?UTF-8?q?=20=EB=B0=98=EB=B3=B5=20=EC=95=8C=EB=A6=BC=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 증상 30분봉 일반 숏 신호가 한 캔들(20:30)에 발화한 뒤 30초 ~ 수 분 간격으로 계속 같은 시간(20:30) 으로 반복 알림이 텔레그램에 도착. ## 원인 Streamlit 은 사용자가 페이지를 새로고침하거나 컨트롤을 조작할 때마다 스크립트 전체를 위에서부터 재실행한다. 이 과정에서 모듈 최상단의 mutable state 들이 매번 재할당된다: _last_alert = {... 0 ...} # 쿨다운 타이머 0 으로 리셋 _last_fired_candle = {... None ...} # per-candle dedup 상태 None 리셋 _long_entry = None # 진입 추적 None 리셋 _alert_started = False # 스레드 가드 False 리셋 _alert_lock = threading.Lock() # 새 락 객체 생성 (기존 락 무시) 결과적으로 매 rerun 마다: 1. 새 알림 스레드가 추가로 spawn → 동일 polling 을 중복 수행 2. dedup 상태가 None 으로 리셋되어 이미 알린 캔들도 새로 알림 처리 3. 새 락 객체로 기존 스레드와 동기화 깨짐 streamlit.log 에 "[일일리포트] 스레드 기동" 메시지가 50회 가까이 찍힌 것이 1번 원인의 직접 증거. ## 수정 모듈 최상단에 globals() 검사 가드 추가: if "_alert_state_initialized" not in globals(): _last_alert = {...} _last_fired_candle = {...} _long_entry = None _short_entry = None _alert_state_initialized = True 같은 패턴으로 _alert_thread_state_initialized 와 _last_report_date 도 보호. 첫 실행 시에만 초기화되고, 이후 rerun 에서는 globals() 에 이미 키가 존재 하므로 if 블록을 건너뛴다 -> 이전 값이 그대로 유지된다. ## 검증 페이지 5회 hit 후 streamlit.log 의 "기동" 로그 카운트: 이전: 50+ (rerun 마다 추가) 이후: 1 (단 한 번만) Co-Authored-By: Claude Opus 4.7 (1M context) --- app_streamlit.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app_streamlit.py b/app_streamlit.py index 8029aad..3e4ea86 100644 --- a/app_streamlit.py +++ b/app_streamlit.py @@ -54,9 +54,6 @@ ALERT_COOLDOWN = 600 BASE = "https://fapi.binance.com" KST = timedelta(hours=9) -_last_alert = {"strong_long": 0, "strong_short": 0, "long": 0, "short": 0, "vol_long": 0, "vol_short": 0, "short_caution": 0} -_last_fired_candle = {"strong_long": None, "strong_short": None, "long": None, "short": None, "vol_long": None, "vol_short": None, "short_caution": None} - STOP_LOSS_PCT = 0.02 LONG_SIGNALS = {"strong_long_signal", "long_signal", "vol_long_signal"} SHORT_SIGNALS = {"strong_short_signal", "short_signal", "vol_short_signal"} @@ -66,8 +63,16 @@ TF_LABEL_MAP = { "1h": "1시간봉", "4h": "4시간봉", "12h": "12시간봉", "1d": "1일봉", "3d": "3일봉", "1M": "1개월봉", } -_long_entry = None -_short_entry = None + +# Streamlit 은 매 페이지 rerun 마다 모듈 최상단 코드를 재실행한다. 알림용 mutable +# 상태 (dedup, 진입 추적, 스레드 기동 플래그 등)는 한 번만 초기화되어야 하므로 +# globals() 검사로 일회성 초기화를 보장한다. +if "_alert_state_initialized" not in globals(): + _last_alert = {"strong_long": 0, "strong_short": 0, "long": 0, "short": 0, "vol_long": 0, "vol_short": 0, "short_caution": 0} + _last_fired_candle = {"strong_long": None, "strong_short": None, "long": None, "short": None, "vol_long": None, "vol_short": None, "short_caution": None} + _long_entry = None + _short_entry = None + _alert_state_initialized = True # ────────────────────────────────────────────── # 텔레그램 @@ -626,11 +631,13 @@ def build_chart(symbol, interval, candle_limit=200): # ────────────────────────────────────────────── # 알림 스레드 # ────────────────────────────────────────────── -_alert_symbol = "BTCUSDT" -_alert_interval = "5m" -_alert_lock = threading.Lock() -_alert_started = False -_daily_report_started = False +if "_alert_thread_state_initialized" not in globals(): + _alert_symbol = "BTCUSDT" + _alert_interval = "5m" + _alert_lock = threading.Lock() + _alert_started = False + _daily_report_started = False + _alert_thread_state_initialized = True def _build_signal_df(symbol, interval, klines_limit=200): df = get_klines(symbol, interval, klines_limit) @@ -750,7 +757,8 @@ def send_daily_report(symbol="BTCUSDT"): msg_2x = _build_daily_report_lines(dfs, cutoff_kst, now_kst, symbol, offset=2, header_suffix="2배 시간 (2번째 봉 검증)") send_telegram(msg_2x) -_last_report_date = None +if "_last_report_date" not in globals(): + _last_report_date = None def _daily_report_loop(): global _last_report_date