6 Commits

Author SHA1 Message Date
chpark d16456cb92 사용자별 격리 시스템 + 사용자 관리 + 라이브 PnL%
# 사용자별 격리
- JWT 토큰에 uid 추가 (auth.get_uid 헬퍼)
- PostgreSQL — exchange_credentials/automation_config/trades/signal_events 에 user_id BIGINT
- SQLite user_settings 테이블 신설 (글로벌 settings 는 옛 호환)
- 모든 DB 함수 시그니처에 user_id 인자 추가 — 다른 사용자 데이터 절대 접근 불가
- alert_state — 모든 dict key 가 (user_id, ...) tuple 로 계층화
- core_logic alert_loop — 활성 사용자 순회 + 각자 settings/symbol/텔레그램 적용
- ensure_user_defaults() / ensure_user_automation() — 첫 사용 시 자동 시드

# 사용자 관리 (admin only)
- users_db: delete_user / admin_reset_password / set_role
- /api/users POST DELETE PUT password PUT role (본인 강등 / 마지막 admin 보호)
- /admin/users 페이지 — 등록/삭제/role 토글/비번 reset 모달
- 사이드바 adminOnly 필터 — admin role 만 메뉴 노출

# 대시보드 개선
- 모바일 / 범례 토글 (모바일 60 캔들, 데스크톱 200)
- 트레이드 이력: open 트레이드 실시간 PnL% (Binance ticker 호출 + 방향별 계산)
- 메트릭 카드 분리 (실거래 vs 실시간 open)

# 안정성
- api.ts: error.detail array/object 안전 처리 ([object Object] 방지)
- Chart.tsx: Plotly yaxis title 객체 형태 + 모바일 height 동적 조정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:14:23 +09:00
ILSEON-RYU 1ecd6241f2 Forming candle 진입 신호 안정성 요구 — 2 polls 연속 True 만 발사
## 문제
forming candle 동안 close 변동에 따라 신호 컬럼이 1폴링 동안 True →
다음 폴링 False 로 깜빡이는 케이스 다수. 1회 True 만으로 진입 알림
발사되어 곧바로 [취소 알림] 도착하는 패턴 반복. 사용자 22:04
5m/15m 취소 알림 폭주 사례.

## 수정
신호별 (interval, sig) 키로 연속 True polling 카운트 추적.
- forming candle 의 신호는 count >= 2 (= 60s 안정 유지) 일 때만 발사
- closed candle 의 신호는 1회로 즉시 발사 (data 확정이라 깜빡 X)
- 신호 False 로 바뀌면 count 리셋 (연속성 보장)
- per-candle dedup 와 cooldown 은 그대로 위에 적용

## 효과
- forming 깜빡 1회는 더 이상 알림 발사 X → false alert + 취소 알림 동반
  감소
- 진짜 신호는 2폴링 (60s) 동안 안정 유지하므로 통과 → latency 증가
  최대 30s
- 닫힌 캔들 알림은 latency 변화 없음

## 추적 state (alert_state.signal_seen_count)
{(interval, sig): {"candle_time": ts, "count": int}}
새 candle 진입 시 자동 리셋. False 시 카운트 0 으로 리셋.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:12:43 +09:00
ILSEON-RYU 3538b0324d 재시작 시 역사적 신호 burst 차단 — interval 별 silent sync
## 사례
Oracle systemd restart 후 첫 polling 에서 24h 내 tail(3) 안의 모든
True 신호가 한꺼번에 발사되어 텔레그램 채널에 알림 5~10건 burst.
사용자: "이거 지금 한번에 다 오게 해서 이 지랄 난거지 씨발 정신 없네"

## 원인
재시작 시 alert_state.last_fired_candle 가 빈 dict -> 첫 polling 의
tail(3) 검사에서 모든 신호가 "처음 보는 candle" 로 인식 -> 알림 발사.
이전에 본 candle 인지를 기억하는 매커니즘이 process restart 에서 끊김.

## 수정
alert_state.synced_intervals (set) 추가. check_and_alert 진입 시 해당
interval 이 set 에 없으면 (= 첫 처리):
1. 현재 df 의 모든 signal 컬럼 스캔
2. 각 signal 의 가장 최근 True candle 의 open_time 을 last_fired_candle
   에 silent set
3. set 에 interval 추가
4. return (알림 발사 안 함)

다음 polling 부터는 sync 된 dedup 상태 위에서 정상 동작 -> 새로 발화
된 candle 만 알림.

## 부작용
재시작 직후 가장 최근 candle 의 신호 1회 누락 (sync 가 dedup 처리해서
다음 polling 에 같은 candle 은 fire 안 함). 다음 candle 의 신호부터는
정상 알림. burst 폭주 vs "방금 막 발화한 신호 1회 놓침" 트레이드오프
에서 사용자 선호 따라 burst 차단 우선.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:29:11 +09:00
ILSEON-RYU b572758682 알림 스레드 multi-TF 모니터링 + per-(interval,key) state 분리
## 동작 변경
이전: 알림 스레드가 사용자 dropdown 선택 1개 시간봉만 polling.
이후: [1m, 3m, 5m, 15m, 30m, 1h] 6개 시간봉을 매 cycle 마다 순회 polling.
같은 신호가 여러 시간봉에서 발화하면 각 시간봉별로 독립 알림 (dedup 도
interval 별로 분리).

## 상태 구조 변경 (alert_state.py)
- last_alert : dict[(interval, key)] -> timestamp
- last_fired_candle : dict[(interval, key)] -> candle_time
- long_entry / short_entry : dict[interval] -> entry_record
  (TF 별로 진입 추적, 손절가 검증도 TF 별)

## 신호 정의는 변경 없음
OI 필터(oi_up / oi_up_2 / oi_down_2) 모두 원복 — 신호 정의는 의도된
대로 유지. "OI 하락 + 가격 하락 = 롱 청산" 케이스는 시스템 설계상
vol_short/strong_short 가 안 잡는 것이 정상 (별도 신호 추가 시 더
정밀한 분리 가능, 이번 변경에는 불포함).

## API 호출 부담
6 TF × ~4 endpoint per cycle = 24 calls / 30s = 48 calls/min. Binance
futures 1200/min limit 대비 4% 사용 — 안전.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:40:39 +09:00
ILSEON-RYU b27e2fcf51 Forming candle 알림 + 마감 후 재검증 + [취소 알림] 패턴
## 동작
이전 커밋 8fd47d0 에서 forming candle 을 알림 대상에서 아예 제외했었지만,
이제 사용자 요청에 따라 "실시간 알림 + 잘못되면 취소 알림" 방식으로 변경.

흐름:
1. 알림 스레드는 매 polling 마다 df.tail(3) (forming candle 포함) 으로
   신호 검사 -> 알림 발사. 빠른 반응 유지.
2. forming candle 기반으로 발사된 알림은 alert_state.pending_groups 에
   등록 (interval, candle_time, msg, sig_cols, direction 보관).
3. 다음 polling 부터, 그 candle 이 더 이상 forming 이 아니면 (=닫힘)
   동일 candle 의 신호 컬럼들을 다시 확인:
   - 하나라도 True 로 살아있음 -> 확정, pending 에서 제거 (조용히)
   - 모두 False 로 바뀜          -> [취소 알림] 발송 + 진입 추적 클리어

## 메시지 예
원래:
  🔽 일반 숏 진입 신호
  BTCUSDT 30분봉
  시간: 2026-05-04 09:30
  진입가: 78,318.10
  손절가: 78,905.49

캔들 마감 후 신호 사라진 경우:
  [취소 알림]
  🔽 일반 숏 진입 신호
  BTCUSDT 30분봉
  시간: 2026-05-04 09:30
  진입가: 78,318.10
  손절가: 78,905.49

## 부가
- pending entry 중 long_entry/short_entry 와 open_time 이 일치하면
  같이 None 으로 클리어 -> 잘못된 손절가 알림 방지.
- 다른 시간봉으로 polling 가는 동안에는 pending 항목 그대로 보존
  (interval 매칭 시점까지 대기). 시간봉 다시 돌아오면 그때 검증 시도.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:14:11 +09:00
ILSEON-RYU 7dcd9591dc 알림 상태를 별도 모듈로 분리해 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>
2026-05-02 21:54:01 +09:00