알림 상태를 별도 모듈로 분리해 Streamlit rerun 영향 차단

## 배경
이전 커밋 fedbf1c 에서 globals() 검사 가드로 rerun 시 상태 초기화를 막으려
했으나 동작하지 않음. streamlit.log 의 "[일일리포트] 스레드 기동" 메시지가
restart 후에도 12 회 이상 누적된 것이 증거.

## 원인
Streamlit 은 rerun 시 메인 스크립트를 새 namespace 에서 exec() 한다.
이 namespace 는 매번 새로 만들어지므로 globals() 안에는 이전 run 의
변수가 존재하지 않음 -> 가드가 항상 True 분기를 타고 mutable state 가
매번 초기화됨. 그 결과:
- 알림 dedup 상태 (last_fired_candle) 가 None 으로 리셋
- 진입 추적 (long_entry / short_entry) 가 None 으로 리셋
- 스레드 기동 가드 (alert_started) 가 False 로 리셋 -> 새 스레드 spawn
- threading.Lock() 도 매번 새로 생성되어 동기화 깨짐

이로 인해 같은 캔들 (예: 30분봉 20:30 일반 숏) 알림이 텔레그램으로 30초
간격 반복 발사되는 증상 발생.

## 수정
mutable state 를 별도 alert_state.py 모듈로 분리. 메인 스크립트는
"import alert_state" 만 실행하는데, 이 import 는 sys.modules 캐싱
덕분에 첫 실행 후 노옵 -> alert_state 모듈은 process lifetime 동안
같은 객체이며 그 attribute 들은 보존된다. 메인 스크립트 namespace 가
매번 새로 만들어져도 alert_state 의 state 는 영향받지 않음.

상태 항목:
- last_alert        (signal type 별 마지막 발사 시각, 쿨다운용)
- last_fired_candle (signal type 별 마지막 발사 캔들 open_time, dedup)
- long_entry / short_entry (진입 추적)
- alert_lock, alert_symbol, alert_interval (스레드 동기화 + UI -> 스레드)
- alert_started, daily_report_started (스레드 1회 기동 가드)
- last_report_date (자정 통과 감지용)

## 검증
import 동작 확인 (별도 process 에서):
- 같은 모듈 객체 (s1 is s2)
- 같은 dict 객체 (s1.last_fired_candle is s2.last_fired_candle)
- attribute set/get 양쪽에 반영

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILSEON-RYU
2026-05-02 21:54:01 +09:00
parent fedbf1c81e
commit 7dcd9591dc
2 changed files with 67 additions and 53 deletions
+34
View File
@@ -0,0 +1,34 @@
"""
Streamlit 의 매 rerun 마다 메인 스크립트는 새 namespace 에서 재실행되어
모듈 최상단의 mutable state 가 모두 초기화된다 (`globals()` 가드도 우회됨).
이 파일은 별도 모듈로 단 한 번만 import 되므로 (Python 의 sys.modules 캐싱)
state 가 process lifetime 동안 보존된다. 알림 dedup, 진입 추적, 스레드
기동 가드 등 다중 rerun 환경에서 살아남아야 하는 모든 mutable 상태는
여기에 둔다.
"""
import threading
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_symbol = "BTCUSDT"
alert_interval = "5m"
alert_lock = threading.Lock()
alert_started = False
daily_report_started = False
last_report_date = None