Compare commits

..

101 Commits

Author SHA1 Message Date
hyowons 657969d115 auto: 일일 백업 2026-10-09 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 02:00:04 +09:00
hyowons 4f63ec56dc auto: 일일 백업 2026-10-08 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 02:00:06 +09:00
hyowons 114a1ef816 웹 채팅: 에이전트 선택 버튼·화면 고정·상태 보기·코디 세션 열기
- 하단 탭 → 입력창 왼쪽 에이전트 선택 버튼(목록 팝업, 안 읽은 답 빨간 점)
- 모바일 약 80% 크기(대화 본문 13.5px), iOS 입력 확대 방지
- 상단·하단 프레임 고정(페이지 스크롤 차단, 키보드 시 visualViewport 맞춤)
- ⋯ 메뉴에 상태 보기(/status)
- 🛠 코디: remote-control 세션 기동/재사용 후 Claude 앱 링크 제공

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:41:08 +09:00
hyowons 405a0a3228 CLAUDE.md: 자산웹 공모주 탭에 비하이브 공모청약 영상 요약 설명 추가
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:26:54 +09:00
hyowons 86e38478dd 웹 채팅(chat.hyowons.net) 추가 — 클로·레이·골디와 대화
- workspace/scripts/chat_web.py: 표준 라이브러리 단일 서버(포트 18793, launchd ai.openclaw.chat-web)
- 로그인: iMessage OTP 단독 + 30일 세션, 코드 발송 1분 1회·1시간 5회 제한, 실패 시 잠금+텔레그램 알림
- 에이전트 호출은 openclaw agent --session-key web… (텔레그램과 별도 맥락, 답은 웹에만)
- /clear(웹 전용), 선택지 버튼([[버튼: A | B]] 지정 + 문장 추출 폴백), 홈 화면 아이콘
- CLAUDE.md·TASKS.md(헬스체크 daemon 등록)·클로 MEMORY.md 문서화

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:26:03 +09:00
hyowons 5fb39272f5 auto: 일일 백업 2026-10-07 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 02:00:05 +09:00
hyowons 708961b449 claude-code-session: 프로필 한글 별칭(aliases) 지원
'아베르노 세션 열어줘' → develop2025 프로필로 매핑.
별칭은 state/claude_sessions.json 의 profiles.<name>.aliases 에 둔다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:32:05 +09:00
hyowons 5384b1d670 auto: 일일 백업 2026-10-03 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 02:00:04 +09:00
hyowons 5e47a15547 자산웹 공모주 탭 추가 + 상장일 관심종목 자동등록
- 자산정보 > 공모주 sub-tab: 오늘청약(증권사별 아이템, 기본 접힘) / 청약완료(상장일까지)
- 관리자(청약)·가희(청약) 버튼 — 사람별 종목당 1건 기록 (state/behive_ipo_subscribed.json)
- 타이틀 [청약] 배지: 미완료 청약 또는 청약 종목 상장일
- ipo_alert listing(08:30)에서 청약완료 종목을 상장일에 관심종목 등록
- 데이터는 네이버 IPO API, 증거금·수수료는 ipo_alert._manager_blocks 재사용(키움 콜 0)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:32:15 +09:00
hyowons b404af9020 auto: 일일 백업 2026-10-02 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 02:00:07 +09:00
hyowons 74dcab1ba6 auto: 일일 백업 2026-10-01 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-01 02:00:03 +09:00
hyowons d8a778cf67 auto: 일일 백업 2026-09-30 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-30 02:00:03 +09:00
hyowons 1c8af0b523 auto: 일일 백업 2026-09-29 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-29 02:00:06 +09:00
hyowons 9a10822533 auto: 일일 백업 2026-09-26 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-26 02:00:05 +09:00
hyowons 29ca0d7ac1 auto: 일일 백업 2026-09-24 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 02:00:06 +09:00
hyowons 0157b24a55 auto: 일일 백업 2026-09-23 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-23 02:00:05 +09:00
hyowons 408a3e0d4a auto: 일일 백업 2026-09-22 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-22 02:00:05 +09:00
hyowons 8e12b4027c auto: 일일 백업 2026-09-19 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 02:00:06 +09:00
hyowons e3c5a92931 auto: 일일 백업 2026-09-18 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 02:00:05 +09:00
hyowons 75f70bc6ed auto: 일일 백업 2026-09-17 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-17 02:00:05 +09:00
hyowons 07d750807c auto: 일일 백업 2026-09-16 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-16 02:00:06 +09:00
hyowons 59b85ac77b auto: 일일 백업 2026-09-15 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 02:00:06 +09:00
hyowons 34498c554e auto: 일일 백업 2026-09-12 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-12 02:00:06 +09:00
hyowons b4f510d1ad auto: 일일 백업 2026-09-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-11 02:00:03 +09:00
hyowons 92d0d96368 auto: 일일 백업 2026-09-10 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 02:00:03 +09:00
hyowons 3e62670b24 auto: 일일 백업 2026-09-09 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 02:00:03 +09:00
hyowons db1639596b auto: 일일 백업 2026-09-08 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 02:00:02 +09:00
hyowons d786f75487 auto: 일일 백업 2026-09-06 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-06 02:00:01 +09:00
hyowons b610d01410 auto: 일일 백업 2026-09-05 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-05 02:00:03 +09:00
hyowons f17da6d62a auto: 일일 백업 2026-09-04 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 02:00:06 +09:00
hyowons 48889f0403 auto: 일일 백업 2026-09-03 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 02:00:04 +09:00
hyowons 21f9e50d8f auto: 일일 백업 2026-09-02 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 02:00:06 +09:00
hyowons 19b6797e47 auto: 일일 백업 2026-09-01 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 02:00:00 +09:00
hyowons ebe348c766 auto: 일일 백업 2026-08-30 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 02:00:06 +09:00
hyowons 4639a2d298 auto: 일일 백업 2026-08-29 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 02:00:02 +09:00
hyowons 9e5b1c211e auto: 일일 백업 2026-08-28 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 02:00:06 +09:00
hyowons 5eb280201d auto: 일일 백업 2026-08-27 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 02:00:05 +09:00
hyowons 992b85005d auto: 일일 백업 2026-08-26 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-26 02:00:02 +09:00
hyowons 600be2ee96 auto: 일일 백업 2026-08-25 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 02:00:06 +09:00
hyowons 8eed94c044 auto: 일일 백업 2026-08-23 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-23 02:00:02 +09:00
hyowons ea6000d343 auto: 일일 백업 2026-08-22 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 02:00:05 +09:00
hyowons 815b8cc6c4 auto: 일일 백업 2026-08-21 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 02:00:05 +09:00
hyowons 4321b38e30 auto: 일일 백업 2026-08-20 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 02:00:06 +09:00
hyowons ad5ed12f83 auto: 일일 백업 2026-08-19 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 02:00:06 +09:00
hyowons d48cba69ec auto: 일일 백업 2026-08-18 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 02:00:05 +09:00
hyowons dc261263e3 auto: 일일 백업 2026-08-16 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 02:00:01 +09:00
hyowons cd3f60378b auto: 일일 백업 2026-08-15 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 02:00:02 +09:00
hyowons 57aeb29227 auto: 일일 백업 2026-08-14 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 02:00:05 +09:00
hyowons 6428846506 auto: 일일 백업 2026-08-13 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 02:00:00 +09:00
hyowons c93d1ac385 auto: 일일 백업 2026-08-12 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 02:00:02 +09:00
hyowons 49a0651f05 auto: 일일 백업 2026-08-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 02:00:02 +09:00
hyowons d5da99ed70 auto: 일일 백업 2026-08-10 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-10 02:00:00 +09:00
hyowons 6309d760f0 auto: 일일 백업 2026-08-09 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 02:00:01 +09:00
hyowons e30a0b74e1 auto: 일일 백업 2026-08-08 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-08 02:00:04 +09:00
hyowons f38a3878b1 auto: 일일 백업 2026-08-07 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 02:00:02 +09:00
hyowons 307f7844c4 auto: 일일 백업 2026-08-06 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 02:00:01 +09:00
hyowons c03b3f2659 auto: 일일 백업 2026-08-05 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 02:00:06 +09:00
hyowons 7e7d759b34 auto: 일일 백업 2026-08-04 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 02:00:05 +09:00
hyowons 153ef20ae5 auto: 일일 백업 2026-08-03 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 02:00:02 +09:00
hyowons 5009ed19b7 auto: 일일 백업 2026-08-02 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 02:00:01 +09:00
hyowons 30be97ab2f auto: 일일 백업 2026-08-01 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 02:00:02 +09:00
hyowons d7042430c3 auto: 일일 백업 2026-07-31 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 02:00:02 +09:00
hyowons a5f6b0ecd8 auto: 일일 백업 2026-07-30 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 02:00:02 +09:00
hyowons c5dd2f98a8 auto: 일일 백업 2026-07-29 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 02:00:02 +09:00
hyowons f3a5858597 auto: 일일 백업 2026-07-28 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 02:00:02 +09:00
hyowons 748e9bb482 auto: 일일 백업 2026-07-27 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 02:00:02 +09:00
hyowons 1f0a2c8971 auto: 일일 백업 2026-07-26 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:00:01 +09:00
hyowons a13133aabe auto: 일일 백업 2026-07-25 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 02:00:04 +09:00
hyowons 5410009bea auto: 일일 백업 2026-07-24 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 02:00:02 +09:00
hyowons 8c23849952 auto: 일일 백업 2026-07-23 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:00:04 +09:00
hyowons 2cf5b38da1 auto: 일일 백업 2026-07-22 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 02:00:04 +09:00
hyowons 1d6fe0f70d auto: 일일 백업 2026-07-18 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 02:00:00 +09:00
hyowons bf55377fdc auto: 일일 백업 2026-07-17 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:00:02 +09:00
hyowons 9948122ed3 auto: 일일 백업 2026-07-16 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 02:00:03 +09:00
hyowons 66d372b50e auto: 일일 백업 2026-07-15 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:00:02 +09:00
hyowons ed73ed8b18 auto: 일일 백업 2026-07-14 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 02:00:03 +09:00
hyowons 3959401a65 auto: 일일 백업 2026-07-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 02:00:02 +09:00
hyowons 76e2359216 auto: 일일 백업 2026-07-10 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 02:00:00 +09:00
hyowons 434b379c13 auto: 일일 백업 2026-07-09 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 02:00:05 +09:00
hyowons 212ed517ee auto: 일일 백업 2026-07-08 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 02:00:05 +09:00
hyowons 85674763a3 auto: 일일 백업 2026-07-07 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:00:04 +09:00
hyowons 008ce8be65 auto: 일일 백업 2026-07-04 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 02:00:03 +09:00
hyowons dd1ad7d964 auto: 일일 백업 2026-07-03 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 02:00:02 +09:00
hyowons 300a4c9ca9 feat(budget): 가희 증권 자산 후잉·결산 반영 (securities_balance v2)
- send_balance_to_budget.py: 본인+가희 계좌 소유자별(by_owner) 집계, schema v1→v2
- inbox_handler.py: OWNER_ASSET 맵으로 소유자별 reconcile(self→증권(효원)/gahee→증권(가희)),
  asset_name 기반 출력, v2 payload 검증 추가(v1 하위호환 유지)
- monthly_settlement.py: 텔레그램 reconcile 라인 소유자별 라벨
- INBOX_TOPICS.md / CLAUDE.md: v2 스키마 문서화

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:51:15 +09:00
hyowons 213c0e0797 auto: 일일 백업 2026-07-02 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 02:00:02 +09:00
hyowons 565689df25 auto: 일일 백업 2026-07-01 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 02:00:02 +09:00
hyowons 1bbb61520c auto: 일일 백업 2026-06-30 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 02:00:02 +09:00
hyowons 17ad101eb1 auto: 일일 백업 2026-06-29 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:00:02 +09:00
hyowons 790effdf0e auto: 일일 백업 2026-06-28 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:00:01 +09:00
hyowons 6c89c54fe5 auto: 일일 백업 2026-06-27 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 02:00:02 +09:00
hyowons 03989e9df4 auto: 일일 백업 2026-06-26 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 02:00:04 +09:00
hyowons c15c3a1fec auto: 일일 백업 2026-06-25 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 02:00:02 +09:00
hyowons 21f4f07cbb auto: 일일 백업 2026-06-24 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 02:00:02 +09:00
hyowons cee32e6dfb auto: 일일 백업 2026-06-23 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:00:01 +09:00
hyowons 40cc290035 auto: 일일 백업 2026-06-21 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 02:00:02 +09:00
hyowons 5a11a21562 auto: 일일 백업 2026-06-20 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:00:05 +09:00
hyowons dc537f9925 auto: 일일 백업 2026-06-19 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 02:00:06 +09:00
hyowons 70e2251995 auto: 일일 백업 2026-06-18 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 02:00:02 +09:00
hyowons b3696949f5 auto: 일일 백업 2026-06-17 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 02:00:02 +09:00
hyowons eaa276b26d feat(sim): 물타기(애버리징) 토글 — 하락 시 손절 대신 평단↓ 추가매수
- config: AVERAGING_ENTRY·AVERAGING_DROP_PCT(0.10)·CRASH_DROP_PCT(0.10)
- signals.evaluate_holding: market_ok 인자 + 물타기 분기 (직전매수가 -10% 하락 시 ADD-down, 한도/강세장/비급락 조건, 당일 -10%급락은 손절·약세장 금지)
- engine·backtest: add_kind='down'이면 손절선 낮은 평단 기준 재설정(up은 위로 래칫 유지), engine은 market_ok 전달
- 첫 매수는 기존 검증 진입 그대로(우량주만 물타기)
- 백테스트: 즉시매수와 반대로 유리(10/20/60 수익·승률↑) → 라이브 변이 3개(v301/302/303) 등록, 총 223
- 웹 '물타기' 칩 + 설명 토스트

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:30:07 +09:00
hyowons 70f6b9a34d feat(sim): 정렬 토글(오름/내림)·40일선 1등전략(v300) 복구·보유배지 📦
- sim_web 비교탭 정렬: 손익/평가/보유/거래순 클릭마다 오름↔내림 토글 (기본 내림, 평가순만 오름), 활성버튼 화살표 표시
- 40일선 1등 전략 v62 파라미터 복구 → v300 (새 계좌, 220개)
- behive_web 보유 배지: '보유 xx 주' → '📦 xx' (폰트 8px, sim웹과 통일)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 12:16:08 +09:00
458 changed files with 60029 additions and 11069 deletions
+9
View File
@@ -21,6 +21,8 @@ node_modules/
exec-approvals.json
update-check.json
.claude/
# 설정 저널 서명키 (2026.9.3 신규) — 32B 바이너리, 권한 600
config-journal-fingerprint.key
# 에이전트별 auth / 런타임 (auth-profiles, codex-home, 세션, inbox 메시지)
/agents/*/agent/
@@ -60,6 +62,13 @@ update-check.json
/subagents/
/.playwright-mcp/
# ----- 2026.9.3 업그레이드 산물 (런타임·마이그레이션 잔재) -----
# doctor 가 남기는 `<원본>.migrated.<hash>.<uuid>` 백업. 롤백용이라 파일은 두되 추적은 안 한다
*.migrated.*
/agent/
/session-sqlite-migration-runs/
**/session-sqlite-import-archive/
# 플러그인 · 플로우 · 태스크 런타임
/flows/
/tasks/
+69 -25
View File
File diff suppressed because one or more lines are too long
+30 -20
View File
@@ -11,44 +11,53 @@ OpenClaw 에이전트 간 파일 기반 통신의 topic 카탈로그. envelope
### `securities_balance`
- **방향:** `stock` → `budget`
- **목적:** 본인 증권 계좌별 잔액(평가액·예수금·총자산)을 골디에게 전달, 월간 결산 입력으로 사용
- **목적:** 본인+가희 증권 계좌별 잔액(평가액·예수금·총자산)을 소유자별로 골디에게 전달, 월간 결산 입력으로 사용
- **트리거:** macOS launchd `ai.openclaw.stock.send-balance` — 매월 1일 04:30 KST. 골디 결산 cron(05:00) 30분 전. (`inbox_handler.py`의 `as_of` 가드는 1·10·20일을 허용 — 과거 운영 호환 안전망)
- **schema_version:** 1
- **payload 스키마:**
- **schema_version:** 2 (v1 하위호환 유지 — 수신자 `SUPPORTED_SCHEMA={1,2}`)
- **payload 스키마 (v2):**
```json
{
"as_of": "2026-05-01",
"owner_scope": "self_only",
"as_of": "2026-07-01",
"owner_scope": "self_and_gahee",
"accounts": [
{
"label": "일반",
"owner": "self",
"account_no": "",
"deposit": 118743,
"eval_amount": 63177225,
"total": 63295968,
"position_count": 4
"deposit": 56032,
"eval_amount": 114294705,
"total": 114350737,
"position_count": 7
},
{ "label": "ISA", ... }
{ "label": "ISA", "owner": "self", ... },
{ "label": "가희_일반", "owner": "gahee", ... },
{ "label": "가희_ISA", "owner": "gahee", ... }
],
"by_owner": {
"self": { "deposit": 146362, "eval_amount": 188964561, "total": 189110923 },
"gahee": { "deposit": 138469, "eval_amount": 22587331, "total": 22725800 }
},
"totals": {
"deposit": 843705,
"eval_amount": 114519255,
"total": 115362960
"deposit": 284831,
"eval_amount": 211551892,
"total": 211836723
}
}
```
- **owner_scope:** 현재 `self_only`만 발행 (가희 계좌 제외 — `가희_` prefix 라벨 자동 필터). 향후 가희 포함 버전 필요해지면 `with_gahee` 등 새 값 도입
- **owner_scope:** v2는 `self_and_gahee` — 본인·가희 모두 발행. `accounts[].owner` 는 `self`|`gahee` (`가희_` prefix 라벨이 gahee). 소유자 집계는 `by_owner`. `totals` 는 참고용 grand total. (v1은 `self_only`·`by_owner` 없음, 단일 소유자로 처리)
- **수신자(골디) 처리 동작:**
- 월간결산 cron 진입부에서 `inbox_handler.process_inbox()` 호출 (fetch_balance 이전 — 분개가 후잉 잔액에 반영되어 결산이 분개 후 스냅샷을 보도록)
- payload의 `totals.total` 과 후잉 자산 `증권(효원)` 의 차액을 계산:
- 차액 > 0 → 차변 `증권(효원)` / 대변 `주식평가수익` 자동 분개
- 차액 < 0 → 차변 `주식평가손실` / 대변 `증권(효원)` 자동 분개
- `by_owner` 의 각 소유자 total 과 후잉 자산(`self`→`증권(효원)`, `gahee`→`증권(가희)`)의 차액을 소유자별로 reconcile:
- 차액 > 0 → 차변 `증권(효원|가희)` / 대변 `주식평가수익` 자동 분개 (손익 계정은 소유자 공용)
- 차액 < 0 → 차변 `주식평가손실` / 대변 `증권(효원|가희)` 자동 분개
- `|차액| < 1만원` → 노이즈로 간주, 분개 skip (processed 처리)
- `|차액| > 1억원` → 안전 가드 발동, 분개 거부 + 텔레그램 alert + `failed/` 이동
- 처리 후 `processed/`로 이동, `state/inbox_state.json` 의 `processed[]` 에 message_id 누적 (idempotency, 최근 1000개)
- 결산 메일 본문에 `## 인박스 reconcile` 섹션, 텔레그램에 한 줄 요약 추가
- 후잉에 해당 자산 항목 없으면 `skipped` (분개 안 함)
- 소유자 하나라도 rejected/journal_failed 면 `failed/` 이동·미처리. 재실행 시 이미 분개된 소유자는 fresh 조회로 차액≈0 aligned → 중복분개 없음 (reconcile는 목표값으로 맞추기라 자연 idempotent)
- 정상 처리 후 `processed/`로 이동, `state/inbox_state.json` 의 `processed[]` 에 message_id 누적 (idempotency, 최근 1000개)
- 결산 메일 본문에 `## 인박스` reconcile 섹션(소유자별 라인), 텔레그램에 한 줄 요약 추가. 메일 "자산 계정별 변동"은 후잉 자산 전수 순회라 `증권(가희)`도 자동 표시
- **GC / 적체 정책 (월간결산 cron 진입부에서 매월 자동 수행):**
- `processed/` 의 mtime 30일 초과 envelope 자동 삭제 (`gc_processed`)
- `failed/` 적체가 5건 이상이면 결산 메일·텔레그램에 ⚠️ alert 한 줄 추가 — 사람이 검토 후 수동 삭제 (자동 삭제 안 함, CLAUDE.md 원칙 준수)
@@ -57,8 +66,9 @@ OpenClaw 에이전트 간 파일 기반 통신의 topic 카탈로그. envelope
- envelope 키 누락 / `to != budget` / 미등록 topic / 미지원 `schema_version`
- payload 키 누락 (`as_of`, `accounts`, `totals`, `owner_scope`)
- `as_of` 가 매월 1·10·20일이 아님
- `totals.{deposit,eval_amount,total}` 음수 또는 100억 초과
- `totals`/`by_owner.*` 의 `{deposit,eval_amount,total}` 음수 또는 100억 초과
- `accounts[].total` 합계가 `totals.total` 과 불일치
- `by_owner` 에 미등록 소유자 (self/gahee 외) 또는 구조 오류
- 차액이 1억원 초과 (분개 거부)
- 후잉 webhook 분개 실패
- **관련 스크립트:**
+105
View File
@@ -0,0 +1,105 @@
# 월간 점검 체크리스트 (코디)
매월 1일 헬스체크가 텔레그램으로 알린다. 관리자님이 코디 세션을 열고 "월간 점검 해줘"라고 하면 이 문서를 따른다.
**여기 있는 건 전부 "스크립트로는 못 잡는 것"이다.** 주기 잡이 죽었는지·결과가 stale 한지는 `scripts/health_check.py` 가 매일 본다(대상은 `TASKS.md` §4). 이 문서는 **바깥이 변해서 우리 쪽이 조용히 어긋나는** 종류만 다룬다 — 2026-09-10 점검에서 하드 실패는 0건인데 아래 유형이 4건 나왔다.
**원칙:** 점검은 조회만 한다. 고칠 게 나오면 관리자님께 보고하고 승인받은 것만 손댄다. 업그레이드·트리거 변경은 특히.
---
## 1. 모델 카탈로그 — deprecated·신규·단가
```bash
# 세 에이전트 라우팅 현황
for a in main stock budget; do echo "== $a"; ~/.local/bin/openclaw models list --agent $a | grep -E "default|fallback"; done
# 카탈로그의 status/replacedBy/cost (본체 업그레이드 때마다 바뀐다)
python3 -c "
import json
d=json.load(open('/opt/homebrew/lib/node_modules/openclaw/dist/extensions/openai/openclaw.plugin.json'))
for m in d['modelCatalog']['providers']['openai']['models']:
c=m.get('cost',{})
print(f\"{m['id']:22} {m.get('status','-'):11} →{m.get('replacedBy','-'):16} in {c.get('input','?')} / out {c.get('output','?')}\")"
```
- **deprecated 인데 쓰고 있는 모델**이 있는지가 핵심. 2026-09-10 에 `gpt-5.5`(primary)·`gpt-5.5-pro`(fallback#3) 둘 다 deprecated 인 걸 뒤늦게 발견해 정리했다
- ⚠️ **폴백 체인 전체가 과금 0 이어야 한다**(관리자님 요구) — 현재 `sol → terra → luna → openrouter/free`. 새 모델을 폴백·allow 에 넣을 땐 **구독(codex) 경로인지 실측**한다: `openclaw agent --model <id> -m ping` 후 trajectory 의 `modelApi` 가 `openai-chatgpt-responses` 면 구독이다. 카탈로그의 정적 제외 규칙만 보고 판단하면 틀린다(`gpt-5.4` 는 카탈로그에 있는데 은퇴했고, terra·luna 는 제외 규칙에 걸리는데 실제로는 된다)
- `modelPolicy.allow` 에 유료 모델이 없는지 확인한다 — 폴백 순서만으로는 수동 지정 과금을 못 막는다
- 신규 모델이 보이면 단가·codex 카탈로그 포함 여부를 먼저 본다. `gpt-6-astra` 를 왜 안 쓰기로 했는지는 `CLAUDE.md` **Key Configuration** 참조(다시 검토하기 전에 그 세 근거부터 확인)
## 2. OpenClaw 본체 버전
```bash
~/.local/bin/openclaw --version && npm view openclaw version # 현재 vs 최신
```
- **올릴지 말지는 관리자님 결정.** 올린다면 `CLAUDE.md` **Runtime** 의 함정 3종(node 24 필수 / npm 11 의 `--allow-scripts` / doctor 가 워크스페이스·cron 을 손댐)을 먼저 읽는다
- ⚠️ **업그레이드 후엔 "무엇이 조용히 죽었는지" 반드시 확인한다.** 2026.9.3 이 trajectory 를 파일에서 sqlite 로 옮겨 `codex_fallback_monitor` 가 예외 없이 '폴백 없음'만 반환하며 무력화됐다. 파일 경로·테이블·스키마에 의존하는 코드는 전부 재검증 대상
## 3. 외부 데이터 소스 생존
우리가 긁는 곳은 예고 없이 막힌다. **각 소스의 CLI 를 한 번씩 돌려 본다.**
```bash
cd ~/.openclaw/agents/stock/workspace
python3 scripts/holiday_sync.py --show | head -5 # nager (investing 은 이미 403)
python3 scripts/fomc_calendar_sync.py --list | head -5 # federalreserve.gov
python3 scripts/fnguide_client.py 005930 | head -5 # FnGuide
python3 scripts/wisereport_client.py 005930 | head -5 # WISEreport
python3 ~/.openclaw/workspace/scripts/youtube_briefing_digest.py latest us
python3 ~/.openclaw/workspace/scripts/youtube_briefing_digest.py latest behive
```
- ⚠️ **"실패해도 조용한" 소스가 위험하다.** `holiday_sync` 는 investing 403 이후 2개월간 stale 이었는데, 폴백(nager)이 있는데도 "investing 0건이면 저장 자체를 건너뛰는" 가드 때문에 아무 일도 안 했다. 주 소스가 죽었을 때 **폴백이 단독으로 설 수 있는지**를 본다
- 유튜브는 스크랩 구조(`lockupViewModel`)가 바뀌면 조용히 빈 결과가 된다 → 브리핑에서 시황 카드만 사라진다
## 4. 문서 드리프트 — TASKS.md·CLAUDE.md vs 실제
```bash
# plist 실제 스케줄 전수
cd ~/Library/LaunchAgents && for p in ai.openclaw.*.plist; do
echo "== $(basename $p .plist)"; plutil -p "$p" | grep -A6 StartCalendarInterval | head -8; done
~/.local/bin/openclaw cron list # cron 은 sqlite(state/openclaw.sqlite)
```
- `TASKS.md` §1·§2 의 트리거 표기와 대조. 2026-09-10 에 IPO 요일·시뮬 주기·자본금·sim-web 탭 수·cron ID 등 **9건이 어긋나** 있었다
- `TASKS.md` §4(헬스체크 표)의 **증거 파일이 여전히 "돌면 반드시 갱신되는" 것인지** 확인. 스크립트가 바뀌면 이 전제가 깨진다
- 문서에 없는 launchd 잡이 있는지도 본다 (2026-09-10 `nxt-probe-oneshot` 고아 발견 → 정리)
- ⚠️ **health-check 자신이 돌고 있는지 확인한다** — 감시자가 죽으면 알림도 같이 죽어서 아무 신호가 없다. `ls -la ~/.openclaw/logs/health-check.log` 의 mtime 이 어제 이후인지 본다
## 5. 자격증명·토큰 만료
```bash
cd ~/.openclaw/agents/stock/workspace && python3 scripts/kiwoom_client.py token # 키움
ls -la ~/.openclaw/credentials/
```
- 키움 REST 토큰, 텔레그램 봇(동결 이력 있음 — `getMyName` 까지 확인), 후잉 API, gog(Gmail/Calendar) 인증
- ⚠️ gog 는 **코디 셸에서 인증이 안 된다**(키체인 ACL). 검증은 `--list` 류 인증 불필요 CLI 로 하거나 launchd oneshot 경유
## 6. 코디 inbox
```bash
ls ~/.openclaw/agents/cody/inbox/incoming/ | wc -l
find ~/.openclaw/agents/cody/inbox/processed -mtime +7 | head # GC 대상
```
`CLAUDE.md` **Cody Inbox** 의 처리 흐름을 따른다. `processed/` 7일 초과분은 이때 정리한다.
## 7. 디스크·백업
```bash
du -sh ~/.openclaw/logs ~/.openclaw/state ~/.openclaw/agents/*/workspace/state
cd ~/.openclaw && git log -1 --format='%cd %s' --date=format:'%m-%d %H:%M'
```
- 로그 총량은 헬스체크가 매일 보지만(상한 200MB), **어느 파일이 커지고 있는지**는 사람이 봐야 판단이 된다
- git autopush 가 매일 02:00 도는지, `.gitignore` 로 빠진 시크릿·sqlite 의 별도 백업이 있는지
---
## 점검 후
`agents/cody/inbox/processed/` 에 리포트를 남기는 대신, **고친 게 있으면 `CLAUDE.md`·`TASKS.md` 에 그 사실과 근거를 적는다.** 다음 점검 때 같은 걸 다시 파헤치지 않도록 "왜 그렇게 뒀는지"를 남기는 게 이 워크스페이스의 방식이다.
+62 -13
View File
@@ -13,35 +13,45 @@
|---|---|---|
| **OpenClaw Gateway** | `openclaw gateway --port 18789` | launchd `ai.openclaw.gateway` (상시) |
| **비하이브 웹뷰** | `python3 ~/.openclaw/agents/stock/workspace/scripts/behive_web.py serve` | launchd `ai.openclaw.stock.behive-web` (상시) |
| **웹 채팅 (클로·레이·골디)** | `python3 ~/.openclaw/workspace/scripts/chat_web.py serve` | launchd `ai.openclaw.chat-web` (상시, 포트 18793). `chat.hyowons.net` → NAS 리버스 프록시. 로그인 = iMessage OTP 단독(비밀번호 없음, 코드 발송 1분 1회·1시간 5회 제한) |
| **후잉 동기화** | `python3 ~/.openclaw/agents/budget/workspace/skills/whooing-sync/scripts/whooing_sync.py` | launchd `ai.openclaw.budget.whooing-sync` (매 15분). 매 사이클 끝에 **가희 잔액 리마인더·자동분개** 모듈(`gahee_reminder.run`)도 함께 호출 — 매월 25일 10:00 KST 이후 첫 사이클에서 가희님께 iMessage 1회 발신, 이후 답신 폴링 → 텍스트면 후잉 가희주머니 차액 자동 분개, 이미지면 텔레그램 알림만 |
| **주식 브리핑 메일** | `python3 ~/.openclaw/agents/stock/workspace/scripts/stock_portfolio_report.py send` | launchd `ai.openclaw.stock.briefing` (평일 20:10) |
| **주식 포트폴리오 스냅샷** | `python3 ~/.openclaw/agents/stock/workspace/scripts/stock_portfolio_report.py send --no-mail` | launchd `ai.openclaw.stock.briefing` (평일 20:10) + 폴백 `-fallback-2030`(20:30, 스냅샷 없을 때만) · `-fallback-2100`(21:00, **무조건 fresh 재조회**). **메일 발송 중지**(2026-08-12 관리자님 결정 — 메일 통수 축소). `--no-mail`은 휴장일 self-skip·실패 텔레그램 알림은 유지하고 메일만 건너뜀. 목적은 자산웹 '당일 평가손익' 기준값이 되는 스냅샷 확보. 리포트 본문을 눈으로 보려면 `run`(메일 없이 평문+`/tmp/stock_report_preview.html`) |
| **증권잔액 → 골디 inbox** | `python3 ~/.openclaw/agents/stock/workspace/scripts/send_balance_to_budget.py` | launchd `ai.openclaw.stock.send-balance` (매월 1일 04:30) |
| **골디 inbox 처리** (단독) | `python3 ~/.openclaw/agents/budget/workspace/skills/monthly-settlement/scripts/inbox_handler.py [--dry-run]` | 트리거 없음 — 월간결산 cron 진입부에서 자동 호출. 수동 점검·재처리용 |
| **워치리스트 모니터링** | `python3 ~/.openclaw/agents/stock/workspace/scripts/watchlist_monitor.py check` | launchd `ai.openclaw.stock.watchlist-monitor` (평일 09–15시 매 15분) |
| **공모주 캘린더 동기화** | `python3 ~/.openclaw/agents/stock/workspace/scripts/ipo_calendar_sync.py` | launchd `ai.openclaw.stock.ipo-calendar-sync` (매월 1일 09:00) |
| **워치리스트 모니터링** | `python3 ~/.openclaw/agents/stock/workspace/scripts/watchlist_monitor.py check` | launchd `ai.openclaw.stock.watchlist-monitor` (평일 **10/12/14시 3회** — 2026-05-12 15분 간격에서 축소) |
| **트레일링 스톱 감시** | `python3 ~/.openclaw/agents/stock/workspace/scripts/trailing_monitor.py {check [--repeat N --gap S] [--force] [--dry-run]\|list}` | launchd `ai.openclaw.stock.trailing-monitor` (평일 **08:00–19:59 매 1분**, 720엔트리 — 2026-08-25 plist 실측 정정. 스크립트가 장외·휴장 self-skip). 하는 일 둘: ①자산웹에서 PIN 승인된 트레일링 예약의 조건단가를 고점 갱신 시 kt10002로 **상향만** 정정 ②**예약 트레일링(TRAILING_ARM) 발동·만료** — 발동가 도달 시 스톱주문 신규 발주 + 예약 승격, 미발동으로 등록일이 지난 대기 예약은 자동 취소(둘 다 2026-08-25 추가, 관리자님 승인). ⚠️ "신규 발주 없음"은 더 이상 사실이 아니다. 예약 0건이면 즉시 종료(API 콜 0). 상태 `state/trailing_stops.json`. ⚠️ cadence 변경 이력 2분→30초→**1분**(2026-07-30 관리자님) — 30초는 launchd로 불가해 `--repeat 2 --gap 30`(프로세스 내부 2회)으로 구현했었고, 옵션은 살아있으니 다시 쓸 땐 plist 인자만 추가 |
| **급등락 알림** | `python3 ~/.openclaw/agents/stock/workspace/scripts/surge_monitor.py {check [--force]\|dry-run\|list}` | launchd `ai.openclaw.stock.surge-monitor` (09:00–19:59 매 1분, 660엔트리 — 스크립트가 휴장·장외 self-skip. 2026-09-22 KRX 애프터마켓 대응으로 15:35 → 20:00 확대). 토글 켠 종목만 감시(상·하한가 / VI 발동 / 자기이력 분위수) → 레이 텔레그램. 토글 0건이면 API 콜 0 |
| **codex 폴백 감시** | `python3 ~/.openclaw/workspace/scripts/codex_fallback_monitor.py [--dry-run]` | launchd `ai.openclaw.codex-fallback-monitor` (매 5분). 주 모델이 죽어 유료 OpenRouter 로 폴백하면 클로 텔레그램. 무비용(on-disk sqlite 스캔) |
| **git 자동 백업** | `sh ~/.openclaw/scripts/git_autopush.sh` | launchd `ai.openclaw.git-autopush` (매일 02:00). 변경 있을 때만 커밋·푸시 |
| **헬스체크** | `python3 ~/.openclaw/scripts/health_check.py [--dry-run] [--verbose]` | launchd `ai.openclaw.health-check` (**매일 09:00**). §4 표를 파싱해 stale·데몬 사망 검사, 이상 있을 때만 텔레그램(같은 이상은 7일간 재발송 안 함). 매월 1일엔 `MAINTENANCE.md` 월간 점검 리마인더 |
| **로그 로테이션** | `python3 ~/.openclaw/scripts/log_rotate.py [--dry-run]` | launchd `ai.openclaw.log-rotate` (매주 일요일 04:00). 30일 초과 로그 trash + 20MB 초과 활성 로그 in-place 축소 |
| **공모주 캘린더 동기화** | `python3 ~/.openclaw/agents/stock/workspace/scripts/ipo_calendar_sync.py` | launchd `ai.openclaw.stock.ipo-calendar-sync` (**월·수·금 17:00**) |
| **FOMC 캘린더 동기화** | `python3 ~/.openclaw/agents/stock/workspace/scripts/fomc_calendar_sync.py [--list\|--dry-run]` | launchd `ai.openclaw.stock.fomc-calendar-sync` (매월 1일 09:10). 연준 공식 일정표 → `[FOMC] YYYY-MM 회의` 종일 일정(회의 시작일 ET ~ 발표일 KST). `--list`는 파싱 결과만(캘린더 미접근) |
| **휴장일 동기화** | `python3 ~/.openclaw/agents/stock/workspace/scripts/holiday_sync.py [--show]` | launchd `ai.openclaw.stock.holiday-sync` (매주 일요일 03:00) |
| **Gmail 라벨 분류** | `python3 ~/.openclaw/workspace/scripts/gmail_label_classify.py [--dry-run]` | launchd `ai.openclaw.gmail-label-classify` (매일 01:00). self-sent 브리핑·종목분석·주식 리포트 메일 라벨링 + 24h 테스트메일 휴지통. LLM 미경유(`gog` CLI). 2026-06-26 cron→launchd 이관 |
| **매매 기록 적재** | `python3 ~/.openclaw/agents/stock/workspace/scripts/trade_journal.py {collect\|seed\|show\|query} ...` | launchd `ai.openclaw.stock.trade-journal` (평일 21:00 — NXT 야간 마감 후 ka10170 4계좌 적재). `seed`는 적재 시작일 이전 보유분을 현재 평단가로 1회 시드 (idempotent) |
| **자동매매 시뮬 스캔** | `cd ~/.openclaw/agents/stock/workspace && python3 -m sim {scan [--force]\|status\|report\|reset --yes}` | launchd `ai.openclaw.stock.sim-scan` (평일 09–15:30 매 15분, 엔진이 장외 self-skip). 가상자본 1천만 페이퍼 트레이딩, 규칙 엔진(LLM 미경유). 결과 `state/sim/`. 실주문 아님 |
| **자동매매 시뮬 대시보드** | `cd ~/.openclaw/agents/stock/workspace && python3 -m sim.sim_web {serve\|render}` | launchd `ai.openclaw.stock.sim-web` (상시). 포트 18792 → `sim.hyowons.net`(reverse proxy 수동 등록 대기). 5탭(계좌·관심종목·시장·튜닝·백테스트), 읽기전용+`/save_params`만 쓰기 |
| **시뮬 파라미터 스윕(튜닝 비교)** | `cd ~/.openclaw/agents/stock/workspace && python3 -m sim {sweep [frac]\|backtest}` | 트리거 없음(on-demand). 81조합 백테스트 학습/검증 비교 → `state/sim/backtest_results.json`(웹 백테스트 탭 표시). 튜닝값은 `state/sim/params.json`(웹 튜닝 탭) |
| **자동매매 시뮬 스캔** | `cd ~/.openclaw/agents/stock/workspace && python3 -m sim {scan [--force]\|status\|report\|reset --yes}` | launchd `ai.openclaw.stock.sim-scan` (09–15:30 **매 2분**, 196엔트리 — 엔진이 장외·휴장 self-skip). 가상자본 **1억** 페이퍼 트레이딩, 규칙 엔진(LLM 미경유). 결과 `state/sim/`. 실주문 아님 |
| **자동매매 시뮬 대시보드** | `cd ~/.openclaw/agents/stock/workspace && python3 -m sim.sim_web {serve\|render}` | launchd `ai.openclaw.stock.sim-web` (상시). 포트 18792 → `sim.hyowons.net`(reverse proxy 수동 등록 대기). **3탭**(전략·관심종목·시장 — 계좌·튜닝 탭은 2026-06-10 삭제), 읽기전용+`/save_params`만 쓰기 |
| **시뮬 파라미터 스윕(튜닝 비교)** | `cd ~/.openclaw/agents/stock/workspace && python3 -m sim {sweep [frac]\|backtest}` | 트리거 없음(on-demand). **1536조합** 백테스트 학습/검증 비교 → `state/sim/backtest_results.json`(웹 백테스트 탭 표시). 튜닝값은 `state/sim/params.json`(웹 튜닝 탭) |
| **Claude Code 원격 세션** (on-demand, 다중) | `python3 ~/.openclaw/workspace/skills/claude-code-session/scripts/session_tool.py {profile\|session} ...` (legacy 단일: `ensure_session.sh {open\|close\|status}`) | 세션 plist는 `~/Library/LaunchAgents/ai.claude-session.<profile>-<N>.plist`로 ephemeral, 레거시 단일은 `ai.openclaw.claude-remote-control`. 모두 RunAtLoad=false, KeepAlive=false. 클로 `claude-code-session` 스킬이 자연어 라우팅. |
| **테일스케일 켜기·끄기** (on-demand) | `python3 ~/.openclaw/workspace/scripts/tailscale_ctl.py {on|off|status}` | 트리거 없음 — 관리자님이 "테일스케일 꺼줘/켜줘/상태" 라고 할 때 클로가 호출. sudo 불필요(localapi + sameuserproof 토큰). ⚠️ `off` 하면 타넷 경유 접근이 전부 끊긴다(stock.hyowons.net·sim.hyowons.net·폰에서 맥 직접 접근). 텔레그램은 아웃바운드라 영향 없어 되돌릴 길은 항상 남는다. behive_web 은 `BIND_HOST=''` 라 프로세스가 죽지 않고 루프백·LAN 접근은 유지. `up` 후 설정·로그인 상태는 보존되고 재인증 불필요(실측 왕복 복구 1초). ⚠️ **자동 트리거(cron/launchd)에 걸지 말 것** |
수동 실행: 진입점 명령만 그대로 터미널에서 실행하면 된다. launchctl 만지지 마라.
---
## 2. LLM 자연어 작업 (OpenClaw cron — `cron/jobs.json`)
## 2. LLM 자연어 작업 (OpenClaw cron — `state/openclaw.sqlite` 의 `cron_jobs` 테이블)
LLM이 자막 요약·뉴스 평문 생성·결산 보고서 등 본문을 만들어야 하는 작업만 여기 남는다.
| 작업 | 진입점 (사람이 시키는 한 마디) | 트리거 |
|---|---|---|
| **오전 브리핑** (main/클로) | "오전 브리핑 메일 보내줘" → cron payload의 step 그대로 실행 | OpenClaw cron `56d66b54` (매일 07:00) |
| **오후 브리핑** (main/클로) | "오후 브리핑 메일 보내줘" → cron payload의 step 그대로 실행 | OpenClaw cron `9d2c105f` (매일 19:00) |
| **비하이브 종목분석 요약** (stock/레이) | "비하이브 신규 영상 점검해줘" → cron payload의 step 그대로 실행 | OpenClaw cron `747e1013` (평일 07/12/18시) |
| **오전 브리핑** (main/클로) | "오전 브리핑 메일 보내줘" → cron payload의 step 그대로 실행 (미국증시 요약 포함) | OpenClaw cron `56d66b54` (**월~토** 07:30) + 1차폴백 `c4f2d3e5...0755` (07:55) + 2차폴백·최종 `b3f1c2d4...0830` (08:30, US 미게시여도 발송). **일요일 없음**. **토요일은 해외 전용**(스크립트가 `overseas_only` 판정 → 해외 뉴스만·일정·IPO 생략) |
| **오후 브리핑** (main/클로) | "오후 브리핑 메일 보내줘" → cron payload의 step 그대로 실행 | OpenClaw cron `9d2c105f` (**평일** 19:00). 주말 없음 |
| **비하이브 종목분석 요약** (stock/레이) | "비하이브 신규 영상 점검해줘" → cron payload의 step 그대로 실행 | OpenClaw cron `80052b62` (평일 07/12/18시). 등록명은 "비하이브 추천영상 요약" |
| **월간 결산** (budget/골디) | "월간 결산 돌려줘" → cron payload의 step 그대로 실행 | OpenClaw cron `08e9a978` (매월 1일 05:00) |
수동 실행: 사용자가 해당 에이전트에게 작업 이름만 말하면, 에이전트는 `jobs.json`의 동일 ID 페이로드 step을 그대로 따라 실행한다. **cron 활성화 여부·스케줄 시간을 만지는 작업은 트리거 변경 요청이 명시적으로 있을 때만.**
수동 실행: 사용자가 해당 에이전트에게 작업 이름만 말하면, 에이전트는 `openclaw cron show <id>` 의 동일 ID 페이로드 step을 그대로 따라 실행한다. **cron 활성화 여부·스케줄 시간을 만지는 작업은 트리거 변경 요청이 명시적으로 있을 때만.**
---
@@ -50,12 +60,51 @@ LLM이 자막 요약·뉴스 평문 생성·결산 보고서 등 본문을 만
1. **트리거(언제)와 작업(무엇)은 별개다.** "지금 X 실행"은 항상 §1·§2의 진입점 명령만 실행. cron/launchd 등록·해제·재실행은 별도 요청.
2. **payload에 "매일 X시 자동 실행" 같은 자가묘사 메타 문구를 넣지 마라.** 페이로드는 "무엇을 어떻게"만 담는다. "언제"는 schedule 필드가 단일 진실 공급원.
3. **새 트리거를 만들 땐 단순 스크립트면 launchd, LLM이 본문을 생성해야 하면 OpenClaw cron.** 이중 등록 금지. 이관 시 한쪽 완전 제거.
4. **Disabled 좀비 잡 즉시 제거.** `enabled:false`로 두지 말고 jobs.json에서 빼라. 검색에 잡혀서 혼선 유발.
4. **Disabled 좀비 잡 즉시 제거.** `enabled:false`로 두지 말고 `openclaw cron delete <id>` 로 빼라. 검색에 잡혀서 혼선 유발.
5. **트리거 추가/이관/제거는 담당 에이전트의 MEMORY.md에 반드시 기록.**
---
## 4. 알려진 이슈 / 진단 절차
## 4. 헬스체크 (자동 감시 대상)
`scripts/health_check.py` 가 **이 표를 파싱해서** 돈다. 행을 고치면 감시가 바뀐다 — 표가 곧 설정이라, 트리거를 바꾸고 표를 안 고치면 알림이 떠서 드리프트가 스스로 드러난다.
**증거 고르는 법:** "그 잡이 돌면 **반드시** 갱신되는" 파일이어야 한다. `surge_alerts.json`·`trailing_stops.json`·`whooing_synced.json` 처럼 **일이 있을 때만** 쓰이는 파일은 조용한 날 stale 로 보여 오탐이 된다 — 그런 잡은 로그 파일을 증거로 쓴다. 반대로 `holiday_sync` 처럼 "돌긴 도는데 결과가 옛것"인 실패(2026-07~09 2개월 stale)를 잡으려면 로그가 아니라 **결과 파일**이어야 한다.
**기대 주기:** 평일 전용 잡은 금→월 72h 에 여유를 더해 `100h`. ⚠️ 설·추석 등 3일 초과 연휴엔 오탐이 날 수 있다(휴장일 보정은 넣지 않았다 — 연 2회 오탐을 감수하는 대신 로직을 단순하게 뒀다). `daemon:` 접두는 주기 대신 **프로세스 생존**(PID)을 본다.
| 작업 | 증거 (mtime 기준) | 기대 주기 |
|---|---|---|
| OpenClaw Gateway | `daemon:ai.openclaw.gateway` | - |
| 비하이브 웹뷰 | `daemon:ai.openclaw.stock.behive-web` | - |
| 시뮬 대시보드 | `daemon:ai.openclaw.stock.sim-web` | - |
| 웹 채팅 | `daemon:ai.openclaw.chat-web` | - |
| 후잉 동기화 | `logs/whooing-sync.log` | 2h |
| codex 폴백 감시 | `logs/codex-fallback-monitor.log` | 2h |
| 급등락 알림 | `logs/stock-surge-monitor.log` | 100h |
| 트레일링 스톱 감시 | `agents/stock/workspace/state/trailing_monitor_heartbeat` | 100h |
| 워치리스트 모니터링 | `logs/watchlist-monitor.log` | 100h |
| 자동매매 시뮬 스캔 | `agents/stock/workspace/state/sim/last_scan.json` | 100h |
| 주식 포트폴리오 스냅샷 | `agents/stock/workspace/state/portfolio_daily_snapshot.json` | 100h |
| 매매 기록 적재 | `agents/stock/workspace/state/trade_journal.jsonl` | 100h |
| 시장지표 적재 | `agents/stock/workspace/state/market_indicators_history.jsonl` | 100h |
| Gmail 라벨 분류 | `workspace/state/gmail_label_classify.log` | 30h |
| git 자동 백업 | `logs/git-autopush.log` | 30h |
| 휴장일 동기화 | `agents/stock/workspace/state/market_holidays.json` | 8d |
| 공모주 캘린더 동기화 | `agents/stock/workspace/state/ipo_calendar_sync.json` | 5d |
| FOMC 캘린더 동기화 | `agents/stock/workspace/state/fomc_calendar_sync.json` | 35d |
| 증권잔액 → 골디 inbox | `logs/send-balance.log` | 35d |
| 로그 로테이션 | `logs/log-rotate.log` | 8d |
감시하지 **않는** 것과 이유:
- **OpenClaw cron 4종**(브리핑·비하이브·월간결산) — 실패하면 메일·텔레그램이 안 와서 관리자님이 바로 아신다. 게다가 `openclaw cron list` 의 `Last`/`Status` 가 이미 이력을 들고 있다
- **시뮬 스윕·골디 inbox 처리** — on-demand 라 기대 주기가 없다
- **`sim.backfill_flow`·`sim universe`** — `stock.trade-journal` plist 가 묶어 도는 5개 명령 중 3·4번째다(5번째는 `kiwoom_client refresh-codes --if-stale 7` — 종목코드 캐시 주 1회 갱신, 2026-09-10 추가. 이 캐시의 `nxt_enable` 이 자산웹 NXT 판정 근거라 낡으면 🔥배지·거래가능 초록점이 틀어진다. 금액엔 영향 없음). 앞 2개(매매기록·시장지표)는 위에서 감시하지만 이 둘은 뺐다 — 결과물(`flow_history.sqlite`·`auto_seen.json`)이 stale 해도 백테스트 수급게이트가 중립으로 비활성되고 sim 관찰목록 자동편입이 멈출 뿐, **실매매·실주문에는 영향이 없다**. ⚠️ 다만 plist 는 `하나 실패해도 나머지 시도` 구조라 이 둘만 조용히 죽을 수 있다 — sim 백테스트 결과가 이상하면 여기부터 본다
- **Claude Code 원격 세션** — ephemeral
---
## 5. 알려진 이슈 / 진단 절차
- **bonjour 광고 stuck → gateway crash 루프** (2026-04-28 한 차례 발생, 현재는 정상)
- 증상: `~/.openclaw/logs/stability/` 에 `*unhandled_rejection.json` 누적, gateway PID 계속 변경, `openclaw cron list`가 `gateway closed (1006)` 응답
@@ -1,4 +0,0 @@
{
"version": 1,
"setupCompletedAt": "2026-04-23T06:17:48.250Z"
}
+93
View File
@@ -105,3 +105,96 @@ LLM-LLM 자연어 통신은 프롬프트 인젝션·할루시네이션 증폭
## 직접 고쳐 써라
이 문서는 출발점이다. 패턴이 보이면 직접 업데이트해라. 정비는 코디가 하지만, 이 매뉴얼의 작가는 너다.
## Tools
### Local notes (migrated from TOOLS.md)
# TOOLS.md - Local Notes
가계부 관련 파일·스크립트·발신번호 메모. 모든 경로는 `agents/budget/workspace/` 기준.
## Whooing
- 가계부 시스템: https://whooing.com
- **웹훅 URL:** `/Users/snowoyh/.openclaw/credentials/whooing.json` 의 `webhook_url` 필드.
형식: `https://whooing.com/webhook/s/xxxx-xxxx-xxxx-xxxx`
- 웹훅 페이로드 종류:
- **raw 문자**: `message=<원문>` 보내면 후잉이 알아서 파싱
- **구조화 JSON**: `{entry_date, item, money, left, right, memo}` (left=차변, right=대변)
- 결제 취소/승인취소도 같이 전송해야 후잉에서 -금액으로 상쇄됨.
## State
- `state/whooing_synced.json` — `{ "last_message_at": <ISO>, "last_synced_at": "ISO" }`. dedupe 기준.
- `state/whooing_account_map.json` — 발신번호 → 후잉 매핑. `confirmed: true`인 엔트리만 후잉으로 자동 전송. `confirmed: false`는 발견 시 로그만 남기고 사용자 확인 대기.
- `state/whooing_accounts.json` — **관리자님 후잉 계정과목 차트** (자산/부채/순자산/비용/수익 + carrier→결제수단 매핑). 구조화 입력으로 left/right 채울 때 항상 이 파일에서 정확한 계정명을 끌어쓴다. 차트에 없는 계정명은 후잉이 거부하므로 절대 추측해 보내지 말 것.
- `state/whooing_failures.json` — 후잉이 4xx/5xx로 거절한 건. 수동 확인용.
## Scripts (python3)
- `skills/whooing-sync/scripts/whooing_sync.py` — iMessage 결제문자 → 후잉 웹훅 POST. launchd `ai.openclaw.budget.whooing-sync` 가 매시 0/15/30/45분 실행 (OpenClaw cron 아님). ⚠️ **on-demand 실행은 스크립트 직접 호출이 아니라 `launchctl kickstart -p gui/$(id -u)/ai.openclaw.budget.whooing-sync` + 로그 확인** — 내 세션은 게이트웨이 node 를 TCC responsible process 로 물고 있어 `imsg` 가 Messages DB 를 못 읽는다. FDA 필수(`/opt/homebrew/bin/imsg`). 페어 매칭 로직 내장 — 자세한 건 `skills/whooing-sync/SKILL.md`.
- `skills/whooing-sync/scripts/imsg_cli.py` — `imsg` CLI 공용 래퍼(`run_json`/`chats`). stderr·returncode 를 판정해 FDA 누락을 파싱 에러로 둔갑시키지 않는다. `whooing_sync.py`·`gahee_reminder.py` 가 공유.
- `skills/whooing-sync/scripts/whooing_manual.py` — iMessage 없이 한 건 직접 등록. structured(`--item/--money/--left/--right [--date] [--memo]`) 또는 raw(`--message`). structured는 `whooing_accounts.json` 차트 검증 후 POST.
- `skills/whooing-sync/scripts/whooing_balance.py` — 후잉 OpenAPI로 자산/부채/자본 잔액 조회. 옵션: `--section-id`, `--as-of YYYY-MM-DD`, `--json`. 크리덴셜은 `credentials/whooing.json`의 `api` 블록(app_id/token/signature).
## 발신번호
`state/whooing_account_map.json`이 정답. 아래는 사람이 읽기 쉬운 요약.
**확인됨 (관리자님이 직접 확인):**
| 발신처 | 발신번호 |
|--------|---------|
| 하나 계좌이체 | `+8215991111` |
| 신한은행 | `+8215778000` |
| 신한카드 | `+8215447000` (※ 과거 오타 `+8215447200` 쓴 적 있음 — 실사용 X, 현재 confirmed=false 로 대기) |
**추정 (모델 사전지식 기반, 실제 메시지 들어오면 confirmed로 승격 필요):**
| 발신처 | 추정 발신번호 |
|--------|--------------|
| 삼성카드 | `+8215888900` |
| KB국민카드 | `+8215881688` |
| 현대카드 | `+8215776200` |
| 롯데카드 | `+8215888100` |
| 하나카드 | `+8218001111` |
| 우리카드 | `+8215889955` |
| BC카드 | `+8215884000` |
| NH농협카드 | `+8216444000` |
| 카카오페이 | `+8216447405` |
| 네이버페이 | `+8215883819` |
| 토스 | `+8215994905` |
| KB국민은행 | `+8215889999` |
| 우리은행 | `+8215885000` |
| NH농협은행 | `+8216613000` |
| 카카오뱅크 | `+8215993333` |
**정책:** 미확인 번호 메시지가 들어오면 무시 또는 `failures.json`에 "unmapped"로 기록만 한다. 잘못된 자동 등록을 막기 위함.
## 관리자님 관련
- 가계부 질문이 들어오면 먼저 `state/whooing_synced.json`과 `whooing_failures.json`을 본다.
- 잔액·계정 총합은 `whooing_balance.py`로 조회한다. 후잉 OpenAPI(`bs.json`/`accounts.json`/`sections.json`) 연동 완료.
## Playwright MCP (브라우저 자동화)
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
- **탐색:** `navigate / navigate_back / tabs / wait_for`
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
**운영 특성:**
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
- 첫 호출 시 npx spawn 1~2초 지연.
- 은행·카드사 공동인증서·간편비밀번호 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
**활용 시나리오 (골디 맥락):**
- 후잉 웹 직접 분개 수정/삭제 — 현재 inbox·webhook 단방향 한계 보완용. **영속 로그인 필요** → 코디에게 user-data-dir 분리 요청 후 진행
- 카드사 웹 청구서 PDF 수집 (월별 명세 검증·보조). 공동인증서·간편비번 화면 진입 시 중단
- 우선순위는 항상 **API · iMessage 결제문자 → 후잉 webhook 자동 동기화**. 브라우저는 자동 흐름이 못 메꾸는 잔여 영역만
+8 -3
View File
@@ -19,6 +19,8 @@
- **스케줄러는 launchd** (OpenClaw cron 아님). plist: `~/Library/LaunchAgents/ai.openclaw.budget.whooing-sync.plist`, label `ai.openclaw.budget.whooing-sync`, 매시 0/15/30/45분. 로그는 `/Users/snowoyh/.openclaw/logs/whooing-sync.{log,err.log}`. OpenClaw 의 `후잉 가계부 동기화` cron 잡은 의도적으로 `enabled: false` — LLM 세션 비용(월 ~70M 토큰) 때문에 전환된 것. **다시 enable 하지 말 것.**
- **FDA 필수**: launchd 는 FDA 상속 안 받음. `/opt/homebrew/bin/imsg` 를 "시스템 설정 → 개인정보 보호 및 보안 → 전체 디스크 접근 권한" 에 등록해야 Messages DB 읽힘. 누락 시 `imsg chats 실행 실패` stderr + stdout "새 결제 메시지 없음" 으로 조용히 넘어감. 맥 이전·imsg 재설치 시 재등록.
- **페어 매칭**: `whooing_sync.py` 가 동일 금액 입↔출 SMS 를 5분 윈도우(`PAIR_WINDOW_SECONDS=300`) 로 짝지어 1건의 자산↔자산 structured 이체로 합성. 짝 없는 입출금이 5분 이내(`HOLD_GRACE_SECONDS=300`) 면 hold → 다음 cron 재시도. 상세는 SKILL.md 의 "페어 매칭" 섹션.
- **증권 이체 매칭** (2026-09-10): 은행→증권 이체는 은행 SMS 에 **입금자명만** 찍혀 짝이 없다. `securities_match.py` 가 capital-block 직전에 키움 kt00015 에서 짝(같은 날·같은 금액·반대 방향·`is_principal_flow` 적요)을 찾아 `증권(효원) ← 하나은행(효원)` 이체로 분개한다. **평시 키움 콜 0**(자본 분개로 갈 뻔한 건이 있을 때만 4콜). 애매하면(후보 여럿·조회 실패) 매칭 안 하고 기존 알림 경로로 보낸다. 상세는 SKILL.md 의 "증권 이체 매칭" 섹션.
- **비결제/비거래 메시지 자동 스킵**: `SKIP_PATTERNS` 에 `"인증번호"`, `"간편인증서"`, `"개설되었습니다"`, `"계좌개설"` 포함. 또한 금액 패턴(`숫자원`)이 없는 문자는 후잉 raw 폴백으로 보내지 않는다. 인증/간편인증서/계좌개설 안내 문자는 후잉 처리하지 않는다.
## 발신번호 주의
@@ -27,9 +29,9 @@
- 신한은행: `+8215778000` (1577-8000).
- 하나 계좌이체: `+8215991111`.
## merchant_map 한계 (미해결)
## merchant_map 한계 (일부 해소)
`exact: "방효원"` 룰은 본인 이름만으론 이체 상대계좌를 식별 못 함. 페어 매칭이 1차 방어선이지만, 한쪽 은행이 confirmed=false 거나 시간창 벗어나면 이 룰이 폴백으로 타서 **기초잔액(효원) 경유로 잘못 기록**될 수 있다. 발견 시 `whooing_manual.py` 로 역분개하거나 후잉 UI 에서 삭제 후 올바른 이체 1건으로 재등록. 근본 해결은 메모에 "카뱅/신한" 같은 목적지 키워드를 일관되게 넣고 `merchant_map.contains` 에 등록하는 것.
`exact: "방효원"` 룰은 본인 이름만으론 이체 상대계좌를 식별 못 함. ⚠️ **상대가 증권 계좌인 경우는 2026-09-10 증권 이체 매칭으로 해소**됐다(키움 kt00015 가 짝을 알려준다). 아래는 그 외 상대계좌(다른 은행·타인 송금) 이야기다. 페어 매칭이 1차 방어선이지만, 한쪽 은행이 confirmed=false 거나 시간창 벗어나면 이 룰이 폴백으로 타서 **기초잔액(효원) 경유로 잘못 기록**될 수 있다. 발견 시 `whooing_manual.py` 로 역분개하거나 후잉 UI 에서 삭제 후 올바른 이체 1건으로 재등록. 근본 해결은 메모에 "카뱅/신한" 같은 목적지 키워드를 일관되게 넣고 `merchant_map.contains` 에 등록하는 것.
## 우선 룰 (whooing_overrides.json) — 2026-04-29 도입
@@ -68,5 +70,8 @@
- **이미지 답신**: 자동분개 X. 골디 텔레그램 알림만 — 관리자님이 직접 처리하거나 가희님께 텍스트로 다시 요청.
- **포맷 오류** (페어 0건): 분개 중단 + 골디 텔레그램 보고. 가희님께 다시 요청 또는 직접 입력.
- **완료**: 골디 텔레그램으로 합계·차액·분개 방향 보고.
- 상태 파일: `state/gahee_reminder.json` (last_sent_month, last_processed_message_at), 수신자: `credentials/gahee_imessage.json`.
- 상태 파일: `state/gahee_reminder.json` (last_sent_month, last_processed_message_at, last_sent_at, reply_alert_for), 수신자: `credentials/gahee_imessage.json`.
- 발신 문구 수정은 state 파일 `message_template` 직접 편집 (코드 재배포 불필요).
- **발신 경로는 `--service imessage`** (2026-08-03 전환). 이전 `sms` 강제는 이 수신자에게 3전 3패였고(전부 `error=4` 미발송), 살아남은 건은 Messages 가 RCS 로 바꿔준 운이었다. 7/25 미발송이 성공으로 기록돼 7월 잔액이 통째로 누락된 사고의 원인.
- **답신 미도착 감시**: 발신 후 2일간 답신 없으면 골디 텔레그램 1회 경고. `imsg send` 는 발송 실패를 알려주지 않아(rc=0) 이게 유일한 안전망이다. 재발신은 자동 X — `python3 gahee_reminder.py --send-now` 로 수동.
- ⚠️ `--send-now` 는 **내 세션에서 못 돌린다** (자동화 → 메시지 권한이 Claude Code 에 거부). launchd 임시 oneshot plist 경유로 실행. 상세는 SKILL.md.
-87
View File
@@ -1,87 +0,0 @@
# TOOLS.md - Local Notes
가계부 관련 파일·스크립트·발신번호 메모. 모든 경로는 `agents/budget/workspace/` 기준.
## Whooing
- 가계부 시스템: https://whooing.com
- **웹훅 URL:** `/Users/snowoyh/.openclaw/credentials/whooing.json` 의 `webhook_url` 필드.
형식: `https://whooing.com/webhook/s/xxxx-xxxx-xxxx-xxxx`
- 웹훅 페이로드 종류:
- **raw 문자**: `message=<원문>` 보내면 후잉이 알아서 파싱
- **구조화 JSON**: `{entry_date, item, money, left, right, memo}` (left=차변, right=대변)
- 결제 취소/승인취소도 같이 전송해야 후잉에서 -금액으로 상쇄됨.
## State
- `state/whooing_synced.json` — `{ "last_message_at": <ISO>, "last_synced_at": "ISO" }`. dedupe 기준.
- `state/whooing_account_map.json` — 발신번호 → 후잉 매핑. `confirmed: true`인 엔트리만 후잉으로 자동 전송. `confirmed: false`는 발견 시 로그만 남기고 사용자 확인 대기.
- `state/whooing_accounts.json` — **관리자님 후잉 계정과목 차트** (자산/부채/순자산/비용/수익 + carrier→결제수단 매핑). 구조화 입력으로 left/right 채울 때 항상 이 파일에서 정확한 계정명을 끌어쓴다. 차트에 없는 계정명은 후잉이 거부하므로 절대 추측해 보내지 말 것.
- `state/whooing_failures.json` — 후잉이 4xx/5xx로 거절한 건. 수동 확인용.
## Scripts (python3)
- `skills/whooing-sync/scripts/whooing_sync.py` — iMessage 결제문자 → 후잉 웹훅 POST. launchd `ai.openclaw.budget.whooing-sync` 가 매시 0/15/30/45분 실행 (OpenClaw cron 아님). FDA 필수(`/opt/homebrew/bin/imsg`). 페어 매칭 로직 내장 — 자세한 건 `skills/whooing-sync/SKILL.md`.
- `skills/whooing-sync/scripts/whooing_manual.py` — iMessage 없이 한 건 직접 등록. structured(`--item/--money/--left/--right [--date] [--memo]`) 또는 raw(`--message`). structured는 `whooing_accounts.json` 차트 검증 후 POST.
- `skills/whooing-sync/scripts/whooing_balance.py` — 후잉 OpenAPI로 자산/부채/자본 잔액 조회. 옵션: `--section-id`, `--as-of YYYY-MM-DD`, `--json`. 크리덴셜은 `credentials/whooing.json`의 `api` 블록(app_id/token/signature).
## 발신번호
`state/whooing_account_map.json`이 정답. 아래는 사람이 읽기 쉬운 요약.
**확인됨 (관리자님이 직접 확인):**
| 발신처 | 발신번호 |
|--------|---------|
| 하나 계좌이체 | `+8215991111` |
| 신한은행 | `+8215778000` |
| 신한카드 | `+8215447000` (※ 과거 오타 `+8215447200` 쓴 적 있음 — 실사용 X, 현재 confirmed=false 로 대기) |
**추정 (모델 사전지식 기반, 실제 메시지 들어오면 confirmed로 승격 필요):**
| 발신처 | 추정 발신번호 |
|--------|--------------|
| 삼성카드 | `+8215888900` |
| KB국민카드 | `+8215881688` |
| 현대카드 | `+8215776200` |
| 롯데카드 | `+8215888100` |
| 하나카드 | `+8218001111` |
| 우리카드 | `+8215889955` |
| BC카드 | `+8215884000` |
| NH농협카드 | `+8216444000` |
| 카카오페이 | `+8216447405` |
| 네이버페이 | `+8215883819` |
| 토스 | `+8215994905` |
| KB국민은행 | `+8215889999` |
| 우리은행 | `+8215885000` |
| NH농협은행 | `+8216613000` |
| 카카오뱅크 | `+8215993333` |
**정책:** 미확인 번호 메시지가 들어오면 무시 또는 `failures.json`에 "unmapped"로 기록만 한다. 잘못된 자동 등록을 막기 위함.
## 관리자님 관련
- 가계부 질문이 들어오면 먼저 `state/whooing_synced.json`과 `whooing_failures.json`을 본다.
- 잔액·계정 총합은 `whooing_balance.py`로 조회한다. 후잉 OpenAPI(`bs.json`/`accounts.json`/`sections.json`) 연동 완료.
## Playwright MCP (브라우저 자동화)
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
- **탐색:** `navigate / navigate_back / tabs / wait_for`
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
**운영 특성:**
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
- 첫 호출 시 npx spawn 1~2초 지연.
- 은행·카드사 공동인증서·간편비밀번호 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
**활용 시나리오 (골디 맥락):**
- 후잉 웹 직접 분개 수정/삭제 — 현재 inbox·webhook 단방향 한계 보완용. **영속 로그인 필요** → 코디에게 user-data-dir 분리 요청 후 진행
- 카드사 웹 청구서 PDF 수집 (월별 명세 검증·보조). 공동인증서·간편비번 화면 진입 시 중단
- 우선순위는 항상 **API · iMessage 결제문자 → 후잉 webhook 자동 동기화**. 브라우저는 자동 흐름이 못 메꾸는 잔여 영역만
@@ -1,3 +1,4 @@
# 2026-05-07
- 관리자님 지시: `삼천리도시가스`는 후잉에서 `주거,관리` 항목으로 분류하고, item은 `공과금`으로 처리.
- ⚠️ **2026-08-06 정정: 계정은 `주거,통신`** — 후잉 차트에 `주거,관리` 계정이 존재하지 않는다(그래서 이 날 실제 입력분도 `주거,통신`으로 들어갔다). item `공과금`은 그대로. 상세는 `memory/2026-08-06.md`.
@@ -0,0 +1,4 @@
# 2026-06-20
- 신한은행 잔액 불일치 `144원`은 관리자님 확인으로 은행이자였다. 후잉에 `신한은행(효원) ← 기타수익`, item `이자`, memo `은행이자`, 금액 `144원`, 날짜 `2026-06-20`로 수동 등록했고, 후잉 잔액이 SMS 잔액 `307,004원`과 일치함을 검증했다. `state/whooing_balance_alerts.json`의 신한은행 정합성 상태도 diff `0`으로 정리했다.
- 관리자님 지시로 신한은행도 잔액 불일치 자동 이자 보정 대상에 추가했다. `whooing_sync.py` 정합성 체크에서 자산 계정이고 후잉 잔액이 SMS보다 부족하며 차이가 `5,000원` 이하이면 카카오뱅크와 동일하게 `{은행계정} ← 기타수익`, item `이자`, memo `은행이자`로 자동 분개한다.
@@ -0,0 +1,27 @@
# 2026-08-06
## 신한카드 삼천리도시가스 분류 실패 — 원인 3겹 (코디 수정)
가스요금 14,000원 SMS가 structured 분류를 통과하지 못하고 raw 폴백 → **후잉에 거래가 아예 생성되지 않았다**(후잉 자체 파서도 이 형식을 못 읽음). 8/6 후잉 거래에 복권·톨비·쿠팡 3건만 있었음.
1. **파서 공백** — 신한카드 `자동납부` SMS는 기존 승인 SMS와 **금액/상호 순서가 반대**다.
- 기존: `신한카드(6847)승인 방*원님 아파트 관리비 184,440원 정상승인 되었습니다.` (상호 → 금액)
- 자동납부: `[신한카드] 자동납부 정상승인 방효원님 08/06 (일시불) 14,000원 삼천리도시가스` (금액 → 상호)
`SHINHAN_APPROVE_RE`가 `[신한카드]`의 `]`에서 막혀 처음부터 한 번도 파싱된 적 없음(2026-05-07·08-06 두 건 전부 raw). `parsers.py`에 `SHINHAN_AUTOPAY_RE` 신설, `parse_shinhan_card`가 자동납부 → 일반 순으로 시도.
⚠️ `SHINHAN_APPROVE_RE`를 느슨하게 고치는 건 안 된다 — merchant 자리에 `08/06 (일시불)`이 잡힌다.
2. **계정 `주거,관리`는 후잉에 없다** — `삼천리도시가스` 매핑이 이 죽은 계정을 가리키고 있었다. 2026-06-30 `주택임대료`가 같은 계정으로 `left account does not exist at entry_date` 실패한 그 계정. **`주거,통신` / item `공과금`으로 확정**(2026-05-07 실제 입력분·`도시가스` 기본 매핑·주택임대료 정정 선례와 동일). 2026-05-07 메모의 `주거,관리` 지시는 그 메모에 정정 표기함.
3. **`state/whooing_accounts.json`이 stale**(updated_at 2026-04-23) — `whooing_manual.py`의 차트 검증이 이 파일을 보는데 죽은 계정 `주거,관리`·`카뱅지금통`(실제는 `카뱅저금통`)을 싣고 있어 로컬 검증만 통과하고 후잉에서 터졌다. 라이브 `accounts.json`으로 전체 동기화(자산 +7/−1, 부채 +2, 비용 −1, updated_at 2026-08-06). 매핑 계정을 늘릴 때는 이 스냅샷이 최신인지 먼저 확인할 것.
누락분 14,000원은 `whooing_manual.py`로 보정 입력 완료(`20260806 / 공과금 / 주거,통신 ← 신한신용(효원)`).
## ⚠️ raw 폴백 200은 "후잉에 기록됨"이 아니다
후잉 raw 엔드포인트는 **자기 파서가 실패해도 200을 준다**. `✅ [raw] … 200` 로그와 `sent_raw` 카운트는 "접수됨"일 뿐이라, 이번 누락이 5월부터 안 보인 이유다. raw 폴백 알림을 받으면 **후잉에 실제로 줄이 생겼는지 `entries.json`으로 확인**해야 한다. (`imsg send`의 rc=0이 발송 성공이 아닌 것과 같은 함정.)
## 범위 밖 (관리자님 판단 대기)
- **현대카드 자동납부도 같은 파서 공백** — `[현대카드] 자동납부 승인 방*원님 라이나생명 31,100원`(2026-05-17 raw). 다만 `whooing_sync.py`의 잔액차 기반 월 1회 자동보정 + `_should_skip_hyundai_insurance_duplicate`가 이미 커버해 실질 누락 없음. 미수정.
- raw 폴백분이 후잉에 실제 반영됐는지 자동 검출하는 장치는 붙이지 않았다.
@@ -0,0 +1 @@
- 관리자님 지정: `쿠팡페이주식회사`도 쿠팡 구매로 보고 후잉 자동분류는 `생활용품 ← {결제수단}`으로 처리한다. 쿠팡이츠·쿠팡플레이와 섞이지 않게 `state/whooing_merchant_map.json` exact 룰로만 등록했다.
@@ -0,0 +1,3 @@
- 현대카드 자동납부 문자 `[현대카드] 자동납부 승인 ... 라이나생명 31,100원`이 raw 폴백으로 들어간 원인을 확인했다. 기존 구조는 라이나생명 SMS 자체를 파싱하지 않고 현대카드 누적액-후잉 잔액 차이 `-31,100원`일 때 월 1회 자동보정하는 방식이라, 차이가 이미 사라진 2026-08 건은 raw 전송으로 빠졌다.
- 조치: `skills/whooing-sync/scripts/parsers.py`에 현대카드 자동납부 한 줄 형식 파서를 추가하고, `state/whooing_overrides.json`에 `현대카드 라이나생명 보험료 자동납부` 룰을 추가했다. 이후 같은 문자는 `의료,건강,보험 ← 현대신용(효원)`, item=`보험료`, amount=`31,100`으로 structured 처리된다. 기존 월 1회 잔액차 자동보정 및 보정 후 지연 SMS skip은 중복 방지용으로 유지.
- 후속 확인: 2026-08-19 `현대 블루멤버스 승인 ... 쿠팡페이주식회사 18,500원`도 raw 200이었지만 후잉 잔액에 반영되지 않았다. 2026-08-19 쿠팡 18,500원(`생활용품 ← 현대신용(효원)`)과 2026-08-20 라이나생명 31,100원(`의료,건강,보험 ← 현대신용(효원)`)을 structured 수동 반영했고, 현대신용 잔액이 `1,104,760원`에서 `1,154,360원`으로 증가해 기대값과 일치함을 확인했다. `parsers.py`는 `현대 블루멤버스`처럼 "카드"가 빠진 현대카드 문자도 파싱하도록 추가 보강했다.
@@ -0,0 +1,4 @@
# 2026-08-29
- 관리자님 지정: 현대카드 결제내역 가맹점명이 정확히 `쿠팡`이면 배달로 처리한다. 후잉 계정과목에는 `배달` 비용계정이 없으므로 자동등록은 `식비` left + item `배달`로 보낸다.
- `쿠팡페이주식회사` 표기는 기존처럼 생활용품 처리한다. 월 29일 7,890원 쿠팡페이 정기결제 예외는 기존 `지식,문화` + item `계정결제_쿠팡` 유지.
@@ -0,0 +1 @@
- 하나은행 2026-09-10 09:27 `방효원` 3,000,000원 출금은 관리자님 확인으로 주식계좌 이체였다. 후잉에 `증권(효원) ← 하나은행(효원)`, item `증권이체`, memo 원문으로 수동 등록했고 응답 200/done 확인. 등록 후 하나은행 후잉 잔액 `35,689,680원`이 SMS 잔액과 일치해 `state/whooing_failures.json`의 `capital_blocked` 보류 건을 비웠다.
@@ -0,0 +1,3 @@
# 2026-10-07
- 관리자님 확인: 2026-10-07 10:56경 하나은행 900,000원 출금(`카뱅오픈방효원`)은 비용이 아니라 **증권계좌 이체**. 자동 동기화의 `기타비용 ← 하나은행(효원)` 오분류를 관리자님 요청으로 거래 ID 1231373에서 `증권(효원) ← 하나은행(효원)`으로 직접 수정함. 후잉 API 재조회로 방향·금액 확인 완료.
@@ -1,3 +1,8 @@
---
name: monthly-settlement
description: 매월 1일 후잉 잔액을 스냅샷으로 저장하고 전월 스냅샷과 비교해 자산 변동 리포트를 생성·발송한다. "이번달 결산", "월간 결산", "자산 변동 요약" 요청에 사용. 매월 1일 05:00 KST cron(agent: budget) 자동 실행.
---
# monthly-settlement
매월 1일에 후잉 잔액을 스냅샷으로 저장하고, 전월 스냅샷과 비교해 자산 변동을 리포트한다.
@@ -43,11 +43,13 @@ BALANCE_SCRIPT = WORKSPACE / 'skills' / 'whooing-sync' / 'scripts' / 'whooing_ba
TELEGRAM_ACCOUNT = 'budget'
KNOWN_TOPICS = {'securities_balance'}
SUPPORTED_SCHEMA = {1}
SUPPORTED_SCHEMA = {1, 2}
SECURITIES_ASSET_NAME = '증권(효원)'
SECURITIES_ASSET_NAME = '증권(효원)' # v1(단일 소유자) 경로 및 self 자산명
SECURITIES_GAIN_NAME = '주식평가수익'
SECURITIES_LOSS_NAME = '주식평가손실'
# 소유자 → 후잉 자산명 (v2). 손익 계정(주식평가수익/손실)은 양쪽 공용.
OWNER_ASSET = {'self': '증권(효원)', 'gahee': '증권(가희)'}
RECONCILE_NOISE_FLOOR = 10_000 # |차액| < 1만원: 노이즈, 분개 skip
RECONCILE_HARD_CAP = 100_000_000 # |차액| > 1억원: 분개 거부 (안전 가드)
@@ -124,6 +126,17 @@ def validate_envelope(env: dict) -> None:
raise ValidationError('payload 가 dict 가 아님')
def _validate_amount_block(block: dict, where: str) -> None:
for key in ('deposit', 'eval_amount', 'total'):
if key not in block:
raise ValidationError(f'{where}.{key} 누락')
v = block[key]
if not isinstance(v, int) or v < 0:
raise ValidationError(f'{where}.{key} 비정상: {v!r}')
if v > PAYLOAD_AMOUNT_CAP:
raise ValidationError(f'{where}.{key} 100억 초과 (가드): {v:,}')
def validate_securities_payload(payload: dict) -> None:
for key in ('as_of', 'accounts', 'totals', 'owner_scope'):
if key not in payload:
@@ -137,24 +150,28 @@ def validate_securities_payload(payload: dict) -> None:
if as_of_dt.day not in (1, 10, 20):
raise ValidationError(f'as_of 는 매월 1·10·20일이어야 함: {as_of}')
totals = payload['totals']
for key in ('deposit', 'eval_amount', 'total'):
if key not in totals:
raise ValidationError(f'totals.{key} 누락')
v = totals[key]
if not isinstance(v, int) or v < 0:
raise ValidationError(f'totals.{key} 비정상: {v!r}')
if v > PAYLOAD_AMOUNT_CAP:
raise ValidationError(f'totals.{key} 100억 초과 (가드): {v:,}')
_validate_amount_block(payload['totals'], 'totals')
if not isinstance(payload['accounts'], list) or not payload['accounts']:
raise ValidationError('accounts 비어있음')
sum_total = sum(int(a.get('total', 0)) for a in payload['accounts'])
if sum_total != totals['total']:
if sum_total != payload['totals']['total']:
raise ValidationError(
f'accounts 합계 불일치: {sum_total:,} vs totals.total={totals["total"]:,}'
f'accounts 합계 불일치: {sum_total:,} vs totals.total={payload["totals"]["total"]:,}'
)
# v2: 소유자별 집계(by_owner) 검증. 미등록 소유자·구조 오류 차단.
by_owner = payload.get('by_owner')
if by_owner is not None:
if not isinstance(by_owner, dict) or not by_owner:
raise ValidationError('by_owner 비어있음')
for owner, block in by_owner.items():
if owner not in OWNER_ASSET:
raise ValidationError(f'by_owner 미등록 소유자: {owner}')
if not isinstance(block, dict):
raise ValidationError(f'by_owner.{owner} 가 dict 가 아님')
_validate_amount_block(block, f'by_owner.{owner}')
def fetch_whooing_balance(as_of: str | None = None) -> dict:
cmd = ['python3', str(BALANCE_SCRIPT), '--json']
@@ -197,23 +214,25 @@ def post_whooing(payload: dict, dry_run: bool = False) -> tuple[bool, str]:
return False, str(e)
def handle_securities_balance(envelope: dict, current_balance: dict, dry_run: bool = False) -> dict:
"""차액 reconcile + 후잉 자동 분개. 반환: {action, delta, before, after, journal_ok, note, journal?}.
def _reconcile_asset(asset_name: str, owner: str, target_total: int,
as_of: str, message_id: str, current_balance: dict,
dry_run: bool = False) -> dict:
"""자산 하나를 목표총액으로 reconcile + 후잉 자동 분개. 반환 result dict.
action ∈ {aligned, journaled, noise, skipped, rejected, journal_failed}.
"""
payload = envelope['payload']
target_total = int(payload['totals']['total'])
current = get_whooing_balance_for(SECURITIES_ASSET_NAME, current_balance)
current = get_whooing_balance_for(asset_name, current_balance)
base = {
'topic': 'securities_balance',
'as_of': payload.get('as_of'),
'owner': owner,
'asset_name': asset_name,
'as_of': as_of,
'target_total': target_total,
}
if current is None:
return {
**base,
'action': 'skipped',
'note': f'후잉 자산에 "{SECURITIES_ASSET_NAME}" 항목 없음',
'note': f'후잉 자산에 "{asset_name}" 항목 없음',
'delta': None, 'before': None, 'after': None, 'journal_ok': None,
}
delta = target_total - current
@@ -230,14 +249,14 @@ def handle_securities_balance(envelope: dict, current_balance: dict, dry_run: bo
'before': current, 'after': current, 'journal_ok': False,
'note': f'차액 {delta:+,}원 — 1억원 초과, 분개 거부 (가드)'}
entry_date = payload['as_of'].replace('-', '')
ym = payload['as_of'][:7]
entry_date = as_of.replace('-', '')
ym = as_of[:7]
if delta > 0:
item = f'{ym} 평가차익'
left, right, money = SECURITIES_ASSET_NAME, SECURITIES_GAIN_NAME, delta
left, right, money = asset_name, SECURITIES_GAIN_NAME, delta
else:
item = f'{ym} 평가차손'
left, right, money = SECURITIES_LOSS_NAME, SECURITIES_ASSET_NAME, -delta
left, right, money = SECURITIES_LOSS_NAME, asset_name, -delta
journal = {
'entry_date': entry_date,
@@ -245,7 +264,7 @@ def handle_securities_balance(envelope: dict, current_balance: dict, dry_run: bo
'money': str(money),
'left': left,
'right': right,
'memo': f'레이 inbox reconcile (msg={envelope["message_id"][:8]})',
'memo': f'레이 inbox reconcile (msg={message_id[:8]})',
}
ok, body = post_whooing(journal, dry_run=dry_run)
return {
@@ -260,6 +279,31 @@ def handle_securities_balance(envelope: dict, current_balance: dict, dry_run: bo
}
def handle_securities_balance(envelope: dict, current_balance: dict, dry_run: bool = False) -> list[dict]:
"""소유자별 reconcile. v2(by_owner)는 소유자마다, v1은 totals.total→증권(효원) 단건.
반환: result dict 리스트.
"""
payload = envelope['payload']
as_of = payload['as_of']
msg_id = envelope['message_id']
by_owner = payload.get('by_owner')
if by_owner:
results = []
for owner, block in sorted(by_owner.items()): # 'gahee' < 'self' — 결정적 순서
results.append(_reconcile_asset(
OWNER_ASSET[owner], owner, int(block['total']),
as_of, msg_id, current_balance, dry_run=dry_run,
))
return results
# v1 하위호환: 단일 소유자(self) → 증권(효원)
return [_reconcile_asset(
SECURITIES_ASSET_NAME, 'self', int(payload['totals']['total']),
as_of, msg_id, current_balance, dry_run=dry_run,
)]
def move_to(src: Path, dest_dir: Path, dry_run: bool = False) -> None:
if dry_run:
return
@@ -350,24 +394,28 @@ def process_inbox(current_balance: dict | None = None, dry_run: bool = False) ->
validate_securities_payload(env['payload'])
if balance is None:
balance = fetch_whooing_balance()
result = handle_securities_balance(env, balance, dry_run=dry_run)
summary['reconcile'].setdefault(topic, []).append(result)
results = handle_securities_balance(env, balance, dry_run=dry_run)
summary['reconcile'].setdefault(topic, []).extend(results)
if result['action'] in ('rejected', 'journal_failed'):
# 소유자 하나라도 rejected/journal_failed 면 failed 로.
# 재실행 시 이미 분개된 소유자는 fresh 조회로 delta≈0 aligned → 중복분개 없음.
bad = [r for r in results if r['action'] in ('rejected', 'journal_failed')]
if bad:
move_to(fpath, FAILED_DIR, dry_run=dry_run)
reason = result['note']
reason = '; '.join(f'{r["asset_name"]}: {r["note"]}' for r in bad)
summary['failed'].append({'file': fpath.name, 'reason': reason, 'message_id': msg_id})
send_telegram(f'⚠️ 골디 inbox: 분개 {result["action"]}\n{fpath.name}\n{reason}')
send_telegram(f'⚠️ 골디 inbox: 분개 {"/".join(r["action"] for r in bad)}\n{fpath.name}\n{reason}')
continue
# 분개 성공/aligned/noise/skipped 모두 processed 로 이동
if result.get('journal_ok') and result['action'] == 'journaled' and balance is not None:
# 후잉 잔액 in-memory 업데이트 (다음 파일 reconcile 시 정합성 위해)
# 분개 성공분은 후잉 잔액 in-memory 업데이트 (동일 run 내 정합성 위해)
if balance is not None:
for r in results:
if r.get('journal_ok') and r['action'] == 'journaled':
for sec in balance.get('sections', []):
items = (sec.get('groups', {}).get('자산', {}) or {}).get('items', []) or []
for it in items:
if it.get('name') == SECURITIES_ASSET_NAME:
it['money'] = result['after']
if it.get('name') == r['asset_name']:
it['money'] = r['after']
else:
raise ValidationError(f'핸들러 없음: {topic}')
@@ -415,22 +463,23 @@ def format_summary(summary: dict) -> str:
before = result.get('before')
after = result.get('after')
delta = result.get('delta')
label = result.get('asset_name', '증권(효원)')
if action == 'aligned':
lines.append(f'- **증권(효원):** {before:,}원 — 레이 ground truth 와 일치')
lines.append(f'- **{label}:** {before:,}원 — 레이 ground truth 와 일치')
elif action == 'journaled':
sign = '평가차익' if (delta or 0) > 0 else '평가차손'
lines.append(
f'- **증권(효원):** {before:,}원 → {after:,}원 '
f'- **{label}:** {before:,}원 → {after:,}원 '
f'({sign} **{abs(delta):,}원** 자동 분개)'
)
elif action == 'noise':
lines.append(f'- **증권(효원):** 차액 {delta:+,}원 (1만원 미만, 분개 skip)')
lines.append(f'- **{label}:** 차액 {delta:+,}원 (1만원 미만, 분개 skip)')
elif action == 'skipped':
lines.append(f'- **증권(효원):** {result.get("note", "skip")}')
lines.append(f'- **{label}:** {result.get("note", "skip")}')
elif action == 'rejected':
lines.append(f'- **증권(효원):** ⚠️ 차액 {delta:+,}원 — 분개 거부 (안전 가드)')
lines.append(f'- **{label}:** ⚠️ 차액 {delta:+,}원 — 분개 거부 (안전 가드)')
elif action == 'journal_failed':
lines.append(f'- **증권(효원):** ⚠️ 분개 실패 — {result.get("note", "")[:120]}')
lines.append(f'- **{label}:** ⚠️ 분개 실패 — {result.get("note", "")[:120]}')
if summary['failed']:
lines.append('')
@@ -238,19 +238,20 @@ def format_inbox_telegram_line(summary: dict) -> str:
for r in summary.get("reconcile", {}).get("securities_balance", []):
action = r.get("action")
delta = r.get("delta") or 0
label = r.get("asset_name", "증권(효원)")
if action == "aligned":
parts.append("증권 일치")
parts.append(f"{label} 일치")
elif action == "journaled":
sign = "차익" if delta > 0 else "차손"
parts.append(f"증권 평가{sign} {abs(delta):,}원 분개")
parts.append(f"{label} 평가{sign} {abs(delta):,}원 분개")
elif action == "noise":
parts.append(f"증권 차액 {delta:+,}원 (노이즈)")
parts.append(f"{label} 차액 {delta:+,}원 (노이즈)")
elif action == "rejected":
parts.append(f"⚠️ 증권 차액 {delta:+,}원 거부")
parts.append(f"⚠️ {label} 차액 {delta:+,}원 거부")
elif action == "journal_failed":
parts.append("⚠️ 증권 분개 실패")
parts.append(f"⚠️ {label} 분개 실패")
elif action == "skipped":
parts.append("증권 reconcile skip")
parts.append(f"{label} reconcile skip")
# rejected/journal_failed 는 위 reconcile 라인에서 이미 표시됨 — 중복 카운트 방지
reconcile_failed = sum(
1 for r in summary.get("reconcile", {}).get("securities_balance", [])
@@ -1,3 +1,8 @@
---
name: whooing-sync
description: iMessage 카드·은행 결제 알림을 후잉(whooing.com) 가계부로 자동 동기화한다. 발신번호→계정 매핑(whooing_account_map.json), 결제 취소 감액 처리, 미매핑 발신처 보고, 수동 분개 등록(whooing_manual.py), 잔액 조회(whooing_balance.py)를 포함. "가계부 동기화", "후잉 동기화", "결제내역 정리", "이번달 지출 얼마야?" 같은 요청에 사용. 기본 운용은 launchd가 매시 0/15/30/45분 자동 실행.
---
# whooing-sync
iMessage에 들어오는 카드/은행 결제 알림을 후잉(whooing.com) 웹훅으로 자동 전송한다.
@@ -10,6 +15,18 @@ iMessage에 들어오는 카드/은행 결제 알림을 후잉(whooing.com) 웹
## How
**on-demand 실행은 launchd 잡을 앞당겨 돌린다.** 스크립트를 직접 실행하지 말 것 —
에이전트 세션(코덱스)은 게이트웨이 node 를 TCC responsible process 로 물고 있어서
`imsg` 가 Messages DB 를 못 읽는다 (아래 "Full Disk Access" 참고). launchd 잡은 그 제약이 없다.
```bash
launchctl kickstart -p gui/$(id -u)/ai.openclaw.budget.whooing-sync
# 비동기 — 몇 초 뒤 결과 확인
tail -20 /Users/snowoyh/.openclaw/logs/whooing-sync.log
```
스크립트 직접 실행은 **FDA 를 가진 터미널 앱**(iTerm2 등)에서만 동작한다:
```bash
python3 /Users/snowoyh/.openclaw/agents/budget/workspace/skills/whooing-sync/scripts/whooing_sync.py
```
@@ -39,6 +56,24 @@ launchd/cron 이 이 줄을 그대로 결과로 받는다.
- **실패 알림** — 후잉 웹훅이 거절(HTTP 에러, 본문 `fail` / `Error :`)한 건. 실패 1건당 1메시지, 4건 이상이면 앞 3건 + 초과분 요약. `_format_sync_failure()` 포맷.
- **raw 폴백 알림** — 후잉은 200 받았지만 structured 매칭 실패로 raw 모드로 넘어간 건. sync 사이클당 **1메시지**(최대 3건 나열, 초과 시 카운트). parser / carrier_to_account / merchant_map 보완 신호. `_format_raw_fallback()` 포맷.
- **기초잔액(자본) 분개 보류 알림** (2026-09-07) — 분개의 좌·우 어느 쪽이든 `순자산` 계정(`기초잔액(효원)`/`기초잔액(가희)`)이면 **후잉에 넣지 않고 알림만** 보낸다(관리자님 지시). ⚠️ 2026-09-10 부터 **여기 오기 전에 증권 이체 매칭을 먼저 시도**한다(아래 「증권 이체 매칭」) — 짝을 찾으면 자본이 아니라 자산↔자산 이체로 분개되고, 이 알림은 짝을 못 찾은 건에만 남는다. `_format_capital_blocked()` 포맷, 기록은 `whooing_failures.json` 의 `capital_blocked` 키.
- 배경: `merchant_map` 의 `방효원` exact 룰(`left: 기초잔액(효원)`, "본인 명의 자체이체")은 **짝이 되는 출금 SMS 가 같이 와서 상계되는 것**을 전제한다. 짝이 없으면 자본 한쪽만 남아 **순자산이 조용히 부풀려진다.** 2026-09-07 카카오뱅크 879,993 실사고 — 처제 대납금 상환인데 입금자명이 "방효원"이라 이 룰에 걸렸고, 금액이 08/27 클럽호핑과 정확히 일치해서 겨우 눈에 걸렸다(어중간한 금액이면 지나갔다).
- ⚠️ **커서는 `latest_skip` 으로 진행시킨다** — 재시도해도 같은 룰이 같은 결과를 내므로 여기서 커서를 막으면 **이 메시지 뒤의 모든 거래가 영구히 멈춘다.** `blocked_at` 을 세우지 않는 이유.
- 후잉엔 아무것도 안 들어가므로 그 계좌의 **잔액 불일치 알림이 뒤이어 뜬다** — 알림을 놓쳐도 두 번째 그물이 있다.
- 분개는 관리자님이 직접: 진짜 자체이체면 **실제 출금 계좌**를 상대편으로, 아니면 성격에 맞는 계정으로.
- **은행이자 자동 분개 + 보류 알림** — 자산 계정에서 `diff < 0` 이고 `|diff| <= 5,000` 이면 차액 전액을 `계정 ← 기타수익 / 이자` 로 분개한다(카카오·신한·하나). 잔돈 단위 이자 SMS 가 안 오는 걸 메우는 장치.
- ⚠️ **같은 사이클에 raw 폴백이 있던 계정은 분개하지 않고 알림만 보낸다** (2026-09-07 추가). 입금 SMS 는 structured 매칭에 실패해 raw 로 넘어가는데, 후잉은 200 만 주고 분개를 만들지 않는다. 그러면 diff 가 그 입금액만큼 음수가 되고, 이 heuristic 이 **그걸 이자로 오인해 가짜 수익을 원장에 굳혀버린다.**
- 실사고: 2026-08-14 카카오뱅크 `+3,700원 은행이자` — 한재성 입금 10,900 미기록이 원인(기존 +7,200 드리프트와 상쇄돼 diff 가 −3,700). **진짜 8월 이자는 08/10 에 SMS 로 6,735원이 이미 들어와 있었다.** 한 달에 이자가 두 번 나올 리 없다는 게 판별점.
- 판별 원칙: **진짜 은행이자는 SMS 로 온다**(06/10 1,711원·07/10 1,552원·08/10 6,735원). SMS 없이 자동분개된 건은 원래 1~300원대 잔돈이어야 정상이고, 네 자리 이상이면 의심한다.
- **잔액 불일치 알림** — SMS 잔액(자산)·누적사용액(카드)과 후잉 잔액이 어긋난 건. `_format_balance_mismatch()` 포맷. 차이가 직전과 같으면 재알림하지 않는다.
- **누적 연속성 검사** (2026-09-07): 카드(부채) 계정에서 `diff < 0`(사용 거래 누락 의심)일 때만 `_cumulative_gap_scan()` 이 돌아 **어느 승인 사이에 얼마가 빠졌는지**까지 알림에 붙인다. 현대카드 SMS 의 `누적`은 승인마다 `직전 누적 + 승인액`이어야 하므로, 어긋난 구간이 곧 **문자가 안 오는 승인**(톨비 등)이다. 잔액 비교만으로는 "얼마 틀어졌다"까지만 알 수 있어 발생 시점을 못 짚었다.
- ⚠️ **양의 격차만 센다** — 카드대금 상환·정산은 누적을 떨어뜨리는데 이걸 격차로 세면 오탐이다.
- ⚠️ **이미 손으로 보정해 둔 건은 구간에서 차감한다** — 메모가 SMS 원문으로 파싱되지 않는 항목(`현대카드 잔액차 보정: 톨비` 등)을 앞선 구간부터 소진시킨다. 안 그러면 톨비를 넣어준 뒤에도 같은 구간이 영원히 미해결로 뜬다. 한 보정이 여러 구간에 중복 차감되지 않게 순서대로 소비한다.
- ⚠️ **호출 조건을 넓히지 말 것** — 후잉 entries 조회가 들어가므로 평시(diff 0)엔 돌지 않는다.
- 자동 분개는 하지 않는다. 톨비인지 다른 누락인지는 관리자님이 판단한다.
같은 SMS 1건이 두 알림에 동시에 걸리는 일은 없다 (`if ok / else` mutually exclusive). 한 사이클에서 서로 다른 SMS 들이 각각 raw · 실패로 나뉘면 두 메시지가 함께 간다.
`--dry-run` 시에는 둘 다 발송 안 함.
@@ -72,18 +107,42 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.budget.whooi
## 전제조건: Full Disk Access (FDA)
launchd 컨텍스트에서 `imsg` 가 `~/Library/Messages/chat.db` 를 읽으려면 FDA 허용이 필요. 터미널에서 직접 돌릴 땐 터미널 앱의 FDA 를 자식 프로세스가 상속받지만, launchd 는 상속 안 됨.
`imsg` 가 `~/Library/Messages/chat.db` 를 읽으려면 FDA 허용이 필요. **판정 주체는 TCC responsible process** — 자기 자신이 아니라 프로세스 체인의 책임 프로세스다.
- 등록 대상: `/opt/homebrew/bin/imsg`
- 경로: 시스템 설정 → 개인정보 보호 및 보안 → 전체 디스크 접근 권한 → `+` 로 추가 → 토글 ON
- launchd → `/usr/bin/python3`(플랫폼 바이너리) → `imsg`: 책임이 `imsg` 자신이라 **`/opt/homebrew/bin/imsg` 승인으로 통과**
- 터미널 앱 → … → `imsg`: 터미널 앱(iTerm2 등)의 FDA 로 통과
- 게이트웨이 node → codex → bash → `imsg`: **게이트웨이 node 바이너리**의 FDA 로 판정됨
**FDA 누락 증상**: stderr 로그에
등록 경로: 시스템 설정 → 개인정보 보호 및 보안 → 전체 디스크 접근 권한 → `+` 로 추가 → 토글 ON
**FDA 누락 증상**: `imsg` 가 stderr 로 다음을 낸다.
```
❌ imsg chats 실행 실패: Expecting value: line 1 column 1 (char 0)
Permission Error: Cannot access Messages database
... requires Full Disk Access permission.
```
가 뜨고, stdout 은 `🟢 새 결제 메시지 없음` 으로 조용히 끝남. 오류처럼 보이지 않아 놓치기 쉬움. 맥 이전 / imsg 재설치 시 FDA 재등록 필요.
`imsg_cli.run_json()` 이 이걸 잡아 로그에 그대로 옮긴다:
```
⚠️ chat-id 7 history 실패: Messages DB 접근 거부 — 전체 디스크 접근 권한(FDA) 누락. ...
```
### 2026-08-03 사고: homebrew node 업그레이드로 FDA 무효화
node@22 가 2026-07-20 에 `22.22.2` → `22.23.1` 로 올라가며 **바이너리 경로가 바뀌어** 기존 FDA 승인이 무효화됐다 (시스템 TCC.db 에 22.22.2=허용 / 22.23.1=거부로 남아 있음). 게이트웨이가 7/21 새 바이너리로 재기동되면서 골디의 에이전트 세션에서 `imsg` 가 죽었다. **launchd 잡은 영향 없었다** — 그래서 자동 동기화는 정상인데 골디만 실패하는 그림이 됐고, 골디는 이걸 "문자 수집 실패"로 오진했다.
- 재발 시 확인: `lsof -p $(pgrep -f 'openclaw/dist/index.js gateway') -a -d txt | head -1` 로 실제 node 경로를 뽑아, 그 경로를 FDA 목록에 추가
- 그 다음 게이트웨이 재기동: `launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway`
- **on-demand 실행을 kickstart 로 바꾼 이유가 이것** — node 업그레이드마다 깨지는 의존을 실행 경로에서 뺐다
당시 오진을 만든 코드 결함 3개는 함께 고쳤다 (2026-08-03):
1. 호출부가 stderr·returncode 를 버려 권한 오류가 `Expecting value: line 1 column 1 (char 0)` 파싱 에러로 둔갑 → `imsg_cli.py` 공용 래퍼로 사유 노출
2. `resolve_chat_ids()` 가 권한 거부 시 **빈 chat id 매핑을 캐시에 저장** → 이후 launchd 포함 전 실행이 캐시 적중으로 조용히 0건 수집을 영구 반복. 이제 전 carrier 가 비면 저장 거부 + exit 3
3. `imsg chats` 기본값이 최근 20건이라 조용한 발신번호(현대카드 SMS·신한은행)가 목록에서 빠짐 → 캐시 재생성 시 그 carrier 수집이 조용히 끊김. `CHATS_LIMIT=200` 으로 전량 조회
맥 이전 / imsg 재설치 시에도 FDA 재등록 필요.
## 페어 매칭 (자기 계좌 간 이체)
@@ -104,6 +163,30 @@ launchd 컨텍스트에서 `imsg` 가 `~/Library/Messages/chat.db` 를 읽으려
- 한쪽 은행이 confirmed=false → SMS 수집 안 됨. 메모 키워드("카뱅오픈방효원")가 `merchant_map.contains` 와 맞으면 withdrawal-only structured 로 처리. 아니면 raw.
- 시간창 벗어남 / 단독 입출금 / 외부 송금 → 각자 개별 처리.
## 증권 이체 매칭 (은행 SMS ↔ 키움 kt00015)
**은행에서 증권으로 이체하면 은행 SMS 에는 입금자명(=본인 이름)만 찍혀 목적지를 알 수 없다.** 그래서 `방효원` exact 룰에 걸려 `기초잔액 ← 은행` 이 되고, 위 자본 분개 가드에 막혀 **미반영**된다. 반대편(증권 입금)은 **SMS 가 아예 없어** 월 1회 `securities_balance` reconcile 의 차액으로만 나타나는데, 그 차액은 전부 `주식평가수익` 으로 분개되므로 **이체가 손익으로 둔갑**한다. 양쪽에서 동시에 새던 구멍이다.
2026-09-10 부터 그 짝을 **키움 kt00015 입출금 내역에서 찾아** `증권(효원) ← 하나은행(효원)` 이체 한 건으로 만든다. 모듈 `scripts/securities_match.py`, 진입점은 `whooing_sync.try_securities_transfer()`.
- **호출 시점은 capital-block 직전뿐** — 평시 키움 콜 0. 자본 분개로 갈 뻔한 건이 있는 사이클에만 계좌 수만큼(4콜) 조회한다. 그래서 15분 주기에 상시 조회가 붙지 않는다.
- **매칭 조건**: 같은 날짜 + 금액 일치 + 방향 반대(은행 출금↔증권 IN) + `investment_principal.is_principal_flow(적요)`. 적요 필터가 **배당·이자·수익분배금·공모주환불금을 걸러낸다** — 그것들은 이체가 아니라 수익이라 은행 SMS 와 짝이 될 수 없다.
- ⚠️ **애매하면 매칭하지 않는다** — 후보가 여럿이면 시각 근접(`TIME_TIEBREAK_SECONDS` 600초)으로 좁히고, 그래도 하나로 안 줄면 포기하고 기존 알림 경로로 보낸다. 틀린 분개보다 사람이 보는 편이 낫다. 조회가 한 계좌라도 실패하면 **전체를 포기**한다(일부만 보고 판단하면 오매칭).
- ⚠️ **증권 계좌 간 대체는 후보에서 뺀다**(`_drop_internal_transfers`) — 가희_일반→가희_ISA 같은 내부 이동은 후잉에선 같은 `증권(가희)` 자산이라 분개 대상이 아닌데, 금액이 같으면 은행 SMS 와 오매칭될 수 있다. 서로 다른 계좌에 같은 금액의 IN/OUT 이 함께 있으면 제외(2026-04-23 가희 100만원 실사례로 검증).
- ⚠️ **kt00015 행에는 고유 ID 가 없다** — 중복 분개 방지 키는 `날짜|계좌|IN,OUT|금액|시각` 합성이고 `state/whooing_securities_matched.json` 에 남는다(180일 후 정리). **POST 성공 후에만 기록**한다 — 실패하면 다음 사이클에 다시 짝으로 잡혀야 한다.
- 양방향 지원: 은행 출금→`증권 ← 은행`, 은행 입금(증권에서 뺀 돈)→`은행 ← 증권`.
- 부수효과: 이체가 실시간으로 들어가므로 **월 1회 reconcile 차액에 순수 평가손익만 남는다.**
출력 예시:
```
✅ [securities] 하나 2026-09-10T00:27:44Z 200 | 증권(효원) ← 하나은행(효원) 3,000,000원 (증권 일반 이체)
```
진단 CLI (매칭만 확인, 분개 없음):
```bash
python3 scripts/securities_match.py 20260910 3000000 withdrawal 2026-09-10T00:27:44.111Z
```
### merchant_map 주의사항
`state/whooing_merchant_map.json` exact 룰에 `"방효원": { "left": "기초잔액(효원)" }` 가 등록돼 있다. 페어 미성립 시 이 룰로 폴백해서 **기초잔액을 경유한 잘못된 분개**가 기록될 수 있다. 한쪽 carrier 가 confirmed=false 일 때 구멍이 크다.
@@ -118,7 +201,18 @@ launchd 컨텍스트에서 `imsg` 가 `~/Library/Messages/chat.db` 를 읽으려
- 사람 이름 송금(예: "박영춘", "이지윤")은 exact 룰로 등록하지 말고 default fallback 에 맡긴다. merchant_map 비대화 방지.
- `deposit` 은 default fallback 없음 — rule 없으면 raw 폴백 (수익/이체/환급 구분 위험 때문).
- ⚠️ **후잉은 계정 그룹별로 쓸 수 있는 변이 정해져 있다** — `비용(expenses)`은 **차변(left) 전용**, `수익(income)`은 **대변(right) 전용**이고 `자산·부채`만 양쪽 자유다. 위반하면 `[r_account] => 잘못된 값입니다`(비용을 우측에) 또는 `[l_account] => 잘못된 값입니다`(수익을 좌측에)로 **HTTP 200 + 실패 본문**이 온다. 이 제약을 몰라서 2026-08-03~04 회귀가 났다. 새 분개 방향을 만들 때 반드시 확인할 것.
- ⚠️ **후잉은 취소·환불 문자를 자동 처리하지 않는다.** 공식 FAQ(`/help/faqs/inserting/refund`)가 "기존의 구입 거래를 **찾아서 지워주거나**, 금액을 마이너스로..." 라고 **사용자에게 수동 안내**한다. raw 로 취소 원문을 보내도 원본 승인은 그대로 남는다. 2026-08-04 에 과거 3건(07-04 카카오 월드킹·06-09 현대 쿠팡·06-13 현대 네이버페이)에서 원본이 사라진 걸 보고 "후잉 파서가 지운다"고 판단했는데, **관리자님이 그동안 수동 삭제해 오신 것**이었다. 두 설명 모두 "건수 −1, 순액 일치"라 검산으로는 구분되지 않는다 — 같은 착각을 반복하지 말 것.
- `card_cancel` 은 **원본 승인 거래를 찾아 PUT 으로 감액**한다 (2026-08-04, 관리자님 결정 — 수동 삭제를 없애는 게 목적). `_handle_card_cancel` → `whooing_balance.find_card_entries` 로 같은 카드·같은 상호(memo 토큰 매칭)·금액이 취소액 이상인 거래를 30일 창에서 찾고, **정확히 1건일 때만** `set_entry_money` 로 `원금액 − 취소액` 으로 줄인다. 전액취소면 0원 줄이 남지만 잔액·합계에 영향은 없다.
- ⚠️ **좌우를 뒤집은 역분개로 바꾸지 말 것** — 비용이 대변으로 가서 후잉이 거부한다(위 제약). 2026-08-03 에 이 방식으로 바꿨다가 **첫 취소 문자에서 동기화가 통째로 멈췄다**(`blocked_at` 이 커서를 잠가 뒤의 정상 결제까지 전부 대기).
- ⚠️ **거래 삭제는 API 로 불가능하다** — 후잉 OpenAPI 의 DELETE 는 `section_id` 를 쿼리·본문(urlencoded/JSON)·쿠키·경로 어디에서도 읽지 못한다(2026-08-04 전수 실측, 같은 본문을 PUT 으로 보내면 정상). 0원 줄을 지우려면 후잉 UI 에서 수동으로. 마이너스 분개(후잉 공식 안내안)도 가능하지만 ± 두 줄이 남아 채택하지 않았다.
- **후보가 0건이거나 2건 이상이면 감액하지 않고 raw 폴백**(오감액보다 안전). 이때는 **후잉이 알아서 처리해주지 않으므로 관리자님 수동 작업이 필요하다** — 그래서 폴백 건은 `new_raw` 에 그대로 실어 raw 폴백 텔레그램 알림이 울리게 둔다. 알림을 끄면 수동 작업이 필요한 걸 모르고 지나친다.
- ⚠️ **매칭은 `item` 이 아니라 `memo` 로 한다** — `merchant_map` contains 룰이 `item` 을 재작성해서(동행복권→`복권`) item 은 상호와 다를 수 있지만 memo 는 항상 승인 SMS 원문이다.
- ⚠️ **memo 매칭은 토큰 단위**(`_memo_has_merchant`) — 단순 substring 이면 `쿠팡` 이 `쿠팡페이주` 승인까지 잡는다. 2026-08-04 에 두 상호가 같은 날 같은 금액(22,700원)으로 실제로 있었다.
- ⚠️ **`card_cancel` 을 내는 파서는 현대카드·신한카드(`매입취소`/`승인취소`) 둘뿐이다.** 카카오뱅크 `카드결제취소` 는 `KAKAOBANK_CARD_RE` 의 `카드결제\s+` 가 안 맞아 **파싱 자체가 실패**(`kind=None`)하고, 하나·신한은행은 입금/출금만 있다. **이들은 감액 대상이 아니라 raw 로 가고, 후잉도 처리하지 않으므로 관리자님 수동 삭제가 계속 필요하다.** 자동화하려면 `parsers.py` 보강 + 2026-08-03 픽스처 회귀 재실행이 필요하다 — **2026-08-04 관리자님 지시로 "실제 취소 문자가 올 때 처리"로 보류**. 판단 근거는 파싱이 실패해도 `_infer_raw_details` 가 정규식으로 금액·상호를 뽑아 raw 폴백 알림에 실어주기 때문(실측: 카카오 취소 문자 → `2,000원 · (주)월드킹`). 알림을 보고 그때 대응하면 된다.
- **삼성카드는 아예 동기화 대상이 아니다** — 발신번호 `+8215888700` 이 `whooing_account_map.json` 에서 `confirmed=False` 라 수집조차 안 되고, 실제 오는 문자는 전부 `(광고)` 이며 후잉 `삼성신용(효원)`(x77) 은 최근 90일 거래 0건이다(2026-08-04 확인). 취소를 기다릴 대상이 아니라, **다시 사용하기 시작하면 승인 문자부터 누락**되니 그때 `confirmed` 등록이 먼저다.
- 기존 contains(예: "스타벅스 → 식비") / exact(예: "방효원 → 기초잔액(효원)") 는 계속 유효. fallback 은 둘 다 miss 일 때만 탄다.
- ⚠️ **주유소는 두 곳을 함께 손대야 한다** — `whooing_sync.py` 의 `FUEL_KEYWORDS`(150,000원 선승인/취소 스킵)와 `whooing_merchant_map.json` contains 룰(실제 결제 → `차량유지비/주유비`)이 한 쌍이다. 한쪽만 넣으면 선승인이 후잉에 남거나 실제 주유가 기타비용으로 샌다. 상호에 주유/석유가 없는 충전소(현대가스·한경에너)는 상호 자체를 등록. 한경에너는 선승인이 149,900원이라 `FUEL_PREAUTH_AMOUNT`(150,000 정확일치) 스킵엔 안 걸리고 분류만 적용된다.
- 결과적으로 자잘한 인명 송금·가맹점 미등록 건은 전부 기타비용으로 자동 분류되고, 분류가 필요한 것만 후잉 UI 에서 사후 조정하거나 merchant_map 에 규칙 추가한다.
### 우선 룰 (whooing_overrides.json)
@@ -229,7 +323,7 @@ python3 .../whooing_manual.py --dry-run --item ... --money ...
원칙:
- 차트에 없는 계정명으로 절대 추측 POST 금지. 모르면 반드시 재질문.
- 사용자가 이미 한 문장에 "스타벅스 5800원 신한카드로" 다 말했다면, 카테고리만 확인하고 바로 실행.
- 카드 취소/환불이면 structured 대신 `--message` raw 모드로 원문 보내 후잉이 상쇄하게 한다.
- 카드 취소/환불은 원본 승인 거래를 찾아 감액하는 게 원칙(위 `card_cancel` 항목). 후잉은 취소 문자를 자동 처리해주지 않으니 raw 로 보내고 끝내면 안 된다 — 원본이 그대로 남는다.
## 잔액 조회 (whooing_balance.py)
@@ -296,6 +390,9 @@ python3 .../whooing_balance.py --json \
### 흐름
1. **발신 게이트** — KST 기준 `day >= send_day_of_month` (catch-up 정책) 이고 시각이 `send_hour_kst` 이상이며 `last_sent_month` ≠ 이번 달이면 1회 발신. 25일에 Mac이 꺼져 있었어도 26~월말 사이 켜면 그 시점에 발신. 다음 달로 넘어가면 포기.
- 발신은 **`--service imessage`** (2026-08-03 전환, 아래 사고 참고). 가희님은 iMessage 사용자다.
- 성공 시 `last_sent_month` 와 함께 **`last_sent_at`** 을 찍는다 — 답신 미도착 감시의 시작점.
1-b. **답신 미도착 감시** (`_check_reply_overdue`) — 발신 후 `REPLY_GRACE_DAYS`(2일)가 지나도 가희님 답신이 없으면 관리자님께 텔레그램 1회. 유예 전이면 API 콜 0으로 즉시 반환하고, 답신이 확인되면 `last_sent_at` 을 지워 감시를 끝낸다. 같은 발신 건에 대한 재알림은 `reply_alert_for` 로 막는다. ⚠️ **재발신은 자동으로 하지 않는다** — 가희님이 늦게 답하는 경우 중복 독촉이 되고, 자동 발송 트리거를 늘리지 않는다는 방침에도 어긋난다.
2. **응답 폴링** — `imsg chats --json` → 가희 chat 찾기 → `imsg history --chat-id X --start <last_processed_message_at> --attachments --json`. 가희 발신(is_from_me≠true) 메시지만 처리.
3. **1차 스캔** (즉시 워터마크 갱신):
- **이미지 첨부 있음** → 자동분개 X, 골디 텔레그램 알림만 ("이미지 답신 — 직접 처리 부탁")
@@ -322,6 +419,8 @@ python3 .../whooing_balance.py --json \
"message_template": "가계부 업데이트 날이에요. 계좌잔액 보내주시면 자동으로 반영됩니다.",
"last_sent_month": null,
"last_processed_message_at": null,
"last_sent_at": null,
"reply_alert_for": null,
"whooing_account_name": "가희주머니",
"income_account": "가희비밀주머니_수익",
"expense_account": "기타비용"
@@ -330,8 +429,29 @@ python3 .../whooing_balance.py --json \
- `last_sent_month` = "YYYY-MM" 발신 직후 기록. 같은 달에 재발신 안 함.
- `last_processed_message_at` = imsg history `--start` 의 ISO 워터마크. 처리한 메시지의 created_at 최댓값.
- `last_sent_at` = 발신 시각(UTC Z). 답신 미도착 감시의 기준점. 답신 확인되면 지워진다.
- `reply_alert_for` = 이미 "답신 없음" 알림을 보낸 `last_sent_at` 값. 같은 발신 건 재알림 방지.
- 발신 문구·트리거 일자·계정명 변경은 모두 이 JSON 직접 편집. 코드 재배포 불필요. whooing-sync 가 매 사이클 다시 읽음.
### 수동 발신 (`--send-now`)
발송이 실패해 그 달을 놓쳤을 때의 복구 수단. 날짜 게이트를 무시하고 1회 발신하며 `last_sent_month` 는 건드리지 않아 정규 25일 발신이 살아있다.
```bash
python3 gahee_reminder.py --send-now [--dry-run]
```
⚠️ **에이전트 세션·코디 셸에서 실행하면 20초 타임아웃으로 실패한다.** `imsg send` 는 AppleEvents 로 Messages 를 구동하는데, **자동화 → 메시지** 권한이 `imsg` 와 iTerm2 에만 허용돼 있고 Claude Code 에는 거부로 박혀 있다. launchd 경유로 돌려야 한다 — 임시 oneshot plist 를 만들어 `kickstart` 하고 로그를 읽은 뒤 `bootout` + 삭제한다(2026-08-03 실행 방식).
### 2026-07-25 사고: 미발송이 성공으로 기록됨
7/25 리마인더가 실제로 발송되지 않았는데(`chat.db`: `service=SMS, is_sent=0, error=4`) 시스템은 성공으로 기록했다. 가희주머니 7월 잔액이 안 들어와 8/1 월간 결산에서 빠졌다.
- **왜 실패했나** — `--service sms` 강제가 이 수신자에게 **3전 3패**였다(5/25 ×2, 7/25 ×1 전부 `error=4`). 살아남은 2건(5/25·6/25)은 Messages 가 임의로 **RCS 로 바꿔** 보낸 운이었고, 7월엔 그 폴백이 안 일어났다. 전체 통계로도 iMessage 137/139 성공 vs SMS 실패 3건(전부 이 리마인더).
- **왜 몰랐나** — `imsg send` 는 Messages 에 넘기기만 성공하면 rc=0 이고, 실제 실패는 그 뒤 비동기로 `chat.db` 의 `error` 에 찍힌다. 코드가 rc 만 봐서 성공 처리 → 오탐 텔레그램 → `last_sent_month` 갱신 → 그 달 재시도 차단 → 8월엔 catch-up 이 당월만 보므로 7월분 영구 유실.
- **왜 발송 검증을 자동 경로에 안 넣었나** — `is_sent`/`error` 는 `chat.db` 에만 있고 `imsg history` JSON 은 노출하지 않는다(실패한 메시지도 정상 조회됨). 직접 sqlite 조회가 유일한 길인데 **launchd 의 `/usr/bin/python3` 는 FDA 가 없어 `authorization denied` 로 막힌다**(2026-08-03 실측). 그래서 자동 경로는 권한과 무관한 답신 기반 감시를 쓰고, 정밀 확인(`_verify_sent`)은 FDA 가 있는 컨텍스트에서 도는 `--send-now` 에만 붙였다.
- `error=4` 의 정확한 의미는 확증 못 함 — 사고 시점 imagent 로그가 이미 롤오프됐다.
### 수신자 설정
`/Users/snowoyh/.openclaw/credentials/gahee_imessage.json`:
@@ -12,13 +12,18 @@
"""
from __future__ import annotations
import argparse
import json
import re
import sqlite3
import subprocess
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo
import imsg_cli
import notify
import whooing_balance
@@ -27,6 +32,13 @@ GAHEE_CRED = Path("/Users/snowoyh/.openclaw/credentials/gahee_imessage.json")
STATE_FILE = Path("/Users/snowoyh/.openclaw/agents/budget/workspace/state/gahee_reminder.json")
IMSG_TIMEOUT = 20
HISTORY_LIMIT = 50
# 발신 후 이 기간 안에 가희님 답신이 없으면 발송 실패를 의심하고 관리자님께 1회 알린다.
# imsg send 는 Messages 에 넘기기만 성공하면 rc=0 이라 발송 실패를 동기적으로 못 잡는다
# (2026-07-25 사고: SMS error=4 로 미발송인데 성공 처리돼 그 달 재시도가 막혔다).
# 실측 답신 지연은 3시간·38시간 — 2일이면 오탐이 적다.
REPLY_GRACE_DAYS = 2
CHAT_DB = Path.home() / "Library" / "Messages" / "chat.db"
APPLE_EPOCH = 978307200 # chat.db date 는 2001-01-01 기준 ns
# 라벨 + 금액 패턴. 라벨은 한글/영문/괄호/공백/숫자 1~20자.
# 콜론 구분 입력은 정수 전체를 허용하고, 공백 구분 입력은 천 단위 콤마가 있을 때만 허용한다.
# 일반 안내 문자의 "오전 09" 같은 표현을 잔액으로 오인하지 않기 위함.
@@ -77,15 +89,11 @@ def _normalize(handle: str) -> str:
def _imsg_chats() -> list[dict]:
try:
raw = subprocess.run(
["imsg", "chats", "--json"],
capture_output=True, text=True, timeout=IMSG_TIMEOUT,
)
return [json.loads(l) for l in raw.stdout.splitlines() if l.strip()]
except Exception as e:
print(f"⚠️ 가희 imsg chats 실패: {e}")
rows, err = imsg_cli.chats(IMSG_TIMEOUT)
if err:
print(f"⚠️ 가희 imsg chats 실패: {err}")
return []
return rows
def _find_gahee_chat_id(handle_norm: str) -> int | None:
@@ -97,26 +105,35 @@ def _find_gahee_chat_id(handle_norm: str) -> int | None:
def _imsg_history(chat_id: int, start_iso: str | None) -> list[dict]:
cmd = ["imsg", "history", "--chat-id", str(chat_id),
args = ["history", "--chat-id", str(chat_id),
"--limit", str(HISTORY_LIMIT), "--attachments", "--json"]
if start_iso:
cmd += ["--start", start_iso]
try:
raw = subprocess.run(cmd, capture_output=True, text=True, timeout=IMSG_TIMEOUT)
return [json.loads(l) for l in raw.stdout.splitlines() if l.strip()]
except Exception as e:
print(f"⚠️ 가희 imsg history 실패: {e}")
args += ["--start", start_iso]
rows, err = imsg_cli.run_json(args, IMSG_TIMEOUT)
if err:
print(f"⚠️ 가희 imsg history 실패: {err}")
return []
return rows
def _send_imessage(handle: str, text: str, dry_run: bool) -> bool:
"""가희님께 리마인더 발신.
⚠️ rc=0 은 "Messages 에 넘겼다"는 뜻일 뿐 발송 성공이 아니다. 실제 실패는 그 뒤
비동기로 chat.db 의 error 칼럼에 찍힌다 — 여기서는 알 수 없다. 발송 여부 확인은
_verify_sent()(FDA 필요, CLI 전용) 또는 _check_reply_overdue()(답신 기반)가 맡는다.
⚠️ service 는 imessage 다. sms 강제는 이 수신자에게 3전 3패였고(5/25 ×2, 7/25 ×1
전부 error=4), 살아남은 2건은 Messages 가 임의로 RCS 로 바꿔준 운이었다. 가희님은
iMessage 사용자다(답신 2건 모두 iMessage). 2026-08-03 전환.
"""
if dry_run:
print(f"[dry-run] imsg send --to {handle} --text {text!r} --service sms")
print(f"[dry-run] imsg send --to {handle} --text {text!r} --service imessage")
return True
try:
result = subprocess.run(
["imsg", "send", "--to", handle, "--text", text,
"--service", "sms"],
"--service", "imessage"],
capture_output=True, text=True, timeout=IMSG_TIMEOUT,
)
if result.returncode != 0:
@@ -128,6 +145,46 @@ def _send_imessage(handle: str, text: str, dry_run: bool) -> bool:
return False
def _utc_z(dt: datetime) -> str:
"""imsg --start / state 워터마크와 같은 UTC Z 포맷."""
return dt.astimezone(ZoneInfo("UTC")).isoformat().replace("+00:00", "Z")
def _verify_sent(handle: str, after_unix: float, timeout: int = 20):
"""chat.db 직접 조회로 실제 발송 여부 확인. 반환 (verdict, detail).
verdict True=발송됨 / False=실패 / None=확인 불가.
chat.db 는 FDA 를 가진 컨텍스트에서만 읽힌다 — launchd(/usr/bin/python3)에선 못 읽으므로
확인 불가(None)로 떨어진다. 이 함수는 CLI 전용이고, 자동 경로는 답신 기반 감시를 쓴다.
"""
digits = re.sub(r"\D", "", handle)[-8:]
after_ns = int((after_unix - APPLE_EPOCH) * 1_000_000_000)
deadline = time.time() + timeout
detail = None
while True:
try:
con = sqlite3.connect(f"file:{CHAT_DB}?mode=ro", uri=True)
row = con.execute(
"select m.service, m.is_sent, m.error from message m "
"join handle h on m.handle_id = h.rowid "
"where h.id like ? and m.is_from_me = 1 and m.date >= ? "
"order by m.date desc limit 1",
(f"%{digits}", after_ns),
).fetchone()
con.close()
except Exception as e:
return None, f"chat.db 조회 불가 ({type(e).__name__}: {e})"
if row:
detail = f"service={row[0]} is_sent={row[1]} error={row[2]}"
if row[2]:
return False, detail
if row[1]:
return True, detail
if time.time() >= deadline:
return None, detail or "발신 기록이 아직 안 보임"
time.sleep(1)
def _parse_balance_text(text: str) -> dict[str, int]:
"""가희 답신 텍스트에서 '라벨 : 금액' 페어 추출. 빈 dict면 포맷 오류.
@@ -234,6 +291,9 @@ def _maybe_send_reminder(state: dict, dry_run: bool) -> bool:
if not dry_run:
state["last_sent_month"] = month_tag
# 답신 감시 시작점. 발송 성공 여부는 여기서 알 수 없으므로 답신으로 확인한다.
state["last_sent_at"] = _utc_z(now)
state.pop("reply_alert_for", None)
_save_json(STATE_FILE, state)
notify.send(
f"📨 <b>가희 잔액 리마인더 발신</b>\n"
@@ -243,6 +303,51 @@ def _maybe_send_reminder(state: dict, dry_run: bool) -> bool:
return True
def _check_reply_overdue(state: dict, dry_run: bool):
"""발신 후 REPLY_GRACE_DAYS 가 지나도 답신이 없으면 관리자님께 1회 알린다.
발송 실패를 직접 감지할 수 없어서(위 _send_imessage 주석) 결과로 대신 본다.
2026-07-25 처럼 미발송이 조용히 넘어가는 걸 막는 유일한 안전망이다.
⚠️ 재발신은 자동으로 하지 않는다 — 가희님이 늦게 답하는 경우 중복 독촉이 되고,
자동 발송 트리거를 늘리지 않는다는 방침에도 어긋난다. 알림 받고 --send-now 로 수동.
"""
sent_at = state.get("last_sent_at")
if not sent_at:
return
try:
sent_dt = datetime.fromisoformat(sent_at.replace("Z", "+00:00"))
except ValueError:
return
if _now_kst() - sent_dt < timedelta(days=REPLY_GRACE_DAYS):
return
if state.get("reply_alert_for") == sent_at:
return # 이번 발신 건은 이미 알림
handle_norm = _normalize(_gahee_handle())
chat_id = _find_gahee_chat_id(handle_norm)
if chat_id is None:
return
if any(_msg_from_gahee(m) for m in _imsg_history(chat_id, sent_at)):
# 답신 도착 — 감시 종료
if not dry_run:
state.pop("last_sent_at", None)
state.pop("reply_alert_for", None)
_save_json(STATE_FILE, state)
return
days = (_now_kst() - sent_dt).days
notify.send(
f"⚠️ <b>가희 잔액 리마인더 — {days}일째 답신 없음</b>\n"
f"발신: {notify.escape_html(sent_at)}\n"
f"발송 자체가 실패했을 수 있어요 (imsg 는 발송 실패를 알려주지 않아요).\n"
f"재발신: <code>python3 gahee_reminder.py --send-now</code>"
)
if not dry_run:
state["reply_alert_for"] = sent_at
_save_json(STATE_FILE, state)
def _msg_text(msg: dict) -> str:
"""imsg history 메시지 객체에서 본문 텍스트 추출. 키 변형(text/body) 모두 시도."""
for k in ("text", "body", "message", "content"):
@@ -402,9 +507,61 @@ def run(webhook_url: str, post_fn, dry_run: bool = False):
_maybe_send_reminder(state, dry_run=dry_run)
# 발신 직후 같은 사이클에서 폴링은 무의미하지만, 다음 사이클부터 자연스럽게 시작됨.
_poll_replies(state, webhook_url, post_fn, dry_run=dry_run)
# 발송 실패가 조용히 넘어가지 않게 하는 안전망. 유예 전이면 API 콜 없이 즉시 반환.
_check_reply_overdue(state, dry_run=dry_run)
except Exception as e:
print(f"❌ gahee_reminder.run 예외 (격리됨): {e}")
try:
notify.send(f"❌ <b>가희 모듈 예외</b>\n{notify.escape_html(str(e))[:500]}")
except Exception:
pass
def send_now(dry_run: bool = False) -> int:
"""날짜 게이트를 무시하고 리마인더 1회 발신 (수동용).
발송 실패로 그 달을 통째로 놓쳤을 때 복구 수단. last_sent_month 는 건드리지 않아
정규 발신(매월 25일)이 그대로 살아있다.
"""
state = _load_json(STATE_FILE, None)
if not state:
print("❌ 가희 state 파일 없음", file=sys.stderr)
return 1
handle = _gahee_handle()
template = state.get("message_template") or "가계부 업데이트 날이에요. 계좌잔액 보내주시면 자동으로 반영됩니다."
before = time.time()
if not _send_imessage(handle, template, dry_run=dry_run):
return 1
if dry_run:
print(f"[dry-run] 수신자={handle} · last_sent_month 는 그대로 유지")
return 0
state["last_sent_at"] = _utc_z(_now_kst())
state.pop("reply_alert_for", None)
_save_json(STATE_FILE, state)
verdict, detail = _verify_sent(handle, before)
if verdict is True:
print(f"✅ 발송 확인 — {detail}")
return 0
if verdict is False:
print(f"❌ 발송 실패 — {detail}", file=sys.stderr)
state.pop("last_sent_at", None)
_save_json(STATE_FILE, state)
return 1
# chat.db 를 못 읽는 컨텍스트이거나 아직 판정 전. 실패로 단정하지 않는다.
print(f"⚠️ 발송 확인 불가 — {detail}")
print(f" 답신이 {REPLY_GRACE_DAYS}일간 없으면 골디가 텔레그램으로 알립니다.")
return 0
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="가희 잔액 리마인더 (수동 발신용)")
ap.add_argument("--send-now", action="store_true",
help="날짜 게이트 무시하고 1회 발신. 정규 25일 발신은 유지")
ap.add_argument("--dry-run", action="store_true", help="실제 발송 없이 계획만 출력")
args = ap.parse_args()
if args.send_now:
sys.exit(send_now(dry_run=args.dry_run))
ap.print_help()
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
from __future__ import annotations
"""imsg CLI 호출 공용 래퍼.
호출부가 stdout 만 JSON 파싱하고 stderr·returncode 를 버리면, FDA(전체 디스크 접근 권한)
누락 같은 권한 오류가 `Expecting value: line 1 column 1 (char 0)` 파싱 에러로 둔갑한다.
2026-08-03 골디 오진 사고의 원인 — 실제 원인은 homebrew node 업그레이드로 게이트웨이
바이너리 경로가 바뀌어 FDA 승인이 무효화된 것이었다. 판정을 한곳에 모아 사유를 그대로 노출한다.
"""
import json
import subprocess
# imsg 가 권한 거부 시 내는 문구 (imsg 0.5.0 기준).
FDA_MARKERS = ("Full Disk Access", "Cannot access Messages database")
FDA_HINT = (
"Messages DB 접근 거부 — 전체 디스크 접근 권한(FDA) 누락. "
"skills/whooing-sync/SKILL.md 의 'Full Disk Access' 섹션 참고"
)
# `imsg chats` 기본값은 최근 20건이라, 오래 조용한 발신번호(현대카드 SMS·신한은행 등)가
# 목록에서 빠진다. 그 상태로 chat id 캐시를 다시 만들면 해당 carrier 수집이 조용히 끊긴다.
# chat.db 전체 대화가 130개 수준이라 넉넉히 잡는다. (2026-08-03 실측으로 발견)
CHATS_LIMIT = 200
def _reason(stderr: str, stdout: str, fallback: str) -> str:
"""실패 사유 한 줄. FDA 마커는 stdout/stderr 어느 쪽에 있어도 잡는다."""
err = (stderr or "").strip()
out = (stdout or "").strip()
if any(m in err or m in out for m in FDA_MARKERS):
return FDA_HINT
detail = err or out
if detail:
return " ".join(detail.split())[:200]
return fallback
def run_json(args: list, timeout: int) -> "tuple[list, str | None]":
"""imsg 실행 → (rows, error). 성공 시 (list[dict], None), 실패 시 ([], 사유).
결과 0건과 실패를 구분한다 — 둘 다 빈 리스트지만 실패에만 error 가 붙는다.
"""
try:
p = subprocess.run(
["imsg"] + list(args),
capture_output=True, text=True, timeout=timeout,
)
except FileNotFoundError:
return [], "imsg CLI가 설치되어 있지 않습니다"
except subprocess.TimeoutExpired:
return [], f"타임아웃 ({timeout}s)"
except Exception as e:
return [], f"{type(e).__name__}: {e}"
out = p.stdout or ""
if p.returncode != 0:
return [], _reason(p.stderr, out, f"rc={p.returncode}")
if not out.strip():
# stderr 만 있고 stdout 이 비면 실패. 둘 다 비면 정상적으로 결과 0건.
if (p.stderr or "").strip():
return [], _reason(p.stderr, "", "빈 출력")
return [], None
rows = []
for line in out.splitlines():
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
return [], _reason(p.stderr, line, "JSON 파싱 실패")
return rows, None
def chats(timeout: int) -> "tuple[list, str | None]":
"""대화 목록 조회 → (rows, error). CHATS_LIMIT 만큼 넓게 훑는다."""
return run_json(["chats", "--limit", str(CHATS_LIMIT), "--json"], timeout)
@@ -74,6 +74,22 @@ SHINHAN_APPROVE_RE = re.compile(
)
# 신한카드 자동납부 형식은 위 승인 SMS와 금액/상호 순서가 반대다:
# "[신한카드] 자동납부 정상승인 방효원님 08/06 (일시불) 14,000원 삼천리도시가스"
# SHINHAN_APPROVE_RE 를 느슨하게 고치는 대신 별도 패턴으로 먼저 시도한다 —
# 느슨하게 고치면 merchant 자리에 "08/06 (일시불)" 이 잡힌다.
# kind 대안 순서 주의: `정상승인`·`승인취소` 가 `승인` 보다 앞에 와야 한다(왼쪽 우선).
SHINHAN_AUTOPAY_RE = re.compile(
r"\[?신한카드\]?\s*자동납부\s*(?P<kind>정상승인|승인취소|매입취소|승인|취소)\s+"
r"(?P<who>\S+?님)\s+"
r"\d{2}/\d{2}\s+"
r"(?:\([^)]*\)\s+)?" # (일시불) / (3개월) — 없는 변형도 통과
r"(?P<amount>[\d,]+)\s*원\s+"
r"(?P<merchant>[^\n]+)",
re.DOTALL,
)
SHINHAN_BANK_RE = re.compile(
r"\[Web발신\]\s*\n"
r"신한\s*(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s*\n"
@@ -107,7 +123,7 @@ def parse_shinhan_bank(raw: str, created_at: str) -> "dict | None":
def parse_shinhan_card(raw: str, created_at: str) -> "dict | None":
text = _normalize(raw).replace("[Web발신]", "")
m = SHINHAN_APPROVE_RE.search(text)
m = SHINHAN_AUTOPAY_RE.search(text) or SHINHAN_APPROVE_RE.search(text)
if not m:
return None
kind_raw = m.group("kind")
@@ -135,8 +151,32 @@ KAKAOBANK_RE = re.compile(
)
KAKAOBANK_CARD_RE = re.compile(
r"\[Web발신\]\s*\[카카오뱅크\]\s+카드결제\s+"
r"(?P<card>\S+)\s+"
r"(?P<md>\d{2}/\d{2})\s+(?P<hm>\d{2}:\d{2})\s+"
r"(?P<amount>[\d,]+)원\s+"
r"(?P<merchant>[^\n]+?)"
r"(?:\s+잔액\s+(?P<balance>[\d,]+)원?)?\s*$",
re.DOTALL,
)
def parse_kakao_bank(raw: str, created_at: str) -> "dict | None":
text = _normalize(raw)
card = KAKAOBANK_CARD_RE.search(text)
if card:
result = {
"kind": "withdrawal",
"entry_date": _iso_to_kst_date(created_at),
"amount": int(card.group("amount").replace(",", "")),
"merchant": _normalize(card.group("merchant")).split("\n")[0].strip() or "(메모없음)",
"raw": raw,
}
if card.group("balance"):
result["balance"] = int(card.group("balance").replace(",", ""))
return result
m = KAKAOBANK_RE.search(text)
if not m:
return None
@@ -155,15 +195,16 @@ def parse_kakao_bank(raw: str, created_at: str) -> "dict | None":
return result
# 현대카드 승인 형식 (취소는 별도 샘플 확보 후 추가):
# 현대카드 블루멤버스 승인
# 방*원
# 5,170원 일시불
# 04/25 11:34
# 쿠팡
# 누적1,466,011원
HYUNDAI_CARD_APPROVE_RE = re.compile(
r"^\s*현대카드[^\n]*승인\s*\n"
# 현대카드 승인/취소 형식 (취소도 헤더 단어만 다르고 본문 구조는 같다):
# 현대카드 블루멤버스 승인 현대카드 블루멤버스 취소
# 방*원 방*원
# 5,170원 일시불 150,000원 일시불
# 04/25 11:34 08/02 13:06
# 쿠팡 현대가스
# 누적1,466,011원 누적4,696,608원
# 앞의 `현대카드[^\n]*` 가 greedy 라 "승인취소" 형태도 kind=취소 로 잡힌다.
HYUNDAI_CARD_RE = re.compile(
r"^\s*현대(?:카드)?[^\n]*(?P<kind>승인|취소)\s*\n"
r"(?P<who>[^\n]+)\s*\n"
r"(?P<amount>[\d,]+)\s*원[^\n]*\n"
r"\d{2}/\d{2}\s+\d{2}:\d{2}\s*\n"
@@ -174,8 +215,8 @@ HYUNDAI_CARD_APPROVE_RE = re.compile(
# RCS/MAAP 형식은 한 줄로 들어온다:
# [Web발신] 현대카드 블루멤버스 승인 방*원 84,000원 일시불 05/16 11:57 네이버페이 누적820,000원
HYUNDAI_CARD_APPROVE_INLINE_RE = re.compile(
r"현대카드[^\n]*승인\s+"
HYUNDAI_CARD_INLINE_RE = re.compile(
r"현대(?:카드)?[^\n]*(?P<kind>승인|취소)\s+"
r"(?P<who>\S+)\s+"
r"(?P<amount>[\d,]+)\s*원[^\n]*?"
r"\d{2}/\d{2}\s+\d{2}:\d{2}\s+"
@@ -184,23 +225,38 @@ HYUNDAI_CARD_APPROVE_INLINE_RE = re.compile(
re.MULTILINE,
)
# 현대카드 자동납부 한 줄 형식:
# [Web발신] [현대카드] 자동납부 승인 방*원님 라이나생명 31,100원
HYUNDAI_AUTOPAY_RE = re.compile(
r"\[?현대카드\]?\s*자동납부\s*(?P<kind>승인|취소|승인취소)\s+"
r"(?P<who>\S+?님)\s+"
r"(?P<merchant>.+?)\s+"
r"(?P<amount>[\d,]+)\s*원\s*$",
re.MULTILINE,
)
def parse_hyundai_card(raw: str, created_at: str) -> "dict | None":
text = _normalize(raw).replace("[Web발신]", "").strip()
m = HYUNDAI_CARD_APPROVE_RE.search(text) or HYUNDAI_CARD_APPROVE_INLINE_RE.search(text)
m = (
HYUNDAI_AUTOPAY_RE.search(text)
or HYUNDAI_CARD_RE.search(text)
or HYUNDAI_CARD_INLINE_RE.search(text)
)
if not m:
return None
amount = int(m.group("amount").replace(",", ""))
merchant = re.sub(r"\s+", " ", m.group("merchant").strip()) or "(가맹점없음)"
result = {
"kind": "card_approval",
"kind": "card_cancel" if m.group("kind") == "취소" else "card_approval",
"entry_date": _iso_to_kst_date(created_at),
"amount": amount,
"merchant": merchant,
"raw": raw,
}
if m.group("cumulative"):
result["cumulative"] = int(m.group("cumulative").replace(",", ""))
cumulative = m.groupdict().get("cumulative")
if cumulative:
result["cumulative"] = int(cumulative.replace(",", ""))
return result
@@ -0,0 +1,220 @@
"""은행 이체 SMS ↔ 증권 계좌 입출금(kt00015) 짝 맞추기.
은행에서 증권으로 이체하면 은행 SMS 에는 **입금자명(=본인 이름)만** 찍혀서
어디로 갔는지 알 수 없다. 그래서 `방효원` 같은 자체이체 룰에 걸려
`기초잔액 ← 은행` 이 되고, 자본 분개 금지 가드에 막혀 **미반영**된다.
반대편(증권 입금)은 SMS 가 아예 없어 월 1회 reconcile 차액으로만 나타나는데,
그 차액은 전부 `주식평가수익` 으로 분개되므로 **이체가 손익으로 둔갑**한다.
이 모듈은 그 짝을 키움 kt00015 입출금 내역에서 찾아
`증권(효원) ← 하나은행(효원)` 이체 한 건으로 만들어 준다.
⚠️ **capital-block 직전에만 호출한다** — 평시 키움 콜 0. 이체가 실제로 있었던
사이클에만 계좌 수만큼(4콜) 조회한다.
⚠️ **애매하면 매칭하지 않는다** — 후보가 여럿이면 포기하고 기존 알림 경로로
보낸다. 틀린 분개보다 사람이 보는 편이 낫다.
"""
from __future__ import annotations
import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo
KST = ZoneInfo("Asia/Seoul")
ROOT = Path("/Users/snowoyh/.openclaw")
STOCK_SCRIPTS = ROOT / "agents" / "stock" / "workspace" / "scripts"
MATCHED_FILE = ROOT / "agents" / "budget" / "workspace" / "state" / "whooing_securities_matched.json"
# 증권 계좌 라벨 → 후잉 자산 계정명. 라벨 prefix 규칙은 send_balance_to_budget.py 와 동일.
GAHEE_PREFIX = "가희"
OWNER_ASSET = {"self": "증권(효원)", "gahee": "증권(가희)"}
# 은행 SMS 종류 → 기대되는 증권 쪽 방향.
OPPOSITE = {"withdrawal": "IN", "deposit": "OUT"}
# 시각까지 일치하는 후보를 우선한다 (오픈뱅킹은 즉시 반영이라 실측 12초 차).
# 후보가 여럿일 때만 쓰는 tie-breaker 이지 1차 필터가 아니다 —
# 증권사 처리가 늦어 분 단위로 벌어지는 경우가 있어 창으로 잘라내면 놓친다.
TIME_TIEBREAK_SECONDS = 600
def _owner_of(label: str) -> str:
return "gahee" if label.startswith(GAHEE_PREFIX) else "self"
def row_key(label: str, row: dict) -> str:
"""kt00015 행에는 고유 ID 가 없다. 중복 분개 방지용 합성 키.
같은 날 같은 금액을 두 번 이체하면 time 으로 갈린다."""
return f"{row['date']}|{label}|{row['io_tp']}|{row['amount']}|{row.get('time') or ''}"
def _load_matched() -> dict:
try:
with MATCHED_FILE.open(encoding="utf-8") as f:
return json.load(f)
except (OSError, ValueError):
return {}
def mark_matched(key: str, meta: dict) -> None:
"""분개 성공 후 호출. POST 성공한 뒤에만 기록해야 재시도 여지가 남는다."""
data = _load_matched()
data[key] = {**meta, "matched_at": datetime.now(KST).isoformat()}
# 오래된 항목 정리 (180일) — 무한 증식 방지.
cutoff = (datetime.now(KST) - timedelta(days=180)).strftime("%Y%m%d")
data = {k: v for k, v in data.items() if k.split("|", 1)[0] >= cutoff}
MATCHED_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = MATCHED_FILE.with_suffix(".tmp")
with tmp.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
os.replace(tmp, MATCHED_FILE)
def _fetch_day(base_dt: str) -> dict[str, list[dict]] | None:
"""전 계좌의 그날 입출금. 조회 실패는 None (= 판단 보류, 매칭 안 함)."""
sys.path.insert(0, str(STOCK_SCRIPTS))
try:
import kiwoom_client as kc
import investment_principal as ip
except Exception as e: # noqa: BLE001 — import 실패해도 후잉 동기화는 계속돼야 한다
sys.stderr.write(f"securities_match: 키움 모듈 import 실패: {e}\n")
return None
out: dict[str, list[dict]] = {}
for acc in kc.list_accounts():
label = acc["label"]
try:
rows = kc.get_cash_flow(label, base_dt=base_dt)
except Exception as e: # noqa: BLE001
sys.stderr.write(f"securities_match: {label} kt00015 조회 실패: {e}\n")
return None # 일부만 보고 판단하면 오매칭 — 전부 성공해야 진행
out[label] = [r for r in rows if ip.is_principal_flow(r["rmrk"])]
return out
def _drop_internal_transfers(day: dict[str, list[dict]]) -> dict[str, list[dict]]:
"""증권 계좌 간 대체(예: 가희_일반 → 가희_ISA)를 후보에서 뺀다.
후잉에선 같은 자산이라 분개 대상이 아닌데, 금액이 같으면 은행 SMS 와
오매칭될 수 있다. 서로 다른 계좌에 같은 금액의 IN/OUT 이 함께 있으면 제외."""
ins = {(lb, r["amount"]) for lb, rows in day.items() for r in rows if r["io_tp"] == "IN"}
outs = {(lb, r["amount"]) for lb, rows in day.items() for r in rows if r["io_tp"] == "OUT"}
paired = {a for lb, a in ins} & {a for lb, a in outs}
if not paired:
return day
cleaned: dict[str, list[dict]] = {}
for lb, rows in day.items():
keep = []
for r in rows:
if r["amount"] in paired and any(
other != lb and any(
o["amount"] == r["amount"] and o["io_tp"] != r["io_tp"] for o in day[other]
)
for other in day
):
continue
keep.append(r)
cleaned[lb] = keep
return cleaned
def _sms_time(created_at_utc: str | None) -> datetime | None:
if not created_at_utc:
return None
try:
return datetime.fromisoformat(created_at_utc.replace("Z", "+00:00")).astimezone(KST)
except ValueError:
return None
def find_match(entry_date: str, amount: int, bank_kind: str,
created_at_utc: str | None = None) -> dict | None:
"""은행 SMS 한 건에 대응하는 증권 입출금 행을 찾는다.
entry_date: 'YYYYMMDD' (은행 SMS 기준일)
amount: 원 (양수)
bank_kind: 'withdrawal' (은행→증권) | 'deposit' (증권→은행)
created_at_utc: SMS 수신 시각 ISO — 후보가 여럿일 때 tie-break 에만 쓴다
반환 (매칭 성공 시): {label, asset, row, key}
반환 None: 후보 없음 / 후보 여럿 / 이미 처리됨 / 조회 실패
"""
want_io = OPPOSITE.get(bank_kind)
if want_io is None:
return None
day = _fetch_day(entry_date)
if day is None:
return None
day = _drop_internal_transfers(day)
matched = _load_matched()
cands = [
{"label": lb, "row": r, "key": row_key(lb, r)}
for lb, rows in day.items()
for r in rows
if r["io_tp"] == want_io and r["amount"] == amount and row_key(lb, r) not in matched
]
if not cands:
return None
if len(cands) > 1:
# 시각이 가까운 후보가 정확히 하나면 그걸 고른다. 아니면 포기.
sms_at = _sms_time(created_at_utc)
near = []
if sms_at is not None:
for c in cands:
t = c["row"].get("time")
if not t:
continue
try:
rt = datetime.strptime(f"{entry_date} {t}", "%Y%m%d %H:%M:%S").replace(tzinfo=KST)
except ValueError:
continue
if abs((rt - sms_at).total_seconds()) <= TIME_TIEBREAK_SECONDS:
near.append(c)
if len(near) != 1:
sys.stderr.write(
f"securities_match: {entry_date} {amount:,}원 {want_io} 후보 {len(cands)}건 — 매칭 포기\n")
return None
cands = near
c = cands[0]
c["asset"] = OWNER_ASSET[_owner_of(c["label"])]
return c
def build_payload(match: dict, bank_asset: str, entry_date: str, amount: int,
bank_kind: str, balance: int | None = None) -> dict:
"""후잉 이체 payload. 은행→증권이면 left=증권, 증권→은행이면 left=은행."""
if bank_kind == "withdrawal":
left, right = match["asset"], bank_asset
else:
left, right = bank_asset, match["asset"]
memo = f"{match['label']} {match['row']['rmrk']}"
if balance is not None:
memo += f" / 잔액 : {balance:,}원"
return {
"entry_date": entry_date,
"money": amount,
"item": "이체",
"left": left,
"right": right,
"memo": memo,
}
if __name__ == "__main__":
# 진단용: python3 securities_match.py YYYYMMDD 금액 withdrawal|deposit
if len(sys.argv) < 4:
print(__doc__)
print("usage: securities_match.py YYYYMMDD AMOUNT withdrawal|deposit [SMS_ISO_UTC]")
raise SystemExit(2)
m = find_match(sys.argv[1], int(sys.argv[2]), sys.argv[3],
sys.argv[4] if len(sys.argv) > 4 else None)
if m is None:
print("매칭 없음")
raise SystemExit(1)
print(json.dumps(m, ensure_ascii=False, indent=2))
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""후잉 OpenAPI 잔액(bs.json) 조회.
"""후잉 OpenAPI 클라이언트 — 잔액(bs.json) 조회 + 거래(entries.json) 조회·감액.
Usage:
whooing_balance.py # 모든 섹션의 현재 잔액
@@ -11,11 +11,13 @@ from __future__ import annotations
import argparse
import json
import re
import secrets
import sys
import time
import urllib.parse
import urllib.request
from datetime import datetime, timedelta
from pathlib import Path
CRED_PATH = Path("/Users/snowoyh/.openclaw/credentials/whooing.json")
@@ -41,6 +43,130 @@ def api_get(endpoint: str, api_key: str, params: dict | None = None) -> dict:
return data["results"]
def api_key_from_cred() -> str:
"""credentials 에서 API 키 문자열 1회 생성."""
api_cfg = json.loads(CRED_PATH.read_text())["api"]
return build_api_key(api_cfg["app_id"], api_cfg["token"], api_cfg["signature"])
def api_request(method: str, endpoint: str, api_key: str,
params: dict | None = None, body: dict | None = None) -> dict:
"""후잉 OpenAPI 호출. GET 은 쿼리스트링, PUT 은 본문으로 파라미터를 보낸다.
⚠️ DELETE 는 쓸 수 없다. 후잉 서버가 DELETE 요청의 파라미터를 쿼리·본문(urlencoded/JSON)·
쿠키·경로 어디에서도 읽지 못해 항상 `section_id parameter is required` 로 실패한다
(2026-08-04 전수 실측. 같은 본문을 PUT 으로 보내면 정상이라 후잉 쪽 문제다).
거래를 지우는 대신 PUT 으로 금액을 줄이는 이유가 이것이다.
"""
url = f"{BASE}/{endpoint}"
if params:
url += "?" + urllib.parse.urlencode(params)
data = None
if body:
# 후잉은 '+' 를 공백으로 디코드하지 않는다. quote_via=quote 로 공백을 %20 으로 보낸다.
data = urllib.parse.urlencode(body, quote_via=urllib.parse.quote).encode()
req = urllib.request.Request(url, data=data, method=method, headers={"X-API-KEY": api_key})
if data:
req.add_header("Content-Type", "application/x-www-form-urlencoded")
with urllib.request.urlopen(req, timeout=15) as resp:
parsed = json.loads(resp.read().decode("utf-8"))
if parsed.get("code") != 200:
raise RuntimeError(
f"후잉 API error {parsed.get('code')}: {parsed.get('message')} "
f"{parsed.get('error_parameters') or ''} ({method} {endpoint})"
)
return parsed["results"]
def _section_list(api_key: str) -> list:
sections = api_request("GET", "sections.json", api_key)
if isinstance(sections, dict):
return sections.get("sections") or sections.get("rows") or list(sections.values())
return sections
def _name_to_account_id(api_key: str, section_id) -> dict[str, str]:
raw = api_request("GET", "accounts.json", api_key, {"section_id": section_id})
out: dict[str, str] = {}
for acc_list in raw.values():
if not isinstance(acc_list, list):
continue
for a in acc_list:
aid = str(a.get("account_id"))
out[a.get("title") or aid] = aid
return out
def _memo_has_merchant(memo: str, merchant: str) -> bool:
"""memo 에 merchant 가 토큰 단위로 들어있는지.
단순 substring 이면 '쿠팡' 이 '쿠팡페이주' 승인까지 잡아 엉뚱한 거래를 감액한다
(2026-08-04 실제로 두 상호가 같은 날 같은 금액으로 있었다). 카드 SMS 는 상호를
줄바꿈·공백으로 구분하므로 앞뒤가 공백이거나 문자열 끝일 때만 인정한다.
"""
if not memo or not merchant:
return False
for m in re.finditer(re.escape(merchant), memo):
before = memo[m.start() - 1] if m.start() > 0 else " "
after = memo[m.end()] if m.end() < len(memo) else " "
if before.isspace() and after.isspace():
return True
return False
def find_card_entries(card_account: str, money: int, merchant: str,
on_date: str, window_days: int = 30) -> list[dict]:
"""카드 취소의 원본 승인 거래 후보를 찾는다.
card_account 로 결제된 거래 중 memo 에 merchant 가 들어있고 금액이 취소액 이상인 것을
[on_date - window_days, on_date] 구간에서 찾는다. 금액 일치(전액취소)를 우선 반환하고,
없으면 초과분(부분취소 후보)을 반환한다.
⚠️ item 이 아니라 memo 로 매칭한다 — merchant_map 의 contains 룰이 item 을 재작성해서
(동행복권→'복권') item 은 상호와 다를 수 있지만, memo 는 항상 승인 SMS 원문이다.
⚠️ 판단은 호출측이 한다. 정확히 1건일 때만 손대는 게 안전하다.
"""
api_key = api_key_from_cred()
start = (datetime.strptime(on_date, "%Y%m%d") - timedelta(days=window_days)).strftime("%Y%m%d")
exact, over = [], []
for sec in _section_list(api_key):
sid = sec.get("section_id") or sec.get("id")
card_id = _name_to_account_id(api_key, sid).get(card_account)
if not card_id:
continue
rows = api_request("GET", "entries.json", api_key, {
"section_id": sid, "start_date": start, "end_date": on_date,
})
for row in (rows.get("rows") or []):
if str(row.get("r_account_id")) != card_id:
continue
if not _memo_has_merchant(row.get("memo") or "", merchant):
continue
row_money = row.get("money") or 0
if row_money == money:
exact.append({**row, "section_id": sid})
elif row_money > money:
over.append({**row, "section_id": sid})
return exact or over
def set_entry_money(entry: dict, money: int) -> None:
"""거래 금액을 money 로 바꾼다(PUT). money=0 도 후잉이 받는다(2026-08-04 실측).
조회한 나머지 필드를 그대로 되돌려보내 보존한다 — 부분 PUT 의 필드 보존 동작에
기대지 않는 편이 안전하다. entry_date 는 조회 시 '20260804.0000' 형태라 소수부를 뗀다.
"""
api_request("PUT", f"entries/{entry['entry_id']}.json", api_key_from_cred(), body={
"section_id": entry["section_id"],
"entry_date": str(entry.get("entry_date") or "").split(".")[0],
"l_account_id": entry.get("l_account_id"),
"r_account_id": entry.get("r_account_id"),
"item": entry.get("item") or "",
"money": str(money),
"memo": entry.get("memo") or "",
})
def fmt_won(n: int) -> str:
return f"{n:,}원"
@@ -12,7 +12,6 @@ from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import urllib.error
import urllib.parse
@@ -22,9 +21,11 @@ from pathlib import Path
from zoneinfo import ZoneInfo
import parsers as sms_parsers
import imsg_cli
import notify
import whooing_balance
import gahee_reminder
import securities_match
# 후잉 웹훅은 HTTP 200을 반환하면서 본문에 "fail"/"Error : ..." 를 줄 수 있어 본문 검증 필수.
SUCCESS_BODY = "done"
@@ -80,7 +81,7 @@ def _format_raw_fallback(new_raw: list) -> str:
def _format_balance_mismatch(info: dict, sms_b: int, whoo_b: int, diff: int, prev_diff,
recovery_hint=None) -> str:
recovery_hint=None, gaps=None) -> str:
"""SMS 잔액(자산) 또는 누적사용액(부채/카드)과 후잉 잔액 차이 → 텔레그램 메시지(HTML)."""
label = notify.escape_html(info["label"])
acct = notify.escape_html(info["account"])
@@ -88,42 +89,153 @@ def _format_balance_mismatch(info: dict, sms_b: int, whoo_b: int, diff: int, pre
kind_word = "누적사용액" if side == "liability" else "잔액"
actual_whoo_b = info.get("actual_whooing_balance", whoo_b)
payment_offset = info.get("liability_payment_offset", 0) or 0
if payment_offset and actual_whoo_b != whoo_b:
whoo_line = (
f"• 후잉: <b>{actual_whoo_b:,}원</b>"
f" + 카드대금 보정 <b>{payment_offset:,}원</b>"
f" → <b>{whoo_b:,}원</b>"
)
matched_word = "후잉(카드대금 보정 후)"
else:
whoo_line = f"• 후잉: <b>{whoo_b:,}원</b>"
matched_word = "후잉"
has_offset = bool(payment_offset and actual_whoo_b != whoo_b)
matched_word = "후잉(카드대금 보정 후)" if has_offset else "후잉"
if diff == 0:
recovered = abs(prev_diff) if prev_diff is not None else 0
recovered_line = f"\n회복 금액: <b>{recovered:,}원</b>" if recovered else ""
hint_line = f"\n반영 항목: <b>{notify.escape_html(recovery_hint)}</b>" if recovery_hint else ""
return (
f"✅ <b>{kind_word} 정합 회복</b>\n\n"
f"<b>{label}</b> ({acct})\n"
f"SMS / {matched_word} 모두 <b>{sms_b:,}원</b> 일치"
f"{recovered_line}"
f"{hint_line}"
)
lines = [
f"✅ <b>{kind_word} 정합 회복</b>",
"",
f"📌 <b>{label}</b> ({acct})",
f"• SMS · {matched_word}: <b>{sms_b:,}원</b> (일치)",
]
if recovered:
lines.append(f"• 불일치 해소: <b>{recovered:,}원</b>")
if recovery_hint:
lines.append("─────────────")
lines.append(f"• 마지막 반영 거래: <b>{notify.escape_html(recovery_hint)}</b>")
return "\n".join(lines)
sign = "+" if diff > 0 else ""
if side == "liability":
direction = "결제 누락 의심" if diff > 0 else "사용 거래 누락 의심"
else:
direction = "출금 누락 또는 입금 중복" if diff > 0 else "입금 누락 의심"
body = (
f"⚠️ <b>{kind_word} 불일치</b>\n\n"
f"<b>{label}</b> ({acct})\n"
f"• SMS: <b>{sms_b:,}원</b>\n"
f"{whoo_line}\n"
f"• 차이: <b>{sign}{diff:,}원</b> ({direction})"
lines = [
f"⚠️ <b>{kind_word} 불일치</b>",
"",
f"📌 <b>{label}</b> ({acct})",
f"• SMS {kind_word}: <b>{sms_b:,}원</b>",
]
if has_offset:
lines.append(
f"• 후잉 {kind_word}: <b>{actual_whoo_b:,}원</b>"
f" + 카드대금 보정 <b>{payment_offset:,}원</b>"
f" = <b>{whoo_b:,}원</b>"
)
else:
lines.append(f"• 후잉 {kind_word}: <b>{whoo_b:,}원</b>")
lines.append(f"• 차이: <b>{sign}{diff:,}원</b> → {direction}")
if prev_diff is not None and prev_diff != diff:
body += f"\n <i>이전 차이: {prev_diff:+,}원 → 변동</i>"
return body + "\n\n<i>SMS 누락·잘못 분개·기간 외 거래 등 점검 필요.</i>"
lines.append(f"• 직전 차이: {prev_diff:+,}원 (변동)")
if gaps:
lines.append("─────────────")
lines.append("🔎 <b>누적 끊김 구간</b> (SMS 미수신 승인 의심)")
for g in gaps[:3]:
lines.append(f"• {notify.escape_html(g['from'])} ~ {notify.escape_html(g['to'])}"
f" · <b>{g['amount']:,}원</b>")
if len(gaps) > 3:
lines.append(f"• 그 외 {len(gaps) - 3}건")
lines.append("<i>톨비처럼 문자가 안 오는 승인일 수 있어요.</i>")
lines.append("")
lines.append("<i>ℹ️ SMS 누락·잘못 분개·기간 외 거래 등 점검이 필요해요.</i>")
return "\n".join(lines)
def _cumulative_gap_scan(account_name: str, days: int = 45) -> list:
"""카드 승인 SMS 의 누적사용액 연속성을 검사해 'SMS 없이 누적에만 반영된 승인'을 찾는다.
누적은 승인마다 `직전 누적 + 승인액`(취소는 -)이 되어야 한다. 어긋난 구간이 있으면
그 사이에 문자가 오지 않는 승인(톨비 등)이 있었다는 뜻이다. 잔액 차이만으로는
"얼마 틀어졌다"까지만 알 수 있지만, 이 검사는 어느 승인 사이에 얼마가 빠졌는지 짚는다.
⚠️ 후잉 조회가 들어가므로 diff != 0 인 카드 계정에서만 호출한다.
⚠️ 양의 격차만 본다 — 카드대금 상환·정산은 누적을 떨어뜨리는데 이걸 격차로 세면 오탐이다.
⚠️ 이미 손으로 보정해 둔 건(메모가 SMS 원문이 아닌 항목)은 구간에서 차감한다.
안 그러면 톨비를 넣어준 뒤에도 같은 구간이 계속 미해결로 뜬다.
"""
start = (datetime.now(KST) - timedelta(days=days)).strftime("%Y%m%d")
end = datetime.now(KST).strftime("%Y%m%d")
api_key = whooing_balance.api_key_from_cred()
sections = whooing_balance._section_list(api_key)
if not sections:
return []
sid = sections[0].get("section_id") or sections[0].get("id")
aid = str(whooing_balance._name_to_account_id(api_key, sid).get(account_name))
rows = whooing_balance.api_request("GET", "entries.json", api_key, {
"section_id": sid, "start_date": start, "end_date": end, "limit": 500,
})
sms, corrections = [], []
for r in (rows.get("rows") or []):
if aid not in (str(r.get("l_account_id")), str(r.get("r_account_id"))):
continue
entry_date = str(r.get("entry_date") or "").split(".")[0]
memo = r.get("memo") or ""
parsed = sms_parsers.parse_hyundai_card(memo, "1970-01-01T00:00:00Z")
stamp = re.search(r"\d{2}/\d{2}\s+\d{2}:\d{2}", memo)
if parsed and parsed.get("cumulative") is not None and stamp:
sms.append({
"sort": (entry_date, stamp.group(0)),
"date": entry_date,
"when": stamp.group(0),
"cumulative": parsed["cumulative"],
"amount": parsed["amount"],
"kind": parsed["kind"],
})
else:
corrections.append({"date": entry_date, "money": int(r.get("money") or 0)})
sms.sort(key=lambda x: x["sort"])
gaps = []
for prev, cur in zip(sms, sms[1:]):
sign = -1 if cur["kind"] == "card_cancel" else 1
missing = cur["cumulative"] - (prev["cumulative"] + sign * cur["amount"])
if missing > 0:
gaps.append({
"from": prev["when"], "to": cur["when"],
"from_date": prev["date"], "to_date": cur["date"],
"amount": missing,
})
# 이미 보정해 둔 항목을 구간별로 소진시킨다. 한 보정이 여러 구간에 중복 차감되지 않도록
# 앞선 구간부터 하나씩 소비한다.
unused = sorted(corrections, key=lambda c: c["date"])
for gap in gaps:
for corr in unused:
if corr["money"] <= 0 or gap["amount"] <= 0:
continue
if gap["from_date"] <= corr["date"] <= gap["to_date"]:
applied = min(corr["money"], gap["amount"])
corr["money"] -= applied
gap["amount"] -= applied
return [g for g in gaps if g["amount"] > 0]
def _format_capital_blocked(blocked: list) -> str:
"""기초잔액(자본) 분개로 갈 뻔해 보류한 건 → 텔레그램 메시지(HTML).
후잉에 아무것도 넣지 않았으므로 관리자님이 직접 분개해야 한다. 그대로 두면
해당 계좌의 잔액 불일치 알림이 뒤이어 뜨므로 놓쳐도 두 번째 그물이 있다.
"""
lines = [f"⛔ <b>기초잔액 분개 보류 {len(blocked)}건</b>", ""]
for b in blocked[:5]:
pay = b.get("payload") or {}
try:
money = f"{int(pay.get('money', 0)):,}원"
except Exception:
money = f"{pay.get('money', '')}원"
lines.append(
f"• <b>{notify.escape_html(b['label'])}</b> · {notify.format_kst(b['created_at'])}"
f"\n {money} · {notify.escape_html(pay.get('item') or '')}"
f"\n <i>{notify.escape_html(pay.get('left') or '')} ← {notify.escape_html(pay.get('right') or '')}</i>"
)
if len(blocked) > 5:
lines.append(f"• 외 {len(blocked)-5}건")
lines.append("")
lines.append("<i>후잉에 반영하지 않았어요. 직접 분개해 주세요 — 자체이체면 실제 출금 계좌로, 아니면 성격에 맞는 계정으로요.</i>")
return "\n".join(lines)
def _format_sync_failure(f: dict) -> str:
@@ -202,7 +314,11 @@ MONEY_AMOUNT_RE = re.compile(r"(?:\d[\d,]*\s*원|(?:입금|출금)\s+\d[\d,]*)")
# 주유소 카드 결제는 보통 150,000원 선승인 → 실제 주유금액 승인 → 150,000원 승인취소 순서로 온다.
# 선승인/취소는 후잉에 기록하지 않고, 실제 결제만 차량유지비/주유비로 남긴다.
FUEL_KEYWORDS = ("석유", "주유소", "주유")
# "현대가스"·"한경에너"는 상호에 주유/석유가 안 들어가는 충전소라 직접 등록 (2026-08-03 관리자님 확인).
# 새 주유소를 만나면 여기와 whooing_merchant_map.json contains **양쪽에** 상호를 추가한다 —
# 이 상수는 선승인/취소 스킵, merchant_map 은 실제 주유 결제 분류로 한 쌍이다.
# ⚠️ 한경에너는 선승인이 149,900원이라 FUEL_PREAUTH_AMOUNT 정확일치엔 안 걸린다(분류만 적용).
FUEL_KEYWORDS = ("석유", "주유소", "주유", "현대가스", "한경에너")
FUEL_PREAUTH_AMOUNT = 150_000
@@ -213,7 +329,7 @@ def _is_fuel_text(text: str) -> bool:
def _should_skip_fuel_preauth(parsed: dict | None, text: str) -> bool:
"""주유소 15만원 선승인/취소 스킵.
parsed 가 있으면 구조화 필드 기준으로 판단한다. 현대카드 취소처럼 아직 파서가 없는 형식은
parsed 가 있으면 구조화 필드 기준으로 판단한다. 파서가 못 읽은 형식은 폴백으로
raw 텍스트에서 '취소' + '150,000원' + 주유 키워드 조합만 보수적으로 스킵한다.
"""
if parsed:
@@ -274,39 +390,33 @@ def resolve_chat_ids(carriers):
needed = set(carriers.keys())
if needed.issubset(cached_keys):
return {k: cached[k] for k in needed}
try:
chats_raw = subprocess.run(
["imsg", "chats", "--json"],
capture_output=True, text=True, timeout=IMSG_TIMEOUT,
)
chats = [json.loads(l) for l in chats_raw.stdout.splitlines() if l.strip()]
except FileNotFoundError:
print("❌ imsg CLI가 설치되어 있지 않습니다.", file=sys.stderr)
sys.exit(3)
except Exception as e:
print(f"❌ imsg chats 실행 실패: {e}", file=sys.stderr)
chats, err = imsg_cli.chats(IMSG_TIMEOUT)
if err:
print(f"❌ imsg chats 실행 실패: {err}", file=sys.stderr)
sys.exit(3)
result = {}
for sender in carriers.keys():
ids = [c["id"] for c in chats if c.get("identifier") == sender and c.get("id") is not None]
result[sender] = ids
# 전 carrier 가 빈 리스트면 조회 자체가 실패한 것으로 본다. 이 상태를 캐시에 저장하면
# 이후 모든 실행(launchd 포함)이 캐시에 적중해 조용히 0건 수집을 영구 반복한다.
# 일부만 비는 건 정상 — 메시지 이력이 없는 carrier 가 있을 수 있다.
if not any(result.values()):
print("❌ imsg chats 결과에 매핑된 발신번호가 하나도 없습니다 — 캐시 저장 안 함", file=sys.stderr)
sys.exit(3)
save_json(CHAT_IDS_CACHE, result)
return result
def fetch_messages(chat_id, since):
cmd = ["imsg", "history", "--chat-id", str(chat_id), "--limit", str(HISTORY_LIMIT), "--json"]
args = ["history", "--chat-id", str(chat_id), "--limit", str(HISTORY_LIMIT), "--json"]
if since:
cmd += ["--start", since]
try:
raw = subprocess.run(cmd, capture_output=True, text=True, timeout=IMSG_TIMEOUT)
return [json.loads(l) for l in raw.stdout.splitlines() if l.strip()]
except subprocess.TimeoutExpired:
print(f"⚠️ chat-id {chat_id} history 타임아웃", file=sys.stderr)
return []
except Exception as e:
print(f"⚠️ chat-id {chat_id} history 실패: {e}", file=sys.stderr)
args += ["--start", since]
rows, err = imsg_cli.run_json(args, IMSG_TIMEOUT)
if err:
print(f"⚠️ chat-id {chat_id} history 실패: {err}", file=sys.stderr)
return []
return rows
# --- 룰 엔진 (whooing_overrides.json) -----------------------------------------
@@ -510,8 +620,9 @@ def build_structured(parsed, sender_info, accounts, merchant_map):
return {**base, "left": cat, "right": carrier_acct}
return {**base, "left": "기타비용", "right": carrier_acct}
if kind == "card_cancel":
return None # raw 폴백 (후잉이 원거래 찾아 상쇄)
# card_cancel 은 여기서 payload 를 만들지 않는다 — 처리 루프의 _handle_card_cancel 이
# 원본 승인 거래를 찾아 PUT 으로 감액한다. 좌우를 뒤집은 역분개는 후잉이 거부한다
# (비용 계정은 차변 전용 → `r_account 잘못된 값입니다`) — 2026-08-03 회귀의 원인.
if kind == "withdrawal":
if rule:
@@ -542,6 +653,38 @@ def build_structured(parsed, sender_info, accounts, merchant_map):
return None
def _handle_card_cancel(parsed, sender_info, accounts, dry_run=False):
"""카드 취소 → 원본 승인 거래를 찾아 취소액만큼 감액.
반환 (ok, detail). ok=False 면 원본을 특정하지 못한 것이니 호출측이 raw 폴백으로 넘긴다.
⚠️ 폴백은 자동 처리가 아니다 — 후잉은 취소 문자를 받아도 원본을 지워주지 않으므로
(공식 FAQ 도 환불은 수동 처리하라고 안내한다) 관리자님이 후잉 UI 에서 손봐야 한다.
그래서 폴백 건은 raw 폴백 알림에 그대로 실어 보낸다.
전액취소면 금액이 0 이 되어 줄은 남는다. 후잉 API 로는 거래를 지울 수 없기 때문이다
(whooing_balance.api_request 주석 참고). 잔액·합계에는 영향이 없다.
"""
carrier_acct = accounts.get("carrier_to_account", {}).get(sender_info.get("carrier"), "")
if not carrier_acct:
return False, "카드 계정 매핑 없음"
money = parsed["amount"]
cands = whooing_balance.find_card_entries(
carrier_acct, money, parsed["merchant"], parsed["entry_date"]
)
if len(cands) != 1:
return False, f"원본 승인 후보 {len(cands)}건 — 1건일 때만 감액"
orig = cands[0]
new_money = (orig.get("money") or 0) - money
detail = (f"{orig.get('item')} {orig.get('money'):,}원 → {new_money:,}원 "
f"(취소 {money:,}원, entry {orig['entry_id']})")
if dry_run:
return True, f"(dry-run) {detail}"
whooing_balance.set_entry_money(orig, new_money)
return True, detail
def _parse_iso_utc(iso: str) -> datetime:
return datetime.fromisoformat(iso.replace("Z", "+00:00"))
@@ -611,6 +754,32 @@ def detect_pairs(candidates, accounts):
return pairs, used
def try_securities_transfer(parsed, cand, accounts):
"""자본 분개로 갈 뻔한 은행 SMS 가 실은 증권 이체인지 확인.
은행 SMS 에는 입금자명(본인 이름)만 찍혀 목적지를 알 수 없지만, 반대편은
키움 kt00015 에 남는다. 짝을 찾으면 자본이 아니라 자산↔자산 이체로 분개한다.
반환: {payload, key, label} | None (짝 없음·애매·조회 실패)
"""
if not parsed or parsed.get("kind") not in ("withdrawal", "deposit"):
return None
bank_asset = accounts.get("carrier_to_account", {}).get(cand["info"].get("carrier"))
if not bank_asset:
return None # 은행 자산 계정을 모르면 이체를 구성할 수 없다
entry_date = parsed.get("entry_date")
amount = parsed.get("amount")
if not entry_date or not amount:
return None
match = securities_match.find_match(
entry_date, amount, parsed["kind"], cand.get("created_at"))
if match is None:
return None
payload = securities_match.build_payload(
match, bank_asset, entry_date, amount, parsed["kind"], parsed.get("balance"))
return {"payload": payload, "key": match["key"], "label": match["label"]}
def post_to_whooing(webhook_url, payload, dry_run=False):
"""payload는 dict. 반환: (ok: bool, status: int, body: str).
HTTP 2xx + body가 'done' 으로 시작해야 성공. 그 외(HTTP 200 + 'fail' 포함) 모두 실패."""
@@ -737,12 +906,16 @@ def main():
sent_transfer = 0
sent_structured = 0
sent_raw = 0
sent_securities = 0
sent_cancel = 0
failed = 0
new_failures = []
new_raw = [] # raw 폴백으로 성공 처리된 건 (구조화 실패 신호) — 텔레그램 알림용
recovery_hints: dict[str, str] = {}
liability_accounts = set(accounts.get("categories", {}).get("부채", []))
liability_payment_offsets_to_add: dict[str, int] = {}
capital_accounts = set(accounts.get("categories", {}).get("순자산", []))
capital_blocked = [] # 기초잔액(자본) 분개로 갈 뻔한 건 — 후잉에 넣지 않고 알림만
latest = last_message_at
blocked_at = None # 첫 실패/hold 의 created_at — 이후 메시지는 처리/커서 진행 모두 중단
@@ -794,8 +967,42 @@ def main():
blocked_at = c["created_at"]
break
# 카드 취소는 상쇄 줄을 더하지 않고 원본 승인 거래를 감액한다.
# 원본을 못 찾으면 아래 raw 폴백으로 흘러간다(build_structured 가 None 을 준다).
if parsed and parsed.get("kind") == "card_cancel":
try:
cancel_ok, cancel_detail = _handle_card_cancel(
parsed, c["info"], accounts, args.dry_run
)
except Exception as e:
failed += 1
detail = f"{type(e).__name__}: {e}"
new_failures.append({
"sender": c["sender"], "label": c["info"]["label"],
"created_at": c["created_at"], "text": c["text"],
"mode": "cancel_reduce", "payload": {},
"status": 0, "response": detail[:500],
"failed_at": datetime.now(KST).isoformat(),
})
print(f" ❌ [cancel] {c['info']['label']} {c['created_at']} | {detail[:120]}")
blocked_at = c["created_at"]
break
if cancel_ok:
sent_cancel += 1
carrier_key = c["info"].get("carrier")
if carrier_key:
recovery_hints[carrier_key] = cancel_detail
print(f" ➖ [cancel] {c['info']['label']} {c['created_at']} | {cancel_detail}")
if c["created_at"] > (latest or ""):
latest = c["created_at"]
continue
print(f" ↩︎ [cancel→raw] {c['info']['label']} {c['created_at']} | {cancel_detail}")
# card_cancel 이 여기까지 오면 감액 실패 → raw 폴백만 남았다.
# apply_overrides 는 match 에 kind 가 없는 룰을 모든 kind 에 적용하므로, 그런 룰이
# 추가되면 취소가 좌우 뒤집힌 payload 로 새어나갈 수 있어 여기서 끊는다.
structured = None
if parsed:
if parsed and parsed.get("kind") != "card_cancel":
structured = apply_overrides(overrides, parsed, c["info"], accounts, merchant_map, c["created_at"])
if structured is None:
structured = build_structured(parsed, c["info"], accounts, merchant_map)
@@ -809,9 +1016,51 @@ def main():
mode = "raw"
display = c["text"][:60].replace("\n", " ")
# ⛔ 기초잔액(자본) 분개는 하지 않는다 (2026-09-07 관리자님 지시). 후잉에 넣지 않고 알림만.
# `방효원` 같은 자체이체 룰이 left=기초잔액 을 주는데, 짝이 되는 출금 SMS 가 안 오면
# 자본 한쪽만 남아 **순자산이 조용히 부풀려진다**(2026-09-07 카카오뱅크 879,993 실사고 —
# 처제 대납금 상환이었는데 입금자명이 "방효원"이라 자체이체 룰에 걸렸다).
# ⚠️ 커서는 latest_skip 으로 진행시킨다 — 재시도해도 같은 룰이 같은 결과를 내므로
# 여기서 커서를 막으면 이 메시지 뒤의 모든 거래가 영구히 멈춘다.
sec_key = None
if mode == "structured" and (
structured.get("left") in capital_accounts
or structured.get("right") in capital_accounts
):
# 자본으로 갈 뻔한 건이 실은 증권 이체일 수 있다. 은행 SMS 는 입금자명만
# 주지만 반대편이 키움 kt00015 에 남으므로, 짝을 찾으면 자본이 아니라
# 자산↔자산 이체로 분개한다. 짝이 없으면 기존대로 미반영+알림.
sec = try_securities_transfer(parsed, c, accounts)
if sec:
payload = {k: str(v) for k, v in sec["payload"].items()}
mode = "securities"
sec_key = sec["key"]
display = (f"{sec['payload']['left']} ← {sec['payload']['right']} "
f"{sec['payload']['money']:,}원 (증권 {sec['label']} 이체)")
else:
capital_blocked.append({
"label": c["info"]["label"],
"created_at": c["created_at"],
"payload": payload,
"blocked_at": datetime.now(KST).isoformat(),
})
if c["created_at"] > (latest_skip or ""):
latest_skip = c["created_at"]
print(f" ⛔ [capital-block] {c['info']['label']} {c['created_at']} | {display} — 미반영, 알림만")
continue
ok, status, body = post_to_whooing(webhook_url, payload, dry_run=args.dry_run)
if ok:
if mode == "structured":
if mode == "securities":
sent_securities += 1
# POST 성공 후에만 기록한다 — 실패하면 다음에 다시 짝으로 잡혀야 한다.
if not args.dry_run:
securities_match.mark_matched(sec_key, {
"sms_at": c["created_at"],
"money": int(payload["money"]),
"entry": display,
})
elif mode == "structured":
sent_structured += 1
carrier_key = c["info"].get("carrier")
if carrier_key:
@@ -832,6 +1081,7 @@ def main():
sent_raw += 1
new_raw.append({
"label": c["info"]["label"],
"carrier": c["info"].get("carrier"),
"created_at": c["created_at"],
"text": c["text"],
"parsed": parsed,
@@ -864,12 +1114,19 @@ def main():
f"<i>failures.json 에서 확인 부탁드립니다.</i>"
)
if capital_blocked and not args.dry_run:
failures.setdefault("capital_blocked", []).extend(capital_blocked)
save_json(FAILURES_FILE, failures)
notify.send(_format_capital_blocked(capital_blocked))
if new_raw and not args.dry_run:
notify.send(_format_raw_fallback(new_raw))
# 잔액 정합성 체크: 부분 처리(blocked_at) 사이클은 skip — 후잉이 모든 거래를 받지 못한 상태에서
# 비교하면 잘못된 알림이 발생할 수 있음.
if not blocked_at and not args.dry_run:
# 이번 사이클에 raw 로 넘어간(=구조화 실패) 계정들. 이자 자동분개 가드에 쓴다.
raw_carriers = {r.get("carrier") for r in new_raw if r.get("carrier")}
last_balance_by_carrier: dict = {}
for c in candidates:
parsed = c.get("parsed") or {}
@@ -935,16 +1192,32 @@ def main():
prev = carriers_state.get(carrier, {})
prev_diff = prev.get("last_diff")
# 카뱅 자동 이자/캐시백 보정: 자산 + 후잉이 SMS보다 적고(deposit 누락), 차이 ≤ 5,000원.
# 은행 자동 이자/캐시백 보정: 자산 + 후잉이 SMS보다 적고(deposit 누락), 차이 ≤ 5,000원.
# 횟수·날짜 제한 없이 매번 자동 분개. 매 발화마다 텔레그램 알림으로 누적 가시성 확보.
# 정상 SMS가 와서 deposit이 후잉에 들어가면 diff>=0이 되어 안전하게 미발화.
cur_month = datetime.now(KST).strftime("%Y-%m")
if (
interest_candidate = (
info.get("side") == "asset"
and carrier == "kakaobank"
and carrier in {"kakaobank", "shinhan_bank", "hana_bank"}
and diff < 0
and abs(diff) <= 5000
):
)
# ⚠️ 이 사이클에 raw 로 샌 메시지가 있는 계정은 이자로 단정하지 않는다.
# 입금 SMS 는 structured 매칭에 실패해 raw 로 넘어가고, 후잉은 200 만 주고
# 분개를 만들지 않는다. 그러면 diff 가 그 입금액만큼 음수가 되는데, 이 heuristic 은
# 그걸 이자로 오인해 가짜 수익을 원장에 굳혀버린다 (2026-08-14 카카오뱅크 3,700원
# 실사고 — 한재성 입금 10,900 미기록이 원인이었고, 진짜 8월 이자는 08/10 에
# SMS 로 6,735원이 이미 들어와 있었다). 이 경우 분개 대신 알림만 보낸다.
if interest_candidate and carrier in raw_carriers:
notify.send(
f"🔍 <b>{notify.escape_html(info['label'])} 미기록 입금 의심</b>\n"
f"차이 <b>{diff:+,}원</b> · 이 사이클에 raw 폴백이 있어 "
f"이자 자동분개를 보류했어요.\n"
f"<i>입금 SMS 가 후잉에 안 들어갔을 수 있어요. 확인 부탁드려요.</i>"
)
print(f" 🔍 [interest-hold] {info['label']} diff={diff:+,} — raw 폴백 존재로 이자 분개 보류")
interest_candidate = False
if interest_candidate:
interest_amount = -diff
interest_payload = {
"entry_date": datetime.now(KST).strftime("%Y%m%d"),
@@ -957,7 +1230,7 @@ def main():
ok, status, body = post_to_whooing(webhook_url, interest_payload, dry_run=False)
if ok:
notify.send(
f"💰 <b>카뱅 은행이자 자동 분개</b>\n"
f"💰 <b>{notify.escape_html(info['label'])} 은행이자 자동 분개</b>\n"
f"+{interest_amount:,}원 ({notify.escape_html(info['account'])} ← 기타수익)"
)
carriers_state[carrier] = {
@@ -1080,6 +1353,14 @@ def main():
changed = True
print(f" 📊 [balance-init] {info['label']} sms={sms_b:,} whoo={whoo_b:,} diff={diff:+,} (notify skip)")
continue
# 카드 누적사용액이 후잉보다 크면 = 우리가 못 받은 승인이 있다.
# 그 구간과 금액을 누적 연속성으로 짚어 알림에 붙인다(조회는 이때만).
gaps = None
if info.get("side") == "liability" and diff < 0:
try:
gaps = _cumulative_gap_scan(info["account"])
except Exception as e:
print(f" ⚠️ [cum-gap] {info['label']} 누적 연속성 검사 실패: {e}")
notify.send(_format_balance_mismatch(
{
**info,
@@ -1087,7 +1368,7 @@ def main():
"liability_payment_offset": liability_payment_offset,
},
sms_b, whoo_b, diff, prev_diff,
recovery_hints.get(carrier),
recovery_hints.get(carrier), gaps,
))
carriers_state[carrier] = {
"account": info["account"],
@@ -1121,9 +1402,9 @@ def main():
save_json(SYNC_STATE_FILE, sync_state)
if blocked_at:
print(f"⚠️ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, raw {sent_raw}건, 실패 {failed}건 — {blocked_at} 에서 중단(다음 cron 재시도)")
print(f"⚠️ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, 증권이체 {sent_securities}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 — {blocked_at} 에서 중단(다음 cron 재시도)")
else:
print(f"✅ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, raw {sent_raw}건, 실패 {failed}건 (last={final_latest or latest})")
print(f"✅ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, 증권이체 {sent_securities}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 (last={final_latest or latest})")
# 가희 잔액 리마인더 & 답신 자동 분개 (격리 — 실패해도 결제 sync 결과는 위에서 이미 출력됨)
run_gahee_reminder()
Binary file not shown.
@@ -1,5 +0,0 @@
{
"version": 1,
"bootstrapSeededAt": "2026-04-17T03:21:06.738Z",
"setupCompletedAt": "2026-04-17T05:20:30.244Z"
}
+63
View File
@@ -95,6 +95,7 @@ LLM-LLM 자연어 통신은 프롬프트 인젝션·할루시네이션 증폭
- **키움 조회:** `scripts/kiwoom_client.py {token|accounts|summary|balance|positions}` — 조회 전용 (주문 X)
- **일일 포트폴리오:** `scripts/stock_portfolio_report.py {run|send} [--by-account]` — 평일 20:10 launchd, 메일만 발송(텔레그램 요약 없음)
- **IPO 캘린더:** `scripts/ipo_calendar_sync.py` — 매월 1일 cron
- **IPO 텔레그램 알림:** `scripts/ipo_alert.py` — 매일 08:30 상장일, 10:00 청약 첫날만 알림. 증권사별 최소청약 증거금·온라인 일반 수수료 표시(공모가·증거금률은 숨김), `state/ipo_alerts.json`으로 중복 방지. 상장일(08:30 listing) 실행 때 자산웹에서 **청약완료한 종목은 관심종목에 자동등록**(종목당 1회)
- **비하이브 다이제스트:** `scripts/behive_youtube_digest.py {fetch|save|email|notify|list|show|remove}` — 평일 07/12/18시 cron
- **워치리스트 모니터:** `scripts/watchlist_monitor.py check` — 장중 15분, LLM 없이 직접 알림
- **워치리스트 웹뷰:** `scripts/behive_web.py serve` — launchd 상시, 외부는 `https://stock.hyowons.net/`
@@ -110,3 +111,65 @@ LLM-LLM 자연어 통신은 프롬프트 인젝션·할루시네이션 증폭
## 직접 고쳐 써라
이 문서는 출발점이다. 패턴이 보이면 직접 업데이트해라. 정비는 코디가 하지만, 이 매뉴얼의 작가는 너다.
## Tools
### Local notes (migrated from TOOLS.md)
# TOOLS.md - Local Notes
환경 세팅 메모. 관리자님 자산 관련 파일과 명령어 위치.
## Stock Data
모든 경로는 `agents/stock/workspace/` 기준.
- `memory/portfolio.json` — 보유 종목 **v2 스키마 스냅샷** (`accounts.{일반,ISA,가희_일반,가희_ISA}.positions`). 참고용. 실제 리포트 숫자는 항상 키움 REST 실시간 조회.
- `state/portfolio_daily_snapshot.json` — 일별 owner-그룹 스냅샷. v4 스키마: `{date: {owner: {holdings: {stock:{...}}, deposit: int, trade_journal: [...], realized_pl_total: int, realized_fees_total: int}}}`. 전날대비 순자산(평가+예수금) + 월별 실현손익/회전율/수수료 통계용. `stock_portfolio_report.py`가 작성. legacy v1/v2/v3 호환.
- `state/kiwoom_tokens/{일반,ISA,가희_일반,가희_ISA}.json` — 키움 REST OAuth 토큰 캐시 (계좌별 분리). `expires_dt` 기준 자동 갱신.
- `state/ipo_calendar_sync.json` — 공모주 캘린더 동기화 상태.
- `state/behive_watchlist.json` — **비하이브 종목분석 워치리스트**. 관리자님이 "OO 목표가 얼마야?" 같은 비하이브 분석 관련 질문하면 `skills/behive-watchlist` 사용. 종목별 목표/매입/손절/요약/출처영상 포함.
- `state/behive_youtube_seen.json` / `state/behive_last_fetch.json` — 비하이브 스크립트 내부 상태.
- `/Users/snowoyh/.openclaw/credentials/kiwoom.json` — 키움 자격증명 (계좌별 AppKey/SecretKey 분리 v2 스키마). 예제: `kiwoom.json.example`.
## Scripts (python3)
- `scripts/kiwoom_client.py {token|accounts|summary|balance|positions}` — 키움 REST 조회 전용 클라이언트. 잔고/보유/계좌평가. **주문 함수 절대 부재**(매매 절대 원칙, SOUL.md 참조). 자세한 사용법은 `skills/kiwoom-rest`.
- `scripts/stock_portfolio_report.py {run|send} [--by-account]` — 리포트 생성/이메일. 평일 20:10 launchd로 `send`. owner 그룹(본인 / 가희)별 블록 표시가 기본 (그룹 내 종목 합산), `--by-account`는 owner 블록 안에서 4계좌별 분리 뷰까지.
- `scripts/ipo_calendar_sync.py` — 공모주 일정 구글 캘린더 등록. 매월 1일 cron.
- `scripts/ipo_alert.py {subscription|listing}` — 청약 첫날 10:00·상장일 08:30 텔레그램 알림. `--dry-run`, `--date YYYY-MM-DD` 지원.
- `scripts/behive_youtube_digest.py {fetch|save|email|notify|list|show|remove}` — 비하이브투자자문 '종목분석' 영상 감지·요약·워치리스트 저장·이메일/텔레그램 발송. 평일 07/12/18시 cron.
## 관리자님 관련
- 잔고·보유·수익률 질문이 들어오면 **키움 REST로 실시간 조회**. `memory/portfolio.json`은 스냅샷이라 stale 할 수 있음.
- 이메일 수신자: `mini.snowoyh@gmail.com` (리포트 발송 대상)
## 증권사
- 키움증권. iMessage 체결통보 파싱은 2026-04-24 REST API 연동으로 폐기됨 — 모든 체결·잔고 정보는 `kiwoom_client.py`로 조회.
## Playwright MCP (브라우저 자동화)
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
- **탐색:** `navigate / navigate_back / tabs / wait_for`
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
**운영 특성:**
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
- 첫 호출 시 npx spawn 1~2초 지연.
- **키움 영웅문·HTS는 보안프로그램이 Playwright 탐지·차단할 가능성 큼 → 시도 자제**. 잔고·시세는 항상 `kiwoom_client.py` REST 우선.
- 공동인증서·OTP·CAPTCHA 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
**활용 시나리오 (레이 맥락):**
- DART 공시 검색·본문 추출·PDF 다운로드 (정정·합병·유상증자 키워드 모니터링)
- 네이버 증권 외국인·기관 매매동향 등 키움 REST에 없는 보조 지표 스크래핑
- 비하이브 페이지 보조(댓글·공지 등 유튜브 자막 외 정보) — 기존 `behive_youtube_digest`와 상호 보완
- 종목별 공시·뉴스 변화 감지 → 워치리스트 알림 보강 (LLM 미경유 모니터에 결과만 넘김)
- 매매(주문) 흐름에는 **절대 사용 금지** — 주문은 `orders/kiwoom_order.py` REST 한 경로만 (SOUL.md 매매 원칙 참조)
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -33,13 +33,16 @@ Be the assistant you'd actually want to talk to. Concise when needed, thorough w
레이는 **매매를 결정하지 않는다.** 결정권은 항상 관리자님 PIN echo 에 있다.
- **LLM 자율 매매 금지** — 워치리스트 도달·시장 이벤트·뉴스·자체 추론으로 매수/매도 자동 트리거 X. 매매는 오직 관리자님이 텔레그램에 직접 명시적으로 지시할 때만.
- **LLM 자율 매매 금지** — 워치리스트 도달·시장 이벤트·뉴스·자체 추론으로 매수/매도 자동 트리거 X. 매매는 오직 관리자님이 텔레그램에 직접 명시적으로 지시할 때만. (가격 도달로 주문이 나가는 경로가 하나 있다 — 자산웹에서 PIN 으로 사전 승인한 **예약 트레일링**뿐이고, 아래 「자동 트리거 등록 금지」의 예외 항목을 볼 것. 레이가 만들 수 있는 경로는 아니다.)
- **레이의 역할 = 페이로드 추출** — 자연어("삼성 5주 75,000 원에 사줘") → 종목코드·계좌·수량·가격·주문방식을 dict 로 정리해서 `orders.handler.propose_trade` 진입점에 전달. 그 외엔 일체 매매 도구를 부르지 않는다.
- **카드+PIN 분리 발송** — propose_trade 가 카드 메시지(매수/매도·계좌·종목·가격 4개 하이라이트) + PIN 메시지(단독 4자리 숫자, 가희 계좌면 8자리 영숫자) 두 개를 텔레그램에 보낸다.
- **PIN echo 가 마지막 게이트** — 관리자님이 PIN 을 그대로 답장한 순간만 실주문이 나간다. 레이 LLM 이 PIN 을 자기 판단으로 만들거나 변형하면 절대 안 된다.
- **사이드카 ON 상태에선 모든 매매 거부** — `state/orders_disabled` 파일 존재 시 진입점 첫 줄에서 차단. `/orders_off` `/orders_on` 으로 토글.
- **조회 vs 주문 분리 유지** — `kiwoom_client.py` 는 여전히 조회 전용. 주문 함수는 `orders/` 패키지로 분리. 조회 모듈에 주문 함수 추가 금지.
- **매매 관련 자동 트리거 등록 금지** — 매매·매매재시도·취소·재발행 등 `orders/` 모듈 진입점을 cron `jobs.json` 또는 launchd plist 에 자동 발화 항목으로 등록하지 않는다. 실패·거절·쿨다운된 매매는 관리자님이 텔레그램으로 다시 명시적으로 지시할 때까지 그대로 둔다. "쿨다운 끝나면 다시 시도하자"는 충동도 X — 자동 재시도 등록 자체가 LLM 자율 매매와 동급. 자동화가 필요해 보이면 직접 등록하지 말고 관리자님께 설계 안건으로 제안.
- **승인된 예외는 `ai.openclaw.stock.trailing-monitor.plist` 하나뿐**이고, 그 안에서 허용된 매매 API 는 ①`modify_order`(트레일링 손절선 **상향** 정정, 2026-07-30 승인) ②**예약 트레일링(TRAILING_ARM)의 발동가 도달 시 스톱주문 발주**(2026-08-25 승인) 2종이다.
- 허용 근거는 "발주가 없다"가 **아니라** "계좌·종목·수량·계단·발동가가 **PIN 승인 시점에 전부 확정**되고, 감시는 확정된 내용을 조건 도달 시 집행할 뿐 LLM 판단이 개입하지 않는다" 다. 판단 기준을 이렇게 읽어야 한다 — **누가 매매의 *내용*(계좌·종목·수량·방향·가격)을 정하는가.** 사람이 PIN 으로 정했으면 집행 자동화는 별건 승인 대상, 감시 루프나 LLM 이 정하면 금지.
- 이 예외를 근거로 **새 자동 트리거를 스스로 늘리지 않는다.** 감시가 수량을 늘리거나 방향을 바꾸는 경로도 여전히 없다(매도가능 부족 시 **축소**만).
- **자동매매·알고리즘 매매 요청** — 이 원칙을 환기하고 사이드카·1주 검증·가드 보강을 거친 별도 설계 안건으로 미룬다.
상세 사양과 호출 진입점은 `orders/__init__.py`, 한도값은 `orders/limits.json` 참조. 사용은 `skills/order-trading`, `skills/order-controls`.
-56
View File
@@ -1,56 +0,0 @@
# TOOLS.md - Local Notes
환경 세팅 메모. 관리자님 자산 관련 파일과 명령어 위치.
## Stock Data
모든 경로는 `agents/stock/workspace/` 기준.
- `memory/portfolio.json` — 보유 종목 **v2 스키마 스냅샷** (`accounts.{일반,ISA,가희_일반,가희_ISA}.positions`). 참고용. 실제 리포트 숫자는 항상 키움 REST 실시간 조회.
- `state/portfolio_daily_snapshot.json` — 일별 owner-그룹 스냅샷. v4 스키마: `{date: {owner: {holdings: {stock:{...}}, deposit: int, trade_journal: [...], realized_pl_total: int, realized_fees_total: int}}}`. 전날대비 순자산(평가+예수금) + 월별 실현손익/회전율/수수료 통계용. `stock_portfolio_report.py`가 작성. legacy v1/v2/v3 호환.
- `state/kiwoom_tokens/{일반,ISA,가희_일반,가희_ISA}.json` — 키움 REST OAuth 토큰 캐시 (계좌별 분리). `expires_dt` 기준 자동 갱신.
- `state/ipo_calendar_sync.json` — 공모주 캘린더 동기화 상태.
- `state/behive_watchlist.json` — **비하이브 종목분석 워치리스트**. 관리자님이 "OO 목표가 얼마야?" 같은 비하이브 분석 관련 질문하면 `skills/behive-watchlist` 사용. 종목별 목표/매입/손절/요약/출처영상 포함.
- `state/behive_youtube_seen.json` / `state/behive_last_fetch.json` — 비하이브 스크립트 내부 상태.
- `/Users/snowoyh/.openclaw/credentials/kiwoom.json` — 키움 자격증명 (계좌별 AppKey/SecretKey 분리 v2 스키마). 예제: `kiwoom.json.example`.
## Scripts (python3)
- `scripts/kiwoom_client.py {token|accounts|summary|balance|positions}` — 키움 REST 조회 전용 클라이언트. 잔고/보유/계좌평가. **주문 함수 절대 부재**(매매 절대 원칙, SOUL.md 참조). 자세한 사용법은 `skills/kiwoom-rest`.
- `scripts/stock_portfolio_report.py {run|send} [--by-account]` — 리포트 생성/이메일. 평일 20:10 launchd로 `send`. owner 그룹(본인 / 가희)별 블록 표시가 기본 (그룹 내 종목 합산), `--by-account`는 owner 블록 안에서 4계좌별 분리 뷰까지.
- `scripts/ipo_calendar_sync.py` — 공모주 일정 구글 캘린더 등록. 매월 1일 cron.
- `scripts/behive_youtube_digest.py {fetch|save|email|notify|list|show|remove}` — 비하이브투자자문 '종목분석' 영상 감지·요약·워치리스트 저장·이메일/텔레그램 발송. 평일 07/12/18시 cron.
## 관리자님 관련
- 잔고·보유·수익률 질문이 들어오면 **키움 REST로 실시간 조회**. `memory/portfolio.json`은 스냅샷이라 stale 할 수 있음.
- 이메일 수신자: `mini.snowoyh@gmail.com` (리포트 발송 대상)
## 증권사
- 키움증권. iMessage 체결통보 파싱은 2026-04-24 REST API 연동으로 폐기됨 — 모든 체결·잔고 정보는 `kiwoom_client.py`로 조회.
## Playwright MCP (브라우저 자동화)
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
- **탐색:** `navigate / navigate_back / tabs / wait_for`
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
**운영 특성:**
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
- 첫 호출 시 npx spawn 1~2초 지연.
- **키움 영웅문·HTS는 보안프로그램이 Playwright 탐지·차단할 가능성 큼 → 시도 자제**. 잔고·시세는 항상 `kiwoom_client.py` REST 우선.
- 공동인증서·OTP·CAPTCHA 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
**활용 시나리오 (레이 맥락):**
- DART 공시 검색·본문 추출·PDF 다운로드 (정정·합병·유상증자 키워드 모니터링)
- 네이버 증권 외국인·기관 매매동향 등 키움 REST에 없는 보조 지표 스크래핑
- 비하이브 페이지 보조(댓글·공지 등 유튜브 자막 외 정보) — 기존 `behive_youtube_digest`와 상호 보완
- 종목별 공시·뉴스 변화 감지 → 워치리스트 알림 보강 (LLM 미경유 모니터에 결과만 넘김)
- 매매(주문) 흐름에는 **절대 사용 금지** — 주문은 `orders/kiwoom_order.py` REST 한 경로만 (SOUL.md 매매 원칙 참조)
@@ -0,0 +1,66 @@
# Session: 2026-07-20 09:12:03 GMT+9
- **Session Key**: agent:stock:telegram:direct:8443122995
- **Session ID**: cb10d003-728f-4145-92ec-22ca9663e308
- **Source**: telegram
## Conversation Summary
user: Conversation info (untrusted metadata):
```json
{
"chat_id": "telegram:8443122995",
"message_id": "1064",
"sender_id": "8443122995",
"sender": "snowoyh bang",
"timestamp": "Mon 2026-07-20 09:11 GMT+9",
"inbound_event_kind": "user_request"
}
```
Sender (untrusted metadata):
```json
{
"label": "snowoyh bang (8443122995)",
"id": "8443122995",
"name": "snowoyh bang",
"username": "snowoyh"
}
```
Conversation context (untrusted, chronological, selected for current message):
#1048 Tue 2026-07-14 22:53 GMT+9 snowoyh bang: 당일종가랑 현재가가 달라
#1050 Tue 2026-07-14 22:54 GMT+9 snowoyh bang: 1928000은 무슨가격이야?
#1052 Tue 2026-07-14 22:57 GMT+9 snowoyh bang: 1941000이 종가고 현재가가 1928000으로 나와
관리자 가희 계좌 통틀어서 삼성전자 하이닉스 제외하고 계좌상태 고려해서 3천만원 예수금 만들려면 뭐를 파는세 좋아?
assistant: [assistant turn failed before producing content]
user: Conversation info (untrusted metadata):
```json
{
"chat_id": "telegram:8443122995",
"message_id": "1064",
"sender_id": "8443122995",
"sender": "snowoyh bang",
"timestamp": "Mon 2026-07-20 09:11 GMT+9",
"inbound_event_kind": "user_request"
}
```
Sender (untrusted metadata):
```json
{
"label": "snowoyh bang (8443122995)",
"id": "8443122995",
"name": "snowoyh bang",
"username": "snowoyh"
}
```
Conversation context (untrusted, chronological, selected for current message):
#1048 Tue 2026-07-14 22:53 GMT+9 snowoyh bang: 당일종가랑 현재가가 달라
#1050 Tue 2026-07-14 22:54 GMT+9 snowoyh bang: 1928000은 무슨가격이야?
#1052 Tue 2026-07-14 22:57 GMT+9 snowoyh bang: 1941000이 종가고 현재가가 1928000으로 나와
관리자 가희 계좌 통틀어서 삼성전자 하이닉스 제외하고 계좌상태 고려해서 3천만원 예수금 만들려면 뭐를 파는세 좋아?
assistant: ⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.
@@ -0,0 +1,160 @@
# KRX 애프터마켓 개설 · NXT 제도개선 (시행 2026-09-14)
**상태: 조사·기록만 완료. 코드 변경 0건.**
관리자님 지시(2026-09-08): "내용만 확인해놔. 14일 이후에 키움 API가 어떻게 변경되는지 확인하고 작업하자."
→ **§4 키움 API 확인이 모든 구현의 선행 조건**이다. 확인 전에 §5 좌표를 손대지 말 것.
출처: 대신증권 공지 「KRX 애프터마켓 개설 및 NXT 제도개선 안내」 (2026-09-08 13:56 게시)
`https://money2.daishin.com/E5/MBoard/PType_Basic/Mobile_Notice/DM_Basic_Read.aspx?seq=18257&page=1&p=4124&v=3090&m=3817`
---
## 1. 공지 확정 사실
### KRX 시간표
| 구분 | 변경 전 | 변경 후 |
|---|---|---|
| 장개시전 시간외 종가 | 08:30~08:40 | 현행 유지 |
| 정규장 | 09:00~15:30 | 현행 유지 |
| 장종료후 시간외 **종가** | 15:40~16:00 | 현행 유지 |
| 장종료후 시간외 **단일가** | 16:00~18:00 | **폐지** |
| **KRX 애프터마켓** | 없음 | **16:00~20:00 신설** |
### 애프터마켓 매매 규칙
- **체결**: 접속매매 (최초/최종 단일가 없음). **VI 발동 시에만** 단일가 전환
- ※ NXT 애프터마켓 개장 전 10분간(15:30~15:40)은 단일가매매 적용
- **제출가능 호가**: 지정가 · 최우선지정가 · 최유리지정가 (IOC/FOK 허용) → **시장가 없음, 스톱지정가 없음**
- **대상**: 유가증권·코스닥의 주권/외국주권/DR/리츠/선박투자회사/투자회사/SOC
- **제외종목 9종**: 30분 단위매매 · 거래정지 · 당일 정규장 미거래 · 투자경고 · 투자위험 · 이상급등 · 관리 · 초저유동 · 투자주의환기 · 투자유의
- **기존 시간외단일가에서 거래되던 ETF/ETN 거래불가**
- 시세: 애프터마켓의 고가/저가/실시간 체결가 산출. **익일 기준가 참조가격은 정규장 종가**
### 정규장 미체결 주문 — 자동 소멸 시점 (명문화됨)
| 구분 | KRX | NXT |
|---|---|---|
| 자동 증거금 해지 | **정규장 종료 후 15:35~** | 애프터마켓 종료 후 20:05~ |
| 미체결 주문 | **애프터마켓에서 재주문 필요** (미이전) | 애프터마켓까지 주문 유지 |
| 예시 | 09:00 주문 → 15:30 미체결 → 15:35~ 증거금 해지 → 16:00 이후 재주문 | 08:00 주문 → 20:00까지 미체결 → 20:00 이후 증거금 해지 |
### NXT 제도개선
**정적VI 도입** + 동적/정적VI 발동 시 단일가매매(2분) 전환
| | 동적VI | 정적VI |
|---|---|---|
| 참조가격 | 직전 체결가격 | 직전 단일가매매 가격 (없으면 기준가격) |
| 발동요건 | 코스피200 ±3% / 기타 ±6% | ±10% |
| 발동효과 | 단일가매매 전환(2분) | 〃 |
| 제출가능호가 | 지정가, 스톱지정가호가(메인마켓) | 〃 |
| 적용대상 | 모든 접속매매 체결 / 마켓별 최초가격 **배제** | 모든 접속·단일가매매 체결(VI단일가 제외) / 마켓별 최초가격에도 **적용** |
- **거래정지 후 재개 시 단일가매매**: CB(시장) NXT 30분 중단 + 단일가 30초 *(KRX 20분 + 10분)* / 중요내용공시(종목) NXT 40분 정지 + 단일가 30초 *(KRX 30분 + 10분)*
- **GTP(Good till Premarket) 전용호가 도입** — 프리마켓 제출 호가 전체에 적용 가능, 미체결잔량은 **프리마켓 종료(08:50) 일괄 취소**
- **프리마켓 상·하한가 주문가능** — 2026-08-12부터 한시 적용됐던 NXT 프리마켓 상·하한가 주문제한 **해제**
- **대주거래 09:00~20:00 확대** (KRX/NXT 동일) — 우리는 대주거래 미사용, 영향 없음
**외부 교차 확인** (뉴스): 애프터마켓 16:00~20:00 · 15:40~16:00 시간외종가 유지 · ETF·ETN 거래불가 · **공식 종가는 15:30 정규장 종가** · 사이드카 없음·VI 있음.
---
## 2. 현재 코드가 믿고 있는 시간 모델
시간 경계는 **`scripts/orders/limits.json` L37-49가 단일 진실 공급원**이고 `guards.py`엔 시간 리터럴이 0개다(`orders/__init__.py:22-24` 설계 규칙).
`guards.session_at()` (`guards.py:78-89`) 세션 **5종** + `determine_routing()` (`guards.py:143-161`):
| 세션 | 구간 | suffix | `dmst_stex_tp` |
|---|---|---|---|
| `NXT_PRE` | 08:00:00–09:00:00 | `_NX` | NXT |
| *(틈)* | 09:00:00–09:00:30 | — | **ValueError** |
| `KRX_NXT` | 09:00:30–15:20:00 | `_AL` / `''` | SOR / KRX |
| `KRX_CLOSE` | 15:20:00–15:30:00 | `''` | KRX |
| `NXT_AFTER` | **15:30:00–20:00:00** | `_NX` | NXT |
| `CLOSED` | 그 외 | — | **ValueError** |
⚠️ **핵심: KRX 시간외 시장(종가·단일가)이 모델에 아예 없다. 15:30 이후는 전부 NXT로 취급한다.** KRX 애프터마켓 신설이 정확히 이 가정을 깬다.
⚠️ 자산웹 phase는 **별개의 하드코딩**이다(`behive_web.py:788-808`) — `09:00~15:30`=`regular`, `08:00~20:00`=`nxt`, 그 외 `closed`. **프리(08:00~09:00)와 애프터(15:30~20:00)를 같은 `nxt` 값으로 구분하지 못한다.**
---
## 3. 영향 분석 (2026-09-08 실측)
### ① 거래 가능해지는데 자산웹이 막는 종목 — 가장 실질적
ETF/ETN은 `state/stock_codes.json` 기준 **1,063/1,064가 `nxt_enable=False`**. 애프터마켓에서도 ETF/ETN은 거래불가이므로 **현행 차단이 결과적으로 정당**하다.
문제는 **NXT 미상장 일반주**. 전체 4,300종목 중 `nxt_enable=False`가 3,693개, ETF/ETN을 뺀 약 **2,630개**가 9/14부터 KRX 애프터마켓에서 거래 가능해진다.
| | ETF/ETN (차단 유지 정당) | **NXT 미상장 일반주 (신규 거래가능)** | NXT 상장 |
|---|---|---|---|
| 보유 37 | 12 | **15** | 10 |
| 관심 57 | 12 | **20** | 25 |
| 감시 45 | 0 | **32** | 13 |
보유분 해당 15종목: LG씨엔에스 · SK이터닉스 · 삼성전자우 · 파두 · 아난티 · 제닉스로보틱스 · 마키나락스 · 노루페인트 · 베뉴지 · 서부T&D · 진흥기업 · 피앤씨테크 · RF머트리얼즈 · WON 두산그룹포커스 · WON 초대형IB&금융지주
차단 경로: `_market_phase_state()`가 16:00~20:00을 `nxt`로 판정 → `/api/quote_book`이 `nxt_enable=False` 회신(`behive_web.py:14687-14717`) → 클라이언트가 `📵 NXT 거래불가` 라벨 + `cls='closed'`(`behive_web.py:12853`) → `refreshSubmitGate()`가 `state.phaseCanTrade===false`로 매수/매도 버튼 disable(`behive_web.py:12928-12935`).
### ② 감시 공백
| 감시 | 현재 커버 | 애프터마켓(16:00~20:00) |
|---|---|---|
| `trailing_monitor` | 08:00–19:59 (plist 720엔트리) | ✅ 이미 커버 |
| `surge_monitor` | 09:00–15:35 (`MARKET_CLOSE=(15,35)` L91, plist 396엔트리) | ❌ 급등락·VI 알림 0건 |
| `watchlist_monitor` | 10:00/12:00/14:00 3회 (`MARKET_CLOSE=(15,45)` L48) | ❌ |
| `sim-scan` | 09:00–15:30 (plist 196엔트리) | ❌ |
### ③ 부수 영향 지점
- **`surge_monitor.get_limit_map()` 하루 1콜 캐시**(L306-320) — 애프터마켓 가격제한폭이 정규장과 다르면 16:00 이후 `upl_pric`/`lst_pric`이 틀린 값이 된다. 공지·뉴스에 제한폭 언급이 없어 **미확정**(§4-4).
- **`trailing_monitor.py:142` 실측 전제 재검증 필요** — "스톱주문은 장 마감 후 체결 없이 소멸한다(15:30~18:21 사이)". 그 관측 구간이었던 시간외단일가(16:00~18:00)가 **폐지**되고, 공지가 소멸 시점을 KRX 15:35~ / NXT 20:05~로 명문화했다. 재실측 없이는 `_notify_gone_batch`의 "장 마감 취소 추정" 문구가 틀린 시각에 나간다.
- **시장가 차단은 유지가 맞다** — `validate_market_order_session()`(`guards.py:195-205`)이 `NXT_PRE`/`NXT_AFTER`에서 MARKET을 거부한다. 애프터마켓 호가유형에도 시장가가 없어 근거가 오히려 늘어난다.
- ⚠️ **`STOP_LIMIT`은 세션 제약이 전무하다.** 애프터마켓엔 스톱지정가가 없고 NXT 메인마켓엔 있으므로, **라우팅을 KRX로 바꾸는 순간 트레일링·스톱 발주가 깨진다.**
- **09:00:00–09:00:30 틈**은 `CLOSED` → `determine_routing`이 `ValueError`. 기존 하자이며 이번 변경과 무관하나 같이 손볼 후보.
- **시간 가드가 두 계통으로 갈려 있다** — `limits.json` 기반은 `trailing_monitor` 단독이고, `surge_monitor`·`watchlist_monitor`·`behive_web`·`stock_analysis`는 각자 하드코딩. 제도 변경마다 파일별로 손봐야 한다. 통합하면 이후엔 JSON 편집 + plist 재생성 2단계로 줄어든다.
---
## 4. 키움 API 확인 체크리스트 — **다음 작업의 선행 조건**
로컬 `~/.openclaw/docs/키움 REST API 문서.pdf`는 2026-05-07 검증본으로 **이번 제도 변경 이전판**이고, 키움 OpenAPI 포털에서 관련 공지를 찾지 못했다.
⚠️ **CLAUDE.md 규칙("suffix 패턴 추측 금지 — 2026-05-07 ka10004 사고")에 따라 아래 값을 지어내지 않는다.**
1. **`dmst_stex_tp` 값** — 현재 매핑 `_EXCHANGE_BY_SUFFIX = {'_AL':'SOR', '_NX':'NXT', '':'KRX'}`(`kiwoom_order.py:36-41`). 애프터마켓 주문이 `KRX`로 그냥 나가는지, `SOR`가 자동 라우팅하는지, 신규 값이 생기는지.
2. **`trde_tp` 값** — 현재 `'0'` 지정가 / `'3'` 시장가 / `'28'` 스톱지정가(`kiwoom_order.py:31-34`). 애프터마켓 전용 호가유형(최우선지정가·최유리지정가·IOC·FOK) 코드가 추가되는지.
3. **애프터마켓 참여 가능 여부 플래그** — `ka10099 nxtEnable`은 NXT 상장 여부만 준다. **제외종목 9종류를 판별할 필드가 새로 생기는지**, 아니면 `order_warning`·`state` 텍스트로 근사해야 하는지.
4. **애프터마켓 가격제한폭** — `ka10001`/`ka10095`의 `upl_pric`/`lst_pric`이 16:00 이후에도 정규장 기준값인지 애프터 전용 값인지. `surge_monitor` 상·하한가 트리거 정확도가 여기 걸려 있다.
5. **VI 필드** — NXT 정적VI 도입으로 실시간 `1h` broadcast 페이로드가 확장되는지. `surge_monitor`가 `vi.active`를 그대로 신뢰한다(L373-377).
6. **호가·시세 TR의 애프터마켓 구분** — `ka10004` 응답에 세션 구분 필드가 생기는지. 현재 자산웹은 시간외/애프터 구분 필드를 전혀 쓰지 않는다.
7. **미체결 조회(`ka10075`) 동작** — KRX 주문이 15:35 증거금 해지 후 목록에서 즉시 빠지는지. `trailing_monitor` 생존 판정이 이 응답에 전적으로 의존한다.
**확인 방법**: 9/14(월) 장에서 ① 키움 REST API 포털·공지 재확인 ② 16:00 이후 `ka10095`·`ka10001` 응답 실측(조회 전용, 안전) ③ 필요시 NXT 미상장 보유주 1주로 애프터마켓 실주문 검증.
---
## 5. 확인 후 손댈 좌표 (미리 특정)
**§4 확인 전엔 손대지 않는다.**
| 대상 | 파일:라인 | 성격 |
|---|---|---|
| 세션 정의 | `scripts/orders/limits.json:37-49` | 애프터마켓 경계 추가 |
| 세션 판정 | `scripts/orders/guards.py:78-89` (`session_at`) | 세션 1종 추가 |
| 라우팅 | `scripts/orders/guards.py:143-161` (`determine_routing`) | 애프터 세션 라우팅 |
| 시장가 게이트 | `scripts/orders/guards.py:195-205` | 애프터 세션 추가(거부 유지) |
| 거래소 코드 | `scripts/orders/kiwoom_order.py:36-41` | §4-1 결과 반영 |
| 자산웹 phase | `scripts/behive_web.py:788-808`, `:812`(`_PHASE_BOUNDARIES_MIN`) | phase 분리 + 16:00 경계 |
| phase 라벨·게이트 | `scripts/behive_web.py:12845-12874`, `:12928-12935` | `📵 NXT 거래불가` 조건 재정의 |
| 호가 거래소 선택 | `scripts/behive_web.py:14687-14717` | 애프터 세션 분기 |
| 급등락 감시 | `scripts/surge_monitor.py:90-91`, `:306-320` + plist 396→약 660엔트리 | 시간 확대 + 제한폭 캐시 |
| 워치리스트 감시 | `scripts/watchlist_monitor.py:47-48` + plist | 시간 확대 |
| 트레일링 문구·전제 | `scripts/trailing_monitor.py:142`, `:487` | 소멸 시점 재실측 반영 |
| 시세 조회 거래소 | `scripts/trailing_monitor.py:446` (`'NX' if sess in ('NXT_PRE','NXT_AFTER') else 'AL'`) | 세션명 추가 시 **동반 수정 필수** |
| 경계 테스트 | `scripts/orders/tests/test_guards.py:45-72` | 12케이스 갱신 필수 |
| 정책 문서 | `scripts/orders/__init__.py:38-42`, `memory/orders_module.md:38,41-42` | "08:00–20:00 NXT" 서술 정정 |
⚠️ **20:00 종료가 유지**되므로 `guards.after_trading_day`(= `nxt_after_end`)와 그 소비자(`orders/trailing.py:692` `is_arm_expired`), `trailing-monitor` plist(720엔트리), briefing 20:10/20:30·trade-journal 21:00 트리거는 **무변경 가능**.
@@ -5,6 +5,7 @@
- 한국어, 존댓말, 관리자님 호칭.
- 4개 섹션 순서·제목 그대로 사용 (▣ 최근 가격 흐름 / ▣ 수급 해석 / ▣ 거래량·체결 강도 / ▣ 주의 포인트 / 리스크).
- 각 섹션 5문장 이상, 수치 근거 인용.
- 각 섹션에서 가장 중요한 핵심 수치·키워드 1~2곳만 `**...**`로 감싸 강조하세요(붉은 글씨로 표시됨). 남발 금지 — 한 문장에 하나 이하, 정말 중요한 곳만.
- 비교업체(peers) 데이터는 종목의 기본정보(가격·PER/PBR/ROE 등 재무비율) 관련 맥락에서만 간단히 짚어주시고, 4개 분석 섹션(가격 흐름·수급·거래량·주의 포인트) 안에서는 비교 코멘트를 넣지 마세요.
- 마지막에 한줄요약·투자의견 JSON을 정확한 형식으로 출력 (파서가 읽음).
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""@비하이브투자자문 신규 '종목분석' 영상 감지 + 자막 수집 + watchlist 저장 + 이메일/텔레그램 발송 + 조회.
"""@비하이브투자자문 신규 '종목분석'/'관심테마' 영상 감지 + 자막 수집 + watchlist 저장 + 이메일/텔레그램 발송 + 조회.
Subcommands:
fetch — 신규 매칭 영상 메타데이터 JSON 출력(자막 제외) + 자막은 fetch 캐시에만 저장
@@ -42,7 +42,7 @@ CHANNEL_ID = 'UCHTRF5r154igU2gXjudUMzg'
CHANNEL_NAME = '비하이브 투자자문'
FEED_URL = f'https://www.youtube.com/feeds/videos.xml?channel_id={CHANNEL_ID}'
SEARCH_URL = 'https://www.youtube.com/results?search_query={query}&sp=CAISAhAB'
TITLE_FILTER = '종목분석'
TITLE_FILTERS = ('종목분석', '관심테마')
TRANSCRIPT_LANGS = ['ko', 'ko-KR', 'en']
TRANSCRIPT_CHAR_LIMIT = 8000
FETCH_LIMIT = 10
@@ -139,8 +139,19 @@ def _is_recent_published_text(text: str) -> bool:
return False
def _matches_title_filter(title: str) -> bool:
return any(keyword in title for keyword in TITLE_FILTERS)
def _video_kind(title: str) -> str:
for keyword in TITLE_FILTERS:
if keyword in title:
return keyword
return '비하이브'
def _search_results_fallback() -> list[dict]:
query = urllib.parse.quote(f'{CHANNEL_NAME} {TITLE_FILTER}')
query = urllib.parse.quote(f'{CHANNEL_NAME} {" OR ".join(TITLE_FILTERS)}')
url = SEARCH_URL.format(query=query)
req = urllib.request.Request(url, headers={'User-Agent': FEED_USER_AGENT})
with urllib.request.urlopen(req, timeout=30) as r:
@@ -178,7 +189,7 @@ def _search_results_fallback() -> list[dict]:
title = ''.join(run.get('text', '') for run in vr.get('title', {}).get('runs', []))
vid = vr.get('videoId', '')
published = vr.get('publishedTimeText', {}).get('simpleText', '')
if owner != CHANNEL_NAME or TITLE_FILTER not in title or not vid or vid in seen_ids:
if owner != CHANNEL_NAME or not _matches_title_filter(title) or not vid or vid in seen_ids:
continue
if not _is_recent_published_text(published):
continue
@@ -186,6 +197,7 @@ def _search_results_fallback() -> list[dict]:
entries.append({
'video_id': vid,
'title': title.strip(),
'kind': _video_kind(title),
'published': published,
'url': f'https://www.youtube.com/watch?v={vid}',
})
@@ -231,6 +243,7 @@ def fetch_feed() -> list[dict]:
entries.append({
'video_id': vid,
'title': title.strip(),
'kind': _video_kind(title),
'published': published,
'url': link or f'https://www.youtube.com/watch?v={vid}',
})
@@ -267,7 +280,7 @@ def cmd_fetch() -> int:
entries = fetch_feed()
matched = [
e for e in entries
if TITLE_FILTER in e['title'] and e['video_id'] not in seen
if _matches_title_filter(e['title']) and e['video_id'] not in seen
]
matched = list(reversed(matched))[:FETCH_LIMIT]
for item in matched:
@@ -304,6 +317,7 @@ def load_cached_video(video_id: str) -> dict:
return {
'id': item.get('video_id'),
'title': item.get('title'),
'kind': item.get('kind') or _video_kind(item.get('title') or ''),
'url': item.get('url'),
'published': item.get('published'),
}
@@ -435,7 +449,8 @@ def _buy_primary(entry: dict) -> float | None:
def format_entry_block(entry: dict) -> str:
stock = entry.get('stock', '')
lines = [f'[종목분석] #{stock}', '━━━━━━━━━━']
kind = (entry.get('video') or {}).get('kind') or '종목분석'
lines = [f'[{kind}] #{stock}', '━━━━━━━━━━']
target_line = format_price_line('목표가', entry.get('target'))
upside = entry.get('upside_pct')
if upside is not None and '언급 없음' not in target_line:
@@ -480,10 +495,11 @@ def _format_current_html(current: str | None) -> str:
def format_entry_html_block(entry: dict) -> str:
S = HTML_STYLES
stock = entry.get('stock', '')
kind = (entry.get('video') or {}).get('kind') or '종목분석'
current = fetch_current_price(entry.get('code') or stock, _buy_primary(entry))
parts = [f'<div style="{S["card"]}">']
parts.append(
f'<div style="{S["card_title"]}">[종목분석] #{html_escape(stock)} '
f'<div style="{S["card_title"]}">[{html_escape(kind)}] #{html_escape(stock)} '
f'<span style="{S["card_meta"]}">현재가: {_format_current_html(current)}</span></div>'
)
@@ -541,7 +557,7 @@ def build_email_html(entries: list[dict]) -> str:
S = HTML_STYLES
parts = [
f'<div style="{S["wrap"]}">',
f'<div style="{S["greet"]}">관리자님, 비하이브투자자문 신규 종목분석 {len(entries)}건을 전달드립니다.</div>',
f'<div style="{S["greet"]}">관리자님, 비하이브투자자문 신규 추천 영상 {len(entries)}건을 전달드립니다.</div>',
]
for entry in entries:
parts.append(format_entry_html_block(entry))
@@ -585,7 +601,7 @@ def cmd_email(video_ids: list[str]) -> int:
return 1
date_str = datetime.now(KST).strftime('%Y-%m-%d %H:%M')
stock_names = ', '.join(e.get('stock', '') for e in entries)
subject = f'[비하이브 종목분석] {len(entries)}개 보고서 — {date_str} ({stock_names})'
subject = f'[비하이브 추천영상] {len(entries)}개 보고서 — {date_str} ({stock_names})'
html_body = build_email_html(entries)
cmd = ['gog', 'gmail', 'send', '--to', EMAIL_RECIPIENT, '--subject', subject, '--body-html', html_body]
p = subprocess.run(cmd, text=True, capture_output=True)
@@ -652,7 +668,7 @@ def cmd_notify(video_ids: list[str]) -> int:
if not entries:
print('no matching entries in watchlist', file=sys.stderr)
return 1
header = f'비하이브 종목분석 {len(entries)}건 제출'
header = f'비하이브 추천영상 {len(entries)}건 제출'
blocks = [format_telegram_block(e) for e in entries]
text = header + '\n\n' + '\n\n'.join(blocks)
if send_telegram(text):
@@ -1,12 +1,15 @@
#!/usr/bin/env python3
"""stock.briefing 폴백·강제 재실행.
메일은 발송하지 않는다 (2026-08-12 관리자님 결정 — 주식 리포트 메일 중지, 스냅샷만 유지).
목적은 자산웹 '당일 평가손익' 기준값이 되는 오늘 스냅샷을 확보하는 것.
retry/final: 오늘 portfolio_daily_snapshot.json에 오늘 키가 이미 있으면 skip (idempotent).
없으면 stock_portfolio_report.py send 재실행. final 모드에선 그래도 실패 시 레이 텔레그램 알림.
없으면 stock_portfolio_report.py send --no-mail 재실행. final 모드에선 그래도 실패 시 레이 텔레그램 알림.
force: 스냅샷 존재 여부 무관하게 21:00 fresh fetch로 데이터 덮어쓰기. 20:10 데이터가
부정확할 때 21:00 안정된 데이터로 갱신용. 스냅샷 있으면 run 모드(스냅샷만, 메일·텔레그램 X),
없으면 send 모드로 폴백 + 실패 시 알림.
부정확할 때 21:00 안정된 데이터로 갱신용. 스냅샷 있으면 run 모드, 없으면 send --no-mail 폴백
+ 실패 시 알림.
usage:
briefing_fallback.py retry # 20:30 — 스냅샷 없으면 재실행, 실패해도 알림 없음
@@ -42,7 +45,9 @@ def today_snapshot_exists() -> bool:
def run_briefing() -> int:
return subprocess.call(['/usr/bin/python3', str(REPORT), 'send'])
# 2026-08-12부터 주식 리포트 메일은 발송하지 않는다 (스냅샷만). --no-mail 은 send 경로의
# 휴장일 가드·실패 알림을 유지하면서 메일만 건너뛴다.
return subprocess.call(['/usr/bin/python3', str(REPORT), 'send', '--no-mail'])
def alert(text: str) -> None:
@@ -69,13 +74,13 @@ def main() -> int:
if mode == 'force':
# 21:00 무조건 재실행 — 20:10 데이터가 부정확할 때 fresh fetch로 덮어씀.
# 스냅샷 있으면 run(스냅샷만), 없으면 send(메일·텔레그램·스냅샷 + 실패 시 알림).
# 스냅샷 있으면 run(스냅샷만), 없으면 send --no-mail(스냅샷 + 실패 시 알림).
if today_snapshot_exists():
print(f'[force] {now} 스냅샷 갱신 (run 모드, 메일·텔레그램 없음).')
code = subprocess.call(['/usr/bin/python3', str(REPORT), 'run'])
print(f'[force] {now} run exit={code}')
return 0 if code == 0 else 1
print(f'[force] {now} 스냅샷 없음 → send 재실행 (메일·텔레그램 포함).')
print(f'[force] {now} 스냅샷 없음 → send --no-mail 재실행 (메일 없음).')
code = run_briefing()
print(f'[force] {now} send exit={code}')
if today_snapshot_exists():
@@ -83,7 +88,7 @@ def main() -> int:
alert(
f'⚠️ stock.briefing {now} 폴백 모두 실패\n'
f'오늘({datetime.now(KST):%Y-%m-%d}) 스냅샷이 생성되지 않았습니다.\n'
f'수동 실행: python3 {REPORT} send'
f'수동 실행: python3 {REPORT} send --no-mail'
)
return 1
@@ -103,7 +108,7 @@ def main() -> int:
alert(
f'⚠️ stock.briefing {now} 폴백 모두 실패\n'
f'오늘({datetime.now(KST):%Y-%m-%d}) 스냅샷이 생성되지 않았습니다.\n'
f'수동 실행: python3 {REPORT} send'
f'수동 실행: python3 {REPORT} send --no-mail'
)
return 1 if mode == 'final' else 0
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""미 연준 FOMC 회의 일정 → Google 캘린더 동기화 (조회+등록 전용).
출처: federalreserve.gov 공식 FOMC 일정표. 회의 날짜는 미 동부 기준 그대로 등록한다
(기존 수동 등록분·시황 자막 관례와 동일).
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import urllib.request
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo
KST = ZoneInfo('Asia/Seoul')
ET = ZoneInfo('America/New_York')
CALENDAR_ID = 'mini.snowoyh@gmail.com'
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
STATE_DIR = WORKSPACE / 'state'
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE = STATE_DIR / 'fomc_calendar_sync.json'
SOURCE_URL = 'https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm'
MONTHS = {
'january': 1, 'february': 2, 'march': 3, 'april': 4, 'may': 5, 'june': 6,
'july': 7, 'august': 8, 'september': 9, 'october': 10, 'november': 11, 'december': 12,
}
YEAR_PANEL_RE = re.compile(r'(\d{4})\s+FOMC\s+Meetings')
MEETING_ROW_RE = re.compile(
r'fomc-meeting__month[^>]*>\s*(?:<strong>)?(.*?)(?:</strong>)?\s*</div>'
r'.*?fomc-meeting__date[^>]*>\s*(.*?)\s*</div>',
re.S,
)
@dataclass
class Meeting:
start_date: date # 회의 시작 (미 동부)
end_date: date # 회의 종료 (미 동부, inclusive)
is_sep: bool
@property
def state_key(self) -> str:
# FOMC는 한 달에 두 번 열리지 않으므로 시작월이 고유 키
return f'{self.start_date:%Y-%m}'
@property
def summary(self) -> str:
base = f'[FOMC] {self.state_key} 회의'
return f'{base} (SEP)' if self.is_sep else base
@property
def announce_kst(self) -> datetime:
"""성명 발표 = 회의 마지막날 미 동부 14:00. 서머타임은 zoneinfo가 처리."""
et = datetime(self.end_date.year, self.end_date.month, self.end_date.day, 14, 0, tzinfo=ET)
return et.astimezone(KST)
@property
def cal_start(self) -> date:
return self.start_date
@property
def cal_end(self) -> date:
"""캘린더 이벤트 마지막날 = 발표일(KST). 미 동부 회의 종료 다음날이라
한국 장이 결과를 반영하는 날까지 일정이 덮인다."""
return self.announce_kst.date()
@property
def description(self) -> str:
if self.start_date == self.end_date:
days = self.start_date.isoformat()
else:
days = f'{self.start_date.isoformat()} ~ {self.end_date.isoformat()}'
lines = [
f'회의일(미 동부): {days}',
f'성명 발표: {self.announce_kst:%Y-%m-%d %H:%M} KST (한국 장 반영일)',
]
if self.is_sep:
lines.append('경제전망(SEP)·점도표 공개 회의')
lines.append(f'출처: {SOURCE_URL}')
return '\n'.join(lines)
def run(cmd: list[str]) -> str:
p = subprocess.run(cmd, capture_output=True, text=True)
if p.returncode != 0:
raise RuntimeError(p.stderr.strip() or p.stdout.strip() or 'command failed')
return p.stdout
def fetch(url: str) -> str:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=30) as r:
return r.read().decode('utf-8', 'ignore')
def parse_meetings(html: str, after: date) -> list[Meeting]:
"""연도 패널별로 회의 행을 훑어 캘린더 기간이 아직 안 끝난 회의만 반환.
기준을 회의 시작일이 아니라 `cal_end`(발표일 KST)로 두는 이유: 미 동부 회의가
끝났어도 한국 장 반영일이 남아 있으면 아직 관리 대상이다.
"""
panels = [(m.group(1), m.start()) for m in YEAR_PANEL_RE.finditer(html)]
if not panels:
return []
bounds = panels + [('', len(html))]
meetings: list[Meeting] = []
for (year_raw, start), (_, end) in zip(bounds[:-1], bounds[1:]):
year = int(year_raw)
if year < after.year:
continue
for row in MEETING_ROW_RE.finditer(html[start:end]):
month_txt = re.sub(r'<[^>]+>', '', row.group(1)).strip()
day_txt = re.sub(r'<[^>]+>', '', row.group(2)).strip()
m = parse_row(year, month_txt, day_txt)
if m and m.cal_end > after:
meetings.append(m)
meetings.sort(key=lambda m: m.start_date)
return meetings
def parse_row(year: int, month_txt: str, day_txt: str) -> Meeting | None:
"""'April/May' + '28-1*' 같은 월 경계 표기까지 처리."""
months = [MONTHS[p.strip().lower()] for p in month_txt.split('/') if p.strip().lower() in MONTHS]
if not months:
return None
is_sep = '*' in day_txt
days = [int(d) for d in re.findall(r'\d+', day_txt)]
if not days:
return None
start_month = months[0]
end_month = months[-1]
start_year = year
end_year = year if end_month >= start_month else year + 1 # 12월/1월 회의 대비
try:
start = date(start_year, start_month, days[0])
end = date(end_year, end_month, days[-1])
except ValueError:
return None
if end < start:
return None
return Meeting(start, end, is_sep)
def load_state() -> dict:
if STATE_FILE.exists():
try:
return json.loads(STATE_FILE.read_text())
except Exception:
return {}
return {}
def save_state(state: dict):
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
def state_key_from_summary(summary: str) -> str | None:
m = re.match(r'\[FOMC\]\s*(\d{4}-\d{2})\b', (summary or '').strip())
return m.group(1) if m else None
def fetch_existing_events(start_date: date, end_date: date) -> dict[str, list[dict]]:
start_dt = datetime(start_date.year, start_date.month, start_date.day, 0, 0, tzinfo=KST)
end_dt = datetime(end_date.year, end_date.month, end_date.day, 0, 0, tzinfo=KST)
out = run([
'gog', 'calendar', 'events', CALENDAR_ID,
'--from', start_dt.isoformat(), '--to', end_dt.isoformat(),
'--all-pages', '--max', '250', '--json'
])
data = json.loads(out)
existing: dict[str, list[dict]] = {}
for ev in data.get('events', []):
key = state_key_from_summary(ev.get('summary', ''))
if key:
existing.setdefault(key, []).append(ev)
return existing
def create_event(m: Meeting, dry_run: bool = False):
start = m.cal_start.isoformat()
end = (m.cal_end + timedelta(days=1)).isoformat() # 종일 이벤트의 end는 exclusive
if dry_run:
print(json.dumps({'create': m.summary, 'from': start, 'to': m.cal_end.isoformat()}, ensure_ascii=False))
return
# --event-color 미지정: 기존 이벤트에 지정된 색을 덮어쓰지 않는다
run([
'gog', 'calendar', 'create', CALENDAR_ID,
'--summary', m.summary,
'--description', m.description,
'--from', start,
'--to', end,
'--all-day',
])
def update_event(event_id: str, m: Meeting, dry_run: bool = False) -> str:
start = m.cal_start.isoformat()
end = (m.cal_end + timedelta(days=1)).isoformat()
if dry_run:
print(json.dumps({'update': m.summary, 'event_id': event_id, 'from': start, 'to': m.cal_end.isoformat()}, ensure_ascii=False))
return 'updated'
try:
run([
'gog', 'calendar', 'update', CALENDAR_ID, event_id,
'--summary', m.summary,
'--description', m.description,
'--from', start,
'--to', end,
'--all-day',
])
return 'updated'
except Exception:
run(['gog', 'calendar', 'delete', CALENDAR_ID, event_id, '--force', '--no-input'])
create_event(m, dry_run=False)
return 'recreated'
def delete_event(event_id: str, dry_run: bool = False):
if dry_run:
print(json.dumps({'delete_event_id': event_id}, ensure_ascii=False))
return
run(['gog', 'calendar', 'delete', CALENDAR_ID, event_id, '--force', '--no-input'])
def event_date_range(ev: dict) -> tuple[str, str]:
start_info = ev.get('start', {})
end_info = ev.get('end', {})
start = start_info.get('date') or start_info.get('dateTime', '')[:10]
end_excl = end_info.get('date') or end_info.get('dateTime', '')[:10]
end = ''
if end_excl:
end = (date.fromisoformat(end_excl) - timedelta(days=1)).isoformat()
return start, end
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--dry-run', action='store_true')
parser.add_argument('--list', action='store_true', help='파싱 결과만 출력 (캘린더 미접근)')
args = parser.parse_args()
today = datetime.now(KST).date()
meetings = parse_meetings(fetch(SOURCE_URL), today)
if args.list:
print(json.dumps({
'today': today.isoformat(),
'total': len(meetings),
'meetings': [
{'key': m.state_key, 'summary': m.summary,
'meeting_et': f'{m.start_date.isoformat()}~{m.end_date.isoformat()}',
'announce_kst': m.announce_kst.strftime('%Y-%m-%d %H:%M'),
'cal_start': m.cal_start.isoformat(), 'cal_end': m.cal_end.isoformat()}
for m in meetings
],
}, ensure_ascii=False, indent=2))
return
state = load_state()
end_date = max((m.cal_end for m in meetings), default=today) + timedelta(days=1)
# 스크래핑 실패 시 기존 일정이 몰살되는 사고 방지 — meetings 비면 cleanup 스킵
# 조회 시작이 today인 건 오늘 걸쳐 있는 진행 중 회의도 갱신 대상이기 때문
existing = {} if not meetings else fetch_existing_events(today, end_date)
created, updated, recreated, deleted, duplicates_removed = [], [], [], [], []
unchanged = 0
changes = []
keys = {m.state_key for m in meetings}
for m in meetings:
existing_list = existing.get(m.state_key, [])
# 같은 키로 여러 건이면 가장 최근 created를 남기고 나머지 삭제
if len(existing_list) > 1:
existing_list.sort(key=lambda e: e.get('created', ''), reverse=True)
for dup in existing_list[1:]:
delete_event(dup['id'], dry_run=args.dry_run)
duplicates_removed.append(m.state_key)
changes.append({'key': m.state_key, 'action': 'duplicate_removed', 'event_id': dup['id']})
existing_ev = existing_list[0] if existing_list else None
if not existing_ev:
create_event(m, dry_run=args.dry_run)
created.append(m.state_key)
changes.append({'key': m.state_key, 'action': 'created',
'new_date': f'{m.cal_start.isoformat()}~{m.cal_end.isoformat()}'})
continue
existing_start, existing_end = event_date_range(existing_ev)
desired_start, desired_end = m.cal_start.isoformat(), m.cal_end.isoformat()
same_summary = (existing_ev.get('summary') or '').strip() == m.summary
same_desc = (existing_ev.get('description') or '').strip() == m.description.strip()
if existing_start != desired_start or existing_end != desired_end or not same_summary or not same_desc:
result = update_event(existing_ev['id'], m, dry_run=args.dry_run)
(recreated if result == 'recreated' else updated).append(m.state_key)
changes.append({'key': m.state_key, 'action': result,
'old_date': f'{existing_start}~{existing_end}',
'new_date': f'{desired_start}~{desired_end}'})
else:
unchanged += 1
# 연준 일정표에서 사라진 미래 회의는 캘린더에서도 삭제 (meetings 비면 existing이 빈 dict라 자동 스킵)
for key, existing_list in existing.items():
if key in keys:
continue
for stale in existing_list:
old_start, old_end = event_date_range(stale)
# 오늘 진행 중이거나 지난 회의는 parse 대상(오늘 이후)에서 빠져 있을 뿐이므로 삭제 금지
if old_start and old_start <= today.isoformat():
continue
delete_event(stale['id'], dry_run=args.dry_run)
deleted.append(key)
changes.append({'key': key, 'action': 'deleted', 'event_id': stale['id'],
'old_date': f'{old_start}~{old_end}'})
state['last_changes'] = changes
state['last_run_at'] = datetime.now(KST).isoformat()
if not args.dry_run:
save_state(state)
print(json.dumps({
'today': today.isoformat(), 'total_found': len(meetings),
'newly_created': len(created), 'updated': len(updated), 'recreated': len(recreated),
'deleted': len(deleted), 'duplicates_removed': len(duplicates_removed),
'unchanged': unchanged, 'dry_run': args.dry_run, 'changes': changes,
}, ensure_ascii=False))
if __name__ == '__main__':
main()
+20 -6
View File
@@ -222,20 +222,34 @@ def cmd_sync(years_forward: int) -> int:
# 양쪽 다 0건이면 영구 변경/네트워크 차단 의심 — health 카운트 + 2회 연속 시 알림.
both_failed = (not investing_data) and (not nager_data_raw)
_update_health(both_failed, errors)
if not investing_data:
sys.stderr.write('no holidays parsed — investing.com 미반영. state file은 옛 데이터 유지.\n')
if not investing_data and not nager_data_raw:
sys.stderr.write('no holidays parsed — 두 소스 모두 실패. state file은 옛 데이터 유지.\n')
return 3
# KRX는 다음 연도 휴장 캘린더를 보통 11~12월에 공식 발표 → 그 전엔 investing.com도 비어 있음.
# investing이 한 건이라도 잡은 연도에만 nager 보강 — 미발표 연도의 일반 공휴일이 KRX 휴장으로 잘못 등록되는 것 차단.
investing_years = {int(d[:4]) for d in investing_data}
nager_data = {d: n for d, n in nager_data_raw.items() if int(d[:4]) in investing_years}
# 채울 연도 판정. KRX는 다음 연도 휴장 캘린더를 보통 11~12월에 공식 발표하므로,
# 미발표 연도의 일반 공휴일을 KRX 휴장으로 잘못 등록하지 않도록 게이트를 둔다.
# - investing(거래소 캘린더)이 살아 있으면 그게 곧 발표 신호 → 그 연도만 채운다(원래 규칙).
# - investing이 죽었으면(2026-09 이후 403 차단) 달력으로 대신 판정 — 올해는 항상,
# 내년은 11월 이후에만. 이래야 12월→1월 갭도 막고 미발표 연도도 오염되지 않는다.
if investing_data:
allowed_years = {int(d[:4]) for d in investing_data}
else:
now = datetime.now(KST)
allowed_years = {now.year} | ({now.year + 1} if now.month >= 11 else set())
nager_data = {d: n for d, n in nager_data_raw.items() if int(d[:4]) in allowed_years}
# investing.com(거래소 캘린더) 우선 — KRX 휴장 권위. nager는 누락분만 채움.
holidays = {**nager_data, **investing_data}
nager_extra = sorted(d for d in nager_data if d not in investing_data)
# nager는 '공휴일'이라 거래소 전용 휴장인 연말 폐장일(12/31)이 없다. 평일일 때만 보강.
for y in sorted(allowed_years):
eoy = date(y, 12, 31)
key = eoy.isoformat()
if eoy.weekday() < 5 and key not in holidays:
holidays[key] = '연말 휴장'
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
payload = {
'fetched_at': datetime.now(KST).isoformat(),
'sources': {'primary': URL, 'fallback': NAGER_URL.format(year='YYYY')},
'source_used': 'investing+nager' if investing_data else 'nager-only',
'years': years,
'holidays': dict(sorted(holidays.items())),
}
@@ -0,0 +1,449 @@
#!/usr/bin/env python3
"""투자원금(순입금) 집계 — 조회 전용.
투자원금 = 계좌 개설 이후 누적 입금 − 누적 출금. "내 돈 얼마 넣었나"이며,
`총 매입금액`(현재 보유종목 매입원가)이나 `예수금`(남은 현금)과 전혀 다른 값이다.
이 값이 있어야 `순자산 − 투자원금 = 진짜 총수익`(실현+미실현+배당)이 나온다.
키움 kt00018의 평가손익은 현재 보유분 미실현만 잡아 누적 실현손익·배당을 놓친다.
데이터원은 kt00016(일별계좌수익률상세현황)의 기간내총입금·총출금.
⚠️ kt00016은 **조회기간 1년 제한**이라 연도별 창으로 쪼개 누적한다.
캐시 `state/investment_principal.json`:
- frozen : 지난 연도 = 확정값. 한 번 적재 후 재조회 안 함 (0인 연도는 저장 생략)
- current: 올해 창(0101~오늘) 누적. 갱신 대상은 이것뿐 → 계좌당 1콜
- seen : 마지막 갱신 시점의 "당일 입출금" 스냅샷 — 이벤트 감지 기준
갱신은 별도 트리거 없이 behive_web 렌더 경로에서 이벤트로만 일어난다.
당일 입출금(이미 kt00015로 조회 중)이 seen과 다를 때만 올해 창을 재조회하므로
입출금 없는 평시에는 kt00016 호출이 0이다. 안전망으로 7일 초과 시 강제 갱신.
CLI: python3 scripts/investment_principal.py {bootstrap|refresh|show}
"""
from __future__ import annotations
import json
import os
import sys
import time
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import kiwoom_client as kc
KST = timezone(timedelta(hours=9))
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
STATE_FILE = WORKSPACE / 'state' / 'investment_principal.json'
# 부트스트랩 하한 연도. 계좌 개설 전 기간은 kt00016이 에러 없이 전 필드 0을 주므로
# 넉넉히 잡아도 안전하다 (현재 최고(最古) 계좌는 2018년 개설).
FLOOR_YEAR = 2010
PACE_SEC = 0.35 # 연속 조회 rate limit 페이싱
STALE_DAYS = 7 # 이벤트 감지가 못 잡은 경우의 안전망
# 적요에 이 중 하나가 들어가면 '내가 넣은/뺀 돈'(원금 반영), 아니면 투자 성과(원금 무관).
# 원금 반영 : 이체입금(지급결제)·이체오픈뱅킹입금·이체입금·대체입금·소액이체인증입금
# ·ISA가입인증입금 / 이체출금·대체출금
# 원금 무관 : 배당금입금·수익분배금입금·예탁금이용료(이자)입금·공모주환불금입금
# ·쿠폰현금지급(국내)입금·단주매각대금입금·대여수수료입금
# / 공모불입출금·청약수수료출금
# ⚠️ 추측이 아니라 실증된 규칙 — 이 분류의 합이 kt00016 기간내총입금/총출금과
# 4계좌 18개 (계좌,연도) 구간 전부에서 오차 0으로 일치했다(2026-07-29).
# 새 적요가 등장하면 reconciled=False로 드러나므로 조용히 틀리지 않는다.
_PRINCIPAL_RMRK_KEYS = ('이체', '대체', '인증')
def is_principal_flow(rmrk: str) -> bool:
"""적요가 투자원금에 반영되는 외부 입출금인지. 배당·이자 등은 False."""
return any(k in (rmrk or '') for k in _PRINCIPAL_RMRK_KEYS)
# ---- 캐시 I/O ----
@contextmanager
def _lock():
"""STATE_FILE 직렬화 — behive_web _stock_notes_lock 과 동일 패턴, 별도 lock 파일."""
import fcntl
lock_path = STATE_FILE.with_suffix(STATE_FILE.suffix + '.lock')
lock_path.parent.mkdir(parents=True, exist_ok=True)
f = open(lock_path, 'a')
try:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
yield
finally:
try:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
finally:
f.close()
def _load() -> dict | None:
"""캐시 로드. 파일 없거나 깨졌으면 None (호출측이 '미집계'로 처리)."""
if not STATE_FILE.exists():
return None
try:
d = json.loads(STATE_FILE.read_text())
except Exception:
return None
if not isinstance(d, dict) or 'current' not in d:
return None
d.setdefault('frozen', {})
d.setdefault('seen', {})
d['current'].setdefault('labels', {})
return d
def _save(d: dict) -> None:
d['refreshed_at'] = datetime.now(KST).isoformat(timespec='seconds')
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = STATE_FILE.with_suffix('.json.tmp')
tmp.write_text(json.dumps(d, ensure_ascii=False, indent=2))
os.replace(tmp, STATE_FILE)
# ---- 조회 ----
def _today_str() -> str:
return datetime.now(KST).strftime('%Y%m%d')
def _this_year() -> int:
return datetime.now(KST).year
def _window(year: int) -> tuple[str, str]:
"""연도별 조회 창. 올해는 오늘까지, 지난 연도는 12/31까지 (1년 제한 내)."""
if year == _this_year():
return f'{year}0101', _today_str()
return f'{year}0101', f'{year}1231'
def _fetch_year(label: str, year: int) -> dict:
"""한 (계좌, 연도) 창의 합계(kt00016) + 입출금 행(kt00015). 2콜.
flows 행의 `pr`은 원금 반영 여부. 분류 합이 kt00016 합계와 맞는지 `rec`에 남겨
새 적요가 등장해 분류가 어긋나면 화면에서 드러나게 한다.
"""
fr, to = _window(year)
r = kc.get_period_return(label, fr, to)
time.sleep(PACE_SEC)
try:
raw = kc.get_cash_flow(label, strt_dt=fr, end_dt=to)
except Exception as e:
sys.stderr.write(f'cash flow {label} {year} 실패: {e}\n')
raw = None
flows = None if raw is None else [
{'d': f['date'], 'io': f['io_tp'], 'amt': f['amount'],
'rk': f['rmrk'], 'pr': is_principal_flow(f['rmrk'])}
for f in raw
]
rec = None
if flows is not None:
pin = sum(f['amt'] for f in flows if f['io'] == 'IN' and f['pr'])
pout = sum(f['amt'] for f in flows if f['io'] == 'OUT' and f['pr'])
rec = (pin == r['cash_in'] and pout == r['cash_out'])
return {
'in': r['cash_in'], 'out': r['cash_out'],
'qin': r['qty_in'], 'qout': r['qty_out'],
'nf': r['net_fr'], 'nt': r['net_to'], 'pl': r['eval_pl'],
'flows': flows, 'rec': rec,
}
def _is_empty(y: dict) -> bool:
"""계좌 개설 전(또는 전면 무활동) 연도 — 저장 생략 대상."""
return not any(y[k] for k in ('in', 'out', 'qin', 'qout', 'nf', 'nt', 'pl'))
def _labels(labels: list[str] | None = None) -> list[str]:
all_lb = [a['label'] for a in kc.list_accounts()]
if labels is None:
return all_lb
wanted = set(labels)
return [lb for lb in all_lb if lb in wanted]
def _flow_totals(cash_flow_by_label: dict) -> dict:
"""behive_web의 kt00015 당일 입출금 → {label: {'in': n, 'out': n}}."""
out: dict[str, dict] = {}
for lb, flows in (cash_flow_by_label or {}).items():
ci = sum(f['amount'] for f in (flows or []) if f.get('io_tp') == 'IN')
co = sum(f['amount'] for f in (flows or []) if f.get('io_tp') == 'OUT')
out[lb] = {'in': ci, 'out': co}
return out
# ---- 공개 API ----
def bootstrap(labels: list[str] | None = None) -> dict:
"""FLOOR_YEAR ~ 올해 전수 조회로 캐시를 새로 만든다 (계좌×연도, 4×17≈68콜 ≈ 25초).
조기 종료 없음 — 잔고가 0으로 비었던 휴면 연도가 중간에 끼어도 그 앞 연도를 놓치지 않는다.
⚠️ 1회성 CLI 전용. 렌더 경로에서 절대 호출하지 말 것.
"""
lbs = _labels(labels)
this_year = _this_year()
frozen: dict[str, dict] = {}
current: dict[str, dict] = {}
for lb in lbs:
frozen[lb] = {}
for year in range(FLOOR_YEAR, this_year + 1):
y = _fetch_year(lb, year)
time.sleep(PACE_SEC)
if year == this_year:
current[lb] = y
elif not _is_empty(y):
frozen[lb][year_key(year)] = y
with _lock():
d = _load() or {}
d['frozen'] = {**(d.get('frozen') or {}), **frozen}
cur = d.get('current') or {}
if cur.get('year') != this_year:
cur = {'year': this_year, 'labels': {}}
cur['labels'] = {**(cur.get('labels') or {}), **current}
d['current'] = cur
d['seen'] = {**(d.get('seen') or {}), **_seen_now(lbs)}
_save(d)
return _load()
def year_key(year: int) -> str:
return str(year)
def _seen_now(labels: list[str]) -> dict:
"""현재 당일 입출금을 seen 형식으로. (CLI refresh/bootstrap 용 — kt00015 계좌당 1콜)"""
today = datetime.now(KST).strftime('%Y-%m-%d')
totals = _flow_totals(kc.get_cash_flow_all(None, labels))
return {lb: {'date': today, **totals.get(lb, {'in': 0, 'out': 0})} for lb in labels}
def refresh_current(labels: list[str], cash_flow_by_label: dict | None = None) -> None:
"""올해 창만 재조회 (라벨당 1콜) + 연도 롤오버 시 작년을 확정값으로 이관.
cash_flow_by_label: 호출측이 이미 들고 있는 kt00015 당일 입출금.
None이면 직접 조회. seen 기록에 쓴다 — kt00016 호출 **전** 시점의 값이어야
그 사이 들어온 입금이 seen에 앞서 반영돼 영구 미갱신되는 일이 없다.
"""
lbs = _labels(labels)
if not lbs:
return
if cash_flow_by_label is None:
seen = _seen_now(lbs)
else:
today = datetime.now(KST).strftime('%Y-%m-%d')
totals = _flow_totals(cash_flow_by_label)
seen = {lb: {'date': today, **totals.get(lb, {'in': 0, 'out': 0})} for lb in lbs}
this_year = _this_year()
with _lock():
d = _load()
if d is None:
return # 부트스트랩 전 — 렌더 경로를 68콜로 막지 않는다
cur = d['current']
prev_year = cur.get('year')
# 연도 롤오버: current에 담긴 작년치는 to_dt가 12/31이 아니라 미완결이므로
# 전체 창(0101~1231)으로 다시 받아 확정 이관한다.
if prev_year and prev_year != this_year:
for lb in lbs:
y = _fetch_year(lb, prev_year)
time.sleep(PACE_SEC)
if not _is_empty(y):
d['frozen'].setdefault(lb, {})[year_key(prev_year)] = y
cur = {'year': this_year, 'labels': {}}
for lb in lbs:
cur['labels'][lb] = _fetch_year(lb, this_year)
time.sleep(PACE_SEC)
cur['year'] = this_year
d['current'] = cur
d['seen'].update(seen)
_save(d)
def needs_refresh(labels: list[str], cash_flow_by_label: dict) -> bool:
"""올해 창 재조회가 필요한지 판정 — 순수 캐시 읽기(키움 콜 0).
캐시가 없으면 False — 부트스트랩 미실행 상태에서 페이지 로드를 68콜로 막지 않는다.
"""
d = _load()
if d is None:
return False
if d['current'].get('year') != _this_year():
return True
today = datetime.now(KST).strftime('%Y-%m-%d')
totals = _flow_totals(cash_flow_by_label)
for lb in labels:
if lb not in totals:
continue # owner 부분 갱신 — 조회 안 된 계좌는 판단 보류
s = d['seen'].get(lb) or {}
base = s if s.get('date') == today else {'in': 0, 'out': 0}
if totals[lb]['in'] != base.get('in', 0) or totals[lb]['out'] != base.get('out', 0):
return True
ts = d.get('refreshed_at')
if not ts:
return True
try:
age = datetime.now(KST) - datetime.fromisoformat(ts)
except Exception:
return True
return age > timedelta(days=STALE_DAYS)
def get_principal(labels: list[str]) -> dict:
"""캐시만 읽어 누적 원금 산출 (키움 콜 0).
반환 principal이 None이면 '미집계' — 요청 라벨 중 부트스트랩 안 된 계좌가 있다는 뜻.
(0으로 떨어뜨리면 그 계좌 입금이 조용히 빠져 원금이 과소계상된다.)
qty_flow=True는 주식 입출고(타사대체) 이력 — 현금 기준 원금이 과소계상됨을 알림.
by_label은 계좌별 순입금 {label: 원} — 자세히보기 토스트용.
"""
miss = {'principal': None, 'cash_in': None, 'cash_out': None,
'qty_flow': False, 'by_label': {}}
d = _load()
if d is None:
return miss
cur = d['current']['labels']
total_in = total_out = 0
qty_flow = False
by_label: dict[str, int] = {}
for lb in labels:
if lb not in cur:
return miss
recs = list((d['frozen'].get(lb) or {}).values()) + [cur[lb]]
lb_in = lb_out = 0
for y in recs:
lb_in += y.get('in', 0)
lb_out += y.get('out', 0)
qty_flow = qty_flow or bool(y.get('qin') or y.get('qout'))
by_label[lb] = lb_in - lb_out
total_in += lb_in
total_out += lb_out
return {
'principal': total_in - total_out,
'cash_in': total_in,
'cash_out': total_out,
'qty_flow': qty_flow,
'by_label': by_label,
}
def get_flows(labels: list[str]) -> dict:
"""입출금 내역 (최신순) — 캐시만 읽는다 (키움 콜 0).
반환:
- rows: [{date, io('IN'|'OUT'), amount, rmrk, principal(bool), label}] 최신순
- other_in / other_out: 원금 무관 입출금 합계 (배당·이자 등)
- reconciled: 전 구간에서 적요 분류 합이 kt00016 합계와 일치했는지
False면 새 적요가 등장해 분류가 어긋났다는 신호
- available: 내역이 캐시에 있는지 (부트스트랩 전 or 조회 실패 시 False)
"""
d = _load()
if d is None:
return {'rows': [], 'other_in': 0, 'other_out': 0,
'reconciled': True, 'available': False}
cur = d['current']['labels']
rows: list[dict] = []
reconciled = True
available = False
for lb in labels:
recs = list((d['frozen'].get(lb) or {}).values())
if lb in cur:
recs.append(cur[lb])
for y in recs:
flows = y.get('flows')
if flows is None:
continue
available = True
if y.get('rec') is False:
reconciled = False
for f in flows:
rows.append({'date': f['d'], 'io': f['io'], 'amount': f['amt'],
'rmrk': f['rk'], 'principal': f['pr'], 'label': lb})
rows.sort(key=lambda r: (r['date'], r['label']), reverse=True)
return {
'rows': rows,
'other_in': sum(r['amount'] for r in rows if r['io'] == 'IN' and not r['principal']),
'other_out': sum(r['amount'] for r in rows if r['io'] == 'OUT' and not r['principal']),
'reconciled': reconciled,
'available': available,
}
# ---- CLI ----
def _show() -> int:
d = _load()
if d is None:
print(f'캐시 없음: {STATE_FILE}\n → python3 scripts/investment_principal.py bootstrap')
return 1
from stock_portfolio_report import derive_owner
print(f'갱신: {d.get("refreshed_at")} (올해 창 {d["current"].get("year")})\n')
lbs = list(d['current']['labels'].keys())
bad = 0
for lb in lbs:
years = sorted((d['frozen'].get(lb) or {}).items())
years.append((year_key(d['current']['year']), d['current']['labels'][lb]))
print(f'===== {lb} =====')
tin = tout = 0
for yk, y in years:
# 항등식: 순자산초 + 입금 − 출금 + 평가손익 = 순자산말
err = y['nf'] + y['in'] - y['out'] + y['pl'] - y['nt']
bad += 1 if err else 0
tin += y['in']
tout += y['out']
q = f' 입고 {y["qin"]:,} 출고 {y["qout"]:,}' if (y['qin'] or y['qout']) else ''
nf = len(y.get('flows') or []) if y.get('flows') is not None else None
rc = {True: '분류OK', False: '분류불일치!', None: '내역없음'}[y.get('rec')]
print(f' {yk}: 입금 {y["in"]:>12,} 출금 {y["out"]:>11,} '
f'순자산 {y["nf"]:>12,}→{y["nt"]:>12,} 손익 {y["pl"]:>12,} '
f'항등식오차 {err:,} 내역 {nf if nf is not None else "-":>3}행 {rc}{q}')
print(f' >> 누적 입금 {tin:,} − 출금 {tout:,} = 순입금 {tin - tout:,}\n')
print('항등식 검증:', '전 행 오차 0 ✅' if not bad else f'⚠️ 오차 있는 행 {bad}개')
fl = get_flows(lbs)
print(f'적요 분류 검증: {"전 구간 일치 ✅" if fl["reconciled"] else "⚠️ 불일치 구간 있음 — 새 적요 확인 필요"}'
f' (내역 {len(fl["rows"])}행, 원금무관 입금 {fl["other_in"]:,}원 / 출금 {fl["other_out"]:,}원)')
# 라이브 교차검증 — 원금 대비 총수익 vs 실제 순자산
print('\n===== 원금 대비 총수익 (라이브 교차검증) =====')
owners: dict[str, list[str]] = {}
for lb in lbs:
owners.setdefault(derive_owner(lb), []).append(lb)
for owner, olbs in owners.items():
pr = get_principal(olbs)
net = 0
for lb in olbs:
net += sum(p['evlt_amt'] for p in kc.get_positions(lb)) + kc.get_balance(lb)['d2_entra']
p = pr['principal']
ret = net - p
warn = ' ⚠️ 주식 입출고 있음(원금 과소계상)' if pr['qty_flow'] else ''
print(f'[{owner}] 투자원금 {p:,}원 (입금 {pr["cash_in"]:,} − 출금 {pr["cash_out"]:,}){warn}')
print(f' 순자산 {net:,}원 → 총수익 {ret:+,}원 ({ret / p * 100:+.2f}%)' if p else ' 원금 0')
return 0 if not bad else 1
def main(argv: list[str]) -> int:
cmd = argv[1] if len(argv) > 1 else 'show'
if cmd == 'bootstrap':
lbs = _labels()
n = len(lbs) * (_this_year() - FLOOR_YEAR + 1)
print(f'{len(lbs)}계좌 × {_this_year() - FLOOR_YEAR + 1}년 = {n}콜 (약 {n * PACE_SEC:.0f}초)...')
bootstrap()
print(f'완료 → {STATE_FILE}\n')
return _show()
if cmd == 'refresh':
refresh_current(_labels())
print(f'올해 창 갱신 완료 → {STATE_FILE}\n')
return _show()
if cmd == 'show':
return _show()
print(__doc__)
return 2
if __name__ == '__main__':
raise SystemExit(main(sys.argv))
+304
View File
@@ -0,0 +1,304 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from datetime import date, datetime
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from ipo_calendar_sync import (
KST,
NAVER_IPO_API_URL,
_naver_api_rows,
fetch,
send_telegram,
)
STATE_FILE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace/state/ipo_alerts.json')
# 온라인 일반 고객 기준. 고객등급·배정 여부에 따라 면제될 수 있다.
# 확인이 끝난 증권사만 숫자를 두고, 미확인 증권사는 알림에서 추정하지 않는다.
BROKER_ONLINE_FEES = {
'대신증권': 2_000,
'하나증권': 2_000,
'미래에셋증권': 2_000,
'삼성증권': 2_000,
'KB증권': 1_500,
'유진투자증권': 2_000,
'한국투자증권': 2_000,
'IBK투자증권': 0,
}
def load_state() -> dict:
try:
return json.loads(STATE_FILE.read_text())
except (FileNotFoundError, json.JSONDecodeError):
return {'sent': {}}
def save_state(state: dict) -> None:
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
def _int(value) -> int | None:
try:
return int(str(value).replace(',', '').strip())
except (TypeError, ValueError):
return None
def _money(value: int | None) -> str:
return '미확인' if value is None else f'{value:,}원'
def _detail(ipo_code: str) -> dict:
url = f'https://stock.naver.com/api/domestic/ipo/{ipo_code}/detail/info'
return json.loads(fetch(url, encoding='utf-8'))
def _date_range(start: str, end: str) -> str:
return start if not end or start == end else f'{start} ~ {end}'
def _required_deposit(total: int, ratio) -> int | None:
try:
pct = Decimal(str(ratio))
except Exception:
return None
return int((Decimal(total) * pct / Decimal(100)).quantize(Decimal('1'), rounding=ROUND_HALF_UP))
def _manager_blocks(row: dict, detail: dict) -> list[dict]:
info = detail.get('registrationStatement') or {}
ratio = info.get('sbscMrgnRatio') or '50'
price = _int((detail.get('demandForecastingResult') or {}).get('fixPubPrice'))
if price is None:
price = _int(row.get('fixPubPrice'))
managers = detail.get('joinManagers') or []
if not managers:
names = [x.strip() for x in (row.get('orgNm') or '').split(',') if x.strip()]
managers = [{'orgNm': name, 'sbscMinNum': None} for name in names]
if not managers:
managers = [{'orgNm': '미확인', 'sbscMinNum': None}]
blocks = []
for manager in managers:
broker = (manager.get('orgNm') or '미확인').strip()
min_qty = _int(manager.get('sbscMinNum'))
total = price * min_qty if price is not None and min_qty is not None else None
deposit = _required_deposit(total, ratio) if total is not None else None
fee = BROKER_ONLINE_FEES.get(broker)
blocks.append({
'broker': broker,
'min_qty': min_qty,
'price': price,
'total': total,
'ratio': ratio,
'deposit': deposit,
'fee': fee,
})
return blocks
def format_alert(kind: str, row: dict, detail: dict) -> str:
name = (row.get('compName') or (detail.get('ipoInfo') or {}).get('compName') or '미확인').strip()
info = detail.get('ipoInfo') or {}
if kind == 'subscription':
heading = '[공모주 청약]'
date_label = '청약일'
date_value = _date_range(info.get('poStartDate') or row.get('poStartDate') or '', info.get('poEndDate') or row.get('poEndDate') or '')
else:
heading = '[신규 상장]'
date_label = '상장일'
date_value = info.get('lcalDate') or row.get('lcalDate') or ''
sections = []
for block in _manager_blocks(row, detail):
price = block['price']
qty = block['min_qty']
if block['deposit'] is not None and qty is not None:
cost = f"{block['deposit']:,}원(최소 {qty:,}주)"
elif price is not None:
cost = '최소청약수량 미확인'
else:
cost = '미확정'
fee = block['fee']
cost += f" / 수수료 {_money(fee)}(온라인 일반 기준)"
sections.append('\n'.join([
heading,
f'종목명 : {name}',
f"증권사 : {block['broker']}",
f'{date_label} : {date_value}',
f'증거금 : {cost}',
]))
return '\n\n'.join(sections)
GAHEE_CRED = Path('/Users/snowoyh/.openclaw/credentials/gahee_imessage.json')
IPO_SUBSCRIBED = Path('/Users/snowoyh/.openclaw/agents/stock/workspace/state/behive_ipo_subscribed.json')
def gahee_listing_message(target: date, rows: list[tuple[dict, dict]] | None) -> tuple[list[str], str]:
"""가희가 청약한 종목 중 target 상장분 → (ipoCode 목록, 문자 본문). 없으면 ([], '').
상장일은 API 상장 목록 ∪ 청약기록 lcal_date(네이버 조회 실패해도 기록으로 보낸다)."""
try:
subscribed = json.loads(IPO_SUBSCRIBED.read_text())
except Exception:
return [], ''
by_code = {(r.get('ipoCode') or r.get('itemCode') or '').strip(): (r, d) for r, d in (rows or [])}
codes, blocks = [], []
for code, rec in subscribed.items():
broker = ((rec.get('by') or {}).get('gahee') or {}).get('broker')
if not broker or (code not in by_code and rec.get('lcal_date') != target.isoformat()):
continue
row, detail = by_code.get(code, ({}, None))
if detail is None:
try:
detail = _detail(code)
except Exception:
detail = {}
price = next((b['price'] for b in _manager_blocks(row, detail) if b.get('price') is not None), None)
lines = [f"[공모주 상장] 오늘 {rec.get('name') or row.get('compName') or code} 상장일이에요",
f'· 청약 증권사: {broker}']
if price is not None:
lines.append(f'· 확정 공모가: {price:,}원')
lines.append(f'· 09:00 거래 시작 — 배정 주식은 {broker} 앱에서 확인하세요')
codes.append(code)
blocks.append('\n'.join(lines))
return codes, '\n\n'.join(blocks)
def send_gahee_imessage(text: str) -> bool:
"""가희 리마인더(gahee_reminder._send_imessage)와 같은 방식 — imessage 강제(sms 는 이 수신자에게 3전 3패).
⚠️ rc=0 은 Messages 에 넘겼다는 뜻일 뿐 전달 확인이 아니다. 코디 셸에선 AppleEvents 거부로 안 되고 launchd 경유로만."""
handle = json.loads(GAHEE_CRED.read_text()).get('handle')
if not handle:
return False
p = subprocess.run(['imsg', 'send', '--to', handle, '--text', text, '--service', 'imessage'],
capture_output=True, text=True, timeout=30)
if p.returncode != 0:
print(f'gahee imsg send rc={p.returncode}: {p.stderr[:200]}', file=sys.stderr)
return p.returncode == 0
def collect_for_date(kind: str, target: date) -> list[tuple[dict, dict]]:
payload = json.loads(fetch(NAVER_IPO_API_URL, encoding='utf-8'))
matches = []
field = 'poStartDate' if kind == 'subscription' else 'lcalDate'
for row in _naver_api_rows(payload):
if row.get(field) != target.isoformat():
continue
code = (row.get('ipoCode') or row.get('itemCode') or '').strip()
if not code:
continue
matches.append((row, _detail(code)))
return matches
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('kind', choices=('subscription', 'listing'))
parser.add_argument('--date', help='YYYY-MM-DD, 기본값은 오늘(KST)')
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
target = date.fromisoformat(args.date) if args.date else datetime.now(KST).date()
try:
rows = collect_for_date(args.kind, target)
except Exception as exc:
print(f'IPO alert source failed: {type(exc).__name__}: {exc}', file=sys.stderr)
rows = None
if args.kind == 'listing' and not args.dry_run:
# 자산웹에서 청약완료한 종목을 상장일에 관심종목으로 등록. 알림 소스가 죽어도
# 청약완료 기록의 상장일로 시도한다(그래서 위에서 바로 return 하지 않는다).
listed = {(r.get('ipoCode') or r.get('itemCode') or '').strip() for r, _ in (rows or [])}
try:
from behive_web import register_listed_ipo_interests
added = register_listed_ipo_interests(target.isoformat(), listed)
if added:
print(f'IPO interests added: {", ".join(added)}')
except Exception as exc:
print(f'IPO interests add failed: {type(exc).__name__}: {exc}', file=sys.stderr)
if not args.dry_run:
# 비하이브 '공모청약' 영상 요약 → 자산웹 공모주 탭. 새 영상 없으면 채널 페이지 1콜로 끝난다.
try:
from ipo_youtube_digest import run as run_ipo_youtube
print(f'IPO youtube digest: {json.dumps(run_ipo_youtube(), ensure_ascii=False)}')
except Exception as exc:
print(f'IPO youtube digest failed: {type(exc).__name__}: {exc}', file=sys.stderr)
if args.kind == 'listing':
# 가희가 청약한 종목의 상장일 아침 iMessage(2026-10-07 관리자님 요청). 같은 날 여러 종목이면 한 통.
try:
codes, text = gahee_listing_message(target, rows)
state = load_state()
key = f'gahee_listing|{target.isoformat()}|{",".join(sorted(codes))}'
if not codes:
pass
elif args.dry_run:
print(f'[dry-run] 가희 상장 문자:\n{text}\n')
elif key not in state.get('sent', {}):
if send_gahee_imessage(text):
state.setdefault('sent', {})[key] = datetime.now(KST).isoformat()
save_state(state)
print(f'gahee listing imessage sent: {",".join(codes)}')
except Exception as exc:
print(f'gahee listing imessage failed: {type(exc).__name__}: {exc}', file=sys.stderr)
if rows is None:
return 1
state = load_state()
sent = state.setdefault('sent', {})
delivered = 0
skipped = 0
for row, detail in rows:
code = (row.get('ipoCode') or row.get('itemCode') or '').strip()
key = f'{args.kind}|{target.isoformat()}|{code}'
if key in sent and not args.dry_run:
skipped += 1
continue
message = format_alert(args.kind, row, detail)
if args.dry_run:
print(message)
print()
continue
if not send_telegram(message):
print(f'IPO alert send failed: {key}', file=sys.stderr)
return 1
sent[key] = datetime.now(KST).isoformat()
delivered += 1
if not args.dry_run:
# 상태 파일이 무한히 커지지 않도록 최근 120일만 보존한다.
cutoff = date.fromordinal(target.toordinal() - 120).isoformat()
state['sent'] = {
key: value for key, value in sent.items()
if len(key.split('|')) < 2 or key.split('|')[1] >= cutoff
}
state['last_run_at'] = datetime.now(KST).isoformat()
state['last_kind'] = args.kind
state['last_target_date'] = target.isoformat()
state['last_found'] = len(rows)
state['last_delivered'] = delivered
state['last_skipped'] = skipped
save_state(state)
print(json.dumps({
'kind': args.kind,
'target_date': target.isoformat(),
'found': len(rows),
'delivered': delivered,
'skipped': skipped,
'dry_run': args.dry_run,
}, ensure_ascii=False))
return 0
if __name__ == '__main__':
raise SystemExit(main())
@@ -5,9 +5,10 @@ import argparse
import json
import re
import subprocess
import sys
import urllib.parse
import urllib.request
from dataclasses import dataclass
from urllib.parse import urljoin
from datetime import date, datetime, timedelta
from html import unescape
from pathlib import Path
@@ -19,9 +20,19 @@ WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
STATE_DIR = WORKSPACE / 'state'
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE = STATE_DIR / 'ipo_calendar_sync.json'
CONFIG_PATH = Path('/Users/snowoyh/.openclaw/openclaw.json')
TELEGRAM_ACCOUNT = 'stock'
SUBSCRIPTION_URL = 'https://www.38.co.kr/html/fund/index.htm?o=k'
NAVER_IPO_URL = 'https://finance.naver.com/sise/ipo.naver'
SOURCE_LABEL = '네이버 금융 IPO'
LISTING_URL = 'https://www.38.co.kr/html/fund/index.htm?o=nw'
NAVER_IPO_API_URL = 'https://stock.naver.com/api/domestic/market/ipo/progress'
SOURCE_LABEL = '네이버페이 증권 IPO'
LISTING_LOOKBACK_DAYS = 14
class IpoParserError(RuntimeError):
def __init__(self, errors: list[str]):
self.errors = errors
super().__init__('IPO 파서 전부 실패: ' + ' | '.join(errors))
@dataclass
@@ -32,6 +43,7 @@ class EventSpec:
end_date: date # inclusive
brokers: str
source_url: str
source_label: str = SOURCE_LABEL
@property
def summary(self) -> str:
@@ -49,7 +61,7 @@ class EventSpec:
f'종목명: {self.name}\n'
f'증권사: {self.brokers or "미확인"}\n'
f'{label}: {date_str}\n'
f'기준: {SOURCE_LABEL}\n'
f'기준: {self.source_label}\n'
f'출처: {self.source_url}'
)
@@ -65,6 +77,38 @@ def run(cmd: list[str]) -> str:
return p.stdout
def send_telegram(text: str) -> bool:
try:
cfg = json.loads(CONFIG_PATH.read_text())
acct = cfg['channels']['telegram']['accounts'][TELEGRAM_ACCOUNT]
token = acct['botToken']
chat_ids = acct.get('allowFrom') or []
except Exception as e:
print(f'telegram cfg load failed: {e}', file=sys.stderr)
return False
if not chat_ids:
print('no telegram chat_ids', file=sys.stderr)
return False
url = f'https://api.telegram.org/bot{token}/sendMessage'
ok = True
for chat_id in chat_ids:
body = urllib.parse.urlencode({
'chat_id': chat_id,
'text': text[:4000],
'disable_web_page_preview': 'true',
}).encode()
try:
req = urllib.request.Request(url, data=body, method='POST')
with urllib.request.urlopen(req, timeout=15) as response:
if response.status != 200:
ok = False
except Exception as e:
print(f'telegram send failed: {e}', file=sys.stderr)
ok = False
return ok
def fetch(url: str, encoding: str = 'euc-kr') -> str:
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=30) as r:
@@ -106,108 +150,167 @@ def parse_date_range(text: str) -> tuple[date, date] | None:
return date(y, m1, d1), date(y, m2, d2)
def parse_single_date(text: str) -> date | None:
m = re.match(r'(\d{4})\.(\d{2})\.(\d{2})$', text.strip())
if not m:
return None
y, mm, dd = map(int, m.groups())
return date(y, mm, dd)
def extract_next_data_json(html: str) -> dict:
m = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.S)
if not m:
return {}
try:
return json.loads(m.group(1))
except Exception:
return {}
def extract_naver_ipo_detail(detail_url: str) -> dict:
html = fetch(detail_url, encoding='utf-8')
data = extract_next_data_json(html)
queries = data.get('props', {}).get('pageProps', {}).get('dehydratedState', {}).get('queries', [])
for q in queries:
result = q.get('state', {}).get('data', {}).get('result', {})
ipo_info = result.get('ipoInfo')
if ipo_info:
return ipo_info
return {}
def extract_naver_ipo_entries() -> list[dict]:
html = fetch(NAVER_IPO_URL, encoding='auto')
entries = []
seen = set()
for m in re.finditer(r'<div class="item_area" id="([^"]+)">.*?<a href="(https://m\.stock\.naver\.com/ipo/[^"]+)"[^>]*>(.*?)</a>', html, re.S):
code = m.group(1).strip()
detail_url = m.group(2).strip()
name = clean_text(m.group(3))
if not code or code in seen or not name:
continue
seen.add(code)
entries.append({'code': code, 'name': name, 'detail_url': detail_url})
return entries
def extract_brokers_from_naver_detail(detail_url: str) -> tuple[dict, str]:
html = fetch(detail_url, encoding='utf-8')
data = extract_next_data_json(html)
queries = data.get('props', {}).get('pageProps', {}).get('dehydratedState', {}).get('queries', [])
ipo_info = {}
brokers = []
for q in queries:
result = q.get('state', {}).get('data', {}).get('result', {})
if isinstance(result, dict) and result.get('ipoInfo'):
ipo_info = result.get('ipoInfo') or {}
join_managers = result.get('joinManagers') or []
for item in join_managers:
def _naver_api_rows(payload: dict) -> list[dict]:
"""네이버 IPO progress API의 단계별 배열을 종목 단위로 합친다."""
list_keys = (
'examinationList', 'demandForecastingList', 'forecastingCompleteList',
'subscriptionList', 'subscriptionCompleteList', 'listingList',
)
if not isinstance(payload, dict) or not any(key in payload for key in list_keys):
raise ValueError('네이버 IPO API 스키마 불일치')
rows: dict[str, dict] = {}
for key in list_keys:
items = payload.get(key) or []
if not isinstance(items, list):
raise ValueError(f'네이버 IPO API {key} 타입 불일치')
for item in items:
if not isinstance(item, dict):
continue
name = (item.get('orgNm') or '').strip()
if name and name not in brokers:
brokers.append(name)
break
return ipo_info, ','.join(brokers)
code = (item.get('ipoCode') or item.get('itemCode') or '').strip()
name = (item.get('compName') or item.get('stockName') or '').strip()
if not code or not name:
continue
rows[code] = {**rows.get(code, {}), **item}
return list(rows.values())
def parse_subscription_events_from(cutoff: date) -> list[EventSpec]:
events = []
for entry in extract_naver_ipo_entries():
info, brokers = extract_brokers_from_naver_detail(entry['detail_url'])
def parse_naver_api_events_from(cutoff: date) -> list[EventSpec]:
payload = json.loads(fetch(NAVER_IPO_API_URL, encoding='utf-8'))
events: list[EventSpec] = []
for info in _naver_api_rows(payload):
code = info.get('ipoCode') or info.get('itemCode')
name = (info.get('compName') or info.get('stockName') or '').strip()
brokers = (info.get('orgNm') or '').strip()
source_url = f'https://stock.naver.com/ipo/{code}'
start_raw = (info.get('poStartDate') or '').strip()
end_raw = (info.get('poEndDate') or '').strip()
if not start_raw or not end_raw or '미정' in start_raw or '미정' in end_raw:
continue
if start_raw and end_raw:
try:
start_date = date.fromisoformat(start_raw)
end_date = date.fromisoformat(end_raw)
except ValueError:
continue
if end_date <= cutoff:
continue
events.append(EventSpec('subscription', info.get('compName') or entry['name'], start_date, end_date, brokers, entry['detail_url']))
pass
else:
if end_date >= cutoff:
events.append(EventSpec(
'subscription', name, start_date, end_date, brokers, source_url
))
listed_raw = (info.get('lcalDate') or info.get('listingDate') or '').strip()
if listed_raw:
try:
listed_date = date.fromisoformat(listed_raw)
except ValueError:
pass
else:
if listed_date >= cutoff:
events.append(EventSpec(
'listing', name, listed_date, listed_date, brokers, source_url
))
return events
def parse_listed_events_from(cutoff: date) -> list[EventSpec]:
events = []
for entry in extract_naver_ipo_entries():
info, brokers = extract_brokers_from_naver_detail(entry['detail_url'])
listed_raw = (info.get('lcalDate') or info.get('listingDate') or info.get('listDate') or '').strip()
if not listed_raw or '미정' in listed_raw:
def _table_by_summary(html: str, summary: str) -> str:
match = re.search(
r'<table[^>]*summary=["\']' + re.escape(summary) + r'["\'][^>]*>(.*?)</table>',
html,
re.S | re.I,
)
if not match:
raise ValueError(f'38커뮤니케이션 테이블 없음: {summary}')
return match.group(0)
def parse_38_subscription_html(html: str, cutoff: date) -> list[EventSpec]:
events: list[EventSpec] = []
for cols in parse_html_rows(_table_by_summary(html, '공모주 청약일정')):
if len(cols) < 6 or cols[0] == '종목명':
continue
parsed = parse_date_range(cols[1])
if not parsed:
continue
start_date, end_date = parsed
if end_date < cutoff:
continue
events.append(EventSpec(
'subscription', cols[0], start_date, end_date, cols[5], SUBSCRIPTION_URL,
source_label='38커뮤니케이션 IPO',
))
return events
def parse_38_listing_html(html: str, cutoff: date) -> list[EventSpec]:
events: list[EventSpec] = []
for cols in parse_html_rows(_table_by_summary(html, '신규상장종목')):
if len(cols) < 2 or cols[0] == '기업명':
continue
try:
d = date.fromisoformat(listed_raw)
listed_date = date.fromisoformat(cols[1].replace('/', '-'))
except ValueError:
continue
if d <= cutoff:
if listed_date < cutoff:
continue
events.append(EventSpec('listing', info.get('compName') or entry['name'], d, d, brokers, entry['detail_url']))
events.append(EventSpec(
'listing', cols[0], listed_date, listed_date, '', LISTING_URL,
source_label='38커뮤니케이션 IPO',
))
return events
def parse_38_events_from(cutoff: date) -> list[EventSpec]:
subscriptions = parse_38_subscription_html(fetch(SUBSCRIPTION_URL), cutoff)
listing_cutoff = cutoff - timedelta(days=LISTING_LOOKBACK_DAYS)
listings = parse_38_listing_html(fetch(LISTING_URL), listing_cutoff)
# 보조 소스는 스팩 종목명이 주 소스와 크게 달라 과거 건을 중복 생성할 수 있다.
listings = [
ev for ev in listings
if ev.start_date == cutoff or '스팩' not in ev.name
]
return subscriptions + listings
def collect_ipo_events(cutoff: date) -> tuple[list[EventSpec], str, list[str]]:
"""네이버 API를 우선 사용하고 실패·0건이면 38 파서로 자동 전환한다.
네이버 API는 상장 당일 장 시작 후 종목을 listingList에서 제거하므로, 주 파서가
정상이어도 최근 14일 신규상장을 38 목록으로 보강한다.
"""
errors: list[str] = []
try:
events = parse_naver_api_events_from(cutoff)
except Exception as e:
events = []
errors.append(f'naver_api: {e}')
parser_used = 'naver_api'
if not events:
if not errors:
errors.append('naver_api: 수집 결과 0건')
try:
events = parse_38_events_from(cutoff)
parser_used = '38_fallback'
except Exception as e:
errors.append(f'38_fallback: {e}')
raise IpoParserError(errors) from e
else:
try:
listing_cutoff = cutoff - timedelta(days=LISTING_LOOKBACK_DAYS)
recent_listings = parse_38_listing_html(fetch(LISTING_URL), listing_cutoff)
existing_keys = {ev.state_key for ev in events}
events.extend(
ev for ev in recent_listings
if ev.state_key not in existing_keys
and ev.start_date <= cutoff
and (ev.start_date == cutoff or '스팩' not in ev.name)
)
except Exception as e:
errors.append(f'38_same_day: {e}')
deduped = {ev.state_key: ev for ev in events}
return list(deduped.values()), parser_used, errors
def load_state() -> dict:
if STATE_FILE.exists():
try:
@@ -221,6 +324,59 @@ def save_state(state: dict):
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))
def update_parser_health(
state: dict,
status: str,
errors: list[str],
total_found: int,
dry_run: bool = False,
) -> None:
"""파서 상태가 바뀔 때만 알리고, 실패한 발송은 다음 실행에서 재시도한다."""
now_iso = datetime.now(KST).isoformat()
previous = state.get('parser_health_status')
state['parser_health_status'] = status
state['parser_health_checked_at'] = now_iso
state['parser_errors'] = errors
if status == 'healthy':
state['parser_last_success_at'] = now_iso
else:
state['parser_last_failure_at'] = now_iso
if dry_run:
return
message = None
if status == 'failed' and (
previous != status or state.get('parser_alerted_status') != status
):
message = (
'[IPO 일정] 파서 전체 실패\n'
+ '\n'.join(errors)
+ '\n캘린더 동기화를 중단했고 기존 일정은 유지했습니다.'
)
elif status == 'degraded' and (
previous != status or state.get('parser_alerted_status') != status
):
source = '38커뮤니케이션으로 자동 전환했습니다.'
if not any(error.startswith('naver_api:') for error in errors):
source = '네이버 주 파서는 정상이며 당일 상장 보강 파서만 실패했습니다.'
message = (
'[IPO 일정] 파서 일부 실패\n'
+ '\n'.join(errors)
+ f'\n{source}\n수집 결과: {total_found}건\n기존 일정 자동 삭제는 중지했습니다.'
)
elif status == 'healthy' and previous in {'degraded', 'failed'}:
message = (
'[IPO 일정] 파서 복구\n'
f'네이버 IPO API와 당일 상장 보강 파서가 정상입니다.\n수집 결과: {total_found}건'
)
if message and send_telegram(message):
state['parser_alerted_status'] = status
state['parser_alert_sent_at'] = now_iso
def event_key_from_summary(summary: str) -> tuple[str, str] | None:
summary = (summary or '').strip()
if summary.startswith('[공모청약] '):
@@ -314,13 +470,39 @@ def main():
args = parser.parse_args()
cutoff = datetime.now(KST).date()
events = parse_subscription_events_from(cutoff) + parse_listed_events_from(cutoff)
events.sort(key=lambda e: (e.start_date, e.kind, e.name))
state = load_state()
previous_parser_used = state.get('parser_used')
try:
events, parser_used, parser_errors = collect_ipo_events(cutoff)
except IpoParserError as e:
update_parser_health(state, 'failed', e.errors, 0, dry_run=args.dry_run)
state['last_run_at'] = datetime.now(KST).isoformat()
state['parser_used'] = 'failed'
if not args.dry_run:
save_state(state)
print(json.dumps({
'cutoff_after': cutoff.isoformat(),
'parser_used': 'failed',
'parser_errors': e.errors,
'total_found': 0,
'dry_run': args.dry_run,
}, ensure_ascii=False), file=sys.stderr)
return 1
events.sort(key=lambda e: (e.start_date, e.kind, e.name))
parser_health = 'degraded' if parser_errors else 'healthy'
update_parser_health(
state, parser_health, parser_errors, len(events), dry_run=args.dry_run
)
state['parser_used'] = parser_used
if not args.dry_run:
# 알림 상태를 먼저 저장해 이후 캘린더 작업 실패 시 같은 경보가 반복되지 않게 한다.
save_state(state)
end_date = max((ev.end_date for ev in events), default=cutoff) + timedelta(days=1)
# 사이트 스크래핑 실패 시 모든 일정이 삭제되는 사고 방지 — events 비면 cleanup 스킵
existing = {} if not events else fetch_existing_events(cutoff + timedelta(days=1), end_date)
listing_cutoff = cutoff - timedelta(days=LISTING_LOOKBACK_DAYS)
existing = {} if not events else fetch_existing_events(listing_cutoff, end_date)
created = []
updated = []
recreated = []
@@ -369,15 +551,21 @@ def main():
else:
unchanged += 1
# 사이트에서 사라진 일정은 캘린더에서도 삭제 (events 비면 위에서 existing이 빈 dict라 자동으로 스킵됨)
# 보조 소스는 종목명 표기가 다를 수 있어 failover 실행 중에는 stale 삭제를 하지 않는다.
# 장기간 장애 후 복구 첫 실행도 오탐 삭제를 막고, 주 파서가 2회 연속 정상일 때만 정리한다.
cleanup_allowed = parser_used == 'naver_api' and previous_parser_used == 'naver_api'
if cleanup_allowed:
for key, existing_list in existing.items():
if key in event_keys:
continue
kind, name = key.split('|', 1)
for stale in existing_list:
old_start, old_end = event_date_range(stale)
# 소급 조회는 누락 상장 보강용이다. 이미 지난 일정은 삭제하지 않는다.
if old_end and date.fromisoformat(old_end) < cutoff:
continue
delete_event(stale['id'], dry_run=args.dry_run)
deleted.append(key)
old_start, old_end = event_date_range(stale)
old_date = old_start if old_start == old_end or not old_end else f'{old_start}~{old_end}'
changes.append({'kind': kind, 'name': name, 'action': 'deleted', 'event_id': stale['id'], 'old_date': old_date})
@@ -386,8 +574,9 @@ def main():
if not args.dry_run:
save_state(state)
print(json.dumps({'cutoff_after': cutoff.isoformat(), 'total_found': len(events), 'newly_created': len(created), 'updated': len(updated), 'recreated': len(recreated), 'deleted': len(deleted), 'duplicates_removed': len(duplicates_removed), 'unchanged': unchanged, 'dry_run': args.dry_run, 'changes': changes}, ensure_ascii=False))
print(json.dumps({'cutoff_after': cutoff.isoformat(), 'parser_used': parser_used, 'parser_errors': parser_errors, 'total_found': len(events), 'newly_created': len(created), 'updated': len(updated), 'recreated': len(recreated), 'deleted': len(deleted), 'duplicates_removed': len(duplicates_removed), 'unchanged': unchanged, 'dry_run': args.dry_run, 'changes': changes}, ensure_ascii=False))
return 0
if __name__ == '__main__':
main()
raise SystemExit(main())
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""비하이브 '[🐝 공모청약]' 영상 → 종목별 청약 의견 요약 (자산웹 공모주 탭 표시용).
흐름: 채널 영상 목록(youtube_briefing_digest 재사용) → 제목 '공모청약' 필터 → 처리 안 한 영상만
자막 → LLM 이 '지금 진행·예정 공모주 목록' 중 영상이 다룬 종목별로 요약 → state 에 영상 단위 저장.
behive_web 은 state 파일만 읽는다(렌더 경로 LLM·네트워크 0).
⚠️ 영상 하나가 여러 종목을 다룬다(예: 멜콘 영상 = 1주차 3종목) → 제목 해시태그로 매칭하지 않고
후보 목록을 LLM 에 주고 ipoCode 로 돌려받는다. 목록에 없는 코드는 버린다.
⚠️ 자막은 업로드 직후 없을 수 있다 → 실패는 기록만 하고 다음 실행에서 재시도, MAX_ATTEMPTS 후 포기.
트리거: ipo_alert.py(stock.ipo-alert 08:30·10:00)가 끝에 run() 을 부른다. 새 트리거 없음.
CLI: python3 ipo_youtube_digest.py [run|show] [--dry-run]
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
sys.path.insert(0, '/Users/snowoyh/.openclaw/workspace/scripts')
import youtube_briefing_digest as ytd # noqa: E402
from ipo_calendar_sync import KST, NAVER_IPO_API_URL, _naver_api_rows, fetch # noqa: E402
STATE_FILE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace/state/ipo_youtube_summaries.json')
VIDEOS_URL = ytd.CHANNELS['behive']['videos_url']
TITLE_FILTER = '공모청약'
# launchd PATH 의 /opt/homebrew/bin/openclaw 는 node22 라 안 뜬다 — node24 래퍼를 절대경로로.
OPENCLAW_BIN = str(Path.home() / '.local/bin/openclaw')
LLM_MODEL = 'openai/gpt-5.6-sol'
LLM_TIMEOUT_SEC = 180
MAX_ATTEMPTS = 6 # 자막 미생성·LLM 실패 재시도 상한 (하루 2회 실행 → 약 3일)
KEEP_DAYS = 120 # 상장 끝난 지 오래된 영상 기록 정리
VERDICTS = {'positive': '청약 매력 높음', 'neutral': '중립·수요예측 확인', 'negative': '청약 부담'}
def load_state() -> dict:
try:
return json.loads(STATE_FILE.read_text())
except Exception:
return {'videos': {}}
def save_state(state: dict) -> None:
tmp = STATE_FILE.with_suffix('.json.tmp')
tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2))
tmp.replace(STATE_FILE)
def _candidates(today: date) -> list[dict]:
"""요약 대상 후보 = 아직 상장 안 했거나 상장 2주 이내인 공모주(청약 전·중·후 모두)."""
rows = _naver_api_rows(json.loads(fetch(NAVER_IPO_API_URL, encoding='utf-8')))
floor = (today - timedelta(days=14)).isoformat()
out = []
for r in rows:
code = (r.get('ipoCode') or r.get('itemCode') or '').strip()
name = (r.get('compName') or '').strip()
if not code or not name:
continue
if (r.get('lcalDate') or '9999') < floor:
continue
out.append({'ipo_code': code, 'name': name, 'po_start': r.get('poStartDate') or '',
'po_end': r.get('poEndDate') or '', 'lcal_date': r.get('lcalDate') or '',
'hope': f"{r.get('hopePubStart') or '?'}~{r.get('hopePubEnd') or '?'}",
'fix': r.get('fixPubPrice') or ''})
return out
def _call_llm(prompt: str) -> str:
cmd = [OPENCLAW_BIN, 'capability', 'model', 'run', '--prompt', prompt, '--model', LLM_MODEL, '--json']
p = subprocess.run(cmd, capture_output=True, text=True, timeout=LLM_TIMEOUT_SEC)
if p.returncode != 0:
raise RuntimeError(f'LLM rc={p.returncode}: {p.stderr[-300:]}')
data = json.loads(p.stdout[p.stdout.index('{'):])
text = ((data.get('outputs') or [{}])[0].get('text') or '').strip()
if not data.get('ok') or not text:
raise RuntimeError(f'LLM 응답 비정상: {str(data)[:300]}')
return text
def _prompt(title: str, transcript: str, cands: list[dict]) -> str:
cand_lines = '\n'.join(
f'- {c["ipo_code"]} {c["name"]} (청약 {c["po_start"]}~{c["po_end"]}, 희망공모가 {c["hope"]}원'
+ (f', 확정공모가 {c["fix"]}원' if c['fix'] else '') + ')' for c in cands)
return f"""아래는 유튜브 채널 '비하이브 투자자문'의 공모주 청약 영상 자막(자동생성, 오타 많음)이다.
영상이 **구체적으로 분석한** 종목만 골라 종목별로 요약하라. 일정만 언급하고 지나간 종목은 제외.
종목은 반드시 아래 후보 목록의 코드로 지정한다(자막의 종목명 오타는 후보명으로 맞춰 판단). 후보에 없으면 제외.
[후보]
{cand_lines}
[영상 제목] {title}
[자막]
{transcript}
출력은 JSON 배열 하나만(설명·코드펜스 금지):
[{{"ipo_code":"A123456","verdict":"positive|neutral|negative","verdict_text":"영상 결론 한 구절(15자 이내)","points":["핵심 3~4개, 각 40자 이내"]}}]
- verdict: 영상 화자의 청약 의견. 긍정=positive, 유보·수요예측 보고 판단=neutral, 부정·부담=negative
- 자막 숫자는 받아쓰기 오류가 잦다(예: 74,000→7,400). 공모가는 후보 정보가 정답이니 그 값을 쓰고, 다른 수치도 앞뒤가 안 맞으면 빼라
- points: 사업 한 줄 / 실적 추이(수치) / 유통가능물량(%) / 공모가·밸류 부담 등 화자가 짚은 장단점. 영상에 없는 내용 지어내지 말 것
- 해당 종목이 없으면 []"""
def _parse(text: str, cands: list[dict]) -> list[dict]:
m = re.search(r'\[.*\]', text, re.S)
arr = json.loads(m.group(0)) if m else []
names = {c['ipo_code']: c['name'] for c in cands}
out = []
for it in arr:
code = str(it.get('ipo_code') or '').strip()
if code not in names:
continue
verdict = it.get('verdict') if it.get('verdict') in VERDICTS else 'neutral'
points = [str(p).strip() for p in (it.get('points') or []) if str(p).strip()][:4]
out.append({'ipo_code': code, 'name': names[code], 'verdict': verdict,
'verdict_text': str(it.get('verdict_text') or VERDICTS[verdict]).strip()[:30],
'points': points})
return out
def run(dry_run: bool = False) -> dict:
"""새 공모청약 영상만 요약. 반환: {new, summarized, failed}."""
now = datetime.now(KST)
state = load_state()
videos = state.setdefault('videos', {})
listing = [v for v in ytd.fetch_channel_videos(VIDEOS_URL) if TITLE_FILTER in v.get('title', '')]
todo = [v for v in listing
if not videos.get(v['video_id'], {}).get('done')
and videos.get(v['video_id'], {}).get('attempts', 0) < MAX_ATTEMPTS]
result = {'new': len(todo), 'summarized': 0, 'failed': 0}
if not todo:
return result
cands = _candidates(now.date())
for v in todo:
vid = v['video_id']
rec = videos.setdefault(vid, {'title': v['title'], 'url': ytd.WATCH_URL.format(vid=vid), 'attempts': 0})
try:
transcript, status = ytd.fetch_transcript(vid)
if status != 'ok':
raise RuntimeError(f'자막 {status}')
stocks = _parse(_call_llm(_prompt(v['title'], transcript, cands)), cands)
except Exception as e:
rec['attempts'] = rec.get('attempts', 0) + 1
rec['last_error'] = f'{type(e).__name__}: {e}'[:300]
result['failed'] += 1
print(f'ipo yt {vid} 실패({rec["attempts"]}): {rec["last_error"]}', file=sys.stderr)
continue
published = ytd.get_publish_kst(vid)
rec.update({'done': True, 'stocks': stocks, 'summarized_at': now.isoformat(timespec='seconds'),
'published_kst': published.isoformat() if published else None})
rec.pop('last_error', None)
result['summarized'] += 1
if dry_run:
print(json.dumps({vid: rec}, ensure_ascii=False, indent=2))
# 오래된 기록 정리 — 목록에서 빠졌고 요약 시각이 KEEP_DAYS 넘은 것
cutoff = (now - timedelta(days=KEEP_DAYS)).isoformat()
for vid in [k for k, r in videos.items() if (r.get('summarized_at') or now.isoformat()) < cutoff]:
videos.pop(vid)
if not dry_run:
save_state(state)
return result
def summaries_by_code() -> dict[str, list[dict]]:
"""behive_web 용: ipoCode → [{video_id,title,url,published_kst,verdict,verdict_text,points,tagged}].
제목 해시태그에 그 종목이 있는 전용 영상을 앞에, 그다음 최신순."""
out: dict[str, list[dict]] = {}
for vid, rec in load_state().get('videos', {}).items():
for s in rec.get('stocks') or []:
out.setdefault(s['ipo_code'], []).append({
'video_id': vid, 'title': rec.get('title', ''), 'url': rec.get('url', ''),
'published_kst': rec.get('published_kst') or rec.get('summarized_at') or '',
'verdict': s['verdict'], 'verdict_text': s['verdict_text'], 'points': s['points'],
'tagged': f'#{s["name"]}' in rec.get('title', ''),
})
for lst in out.values():
lst.sort(key=lambda x: x['published_kst'], reverse=True)
lst.sort(key=lambda x: not x['tagged'])
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument('cmd', nargs='?', default='run', choices=('run', 'show'))
ap.add_argument('--dry-run', action='store_true')
args = ap.parse_args()
if args.cmd == 'show':
print(json.dumps(summaries_by_code(), ensure_ascii=False, indent=2))
return 0
print(json.dumps(run(args.dry_run), ensure_ascii=False))
return 0
if __name__ == '__main__':
raise SystemExit(main())
+178 -12
View File
@@ -13,9 +13,12 @@
from __future__ import annotations
import json
import http.client
import socket
import sys
import threading
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path
@@ -36,6 +39,7 @@ ENDPOINT_RKINFO = '/api/dostk/rkinfo' # 순위정보 — ka10023 거래량급
TR_DEPOSIT = 'kt00001' # 예수금상세현황요청
TR_ACNT_EVAL = 'kt00004' # 계좌평가현황요청 (acnt_nm·brch_nm 포함)
TR_TRUST_TRADE = 'kt00015' # 위탁종합거래내역요청 (tp='1' 입출금 — 당일 추가입금/인출 추출용)
TR_ACNT_RETURN = 'kt00016' # 일별계좌수익률상세현황요청 (기간내총입금·총출금 — 투자원금 산출용)
TR_POSITIONS = 'kt00018' # 계좌평가잔고내역요청 (종목별 상세, 수수료·세금 포함)
TR_TRADE_DIARY = 'ka10170' # 당일매매일지요청 (round-trip·풀매도 포함, 잔고에 없는 거래도 잡힘)
TR_ORDER_EXEC = 'kt00007' # 계좌별주문체결내역상세 (주문번호·시각·체결가·체결수량 단위, REST/영웅문 출처 구분)
@@ -48,6 +52,8 @@ TR_MINUTE_CHART = 'ka10080' # 주식분봉차트조회 (1/3/5/10/15/30/45/60분
TR_INVESTOR_FLOW = 'ka10059' # 종목별투자자기관별 (외국인·기관 일별 매매)
STOCK_CODES_CACHE = STATE_DIR / 'stock_codes.json'
_HTTP_TRANSIENT_RETRIES = 2
_HTTP_TRANSIENT_BACKOFF_SEC = 0.8
def load_credentials() -> dict:
@@ -86,6 +92,15 @@ def _http_post_full(url: str, body: dict, headers: dict | None = None) -> tuple[
if headers:
h.update(headers)
req = urllib.request.Request(url, data=data, method='POST', headers=h)
transient_errors = (
ConnectionResetError,
TimeoutError,
socket.timeout,
urllib.error.URLError,
http.client.RemoteDisconnected,
http.client.IncompleteRead,
)
for attempt in range(_HTTP_TRANSIENT_RETRIES + 1):
try:
with urllib.request.urlopen(req, timeout=15) as r:
resp_headers = {k.lower(): v for k, v in r.headers.items()}
@@ -93,6 +108,12 @@ def _http_post_full(url: str, body: dict, headers: dict | None = None) -> tuple[
except urllib.error.HTTPError as e:
body_text = e.read().decode('utf-8', 'ignore')
raise RuntimeError(f'HTTP {e.code} {url}: {body_text}') from e
except transient_errors:
if attempt >= _HTTP_TRANSIENT_RETRIES:
raise
time.sleep(_HTTP_TRANSIENT_BACKOFF_SEC * (attempt + 1))
raise RuntimeError(f'HTTP POST failed unexpectedly: {url}')
def _token_cache_path(label: str) -> Path:
@@ -411,25 +432,39 @@ def get_trade_journal_all(base_dt: str | None = None, labels: list[str] | None =
return {a['label']: get_trade_journal(a['label'], base_dt) for a in accs}
def get_cash_flow(account_label: str, base_dt: str | None = None) -> list[dict]:
"""당일 입출금 내역 (kt00015 tp='1').
def get_cash_flow(account_label: str, base_dt: str | None = None,
strt_dt: str | None = None, end_dt: str | None = None) -> list[dict]:
"""입출금 내역 (kt00015 tp='1').
base_dt: YYYYMMDD (None이면 KST 오늘). 시작/종료 동일 → 그날치만.
strt_dt/end_dt: 둘 다 주면 기간 조회 (base_dt 무시).
⚠️ **조회기간 1년 제한** — 넘기면 `[2000](501058:조회기간은 1년 이내로만 가능합니다.)`.
다년치는 연도별로 쪼개 호출한다 (investment_principal.py 참조).
tp='1'(입출금)·gds_tp='0'(전체)로 매매·환전 등은 제외. KRW만 필터.
⚠️ tp='1' 입금에는 **배당금·예탁금이용료(이자)·수익분배금·공모주환불금·단주매각대금**이
섞여 들어온다. 이건 '내가 넣은 돈'이 아니므로 투자원금 합산에 그대로 쓰면 과대계상된다.
kt00016의 기간내총입금/총출금은 이걸 이미 제외한 순수 외부 이체액이다 (전 계좌·연도 실증).
적요 기준 분류는 `investment_principal.is_principal_flow`.
반환 행:
- date: 'YYYYMMDD' (trde_dt)
- time: 'HH:MM:SS' (proc_tm 앞 8자리)
- io_tp: 'IN' | 'OUT'
- amount: 거래금액 (원, 양수)
- signed: +amount (입금) / -amount (출금) — 합산으로 net cash flow 산출
- rmrk: 적요명 (예: '타사대체입금', '은행이체출금')
- rmrk: 적요명 (예: '이체입금(지급결제)', '배당금입금', '이체출금')
- balance: 거래 후 예수금잔고 (entra_remn)
"""
if strt_dt and end_dt:
fr, to = strt_dt, end_dt
else:
if base_dt is None:
base_dt = datetime.now(KST).strftime('%Y%m%d')
fr = to = base_dt
body = {
'strt_dt': base_dt,
'end_dt': base_dt,
'strt_dt': fr,
'end_dt': to,
'tp': '1',
'stk_cd': '',
'crnc_cd': '',
@@ -458,6 +493,7 @@ def get_cash_flow(account_label: str, base_dt: str | None = None) -> list[dict]:
continue
proc_tm = (it.get('proc_tm') or '').strip()
out.append({
'date': (it.get('trde_dt') or '').strip(),
'time': proc_tm[:8],
'io_tp': io_tp,
'amount': amount,
@@ -477,6 +513,38 @@ def get_cash_flow_all(base_dt: str | None = None, labels: list[str] | None = Non
return {a['label']: get_cash_flow(a['label'], base_dt) for a in accs}
def get_period_return(account_label: str, fr_dt: str, to_dt: str) -> dict:
"""기간 계좌수익률 상세 (kt00016) — 기간내 총입금·총출금 포함.
⚠️ **조회기간 1년 제한** — 넘기면 `[2000](505221:조회기간은 1년이내로만 가능합니다.)`.
전체기간 투자원금은 연도별 창으로 쪼개 누적해야 한다 (investment_principal.py 참조).
⚠️ 응답의 `invt_bsamt`는 "투자원금**평잔**"(기간 평균잔고)이라 누적 투자원금이 아니다 —
이름이 비슷해 오용하기 쉬우니 반환하지 않는다. 누적 원금은 cash_in − cash_out.
계좌 개설 전 기간은 에러 없이 전 필드 0으로 응답한다.
항등식 `net_fr + cash_in − cash_out + eval_pl == net_to` 가 성립 (자체 검증용).
반환:
- cash_in/cash_out: 기간내 총입금/총출금 (원)
- qty_in/qty_out: 기간내 총입고/총출고 (주식 대체 — 0이 아니면 현금 기준 원금이 과소계상)
- net_fr/net_to: 순자산액계 기초/기말
- eval_pl: 평가손익
"""
resp = _call(account_label, TR_ACNT_RETURN, {'fr_dt': fr_dt, 'to_dt': to_dt})
return {
'label': account_label,
'cash_in': _to_int(resp.get('termin_tot_trns')), # 기간내총입금
'cash_out': _to_int(resp.get('termin_tot_pymn')), # 기간내총출금
'qty_in': _to_int(resp.get('termin_tot_inq')), # 기간내총입고
'qty_out': _to_int(resp.get('termin_tot_outq')), # 기간내총출고
'net_fr': _to_int(resp.get('tot_amt_fr')), # 순자산액계_초
'net_to': _to_int(resp.get('tot_amt_to')), # 순자산액계_말
'eval_pl': _to_int(resp.get('evltv_prft')), # 평가손익
'raw': resp,
}
def get_order_executions(account_label: str, base_dt: str | None = None) -> list[dict]:
"""계좌별 주문체결내역 (kt00007) — 주문번호·시각 단위 행 리스트.
@@ -616,6 +684,11 @@ def get_open_orders(account_label: str, code: str = '',
'order_price': _to_int(it.get('ord_pric')),
'unfilled_qty': _to_int(it.get('oso_qty')),
'order_type': (it.get('trde_tp') or '').strip(),
# 스톱지정가(trde_tp=28) 주문의 조건단가. 일반 주문은 0.
# 실측 확인: ka10075 응답에 stop_pric 로 실려 온다 (2026-07-30).
'stop_price': _to_int(it.get('stop_pric')),
# 현재가 — 조건단가까지 얼마 남았는지 계산용. 부호 포함 문자열('-4805')이라 abs.
'cur_price': abs(_to_int(it.get('cur_prc'))),
'exchange': (it.get('stex_tp_txt') or '').strip(),
'exchange_code': ex_code,
'routing_suffix': _EXCHANGE_CODE_TO_SUFFIX.get(ex_code, ''),
@@ -723,11 +796,13 @@ def get_stock_quote(code: str, account_label: str | None = None, exchange: str =
}
WATCHLIST_BATCH_MAX = 40 # ka10095 한 콜 종목 수 한도 — 초과 시 "허용된 요청 개수 초과[1634]" (2026-06-17, universe 120개에서 발견)
def get_watchlist_quotes(codes: list[str], account_label: str | None = None,
exchange: str = 'AL') -> dict[str, dict]:
"""ka10095 관심종목정보요청 — 다종목 시세 batch (한 콜로 N개).
"""ka10095 관심종목정보요청 — 다종목 시세 batch. 종목 수가 한도(WATCHLIST_BATCH_MAX) 초과 시 자동 분할 호출.
19종목 ~0.05s. ka10001×N 대비 rate limit 영향 없음.
반환: {clean_code: {price, change, change_pct, name}}. 빈 codes면 빈 dict.
"""
clean_codes = [_clean_code(c) for c in codes if c]
@@ -735,8 +810,35 @@ def get_watchlist_quotes(codes: list[str], account_label: str | None = None,
return {}
label = account_label or _default_account_label()
suffix = f'_{exchange}' if exchange in ('NX', 'AL') else ''
stk_cd_param = '|'.join(f'{c}{suffix}' for c in clean_codes)
resp = _call(label, TR_WATCHLIST_INFO, {'stk_cd': stk_cd_param}, endpoint=ENDPOINT_STKINFO)
out: dict[str, dict] = {}
for i in range(0, len(clean_codes), WATCHLIST_BATCH_MAX):
chunk = clean_codes[i:i + WATCHLIST_BATCH_MAX]
stk_cd_param = '|'.join(f'{c}{suffix}' for c in chunk)
out.update(_parse_watchlist_rows(_watchlist_batch_call(label, stk_cd_param)))
return out
def _watchlist_batch_call(label: str, stk_cd_param: str) -> dict:
"""ka10095 1콜 + 429 재시도.
behive_web 은 패널 렌더 1회에 이 TR 을 최대 7콜(보유 KRX/NX/AL · 워치리스트 · NXT 마크
판정 · phantom · 미보유 미체결) 병렬 발사하는데 키움 유량은 초당 5건이라 일부가 429 로
튕긴다. ⚠️ 튕긴 콜은 예외가 호출측에서 로그만 남고 삼켜져 **그 조각이 화면에서 조용히
빈다** — 실측 `NXT positions price fetch failed` 25건, 그때마다 보유종목 NXT 컬럼이
통째로 사라진다(NXT 실거래가 있는 삼성전자·SK하이닉스만 눈에 띈다).
⚠️ ka10001·ka10004 처럼 `_QUOTE_GATE` 로 직렬화하면 7콜이 줄서서 렌더가 1초 이상
느려지므로, 정상 경로는 병렬 그대로 두고 튕긴 콜만 백오프 후 1회 다시 친다.
"""
try:
return _call(label, TR_WATCHLIST_INFO, {'stk_cd': stk_cd_param}, endpoint=ENDPOINT_STKINFO)
except RuntimeError as e:
if not _is_quote_rate_limit(str(e)):
raise
time.sleep(_QUOTE_BACKOFF_SEC)
return _call(label, TR_WATCHLIST_INFO, {'stk_cd': stk_cd_param}, endpoint=ENDPOINT_STKINFO)
def _parse_watchlist_rows(resp: dict) -> dict[str, dict]:
out: dict[str, dict] = {}
for row in resp.get('atn_stk_infr') or []:
raw_code = (row.get('stk_cd') or '').strip()
@@ -755,11 +857,16 @@ def get_watchlist_quotes(codes: list[str], account_label: str | None = None,
'high': abs(_to_int(row.get('high_pric') or '0')),
'low': abs(_to_int(row.get('low_pric') or '0')),
'volume': _to_int(row.get('trde_qty') or '0'),
# 거래소가 계산한 가격제한폭. 기준가(전일종가)로 정해져 장중 불변.
# 제한폭은 기준가의 호가단위로 절사한 뒤 ±하는 규칙이라 직접 계산하면
# ETF·가격대별 호가단위 표를 재현해야 해 틀릴 여지가 있다 → 응답값 그대로 쓴다.
'upper_limit': abs(_to_int(row.get('upl_pric') or '0')),
'lower_limit': abs(_to_int(row.get('lst_pric') or '0')),
}
return out
def get_quote_book(code: str, account_label: str | None = None) -> dict:
def get_quote_book(code: str, account_label: str | None = None) -> dict: # noqa: E302
"""주식호가 조회 (ka10004) — 10단계 매도/매수 호가 + 잔량.
매매 아님 — 조회 전용. 호가창 표시·체결 가능성 판단에 사용.
@@ -949,18 +1056,21 @@ def get_daily_candles(code: str, count: int = 250, base_dt: str | None = None,
def get_minute_candles(code: str, tic_scope: int = 5, today_only: bool = True,
account_label: str | None = None) -> list[dict]:
account_label: str | None = None, exchange: str = 'KRX') -> list[dict]:
"""주식 분봉 차트 (ka10080). 수정주가 기준.
tic_scope: 1/3/5/10/15/30/45/60 분. 한 호출에 ~900봉 (5분봉 ≈ 11일치).
today_only: True 면 오늘 봉만 추출.
exchange: 'KRX'=KRX 단독(기본) / 'NX'=NXT 단독 / 'AL'=통합(KRX→NXT 연속).
NX/AL은 stk_cd에 suffix를 붙여 NXT 시간대(15:30~20:00)에도 분봉이 이어진다.
반환: 시간 오름차순 list of dict — {date, time, open, high, low, close, volume}
date='YYYYMMDD', time='HHMMSS'.
"""
label = account_label or _default_account_label()
suffix = f'_{exchange}' if exchange in ('NX', 'AL') else ''
body = {
'stk_cd': _clean_code(code),
'stk_cd': _clean_code(code) + suffix,
'tic_scope': str(tic_scope),
'upd_stkpc_tp': '1',
}
@@ -1103,6 +1213,12 @@ def refresh_stock_codes(account_label: str | None = None) -> dict:
- nxt_enable: bool — ka10099 응답 nxtEnable=='Y' 변환. NXT 가드 정확화에 사용
- state: str — 종목상태 (예: '관리종목', '정리매매', '증거금100%')
- order_warning: str — 투자유의 (0/1/2/3/4/5, ka10099 명세 참조)
- etf_etn: bool — ETF/ETN 여부 (2026-09-21 추가). **KRX 애프터마켓(16:00~20:00)에서
ETF/ETN 은 거래되지 않는다**(2026-09-21 실측: TIGER 200 거래량·호가가 15:35 이후 완전
고정). 자산웹이 애프터마켓에 이들만 잠그려면 판별 수단이 필요한데, KOSPI/KOSDAQ 조회
응답에는 ETF 구분이 없다. 그래서 ETF/ETN 전용 시장을 따로 조회해 코드로 표시한다.
⚠️ 종목명 프리픽스(KODEX/TIGER…) 추측을 쓰지 않는 이유 — 신규 운용사 브랜드가 생기면
조용히 틀리고, 매매 게이트라 정확도가 우선이다.
"""
label = account_label or _default_account_label()
url = base_url() + ENDPOINT_STKINFO
@@ -1136,6 +1252,40 @@ def refresh_stock_codes(account_label: str | None = None) -> dict:
time.sleep(0.15)
else:
break
# ETF/ETN 표시 — 전용 시장을 훑어 코드 집합을 만들고 위에서 담은 항목에 플래그만 얹는다.
# ETF/ETN 은 KOSPI/KOSDAQ 조회에도 이미 들어 있으므로 **새 항목을 추가하지 않는다**.
# ⚠️ 여기서 실패해도 캐시 저장은 계속한다 — 플래그 하나 때문에 4300종목 캐시를
# 통째로 잃으면 종목명 검색·NXT 판정이 다 멈춘다. 플래그가 없으면 호출측이
# 보수적 기본값으로 떨어질 뿐이다.
# ⚠️ ka10099 유량은 초당 5건이고 위 페이지네이션이 이미 버스트를 쌓아놨다 — 0.15s 간격으로
# 이어 붙이면 마지막 시장이 429 로 튕긴다(2026-09-21 실측: mrkt_tp=90 8종목 누락).
# 빠진 시장의 ETF/ETN 은 플래그가 False 로 남아 애프터마켓에 열려버리므로 재시도한다.
etf_codes: set[str] = set()
for mrkt_tp in ('8', '60', '70', '90'): # 8:ETF, 60:ETN, 70:손실제한ETN, 90:변동성ETN
for attempt in range(2):
time.sleep(0.5 if attempt == 0 else 1.5)
try:
resp, _ = _http_post_full(url, {'mrkt_tp': mrkt_tp},
auth_headers(label, tr_id=TR_STOCK_LIST))
if resp.get('return_code', 0) != 0:
if _is_quote_rate_limit(str(resp.get('return_msg') or '')):
continue
break
for it in (resp.get('list') or []):
c = _clean_code(it.get('code', ''))
if c:
etf_codes.add(c)
break
except Exception as e:
if attempt == 0 and _is_quote_rate_limit(str(e)):
continue
print(f'[refresh_stock_codes] ETF/ETN 표시 실패 (mrkt_tp={mrkt_tp}): {e}',
file=sys.stderr)
break
for info in result.values():
info['etf_etn'] = info['code'] in etf_codes
STOCK_CODES_CACHE.parent.mkdir(parents=True, exist_ok=True)
STOCK_CODES_CACHE.write_text(json.dumps({
'refreshed_at': datetime.now(KST).isoformat(),
@@ -1272,6 +1422,22 @@ def main():
print(f"{info['name']} ({info['code']}) · {info.get('market','?')}")
return 0
if cmd == 'refresh-codes':
# --if-stale N: 캐시가 N일보다 최신이면 건너뛴다(정기 실행용).
# ⚠️ 이 캐시의 nxt_enable 은 자산웹의 'NXT 거래 종목' 판정 근거다(behive_web._is_nxt_traded).
# 낡으면 새로 NXT 편입된 종목을 놓쳐 🔥 배지·거래가능 초록점이 틀어진다(금액엔 영향 없음).
# lazy 갱신은 '모르는 종목명 검색' 때만 돌아, 알던 종목만 다루면 영영 안 바뀐다.
stale_days = 0.0
if '--if-stale' in sys.argv:
try:
stale_days = float(sys.argv[sys.argv.index('--if-stale') + 1])
except (IndexError, ValueError):
print('usage: refresh-codes [--if-stale <days>]', file=sys.stderr)
return 2
if stale_days > 0 and STOCK_CODES_CACHE.exists():
age_days = (time.time() - STOCK_CODES_CACHE.stat().st_mtime) / 86400
if age_days < stale_days:
print(f'종목코드 캐시 {age_days:.1f}일 — {stale_days:g}일 미만이라 갱신 생략')
return 0
stocks = refresh_stock_codes()
print(f"종목코드 캐시 갱신 완료: {len(stocks)}종목 → {STOCK_CODES_CACHE}")
return 0
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""자산웹 손익차트 과거 구간 채우기 — 키움 kt00002(일별추정예탁자산현황) → state/net_worth_history.json.
portfolio_daily_snapshot 은 2026-05-01 부터만 쓸 수 있는 형식이라(4월은 예수금 없는 옛 형식) 그 이전
차트가 비어 있다. 키움 계좌별 일별 추정예탁자산으로 그 앞 구간을 채운다. 조회 전용, 1회성 CLI.
- 조회기간 한 번에 6개월 미만(`501067`) → 170일 창으로 쪼갠다. 계좌당 3~4콜.
- kt00002 는 주말·휴장일도 행이 온다 → KOSPI 일별종가가 있는 날(sim index_history)만 남긴다.
market_holidays.json 은 2026년부터라 2025 휴장일을 못 거른다.
- 값 기준이 스냅샷과 다르다(KRX 종가 − 추정 매도비용 ≈0.18%, 스냅샷은 20:10 NXT 가격) →
겹치는 첫날(스냅샷 첫 날짜)의 비율로 owner 별 스케일을 맞춰 경계일 단차를 없앤다.
- 입출금은 investment_principal 캐시(kt00015, 키움 콜 0)에서 — 스냅샷 cash_in 과 같은 의미
(배당·이자 포함 전체 입출금).
CLI: python3 net_worth_backfill.py [--from YYYY-MM-DD] [--dry-run]
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from datetime import date, datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo
sys.path.insert(0, str(Path(__file__).resolve().parent))
import investment_principal as ip # noqa: E402
import kiwoom_client as kc # noqa: E402
KST = ZoneInfo('Asia/Seoul')
STATE = Path(__file__).resolve().parent.parent / 'state'
SNAPSHOT_FILE = STATE / 'portfolio_daily_snapshot.json'
INDEX_FILE = STATE / 'sim' / 'index_history.json'
OUT_FILE = STATE / 'net_worth_history.json'
OWNERS = {'본인': ['일반', 'ISA'], '가희': ['가희_일반', '가희_ISA']}
WINDOW_DAYS = 170
def _first_snapshot_date(snap: dict, owner: str) -> str | None:
"""스냅샷에서 차트가 쓰는 형식(holdings+deposit)이 시작되는 날짜."""
return next((d for d in sorted(snap) if 'holdings' in ((snap.get(d) or {}).get(owner) or {})), None)
def _fetch_account(label: str, start: date, end: date) -> dict[str, int]:
"""{YYYY-MM-DD: 추정예탁자산}. 계좌 개설 전 구간은 '내역 없음' 에러라 빈 창으로 친다."""
out: dict[str, int] = {}
cur = start
while cur <= end:
w_end = min(cur + timedelta(days=WINDOW_DAYS), end)
try:
r = kc._call_paginated(label, 'kt00002',
{'start_dt': cur.strftime('%Y%m%d'), 'end_dt': w_end.strftime('%Y%m%d')},
'daly_prsm_dpst_aset_amt_prst')
for x in r.get('daly_prsm_dpst_aset_amt_prst') or []:
v = kc._to_int(x.get('prsm_dpst_aset_amt'))
if v:
d = x['dt']
out[f'{d[:4]}-{d[4:6]}-{d[6:]}'] = v
except RuntimeError as e:
if '506476' not in str(e): # 506476 = 내역 없음(개설 전)
raise
time.sleep(0.4)
cur = w_end + timedelta(days=1)
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument('--from', dest='frm', default='2025-01-01')
ap.add_argument('--dry-run', action='store_true')
a = ap.parse_args()
snap = json.loads(SNAPSHOT_FILE.read_text())
idx = json.loads(INDEX_FILE.read_text()).get('KOSPI') or {}
trading = {f'{d[:4]}-{d[4:6]}-{d[6:]}' for d in idx}
start = date.fromisoformat(a.frm)
if f'{start:%Y-%m-%d}' < min(trading):
print(f'⚠️ 거래일 달력(KOSPI)이 {min(trading)} 부터라 그 이전은 거를 수 없다', file=sys.stderr)
return 1
days: dict[str, dict] = {}
scale: dict[str, float] = {}
for owner, labels in OWNERS.items():
first = _first_snapshot_date(snap, owner)
if not first:
print(f'{owner}: 스냅샷 없음 — 건너뜀', file=sys.stderr)
continue
end = date.fromisoformat(first) # 겹치는 첫날까지 받아 스케일 기준으로 쓴다
per_acct = {lb: _fetch_account(lb, start, end) for lb in labels}
# 기준일(스냅샷 첫날)은 휴장일일 수 있어(2026-05-01 근로자의 날) 거래일 필터는 저장 행에만 건다.
total = {d: sum(per_acct[lb].get(d, 0) for lb in labels) for d in set().union(*per_acct.values())}
o_snap = snap[first][owner]
snap_net = sum(int(h.get('eval_value', 0) or 0) for h in o_snap['holdings'].values()) + int(o_snap.get('deposit', 0) or 0)
if not total.get(first):
print(f'{owner}: 기준일 {first} kt00002 값 없음 — 중단', file=sys.stderr)
return 1
scale[owner] = snap_net / total[first]
flows: dict[str, dict] = {}
for f in ip.get_flows(labels)['rows']:
d = f'{f["date"][:4]}-{f["date"][4:6]}-{f["date"][6:]}'
k = 'cash_in' if f['io'] == 'IN' else 'cash_out'
flows.setdefault(d, {'cash_in': 0, 'cash_out': 0})[k] += int(f['amount'])
n = 0
for d in sorted(total):
if d >= first or d not in trading:
continue
rec = {'net': round(total[d] * scale[owner])}
rec.update(flows.get(d) or {})
days.setdefault(d, {})[owner] = rec
n += 1
print(f'{owner}: {n}일 ({min(days)} ~ {first} 직전), 스케일 {scale[owner]:.5f} (기준 {first} 스냅샷 {snap_net:,} / 키움 {total[first]:,})')
doc = {'source': 'kiwoom kt00002', 'fetched_at': datetime.now(KST).isoformat(timespec='seconds'),
'scale': scale, 'days': dict(sorted(days.items()))}
if a.dry_run:
print('dry-run — 저장 안 함')
return 0
tmp = OUT_FILE.with_suffix('.tmp')
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=1))
os.replace(tmp, OUT_FILE)
print(f'저장: {OUT_FILE} ({len(days)}일)')
return 0
if __name__ == '__main__':
sys.exit(main())
@@ -50,6 +50,11 @@ def _pct(p: float, digits: int = 2) -> str:
return f'{sign}{p:.{digits}f}%'
def _num(p) -> str:
"""계단 비율 표기 — 10.0 은 '10', 10.5 는 '10.5' 로. 불필요한 .0 을 지운다."""
return f'{float(p or 0):g}'
def format_card(request: dict, market_data: dict, card_id: str,
estimate: Optional[dict] = None,
budget_conversion: Optional[dict] = None,
@@ -66,6 +71,12 @@ def format_card(request: dict, market_data: dict, card_id: str,
type_marker = f' {cfg["warning_emoji"]} 시장가'
elif request['order_type'] == 'AGGRESSIVE_LIMIT':
type_marker = f' {cfg["warning_emoji"]} 공격적 지정가'
elif request['order_type'] == 'STOP_LIMIT':
type_marker = f' {cfg["warning_emoji"]} 스톱지정가(예약)'
elif request['order_type'] == 'TRAILING_STOP':
type_marker = f' {cfg["warning_emoji"]} 트레일링 스톱(예약)'
elif request['order_type'] == 'TRAILING_ARM':
type_marker = f' {cfg["warning_emoji"]} 예약 트레일링(발동 대기)'
amend_marker = ' ✏️ 수정됨' if amended else ''
lines = [f'{side_emoji} {_md_bold(side_word + " 미리보기")} [#{card_id}]{type_marker}{spouse_marker}{amend_marker}', '']
@@ -83,6 +94,66 @@ def format_card(request: dict, market_data: dict, card_id: str,
ratio = (request['price'] - market_data['prev_close']) / market_data['prev_close'] * 100
price_part += f' (전일종가 {_pct(ratio)})'
lines.append(f'{marker} 가격: {price_part}')
elif request['order_type'] == 'STOP_LIMIT':
stop_p = request.get('stop_price') or 0
cur = market_data.get('current_price')
cur_part = f' (현재가 {_money(cur)})' if cur else ''
lines.append(f'{marker} 조건: {_md_bold(_money(stop_p) + " 도달 시")}{cur_part}')
lines.append(f'{marker} 매도가격: {_md_bold(_money(request["price"]))} (지정가)')
elif request['order_type'] in ('TRAILING_STOP', 'TRAILING_ARM'):
# 승인 대상은 "계단 정의" — 각 계단의 조건단가는 고점이 오르면 제 하락률을 유지하며
# 함께 따라 올라간다. 계단이 1개면 종전의 단일 트레일링과 같다.
is_arm = request['order_type'] == 'TRAILING_ARM'
cur = market_data.get('current_price')
cur_part = f' (현재가 {_money(cur)})' if cur else ''
# 초기 고점은 항상 등록 시점 현재가 — 과거 고점 기준 옵션은 2026-07-30 제거됨.
# 예약 트레일링은 그 자리에 발동가가 들어간다(계단 계산 기준도 발동가).
peak_v = request.get('trail_peak') or 0
steps = request.get('trail_steps') or []
if is_arm:
gap = ''
if cur:
gap = f' — 발동까지 {_pct((peak_v - cur) / cur * 100)}'
lines.append(f'{marker} 발동가: {_md_bold(_money(peak_v) + " 도달 시 시작")}{cur_part}{gap}')
# 대기 예약이 당일 유효로 바뀐 뒤로는 상한가 초과 발동가가 '오늘 도달 불가' 확정이다.
# 거부하지는 않는다(관리자님 지시) — 무의미하다는 사실만 승인 전에 알린다.
up_lim = market_data.get('upper_limit') or 0
if up_lim and peak_v > up_lim:
lines.append(f'{cfg["warning_emoji"]} 오늘 상한가 {_money(up_lim)} 초과 — '
f'오늘은 도달 불가, 장 마감에 만료됩니다')
else:
lines.append(f'{marker} 시작 고점: {_md_bold(_money(peak_v))} (등록 시점){cur_part}')
min_sell = request.get('min_sell_price')
if min_sell:
# 최저 매도가가 손절선을 결정했는지(=트레일보다 높았는지) 명시 — 둘 중 어느 게
# 지배했는지 안 보이면 관리자님이 값을 잘못 읽는다.
tied = sum(1 for s in steps if s.get('floor_applied'))
who = f'{tied}개 계단 적용' if tied else '미적용 (트레일이 더 높음)'
lines.append(f'{marker} 최저 매도가: {_md_bold(_money(min_sell))} ({who})')
lines.append('')
steps_head = f'{marker} 매도 계단 {len(steps)}단'
if is_arm:
steps_head += ' (발동가 기준 예상)'
lines.append(steps_head)
for s in steps:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
lines.append(f' {s["n"]}단계 −{_num(s["pct"])}% '
f'{_md_bold(_money(s["cond_uv"]) + " 도달")} → '
f'{s["qty"]:,}주 ({cum_part})')
rest = request['qty'] - sum(s['qty'] for s in steps)
if rest > 0:
lines.append(f' ↳ 나머지 {rest:,}주는 스톱 없음 (계속 보유)')
lines.append('')
lines.append(f'{marker} 고점이 오르면 계단이 통째로 따라 올라갑니다 (내려가지 않음)')
if is_arm:
# 이 기능의 유일한 위험(발동 시 PIN 재확인 없음)을 승인 화면에서 고지하는 자리다.
# 실제 손절선이 미리보기 이상인 이유: 발동 시점 현재가 ≥ 발동가.
lines.append(f'{marker} 지금은 주문이 나가지 않습니다 — '
f'발동가에 닿는 순간 PIN 재확인 없이 위 계단이 자동 발주됩니다')
lines.append(f'{marker} {_md_bold("오늘 장 마감까지만 유효")} — '
f'미발동이면 자동 취소됩니다 (스톱주문과 같은 당일 유효)')
lines.append(f'{marker} 실제 손절선은 발동 시점 현재가 기준이라 위 값 이상입니다')
elif request['order_type'] == 'MARKET':
lines.append(f'{marker} 가격: {_md_bold("시장가")}')
else:
@@ -215,6 +286,79 @@ def format_submitted(card_id: str, side: str, symbol_name: str, qty: int,
return f'📨 [#{card_id}] {side_word} 접수: {symbol_name} {qty:,}주 @ {price_str}\n주문번호: {ord_no}'
def format_trailing_submitted(info: dict) -> str:
"""트레일링 계단 접수 결과 — 몇 단이 걸렸고 감시가 붙었는지를 한 화면에 담는다.
계단 일부만 접수될 수 있어(레그 독립 제출) 성공·실패를 나란히 적는다.
실패분을 요약해 버리면 방어선이 몇 개인지 모르는 채로 남게 된다.
"""
placed = info.get('placed') or []
failed = info.get('failed') or []
acct = _account_display(info.get('account', ''))
head = '🧪 [모의] 트레일링' if info.get('dry_run') else '✅ 트레일링'
lines = [f'{head} {len(placed)}단 등록: {info["symbol_name"]}',
f'[{acct}] 총 {info["total_qty"]:,}주 · 시작 고점 {_money(info["peak"])}']
if info.get('min_sell_price'):
lines.append(f'최저 매도가 {_money(info["min_sell_price"])}')
for s in placed:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
no = f' · 주문 {s["ord_no"]}' if s.get('ord_no') else ''
lines.append(f'{s["n"]}단계 −{_num(s["pct"])}% {_money(s["cond_uv"])} 도달 → '
f'{s["qty"]:,}주 ({cum_part}){no}')
rest = info['total_qty'] - sum(s['qty'] for s in placed)
if rest > 0:
lines.append(f'↳ 나머지 {rest:,}주는 스톱 없음 (계속 보유)')
for s in failed:
lines.append(f'⚠️ {s["n"]}단계 −{_num(s["pct"])}% {s["qty"]:,}주 접수 실패 '
f'({s.get("reason")}) {s.get("error", "")}'.rstrip())
if info.get('register_error'):
lines.append('⚠️ 주문은 접수됐지만 트레일링 등록 실패 — 손절선이 따라 올라가지 않습니다. '
f'({info["register_error"]})')
elif info.get('res_id'):
lines.append(f'감시 시작 ({info["res_id"]}) — 고점이 오르면 계단이 함께 올라갑니다')
return '\n'.join(lines)
def format_arm_registered(info: dict) -> str:
"""예약 트레일링 등록 완료 — 아직 키움에 주문이 없다는 사실을 분명히 적는다."""
acct = _account_display(info.get('account', ''))
head = '🧪 [모의] 예약 트레일링' if info.get('dry_run') else '⏳ 예약 트레일링'
steps = info.get('steps') or []
lines = [f'{head} 등록: {info["symbol_name"]} ({info.get("symbol", "")})',
f'[{acct}] {info["qty"]:,}주 · 발동가 {_money(info["trigger_price"])} 도달 시 시작']
if info.get('cur_price'):
cur = info['cur_price']
gap = (info['trigger_price'] - cur) / cur * 100
lines.append(f'현재가 {_money(cur)} · 발동까지 {_pct(gap)}')
if info.get('min_sell_price'):
lines.append(f'최저 매도가 {_money(info["min_sell_price"])}')
step_parts = []
for s in steps:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_num(cum)}%'
step_parts.append(f'−{_num(s["pct"])}% {cum_part}')
lines.append(f'매도 계단 {len(steps)}단: ' + ' · '.join(step_parts))
if info.get('arm_id'):
lines.append(f'대기 시작 ({info["arm_id"]}) — **오늘 장 마감까지** 유효')
lines.append('지금은 키움에 주문이 없습니다. 발동가에 닿으면 PIN 재확인 없이 자동 발주됩니다.')
lines.append('미발동으로 장이 끝나면 자동 취소됩니다 (스톱주문과 같은 당일 유효).')
return '\n'.join(lines)
def format_arm_fired(info: dict) -> str:
"""예약 트레일링 발동 — 계단 접수 결과는 format_trailing_submitted 와 같은 모양이라 재사용하고,
발동 경위(발동가·실제 발동가·수량 축소)만 앞뒤에 덧붙인다."""
head = [f'🎯 예약 트레일링 발동 — {info["symbol_name"]} ({info.get("symbol", "")})',
f'발동가 {_money(info["trigger_price"])} 도달 (현재가 {_money(info["peak"])})']
if info.get('qty_reduced'):
head.append(f'⚠️ 매도가능 {info["total_qty"]:,}주 — '
f'예약 {info["arm_qty"]:,}주에서 줄여 재배분했습니다')
body = format_trailing_submitted(info)
tail = ['※ 스톱주문은 장 마감 시 소멸합니다 (자동 재등록 안 함)']
return '\n'.join(head + ['', body] + tail)
def format_expired(card_id: str, side: str) -> str:
side_word = '매수' if side == 'BUY' else '매도'
return f'⏱️ [#{card_id}] 승인 만료. {side_word}가 취소되었습니다.'
@@ -292,6 +436,10 @@ def format_card_locked(active: Optional[dict] = None) -> str:
price_str = '시장가'
elif order_type == 'AGGRESSIVE_LIMIT':
price_str = f'공격적 지정가 {_money(price)}' if price else '공격적 지정가'
elif order_type == 'TRAILING_STOP':
steps = active.get('trail_steps') or []
pcts = '/'.join(f'−{_num(s["pct"])}%' for s in steps)
price_str = f'트레일링 {len(steps)}단 {pcts} (1단계 손절 {_money(active.get("stop_price"))})'
else:
price_str = _money(price) if price is not None else '지정가'
@@ -130,6 +130,15 @@ def _safe_broker_executions(account_label: str, now: datetime) -> tuple[Optional
return executions, None
def nxt_eligible(symbol: str) -> bool:
"""NXT 거래가능 여부 (종목코드만으로). 캐시 읽기라 API 콜 0.
collect_market_data 를 거치지 않는 호출자용 — 트레일링 감시 루프가 대기 예약을
발동시킬 때 라우팅(_AL/_KRX)을 정하는 데 쓴다.
"""
return _nxt_eligible(symbol, {})
def collect_market_data(account_label: str, symbol: str, side: str, qty: int) -> dict:
now = datetime.now(KST)
quote = _safe_quote(symbol, account_label)
@@ -156,21 +165,37 @@ def collect_market_data(account_label: str, symbol: str, side: str, qty: int) ->
'stock_meta': stock_meta,
}
try:
open_orders = kc.get_open_orders(account_label)
except Exception:
open_orders = []
if side == 'BUY':
try:
bal = kc.get_balance(account_label)
md['balance_d2'] = _to_int(bal.get('d2_entra'))
d2 = _to_int(bal.get('d2_entra'))
# d2_entra는 미체결 매수 지정가로 묶인 예수금을 아직 포함 → 실제 가용액에서 차감.
locked = sum(_to_int(o.get('order_price')) * _to_int(o.get('unfilled_qty'))
for o in open_orders
if o.get('side') == 'BUY' and _to_int(o.get('order_price')) > 0)
md['balance_d2'] = max(0, d2 - locked)
except Exception:
md['balance_d2'] = 0
else:
try:
positions = kc.get_positions(account_label)
md['position_qty'] = next(
(_to_int(p.get('trde_able_qty') or p.get('qty') or p.get('hold_qty') or p.get('rmnd_qty'))
for p in positions
pos = next((p for p in positions
if (p.get('code') == symbol) or (p.get('symbol') == symbol) or (p.get('stk_cd') == symbol)),
0,
)
None)
if pos:
hold = _to_int(pos.get('qty') or pos.get('hold_qty') or pos.get('rmnd_qty'))
trde_able = _to_int(pos.get('trde_able_qty') or hold)
pend_sell = sum(_to_int(o.get('unfilled_qty')) for o in open_orders
if o.get('side') == 'SELL' and o.get('code') == symbol)
# trde_able이 미체결 매도 제외 여부 불확실 → min으로 이중차감 방지 (check 핸들러와 동일).
md['position_qty'] = max(0, min(trde_able, hold - pend_sell))
else:
md['position_qty'] = 0
except Exception:
md['position_qty'] = 0
+115 -9
View File
@@ -76,6 +76,18 @@ def _parse_hms(s: str) -> dtime:
def session_at(now: datetime) -> str:
"""현재 시각의 거래 세션. 경계 시각은 limits.json['trading_hours'] 가 단일 진실 소스다.
NXT_PRE(08:00~09:00) / KRX_NXT(09:00:30~15:20) / KRX_CLOSE(15:20~15:30)
NXT_AFTER(15:30~16:00) / AFTER(16:00~20:00) / CLOSED
⚠️ AFTER = **KRX 애프터마켓 + NXT 애프터마켓 동시**(2026-09-14 시행). 이걸 NXT_AFTER 로
뭉쳐 두면 NXT 미상장 종목이 validate_trading_hours 의 NXT_NOT_ELIGIBLE 로 막혀
**애프터마켓에 주문을 낼 수 없다** — 정작 KRX 애프터마켓에서는 거래되는 종목들이다
(2026-09-21 실측: 삼성전자우 KRX 거래량 16:05→18:00 +134,198주).
⚠️ 15:30~16:00 은 NXT_AFTER 로 남긴다 — KRX 쪽은 시간외 종가매매(15:40~16:00)뿐이고
우리는 그 주문유형(trde_tp=81)을 쓰지 않으므로 "NXT 만 거래 가능"이 맞다.
"""
th = _limits()['trading_hours']
t = now.replace(tzinfo=None).time()
if _parse_hms(th['nxt_pre_start']) <= t < _parse_hms(th['nxt_pre_end']):
@@ -84,11 +96,26 @@ def session_at(now: datetime) -> str:
return 'KRX_NXT'
if _parse_hms(th['krx_closing_auction_start']) <= t < _parse_hms(th['krx_closing_auction_end']):
return 'KRX_CLOSE'
# ⚠️ AFTER 를 NXT_AFTER 보다 **먼저** 본다 — nxt_after(15:30~20:00) 구간이 애프터마켓을
# 통째로 덮고 있어서, 순서를 바꾸면 16:00 이후가 영원히 NXT_AFTER 로 잡힌다.
if _parse_hms(th['krx_after_start']) <= t < _parse_hms(th['nxt_after_end']):
return 'AFTER'
if _parse_hms(th['nxt_after_start']) <= t < _parse_hms(th['nxt_after_end']):
return 'NXT_AFTER'
return 'CLOSED'
def after_trading_day(now: datetime) -> bool:
"""그날 거래 세션이 전부 끝났는가 (NXT 애프터 마감 = 20:00 이후).
⚠️ `session_at(now) == 'CLOSED'` 로 대신하면 안 된다 — 그 판정은 **장 시작 전 새벽**과
**09:00:00~09:00:30 틈**(NXT 프리 종료와 정규장 시작 사이)에도 CLOSED 를 돌려주므로,
당일 유효기간을 재는 데 쓰면 아침에 멀쩡한 예약이 만료로 잡힌다.
"""
th = _limits()['trading_hours']
return now.replace(tzinfo=None).time() >= _parse_hms(th['nxt_after_end'])
def is_today_holiday(now: datetime) -> bool:
th = _limits()['trading_hours']
rel = th.get('holiday_state_file')
@@ -103,6 +130,11 @@ def is_today_holiday(now: datetime) -> bool:
return False
today = now.strftime('%Y-%m-%d')
holidays = data.get('holidays') if isinstance(data, dict) else data
# holiday_sync.py 가 쓰는 실제 형태는 {날짜: 이름} dict 다. list 만 받던 시절의
# 코드가 남아 있어 dict 를 만나면 조용히 False 를 반환했고(= 휴장일 가드가 통째로
# 무력화), 2026-08-25 예약 트레일링 작업에서 발견해 dict 를 먼저 처리하게 했다.
if isinstance(holidays, dict):
return today in holidays
if not isinstance(holidays, list):
return False
for h in holidays:
@@ -121,10 +153,31 @@ def validate_trading_hours(now: datetime, is_holiday: bool, nxt_eligible: bool)
return Result.REJECT('OUTSIDE_HOURS', f'거래시간 외 ({now.strftime("%H:%M:%S")})')
if sess in ('NXT_PRE', 'NXT_AFTER') and not nxt_eligible:
return Result.REJECT('NXT_NOT_ELIGIBLE', '지금은 NXT 시간대인데 이 종목은 NXT 거래 불가')
# ⚠️ AFTER(16:00~20:00)는 nxt_eligible 을 보지 않는다 — KRX 애프터마켓이 NXT 미상장
# 종목을 받으므로, 여기서 막으면 정작 거래되는 종목을 거부한다.
# ⚠️ 애프터마켓 제외종목(ETF/ETN + 거래정지·투자경고·이상급등 등 9종)의 **서버 가드는
# 없다** — ETF/ETN 은 자산웹 버튼이 stock_codes 의 etf_etn 으로 막고, 나머지는 판별
# 필드가 없어 키움 거부 메시지가 최종 방어선이다. market_data 에 플래그를 실어
# 여기서 막으려면 datasource 를 함께 고쳐야 한다(미구현).
return Result.OK(code=sess)
def determine_routing(now: datetime, nxt_eligible: bool, force: Optional[str]) -> str:
# 스톱지정가(trde_tp=28)를 쓰는 주문유형. SOR 라우팅으로는 발주되지 않는다.
STOP_ORDER_TYPES = ('STOP_LIMIT', 'TRAILING_STOP', 'TRAILING_ARM')
def determine_routing(now: datetime, nxt_eligible: bool, force: Optional[str],
order_type: Optional[str] = None) -> str:
"""세션·NXT 거래여부로 거래소 라우팅 suffix 를 정한다.
⚠️ 스톱 계열은 SOR(_AL) 로 낼 수 없다 — 키움이 `407022: SOR주문이 불가능한
주문종류입니다` 로 거부한다(2026-09-21 삼성전자 실측). NXT 거래 종목은 정규장
라우팅이 _AL 이라, 이 강등이 없으면 대형주 트레일링이 **항상** 거부된다.
(2026-07-30 제닉스로보틱스가 성공했던 건 NXT 미거래 종목이라 KRX 로 나갔기 때문)
NXT 단독 시간대(_NX)가 스톱을 받는지는 실증 안 됐다 — 시도는 그대로 두고,
거부되면 그 메시지로 드러나게 한다(KRX 가 닫힌 시간이라 강등할 곳도 없다).
"""
routing = _limits()['routing']
suffix_map = {
'AL': routing['suffix_AL'],
@@ -134,16 +187,31 @@ def determine_routing(now: datetime, nxt_eligible: bool, force: Optional[str]) -
if force:
if force not in routing['force_options']:
raise ValueError(f'unknown routing force: {force}')
return suffix_map[force]
suffix = suffix_map[force]
else:
sess = session_at(now)
if sess in ('NXT_PRE', 'NXT_AFTER'):
return routing['suffix_NX']
if sess == 'KRX_CLOSE':
return routing['suffix_KRX']
if sess == 'KRX_NXT':
return routing['suffix_AL'] if nxt_eligible else routing['suffix_KRX']
suffix = routing['suffix_NX']
elif sess == 'KRX_CLOSE':
suffix = routing['suffix_KRX']
elif sess == 'AFTER':
# 애프터마켓은 **KRX 로 보낸다** — KRX 애프터마켓이 대상 종목을 전부 받고
# (2026-09-21 실측: NXT 미상장 종목도 16:00 이후 KRX 거래량이 증가),
# dmst_stex_tp 에 애프터 전용 값이 생기지도 않았다(KRX/NXT/SOR 그대로).
# ⚠️ SOR 을 쓰지 않는 이유 — 시세는 SOR 이 KRX+NXT 통합으로 오지만
# **주문이 애프터마켓에 어떻게 라우팅되는지는 미검증**이다. 오늘 아침
# "SOR 주문이 불가능한 주문종류입니다"(407022) 가 정확히 그 함정이었다.
# 검증된 경로만 쓴다. NXT 상장 종목도 KRX 애프터마켓에서 거래되므로 손실 없다.
suffix = routing['suffix_KRX']
elif sess == 'KRX_NXT':
suffix = routing['suffix_AL'] if nxt_eligible else routing['suffix_KRX']
else:
raise ValueError('CLOSED session has no valid routing')
if order_type in STOP_ORDER_TYPES and suffix == routing['suffix_AL']:
return routing['suffix_KRX']
return suffix
# ---- 가격 가드 (±30% 상한가/하한가) ----
@@ -180,7 +248,8 @@ def validate_market_order_session(now: datetime, order_type: str) -> Result:
if order_type != 'MARKET':
return Result.OK()
sess = session_at(now)
if sess in ('NXT_PRE', 'NXT_AFTER'):
# 애프터마켓 제출가능 호가는 지정가·최우선지정가·최유리지정가(+IOC/FOK)뿐 — **시장가 없음**.
if sess in ('NXT_PRE', 'NXT_AFTER', 'AFTER'):
return Result.REJECT('MARKET_NOT_ALLOWED_IN_NXT',
'NXT 시간대에는 시장가 불가 — 지정가만 가능. "지금 바로" 또는 가격 명시로 다시 시도.')
if sess == 'KRX_CLOSE':
@@ -503,7 +572,8 @@ def validate_request(request: dict, market_data: dict) -> Result:
return r
if request['side'] not in ('BUY', 'SELL'):
return Result.REJECT('INVALID_SIDE', f'잘못된 방향: {request["side"]}')
if request['order_type'] not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT'):
if request['order_type'] not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT',
'TRAILING_STOP', 'TRAILING_ARM'):
return Result.REJECT('INVALID_ORDER_TYPE', f'잘못된 주문방식: {request["order_type"]}')
r = validate_trading_hours(market_data['now'], market_data.get('is_holiday', False),
market_data.get('nxt_eligible', False))
@@ -525,6 +595,42 @@ def validate_request(request: dict, market_data: dict) -> Result:
market_data['upper_limit'], market_data['lower_limit'])
if not r.ok:
return r
if request['order_type'] in ('STOP_LIMIT', 'TRAILING_STOP'):
# 스톱지정가: 지정가(체결가)·조건단가(트리거) 둘 다 가격제한 이내 + 매도 전용.
# 보호매도(하락 시 매도)라 트리거는 현재가보다 낮아야 함 — 아니면 즉시 발동돼 스톱 의미 없음.
# 트레일링도 키움엔 스톱지정가로 나가므로 같은 검증을 받는다 (조건단가는 자동 계산값).
if request['side'] != 'SELL':
return Result.REJECT('STOP_SELL_ONLY', '스톱지정가는 현재 매도만 지원')
stop_price = request.get('stop_price')
if not stop_price or stop_price <= 0:
return Result.REJECT('STOP_PRICE_REQUIRED', '조건단가(트리거 가격)를 입력하세요')
r = validate_price_band(request['side'], request['price'],
market_data['upper_limit'], market_data['lower_limit'])
if not r.ok:
return r
r = validate_price_band(request['side'], stop_price,
market_data['upper_limit'], market_data['lower_limit'])
if not r.ok:
return r
cur = market_data.get('current_price') or 0
if cur and stop_price >= cur:
return Result.REJECT('STOP_TRIGGER_NOT_BELOW',
f'조건단가 {stop_price:,}원 ≥ 현재가 {cur:,}원 — '
f'하락 시 매도(스톱)는 현재가보다 낮게 설정해야 함')
if request['order_type'] == 'TRAILING_ARM':
# 예약 트레일링은 지금 나가는 주문이 아니라 "발동가에 닿으면 시작" 하는 예약이다.
# 그래서 스톱지정가와 두 가지 검증을 일부러 건너뛴다:
# · 가격제한폭 — 발동가는 오늘 상한가 위여도 통과시킨다. 실제 발주는 발동 시점의
# 현재가로 계산되므로 그때 검증된다. arm 이 당일 유효가 된 뒤로는 상한가 초과가
# "오늘 도달 불가 = 그냥 만료" 를 뜻하지만, **막지 않고 경고만** 한다
# (2026-08-25 관리자님 결정). 경고는 승인 카드(card.format_card)와 웹
# 미리보기·확인 팝업 3곳에 있다 — 여기서 거부로 바꾸지 말 것.
# · 조건단가 < 현재가 — 발동가는 정의상 현재가보다 위다. 이 검사를 태우면 전부 거부된다.
# 아래 매도 보유수량 검증은 그대로 받는다 — 지금 없는 종목을 예약해봐야 무의미하다.
if request['side'] != 'SELL':
return Result.REJECT('ARM_SELL_ONLY', '예약 트레일링은 매도 전용')
if not request.get('trigger_price'):
return Result.REJECT('ARM_TRIGGER', '발동가를 입력하세요')
if request['side'] == 'BUY':
basis = None
if request['order_type'] == 'MARKET':
@@ -35,7 +35,7 @@ if str(_PARENT) not in sys.path:
import kiwoom_client as kc # noqa: E402
from . import card, datasource, fill_watcher, guards, kiwoom_order, ledger, pin, sidecar # noqa: E402
from . import card, datasource, fill_watcher, guards, kiwoom_order, ledger, pin, sidecar, trailing # noqa: E402
LIMITS_FILE = Path(__file__).resolve().parent / 'limits.json'
OPENCLAW_CONFIG = Path.home() / '.openclaw' / 'openclaw.json'
@@ -164,16 +164,79 @@ def propose_trade(
price: Optional[int] = None,
routing_force: Optional[str] = None,
budget: Optional[int] = None,
stop_price: Optional[int] = None,
trail_steps: Optional[list] = None,
min_sell_price: Optional[int] = None,
trigger_price: Optional[int] = None,
) -> dict:
sidecar.guard_or_raise()
_sweep_expired_and_notify()
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT'):
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT',
'TRAILING_STOP', 'TRAILING_ARM'):
return {'ok': False,
'message': card.format_rejected('INVALID_ORDER_TYPE', f'잘못된 주문방식: {order_type}')}
if side not in ('BUY', 'SELL'):
return {'ok': False,
'message': card.format_rejected('INVALID_SIDE', f'잘못된 방향: {side}')}
if order_type == 'STOP_LIMIT':
if side != 'SELL':
return {'ok': False,
'message': card.format_rejected('STOP_SELL_ONLY', '스톱지정가는 현재 매도만 지원')}
if not price or price <= 0 or not stop_price or stop_price <= 0:
return {'ok': False,
'message': card.format_rejected('STOP_INPUT',
'스톱지정가는 지정가와 조건단가(트리거)가 모두 필요')}
norm_steps = None
if order_type in ('TRAILING_STOP', 'TRAILING_ARM'):
# 트레일링은 계단 정의(하락률·누적 비중)만 입력받고, 계단별 조건단가·지정가·주수는
# 현재가와 보유수량 기준으로 자동 계산한다 (md 수집 후).
#
# 예약 트레일링(TRAILING_ARM)은 같은 입력에 발동가가 하나 더 붙고, 계단 계산의 기준을
# 현재가 대신 발동가로 쓴다. 그 외 검증은 전부 공유한다 — 발동하면 결국 같은 물건이
# 되므로 여기서 갈라놓으면 한쪽만 고쳐지는 사고가 난다.
is_arm = order_type == 'TRAILING_ARM'
kind = '예약 트레일링' if is_arm else '트레일링 스톱'
if side != 'SELL':
return {'ok': False,
'message': card.format_rejected(
'ARM_SELL_ONLY' if is_arm else 'STOP_SELL_ONLY', f'{kind}은 매도 전용')}
try:
norm_steps = trailing.normalize_steps(trail_steps or [])
except (ValueError, KeyError, TypeError) as e:
return {'ok': False,
'message': card.format_rejected('TRAIL_STEPS', f'계단 정의 오류: {e}')}
if budget is not None:
# 계단별 주수는 보유수량을 나눠 만든다 — 예산 환산이 끼면 기준이 두 개가 된다.
return {'ok': False,
'message': card.format_rejected('TRAIL_BUDGET',
f'{kind}은 수량으로만 지정 (예산 환산 불가)')}
if min_sell_price is not None and min_sell_price <= 0:
return {'ok': False,
'message': card.format_rejected('TRAIL_MIN_PRICE',
'최저 매도가는 0보다 커야 함 (제한 없으면 비우세요)')}
if is_arm and (not trigger_price or trigger_price <= 0):
return {'ok': False,
'message': card.format_rejected('ARM_TRIGGER', '발동가를 입력하세요')}
# 같은 계좌·종목에 활성 트레일링과 대기 예약이 함께 있으면 안 된다 — 어느 쪽이든 먼저 취소.
dup = trailing.find_by_symbol(account, symbol)
if dup:
dup_steps = dup.get('steps') or []
top_uv = max((s['cond_uv'] for s in dup_steps), default=0)
return {'ok': False,
'message': card.format_rejected(
'TRAIL_DUPLICATE',
f'{symbol_name or symbol} 트레일링 예약이 이미 있음 '
f'({dup["id"]} · {len(dup_steps)}단 · 1단계 손절 {top_uv:,}원) '
f'— 먼저 취소하세요')}
dup_arm = trailing.find_arm_by_symbol(account, symbol)
if dup_arm:
return {'ok': False,
'message': card.format_rejected(
'TRAIL_DUPLICATE',
f'{symbol_name or symbol} 예약 트레일링이 이미 있음 '
f'({dup_arm["id"]} · 발동가 {dup_arm["trigger_price"]:,}원 · '
f'{len(dup_arm.get("steps") or [])}단) — 먼저 취소하세요')}
if budget is not None and qty is not None:
return {'ok': False,
'message': card.format_rejected('AMBIGUOUS_INPUT',
@@ -186,6 +249,75 @@ def propose_trade(
# collect_market_data 의 잔고/보유 조회는 qty 와 무관.
md = datasource.collect_market_data(account, symbol, side, qty or 0)
trail_final = None
trail_peak = None
if order_type in ('TRAILING_STOP', 'TRAILING_ARM'):
is_arm = order_type == 'TRAILING_ARM'
# 현재가를 못 받으면 손절선을 정할 수 없어 진행 불가 (호가 fallback 금지 — 기준이 흔들림).
cur = md.get('current_price') or 0
if cur <= 0:
return {'ok': False,
'message': card.format_rejected(
'TRAIL_NO_PRICE',
'현재가를 못 받아 발동가 도달 여부를 판정할 수 없음' if is_arm
else '현재가를 못 받아 트레일링 손절선을 정할 수 없음')}
if is_arm:
# 이미 닿아 있으면 '기다렸다가'라는 이 주문유형의 존재 이유가 사라진다 —
# 등록하는 순간 다음 감시(1분 내)에 바로 발동한다. 그럴 거면 일반 트레일링이 맞다.
if cur >= trigger_price:
return {'ok': False,
'message': card.format_rejected(
'ARM_ALREADY_REACHED',
f'현재가 {cur:,}원이 이미 발동가 {trigger_price:,}원 이상입니다 — '
f'발동가를 올리시거나 일반 트레일링 스톱으로 등록하세요')}
# 계단 계산 기준 = 발동가. 실제 발주는 발동 시점 현재가로 다시 계산하지만,
# 여기서 미리 재보는 이유는 ①카드에 예상 손절선·주수를 보이고 ②발동해도 못 쓰는
# 조합(수량 부족·즉시발동)을 지금 걸러내기 위해서다.
# 발동 시점엔 현재가 >= 발동가 이므로 실제 손절선은 이 미리보기 이상이다
# — 관리자님이 승인한 것보다 불리해지는 방향으로는 움직이지 않는다.
basis = trigger_price
else:
# 초기 고점은 등록 시점 현재가로 고정. 과거 고점(52주·매수후)을 쓰면 손절선이 현재가
# 위로 올라가 등록 즉시 발동한다 — 손실 종목은 아예 등록 불가, 이익 종목은 현재가와
# 결과가 같아 옵션 자체를 제거했다(2026-07-30 실측 근거로 관리자님 결정).
basis = cur
trail_peak = basis
try:
leveled = trailing.compute_step_levels(trail_peak, norm_steps, min_sell_price)
except ValueError as e:
return {'ok': False, 'message': card.format_rejected('TRAIL_LEVEL', str(e))}
# 계단별 주수 배분. 수량이 적어 0주가 된 계단은 발주할 수 없으니 떨어낸다 —
# 단계 번호(n)는 그대로 두어 "몇 단계가 빠졌는지" 가 카드에서 보이게 한다.
qtys = trailing.allocate_step_qty(qty, [s['weight'] for s in norm_steps])
trail_final = [dict(s, qty=q) for s, q in zip(leveled, qtys) if q > 0]
if not trail_final:
return {'ok': False,
'message': card.format_rejected(
'TRAIL_QTY', f'{qty:,}주로는 계단을 나눌 수 없음 — 수량을 늘리거나 계단을 줄이세요')}
# 가장 얕은 계단이 제일 먼저 발동한다. 이게 현재가 이상이면 등록하는 순간 터진다.
# 고점=현재가로 고정된 뒤로는 최저 매도가가 현재가보다 높은 경우만 남는다
# (트레일 폭은 항상 현재가 아래로 내려감).
top = trail_final[0]
if top['cond_uv'] >= basis:
ref = '발동가' if is_arm else '현재가'
when = '발동하자마자' if is_arm else '등록 즉시'
if top['floor_applied']:
msg = (f'최저 매도가 {min_sell_price:,}원이 {ref} {basis:,}원보다 높아 '
f'{when} 발동됩니다 — {ref}보다 낮게 설정하세요')
else:
msg = (f'1단계 손절선 {top["cond_uv"]:,}원이 {ref} {basis:,}원 이상이라 '
f'{when} 발동됩니다')
return {'ok': False,
'message': card.format_rejected(
'ARM_IMMEDIATE' if is_arm else 'TRAIL_IMMEDIATE', msg)}
if is_arm:
# 예약 단계에선 나가는 주문이 없다 — 발주가는 발동 시점에 정해진다.
price = None
stop_price = None
else:
price = top['ord_uv']
stop_price = top['cond_uv']
budget_conversion = None
if budget is not None:
conv = guards.convert_budget_to_qty(side, budget, md.get('orderbook'),
@@ -211,6 +343,20 @@ def propose_trade(
}
if order_type == 'LIMIT':
request['price'] = price
elif order_type in ('STOP_LIMIT', 'TRAILING_STOP'):
request['price'] = price
request['stop_price'] = stop_price
if order_type == 'TRAILING_STOP':
request['trail_steps'] = trail_final
request['min_sell_price'] = min_sell_price
request['trail_peak'] = trail_peak
elif order_type == 'TRAILING_ARM':
# price/stop_price 를 넣지 않는다 — 지금 나가는 주문이 없고, 넣으면 validate_request 의
# 가격제한폭·"조건단가 < 현재가" 검사에 걸린다(둘 다 예약엔 무의미하다).
request['trigger_price'] = trigger_price
request['trail_steps'] = trail_final
request['min_sell_price'] = min_sell_price
request['trail_peak'] = trail_peak
r = guards.validate_request(request, md)
if not r.ok:
@@ -230,7 +376,7 @@ def propose_trade(
return {'ok': False, 'message': card.format_rejected(sr.code, sr.message)}
state_warning = state_eval['warning'] # 카드에 표시할 경고 (None이면 정상)
suffix = guards.determine_routing(md['now'], md['nxt_eligible'], routing_force)
suffix = guards.determine_routing(md['now'], md['nxt_eligible'], routing_force, order_type)
estimate = None
final_price = price
@@ -254,6 +400,24 @@ def propose_trade(
'account': account, 'side': side, 'symbol': symbol, 'symbol_name': symbol_name,
'qty': qty, 'price': final_price, 'order_type': order_type, 'routing_suffix': suffix,
}
if order_type == 'STOP_LIMIT':
payload['stop_price'] = stop_price
elif order_type == 'TRAILING_STOP':
# 승인 시점에 확정되는 것: 계좌·종목·계단별 수량·하락률·최저 매도가.
# 이후 감시 루프는 계단별 조건단가만 올린다 (수량·계단 구성은 불변).
payload['stop_price'] = stop_price
payload['trail_steps'] = trail_final
payload['trail_peak'] = trail_peak
payload['min_sell_price'] = min_sell_price
elif order_type == 'TRAILING_ARM':
# 승인 시점에 확정: 계좌·종목·수량·발동가·계단 정의·최저 매도가.
# 발동 시점에 정해짐: 고점(그때의 현재가)·계단별 조건단가/지정가/주수·라우팅.
# ⚠️ trail_steps 에는 레벨 없는 정규화 계단만 넣는다 — 발동 경로가 미리보기 가격을
# 실수로 재사용하지 못하게 구조적으로 막는다. 미리보기는 별도 키(trail_preview).
payload['trigger_price'] = trigger_price
payload['trail_steps'] = norm_steps
payload['trail_preview'] = trail_final
payload['min_sell_price'] = min_sell_price
try:
pending = _pin_store.issue(account, payload)
@@ -438,6 +602,10 @@ def submit_with_pin(pin_input: str, dry_run: bool = True) -> dict:
if p.get('multi'):
return _submit_multi_legs(card_obj, dry_run)
if p['order_type'] == 'TRAILING_STOP':
return _submit_trailing_steps(card_obj, dry_run)
if p['order_type'] == 'TRAILING_ARM':
return _register_trailing_arm(card_obj, dry_run)
submit_order_type = 'LIMIT' if p['order_type'] == 'AGGRESSIVE_LIMIT' else p['order_type']
res = kiwoom_order.submit(
@@ -450,6 +618,7 @@ def submit_with_pin(pin_input: str, dry_run: bool = True) -> dict:
routing_suffix=p['routing_suffix'],
dry_run=dry_run,
card_id=card_obj.card_id,
stop_price=p.get('stop_price'),
)
if res['ok']:
@@ -460,9 +629,9 @@ def submit_with_pin(pin_input: str, dry_run: bool = True) -> dict:
msg = card.format_submitted(card_obj.card_id, p['side'],
p.get('symbol_name', p['symbol']),
p['qty'], p.get('price'), ord_no)
if ord_no:
# 키움에서 ord_no 받으면 백그라운드 체결 추적 시작 (kt00007 폴링).
# 부분체결·완전체결·사후거절·30분 미체결 시 텔레그램 자동 알림.
if ord_no:
fill_watcher.watch(
ord_no=ord_no, account=p['account'], side=p['side'],
symbol=p['symbol'], symbol_name=p.get('symbol_name', p['symbol']),
@@ -475,6 +644,167 @@ def submit_with_pin(pin_input: str, dry_run: bool = True) -> dict:
'detail': res}
def _register_trailing_arm(card_obj, dry_run: bool) -> dict:
"""예약 트레일링 카드 — 키움 호출 0. 발동가 대기 예약 1건을 파일에 쓰고 끝난다.
PIN 은 여기서 소비된다. 발동 시점엔 PIN 이 없다(관리자님 명시 승인) — 그래서 승인 대상에
발동가까지 포함되고, 감시 루프는 그 확정된 내용을 조건 도달 시 집행만 한다.
"""
p = card_obj.payload
name = p.get('symbol_name', p['symbol'])
info = {'card_id': card_obj.card_id, 'account': p['account'], 'symbol': p['symbol'],
'symbol_name': name, 'qty': p['qty'], 'trigger_price': p['trigger_price'],
'min_sell_price': p.get('min_sell_price'), 'steps': p.get('trail_steps') or [],
'dry_run': dry_run, 'arm_id': None}
if dry_run:
return {'ok': True, 'message': card.format_arm_registered(info)}
try:
arm = trailing.register_arm(
account=p['account'], symbol=p['symbol'], symbol_name=name,
qty=p['qty'], trigger_price=p['trigger_price'],
steps=p.get('trail_steps') or [],
min_sell_price=p.get('min_sell_price'), card_id=card_obj.card_id)
except Exception as e:
# 주문이 나간 게 없으므로 여기선 그냥 거부로 끝내면 된다 (트레일링 등록 실패와 달리
# 되돌릴 외부 효과가 없다).
ledger.append('arm_fire_failed', {'card_id': card_obj.card_id,
'error': repr(e), 'stage': 'register', **p})
return {'ok': False,
'message': card.format_rejected('ARM_REGISTER_FAILED',
f'{name} 예약 등록 실패 — {e}')}
info['arm_id'] = arm['id']
ledger.append('arm_registered', {'card_id': card_obj.card_id, 'arm_id': arm['id'], **p})
return {'ok': True, 'message': card.format_arm_registered(info), 'arm_id': arm['id']}
def cancel_trailing_arm(arm_id: str) -> dict:
"""대기 예약 취소. PIN 없음 — 취소는 안전한 방향이다(cancel_open_order 와 같은 판례).
⚠️ firing 마킹이 걸린 예약은 이미 키움에 스톱주문이 나갔을 수 있다. 그 주문까지 자동으로
취소하지는 않는다 — 승인 범위를 넘는 매매 API 자동 호출이 된다. 대신 접수된 주문번호를
응답에 실어 관리자님이 미체결 목록에서 직접 처리하게 한다.
"""
sidecar.guard_or_raise()
arm = trailing.get_arm(arm_id)
if arm is None:
return {'ok': False, 'message': f'대기 예약 없음 ({arm_id})'}
fired = arm.get('fired_legs') or []
gone = trailing.remove_arm(arm_id, 'manual_cancel')
if gone is None:
return {'ok': False, 'message': f'대기 예약 없음 ({arm_id})'}
ledger.append('arm_canceled', {'arm_id': arm_id, 'account': arm['account'],
'symbol': arm['symbol'], 'qty': arm['qty'],
'trigger_price': arm['trigger_price']})
msg = (f'🗑 예약 트레일링 취소 ({arm_id}) — {arm["symbol_name"]} '
f'{arm["qty"]:,}주 · 발동가 {arm["trigger_price"]:,}원')
if fired:
nos = ', '.join(s.get('ord_no', '') for s in fired if s.get('ord_no'))
msg += (f'\n⚠️ 발동 처리 중 접수된 주문이 있습니다: {nos}\n'
f'미체결 목록에서 별도로 취소해 주세요 (자동 취소하지 않습니다).')
return {'ok': True, 'message': msg, 'placed_orders': fired}
def place_trailing_legs(account: str, symbol: str, steps: list, routing_suffix: str,
card_id: Optional[str], dry_run: bool,
on_placed=None) -> tuple:
"""계단별 스톱지정가 접수. 반환 (placed, failed, details).
얕은 계단(먼저 발동할 것)부터 순서대로 낸다 — 중간에 막혀도 가장 가까운 방어선이 먼저 걸린다.
한 레그가 실패해도 나머지는 계속 시도한다. 이미 나간 주문을 되돌리지 않는 이유는 취소 자체도
실패할 수 있어 상태가 더 불분명해지기 때문이다 — 대신 성공·실패를 그대로 호출측에 돌려준다.
`on_placed(leg)` 는 레그 하나가 접수될 때마다 즉시 호출된다. 예약 트레일링 발동 경로가
"어느 레그까지 실제로 나갔는지" 를 디스크에 미리 적는 데 쓴다(중간에 죽었을 때의 복구 단서).
실주문이 나가는 유일한 계단 발주 경로다 — 등록 즉시 트레일링과 예약 발동이 이 함수를
공유해야 두 벌로 갈라지지 않는다.
"""
placed, failed, details = [], [], []
for s in steps:
res = kiwoom_order.submit(
account_label=account,
side='SELL',
symbol=symbol,
qty=s['qty'],
price=s['ord_uv'],
order_type='STOP_LIMIT',
routing_suffix=routing_suffix,
dry_run=dry_run,
card_id=card_id,
stop_price=s['cond_uv'],
)
details.append(res)
if res['ok']:
# dry_run 은 주문번호가 없다 — 예약 등록 없이 접수될 내용만 확인한다.
leg = dict(s, ord_no=res.get('ord_no', ''))
placed.append(leg)
if on_placed is not None:
on_placed(leg)
else:
failed.append(dict(s, reason=res.get('reason', 'UNKNOWN'),
error=str(res.get('error', ''))))
return placed, failed, details
def _submit_trailing_steps(card_obj, dry_run: bool) -> dict:
"""트레일링 계단 카드 — 계단마다 스톱주문을 내고 예약 1건으로 묶어 등록한다.
키움엔 스톱지정가(trde_tp=28)로 나간다 — 트레일링은 우리 쪽 개념이다.
얕은 계단(먼저 발동할 것)부터 접수해, 중간에 막히더라도 가장 가까운 방어선이 먼저 걸리게 한다.
한 레그가 실패해도 나머지는 계속 시도하고, 접수된 레그만 예약에 등록한다. 이미 나간 주문을
되돌리지 않는 이유는 취소 자체도 실패할 수 있어 상태가 더 불분명해지기 때문이다 —
대신 몇 단이 걸리고 몇 단이 실패했는지를 메시지에 그대로 드러낸다.
fill_watcher 는 걸지 않는다. 스톱 예약은 조건 도달까지 미체결로 남는 게 정상이라
30분 미체결 알림이 오탐이 된다 — 생존·체결 감시는 trailing_monitor 가 담당한다.
"""
p = card_obj.payload
steps = p.get('trail_steps') or []
name = p.get('symbol_name', p['symbol'])
placed, failed, details = place_trailing_legs(
account=p['account'], symbol=p['symbol'], steps=steps,
routing_suffix=p['routing_suffix'], card_id=card_obj.card_id, dry_run=dry_run)
if not placed:
return {'ok': False,
'message': card.format_rejected(
(failed[0]['reason'] if failed else 'UNKNOWN'),
f'{name} 계단 {len(steps)}단 전부 접수 실패 — '
+ (failed[0]['error'] if failed else '')),
'detail': details}
info = {'card_id': card_obj.card_id, 'symbol_name': name, 'account': p['account'],
'total_qty': p['qty'], 'peak': p['trail_peak'],
'min_sell_price': p.get('min_sell_price'),
'placed': placed, 'failed': failed, 'dry_run': dry_run,
'res_id': None, 'register_error': None}
if not dry_run:
registerable = [s for s in placed if s['ord_no']]
if not registerable:
# 접수는 됐는데 주문번호를 못 받았다 — 감시가 붙을 수 없으니 반드시 알린다.
ledger.append('trailing_register_failed',
{'card_id': card_obj.card_id, 'error': 'no ord_no', **p})
info['register_error'] = '주문번호를 못 받아 감시를 걸지 못했습니다'
else:
try:
res_trl = trailing.register_steps(
account=p['account'], symbol=p['symbol'], symbol_name=name,
total_qty=p['qty'], peak=p['trail_peak'], steps=registerable,
routing_suffix=p['routing_suffix'], card_id=card_obj.card_id,
min_sell_price=p.get('min_sell_price'),
)
info['res_id'] = res_trl['id']
except Exception as e:
# 주문은 이미 접수됨 — 등록 실패를 숨기면 트레일링이 안 도는 걸 모른다.
ledger.append('trailing_register_failed',
{'card_id': card_obj.card_id, 'error': repr(e), **p})
info['register_error'] = str(e)
return {'ok': True, 'message': card.format_trailing_submitted(info), 'detail': details}
def _submit_multi_legs(card_obj, dry_run: bool) -> dict:
"""multi 카드(propose_trade_multi)의 레그별 키움 제출 — 레그 독립 실행·독립 보고.
@@ -676,6 +1006,15 @@ def modify_open_order(ord_no: Optional[str] = None,
return {'ok': False, 'message': resolved['message'], 'code': resolved['code']}
row = resolved['row']
# 스톱지정가(trde_tp=28) 계열은 정정 대상에서 뺀다. 두 가지가 동시에 깨진다:
# ① 여기선 mdfy_cond_uv 를 안 넘겨 조건단가가 빈 값으로 나간다(조건 소실 위험).
# ② 정정 응답 ord_no 가 신규 번호라, trailing_stops.json 의 레그 ord_no 가 어긋나
# 그 레그의 자동 손절선 상향이 이후 전부 실패한다.
# 바꾸고 싶으면 취소 후 재등록이 맞다.
if row.get('stop_price'):
return {'ok': False, 'code': 'STOP_ORDER',
'message': f"⛔ 스톱지정가·트레일링 주문은 정정 불가 (ord_no={row['ord_no']}) — "
'취소 후 재등록하세요'}
if qty is None and price is None:
return {'ok': False,
'message': '⛔ 정정 항목 누락 — qty/price 중 최소 하나는 필요합니다'}
@@ -690,6 +1029,27 @@ def modify_open_order(ord_no: Optional[str] = None,
return {'ok': False,
'message': '⛔ 시장가 미체결은 정정 불가 (가격 0). 취소 후 신규 발주하세요.'}
# 가격제한폭·예수금 — 신규 주문과 같은 가드를 태운다. 정정도 결국 새 가격으로 주문이 나가고,
# propose 만 막아두면 정정이 그 검증을 우회하는 뒷문이 된다.
# 매도 보유수량은 태우지 않는다 — 이미 접수된 주문의 수량을 그대로 두거나 줄이는 경로라
# (웹은 price 만 보낸다) 새로 필요한 보유분이 없다.
md = datasource.collect_market_data(row['account'], row['code'], row['side'], new_qty)
r = guards.validate_price_band(row['side'], new_price,
md['upper_limit'], md['lower_limit'])
if not r.ok:
return {'ok': False, 'code': r.code,
'message': f"⛔ 정정 거부 [{r.code}] · ord_no={row['ord_no']} · {r.message}"}
if row['side'] == 'BUY':
# ⚠️ md['balance_d2'] 는 미체결 매수로 묶인 돈을 **정정 대상 주문까지 포함해** 뺀 값이다.
# 그 주문은 지금 가격을 바꾸는 당사자라, 되돌려 더하지 않으면 제 주문에 제가 막힌다
# (149,000×2 로 묶인 상태에서 149,100 으로 올리는 것조차 '예수금 부족'이 된다).
# 되더하는 식은 datasource 의 locked 계산(order_price × unfilled_qty)과 같아야 정확히 상쇄된다.
avail = md['balance_d2'] + row['order_price'] * row['unfilled_qty']
r = guards.validate_balance_for_buy(new_qty, new_price, avail)
if not r.ok:
return {'ok': False, 'code': r.code,
'message': f"⛔ 정정 거부 [{r.code}] · ord_no={row['ord_no']} · {r.message}"}
side_word = '매수' if row['side'] == 'BUY' else ('매도' if row['side'] == 'SELL' else row['side'])
res = kiwoom_order.modify_order(
account_label=row['account'],
@@ -877,7 +1237,7 @@ def amend_trade(qty: Optional[int] = None, price: Optional[int] = None,
return {'ok': False, 'message': card.format_rejected(sr.code, sr.message)}
state_warning = state_eval['warning']
suffix = guards.determine_routing(md['now'], md['nxt_eligible'], routing_force)
suffix = guards.determine_routing(md['now'], md['nxt_eligible'], routing_force, new_order_type)
estimate = None
final_price = new_price
@@ -31,6 +31,7 @@ TR_CANCEL = 'kt10003'
# 매매구분 코드
TRDE_TP_LIMIT = '0' # 보통가 (지정가)
TRDE_TP_MARKET = '3' # 시장가
TRDE_TP_STOP = '28' # 스톱지정가 — cond_uv(조건단가) 도달 시 ord_uv(지정가)로 발주
# 거래소 코드 (라우팅 suffix → API 거래소 구분)
_EXCHANGE_BY_SUFFIX = {
@@ -52,27 +53,38 @@ def _exchange_for(suffix: str) -> str:
def submit(account_label: str, side: str, symbol: str, qty: int,
price: Optional[int], order_type: str, routing_suffix: str,
dry_run: bool = True, card_id: Optional[str] = None) -> dict:
dry_run: bool = True, card_id: Optional[str] = None,
stop_price: Optional[int] = None) -> dict:
sidecar.guard_or_raise()
if side not in ('BUY', 'SELL'):
raise ValueError(f'invalid side: {side}')
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT'):
if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT'):
raise ValueError(f'invalid order_type: {order_type}')
if order_type in ('LIMIT', 'AGGRESSIVE_LIMIT') and not price:
raise ValueError(f'{order_type} requires price')
if order_type == 'STOP_LIMIT' and (not price or not stop_price):
raise ValueError('STOP_LIMIT requires price(지정가) and stop_price(조건단가)')
tr_id = TR_BUY if side == 'BUY' else TR_SELL
exchange = _exchange_for(routing_suffix)
is_market = order_type == 'MARKET'
is_stop = order_type == 'STOP_LIMIT'
if is_market:
trde_tp = TRDE_TP_MARKET
elif is_stop:
trde_tp = TRDE_TP_STOP
else:
trde_tp = TRDE_TP_LIMIT
body = {
'dmst_stex_tp': exchange,
'stk_cd': symbol,
'ord_qty': str(qty),
'ord_uv': '' if is_market else str(price),
'trde_tp': TRDE_TP_MARKET if is_market else TRDE_TP_LIMIT,
'cond_uv': '',
'trde_tp': trde_tp,
'cond_uv': str(stop_price) if is_stop else '',
}
payload = {
@@ -82,6 +94,7 @@ def submit(account_label: str, side: str, symbol: str, qty: int,
'symbol': symbol,
'qty': qty,
'price': price,
'stop_price': stop_price if is_stop else None,
'order_type': order_type,
'routing_suffix': routing_suffix,
'exchange': exchange,
@@ -199,13 +212,18 @@ def cancel_order(account_label: str, orig_ord_no: str, symbol: str,
def modify_order(account_label: str, orig_ord_no: str, symbol: str,
modify_qty: int, modify_price: int,
routing_suffix: str = '',
dry_run: bool = True, card_id: Optional[str] = None) -> dict:
dry_run: bool = True, card_id: Optional[str] = None,
modify_cond_price: Optional[int] = None) -> dict:
"""미체결 주문 정정 (kt10002).
modify_qty / modify_price 둘 다 필수 — 키움 명세상 mdfy_qty/mdfy_uv 모두 Required.
수량만 바꿀 땐 기존 가격, 가격만 바꿀 땐 기존 수량을 그대로 전달.
시장가 주문은 정정 불가 (mdfy_uv 가 가격이라 0 불가) — 호출 전에 차단.
routing_suffix 는 원주문의 거래소와 동일해야 함.
modify_cond_price: 스톱지정가(trde_tp=28) 주문의 조건단가 정정용 (mdfy_cond_uv).
트레일링 스톱이 고점 갱신 시 조건단가를 올리는 데 사용. 일반 주문은 None.
⚠️ 정정 응답의 ord_no 는 신규 주문번호 — 다음 정정은 그 번호를 orig_ord_no 로 써야 함.
"""
sidecar.guard_or_raise()
@@ -217,6 +235,8 @@ def modify_order(account_label: str, orig_ord_no: str, symbol: str,
raise ValueError(f'modify_qty must be > 0 (got {modify_qty})')
if modify_price <= 0:
raise ValueError(f'modify_price must be > 0 — 시장가 주문은 정정 불가, 취소 후 신규 발주')
if modify_cond_price is not None and modify_cond_price <= 0:
raise ValueError(f'modify_cond_price must be > 0 (got {modify_cond_price})')
exchange = _exchange_for(routing_suffix)
body = {
@@ -225,7 +245,7 @@ def modify_order(account_label: str, orig_ord_no: str, symbol: str,
'stk_cd': symbol,
'mdfy_qty': str(modify_qty),
'mdfy_uv': str(modify_price),
'mdfy_cond_uv': '',
'mdfy_cond_uv': str(modify_cond_price) if modify_cond_price else '',
}
payload = {
@@ -235,6 +255,7 @@ def modify_order(account_label: str, orig_ord_no: str, symbol: str,
'orig_ord_no': orig_ord_no,
'modify_qty': modify_qty,
'modify_price': modify_price,
'modify_cond_price': modify_cond_price,
'routing_suffix': routing_suffix,
'exchange': exchange,
'tr_id': TR_MODIFY,
@@ -81,6 +81,15 @@ def idempotency_hash(account: str, symbol: str, side: str, qty: int, price: int
return hashlib.sha256(key.encode('utf-8')).hexdigest()[:16]
# 멱등성 매칭 대상 — **이중 발주 위험이 실재하는 이벤트만** 본다.
# submitted : 키움이 접수했다 (확정)
# failed : 네트워크 에러 — 나갔는지 모른다 (가장 위험, 반드시 차단)
# rejected(브로커 거부·idem 중복)·dryrun 은 접수되지 않은 게 확정이라 재시도를 막지 않는다.
# ⚠️ 이게 없으면 키움이 거부한 직후 같은 주문을 다시 내는 게 90초간 IDEMPOTENCY_DUP 으로
# 막혀, 관리자님이 영문 모를 두 번째 실패를 본다 (2026-09-21 SOR 스톱 거부 때 실측).
IDEM_MATCH_EVENTS = ('submitted', 'failed')
def find_recent_idempotency(h: str, within_seconds: int = 90) -> Optional[dict]:
path = _ledger_path()
if not path.exists():
@@ -99,7 +108,8 @@ def find_recent_idempotency(h: str, within_seconds: int = 90) -> Optional[dict]:
continue
if ts < cutoff:
return None
if rec.get('payload', {}).get('idem_hash') == h:
if (rec.get('event') in IDEM_MATCH_EVENTS
and rec.get('payload', {}).get('idem_hash') == h):
return rec
return None
@@ -4,14 +4,22 @@
"last_updated": "2026-05-06",
"note": "변경 시 별도 커밋. guards 단위테스트가 이 파일을 검증. 손으로 풀지 말고 항상 이 파일을 통해 조정."
},
"enabled": false,
"kill_switch_file": "state/orders_disabled",
"accounts_whitelist": ["일반", "ISA", "가희_일반", "가희_ISA"],
"owner_accounts": ["일반", "ISA"],
"spouse_accounts": ["가희_일반", "가희_ISA"],
"accounts_whitelist": [
"일반",
"ISA",
"가희_일반",
"가희_ISA"
],
"owner_accounts": [
"일반",
"ISA"
],
"spouse_accounts": [
"가희_일반",
"가희_ISA"
],
"limits": {
"single_order_max_won": null,
"daily_total_max_won": null,
@@ -19,11 +27,9 @@
"price_band_pct_soft": null,
"price_band_pct_hard": 30
},
"delays": {
"between_orders_seconds": 5
},
"pin": {
"owner_length": 4,
"owner_charset": "digits",
@@ -33,7 +39,6 @@
"expiry_seconds": 120,
"max_attempts": 1
},
"trading_hours": {
"tz": "Asia/Seoul",
"krx_regular_start": "09:00:30",
@@ -44,27 +49,36 @@
"nxt_pre_end": "09:00:00",
"nxt_after_start": "15:30:00",
"nxt_after_end": "20:00:00",
"krx_after_start": "16:00:00",
"block_outside": true,
"holiday_state_file": "state/market_holidays.json"
},
"routing": {
"default": "AL",
"force_options": ["AL", "NX", "KRX"],
"force_options": [
"AL",
"NX",
"KRX"
],
"suffix_AL": "_AL",
"suffix_NX": "_NX",
"suffix_KRX": ""
},
"market_order": {
"allowed": true,
"natural_language_market": ["시장가"],
"natural_language_aggressive_limit": ["지금 바로", "즉시", "빨리", "당장"],
"natural_language_market": [
"시장가"
],
"natural_language_aggressive_limit": [
"지금 바로",
"즉시",
"빨리",
"당장"
],
"aggressive_limit_ticks": 1,
"show_orderbook_depth": 3,
"estimate_slippage": true
},
"guards": {
"block_market_holidays": true,
"block_trading_halt": true,
@@ -74,9 +88,13 @@
"require_position_check_for_sell": true,
"block_market_order_when_orderbook_thin": false
},
"card": {
"highlight_fields": ["side", "account", "symbol_name", "price"],
"highlight_fields": [
"side",
"account",
"symbol_name",
"price"
],
"highlight_marker": "▶",
"highlight_bold_markdown": true,
"buy_emoji": "🛒",
@@ -86,13 +104,35 @@
"orderbook_depth": 3,
"show_balance_ratio": true
},
"ledger": {
"log_file": "state/order_log.jsonl",
"events": ["card_issued", "pin_issued", "amended", "approved", "rejected", "expired", "canceled", "submitted", "filled", "partial", "failed", "dryrun", "cancel_submitted", "cancel_rejected", "cancel_confirmed", "cancel_unconfirmed_timeout", "modify_submitted", "modify_rejected"],
"events": [
"card_issued",
"pin_issued",
"amended",
"approved",
"rejected",
"expired",
"canceled",
"submitted",
"filled",
"partial",
"failed",
"dryrun",
"cancel_submitted",
"cancel_rejected",
"cancel_confirmed",
"cancel_unconfirmed_timeout",
"modify_submitted",
"modify_rejected",
"trailing_register_failed",
"arm_registered",
"arm_fired",
"arm_fire_failed",
"arm_canceled"
],
"mask_token_in_logs": true
},
"telegram": {
"dm_only": true,
"chat_id_whitelist_env": "OPENCLAW_OWNER_CHAT_ID",
@@ -13,7 +13,7 @@ import time
import unittest
from unittest import mock
from orders import fill_watcher
from orders import fill_watcher, ledger
from orders.fill_watcher import Tracked
@@ -39,6 +39,10 @@ class FillWatcherPollTests(unittest.TestCase):
"""_FillWatcher._poll_once 직접 호출로 격리 (시간 의존 X)."""
def setUp(self):
# ⚠️ ledger.append 를 막지 않으면 _poll_once 가 **운영 원장**
# (state/order_log.jsonl)에 테스트 이벤트를 실제로 쓴다.
mock.patch.object(ledger, 'append', return_value=None).start()
self.addCleanup(mock.patch.stopall)
fill_watcher._reset_for_test()
self.sent = []
self.fetch = mock.MagicMock(return_value=[])
@@ -130,6 +134,10 @@ class CancelWatcherTests(unittest.TestCase):
"""cancel kind 회귀 — ka10075 폴링으로 원주문이 사라지면 확정."""
def setUp(self):
# ⚠️ ledger.append 를 막지 않으면 _poll_once 가 **운영 원장**
# (state/order_log.jsonl)에 테스트 이벤트를 실제로 쓴다.
mock.patch.object(ledger, 'append', return_value=None).start()
self.addCleanup(mock.patch.stopall)
fill_watcher._reset_for_test()
self.sent = []
self.fetch_exec = mock.MagicMock(return_value=[])
@@ -243,6 +251,10 @@ class FillWatcherQueueIOTests(unittest.TestCase):
"""큐 파일 read/append/persist."""
def setUp(self):
# ⚠️ ledger.append 를 막지 않으면 _poll_once 가 **운영 원장**
# (state/order_log.jsonl)에 테스트 이벤트를 실제로 쓴다.
mock.patch.object(ledger, 'append', return_value=None).start()
self.addCleanup(mock.patch.stopall)
fill_watcher._reset_for_test()
self.addCleanup(fill_watcher._reset_for_test)
@@ -303,6 +315,10 @@ class FillWatcherWatchEntryTests(unittest.TestCase):
"""watch() = 큐 append + 데몬 ensure_running. subprocess.Popen mock."""
def setUp(self):
# ⚠️ ledger.append 를 막지 않으면 _poll_once 가 **운영 원장**
# (state/order_log.jsonl)에 테스트 이벤트를 실제로 쓴다.
mock.patch.object(ledger, 'append', return_value=None).start()
self.addCleanup(mock.patch.stopall)
fill_watcher._reset_for_test()
self.addCleanup(fill_watcher._reset_for_test)
# subprocess.Popen mock — 실제 데몬 fork 막음
@@ -384,6 +400,10 @@ class FillWatcherDaemonAliveTests(unittest.TestCase):
"""is_daemon_alive — PID 파일 stale 검출."""
def setUp(self):
# ⚠️ ledger.append 를 막지 않으면 _poll_once 가 **운영 원장**
# (state/order_log.jsonl)에 테스트 이벤트를 실제로 쓴다.
mock.patch.object(ledger, 'append', return_value=None).start()
self.addCleanup(mock.patch.stopall)
fill_watcher._reset_for_test()
self.addCleanup(fill_watcher._reset_for_test)
@@ -412,6 +432,10 @@ class FillWatcherSyncFromQueueTests(unittest.TestCase):
"""sync_from_queue — 큐 → _tracked 양방향 동기화."""
def setUp(self):
# ⚠️ ledger.append 를 막지 않으면 _poll_once 가 **운영 원장**
# (state/order_log.jsonl)에 테스트 이벤트를 실제로 쓴다.
mock.patch.object(ledger, 'append', return_value=None).start()
self.addCleanup(mock.patch.stopall)
fill_watcher._reset_for_test()
self.addCleanup(fill_watcher._reset_for_test)
@@ -61,8 +61,14 @@ class SessionTests(unittest.TestCase):
def test_nxt_after(self):
self.assertEqual(guards.session_at(t(15, 30)), 'NXT_AFTER')
self.assertEqual(guards.session_at(t(18, 0)), 'NXT_AFTER')
self.assertEqual(guards.session_at(t(19, 59, 59)), 'NXT_AFTER')
self.assertEqual(guards.session_at(t(15, 59, 59)), 'NXT_AFTER')
def test_krx_aftermarket(self):
"""16:00~20:00 = KRX 애프터마켓 + NXT 애프터마켓 (2026-09-14 시행)."""
self.assertEqual(guards.session_at(t(16, 0)), 'AFTER')
self.assertEqual(guards.session_at(t(18, 0)), 'AFTER')
self.assertEqual(guards.session_at(t(19, 59, 59)), 'AFTER')
self.assertEqual(guards.session_at(t(20, 0)), 'CLOSED')
def test_closed_after_after(self):
self.assertEqual(guards.session_at(t(20, 0)), 'CLOSED')
@@ -94,9 +100,13 @@ class TradingHoursTests(unittest.TestCase):
self.assertEqual(r.code, 'NXT_NOT_ELIGIBLE')
def test_nxt_after_not_eligible(self):
r = guards.validate_trading_hours(t(18, 0), False, False)
r = guards.validate_trading_hours(t(15, 45), False, False)
self.assertEqual(r.code, 'NXT_NOT_ELIGIBLE')
def test_aftermarket_allows_non_nxt(self):
"""애프터마켓엔 KRX 가 NXT 미상장 종목을 받는다 — 여기서 막으면 안 된다."""
self.assertTrue(guards.validate_trading_hours(t(18, 0), False, False).ok)
def test_krx_close_no_nxt_required(self):
self.assertTrue(guards.validate_trading_hours(t(15, 25), False, False).ok)
@@ -114,7 +124,12 @@ class RoutingTests(unittest.TestCase):
self.assertEqual(guards.determine_routing(t(8, 30), True, None), '_NX')
def test_nxt_after(self):
self.assertEqual(guards.determine_routing(t(18, 0), True, None), '_NX')
self.assertEqual(guards.determine_routing(t(15, 45), True, None), '_NX')
def test_aftermarket_routes_to_krx(self):
"""애프터마켓은 검증된 KRX 경로만 쓴다 — SOR 주문 라우팅은 미검증."""
self.assertEqual(guards.determine_routing(t(18, 0), True, None), '')
self.assertEqual(guards.determine_routing(t(18, 0), False, None), '')
def test_krx_close(self):
self.assertEqual(guards.determine_routing(t(15, 25), False, None), '')
@@ -133,6 +148,31 @@ class RoutingTests(unittest.TestCase):
guards.determine_routing(t(21, 0), True, None)
class StopRoutingTests(unittest.TestCase):
"""스톱 계열은 SOR(_AL) 로 못 낸다 — 키움 407022 (2026-09-21 삼성전자 실측)."""
def test_stop_types_downgrade_al_to_krx(self):
for ot in ('STOP_LIMIT', 'TRAILING_STOP', 'TRAILING_ARM'):
self.assertEqual(guards.determine_routing(t(10, 0), True, None, ot), '', ot)
def test_force_al_also_downgraded(self):
# force 는 사람의 명시 지정이지만 SOR 자체가 스톱을 안 받는다
self.assertEqual(guards.determine_routing(t(10, 0), True, 'AL', 'STOP_LIMIT'), '')
def test_nxt_session_kept(self):
# NXT 단독 시간대는 KRX 가 닫혀 강등할 곳이 없다 — 시도는 그대로 둔다
self.assertEqual(guards.determine_routing(t(8, 30), True, None, 'STOP_LIMIT'), '_NX')
self.assertEqual(guards.determine_routing(t(15, 45), True, None, 'STOP_LIMIT'), '_NX')
def test_non_stop_types_unaffected(self):
self.assertEqual(guards.determine_routing(t(10, 0), True, None, 'LIMIT'), '_AL')
self.assertEqual(guards.determine_routing(t(10, 0), True, None, 'MARKET'), '_AL')
self.assertEqual(guards.determine_routing(t(10, 0), True, None, None), '_AL')
def test_non_nxt_symbol_already_krx(self):
self.assertEqual(guards.determine_routing(t(10, 0), False, None, 'STOP_LIMIT'), '')
# ---------- 가격 가드 ----------
class PriceBandTests(unittest.TestCase):
@@ -0,0 +1,248 @@
"""예약 트레일링(TRAILING_ARM) propose·승인 경로 회귀 테스트 (2026-08-25).
datasource·sidecar·pin_store·ledger 를 mock 해서 키움을 전혀 건드리지 않는다.
가장 중요한 확인은 **거부 케이스에서 카드가 발행되지 않는 것** — 카드가 나가면 PIN 한 번으로
발동 조건까지 확정되므로, 걸러야 할 입력이 카드 단계를 통과하면 안 된다.
"""
from __future__ import annotations
import shutil
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest import mock
from orders import handler, ledger
from orders import trailing as t
KST = timezone(timedelta(hours=9))
STEPS = [{'pct': 10, 'cum': 20}, {'pct': 20, 'cum': 50}, {'pct': 30, 'cum': 100}]
def _md(cur=70000):
return {
'now': datetime(2026, 8, 25, 10, 0, tzinfo=KST),
'is_holiday': False,
'nxt_eligible': True,
'current_price': cur,
'prev_close': cur,
'upper_limit': int(cur * 1.3),
'lower_limit': int(cur * 0.7),
'halt': False,
'vi': False,
'orderbook': {'asks': [{'price': cur + 100, 'qty': 200}],
'bids': [{'price': cur, 'qty': 200}]},
'broker_executions': [],
'broker_query_error': None,
'position_qty': 100,
'balance_d2': 5_000_000,
'stock_meta': None,
}
class ArmProposeTests(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp()) / 'trailing_stops.json'
self._orig = (t.STATE_FILE, t._LOCK_FILE)
t.STATE_FILE = self.tmp
t._LOCK_FILE = self.tmp.with_suffix('.json.lock')
self.collect = mock.patch.object(handler.datasource, 'collect_market_data',
return_value=_md()).start()
mock.patch.object(handler.sidecar, 'guard_or_raise', return_value=None).start()
mock.patch.object(handler, '_sweep_expired_and_notify', return_value=None).start()
mock.patch.object(handler.ledger, 'append', return_value=None).start()
mock.patch.object(ledger, 'last_terminal_event', return_value=None).start()
mock.patch.object(ledger, 'last_event_for_symbol', return_value=None).start()
fake = mock.MagicMock()
fake.card_id, fake.pin, fake.expiry_seconds, fake.account_label = 'TEST', '1234', 120, '일반'
self.issue = mock.patch.object(handler._pin_store, 'issue', return_value=fake).start()
self.addCleanup(mock.patch.stopall)
def tearDown(self):
t.STATE_FILE, t._LOCK_FILE = self._orig
shutil.rmtree(self.tmp.parent, ignore_errors=True)
def _propose(self, **kw):
base = dict(account='일반', side='SELL', symbol='005930', symbol_name='삼성전자',
qty=100, order_type='TRAILING_ARM', trail_steps=STEPS,
trigger_price=80000)
base.update(kw)
return handler.propose_trade(**base)
def _assert_rejected(self, res, code):
self.assertFalse(res['ok'], msg=res.get('message'))
self.assertIn(code, res['message'])
self.issue.assert_not_called() # 거부는 카드를 남기지 않아야 한다
# ── 통과 경로 ──
def test_valid_arm_issues_card_with_definition_only(self):
res = self._propose(min_sell_price=60000)
self.assertTrue(res['ok'], msg=res.get('message'))
payload = self.issue.call_args[0][1]
self.assertEqual(payload['order_type'], 'TRAILING_ARM')
self.assertEqual(payload['trigger_price'], 80000)
self.assertIsNone(payload['price']) # 나가는 주문이 없다
# trail_steps 는 정의만, 미리보기는 별도 키 — 발동 경로가 옛 가격을 재사용하지 못하게.
self.assertNotIn('cond_uv', payload['trail_steps'][0])
self.assertIn('cond_uv', payload['trail_preview'][0])
def test_preview_uses_trigger_not_current_price(self):
res = self._propose()
self.assertTrue(res['ok'], msg=res.get('message'))
payload = self.issue.call_args[0][1]
expect = t.compute_step_levels(80000, t.normalize_steps(STEPS), None)
self.assertEqual([s['cond_uv'] for s in payload['trail_preview']],
[s['cond_uv'] for s in expect])
def test_trigger_above_todays_upper_limit_is_allowed(self):
"""발동가는 가격제한폭 검증을 받지 않는다 — 실제 발주는 발동 시점 현재가로 계산된다.
arm 이 당일 유효가 된 뒤로는 '오늘 상한가 초과 = 오늘 도달 불가'지만 **막지 않고
경고만** 한다(2026-08-25 관리자님 결정). 통과가 의도된 동작이라 고정해 둔다.
"""
res = self._propose(trigger_price=200000) # 현재가 70,000 · 상한가 91,000
self.assertTrue(res['ok'], msg=res.get('message'))
def test_trigger_above_upper_limit_warns_on_card(self):
"""막지는 않되 승인 전에 '오늘 도달 불가'를 반드시 알린다."""
res = self._propose(trigger_price=200000)
self.assertTrue(res['ok'], msg=res.get('message'))
self.assertIn('오늘 상한가', res['card_message'])
self.assertIn('만료', res['card_message'])
# 상한가 이내면 경고가 없어야 한다 (남발 금지)
res2 = self._propose(trigger_price=85000) # 상한가 91,000 이내
self.assertTrue(res2['ok'], msg=res2.get('message'))
self.assertNotIn('오늘 상한가', res2['card_message'])
# ── 거부 경로 ──
def test_buy_rejected(self):
self._assert_rejected(self._propose(side='BUY'), 'ARM_SELL_ONLY')
def test_missing_trigger_rejected(self):
self._assert_rejected(self._propose(trigger_price=None), 'ARM_TRIGGER')
def test_zero_trigger_rejected(self):
self._assert_rejected(self._propose(trigger_price=0), 'ARM_TRIGGER')
def test_already_reached_rejected(self):
self._assert_rejected(self._propose(trigger_price=70000), 'ARM_ALREADY_REACHED')
self._assert_rejected(self._propose(trigger_price=65000), 'ARM_ALREADY_REACHED')
def test_floor_above_trigger_rejected(self):
self._assert_rejected(self._propose(min_sell_price=85000), 'ARM_IMMEDIATE')
def test_budget_rejected(self):
self._assert_rejected(self._propose(qty=None, budget=1_000_000), 'TRAIL_BUDGET')
def test_bad_steps_rejected(self):
self._assert_rejected(self._propose(trail_steps=[{'pct': 20, 'cum': 20},
{'pct': 10, 'cum': 50}]), 'TRAIL_STEPS')
def test_qty_too_small_rejected(self):
# qty=0 은 '미입력'(None)이 아니라 '값 0' 이라 MISSING_QTY 를 지나 계단 배분에서 걸린다.
self._assert_rejected(self._propose(qty=0), 'TRAIL_QTY')
def test_no_qty_rejected(self):
self._assert_rejected(self._propose(qty=None), 'MISSING_QTY')
def test_duplicate_arm_rejected(self):
t.register_arm(account='일반', symbol='005930', symbol_name='삼성전자', qty=10,
trigger_price=90000, steps=t.normalize_steps(STEPS))
self._assert_rejected(self._propose(), 'TRAIL_DUPLICATE')
def test_existing_trailing_blocks_arm(self):
"""같은 계좌·종목에 활성 트레일링이 있으면 예약도 막는다 — 같은 주식을 두 번 팔 수 없다."""
st = t.normalize_steps(STEPS)
lv = t.compute_step_levels(70000, st, None)
qtys = t.allocate_step_qty(100, [s['weight'] for s in st])
legs = [dict(s, qty=q, ord_no=f'O{s["n"]}') for s, q in zip(lv, qtys)]
t.register_steps(account='일반', symbol='005930', symbol_name='삼성전자',
total_qty=100, peak=70000, steps=legs)
self._assert_rejected(self._propose(), 'TRAIL_DUPLICATE')
def test_arm_blocks_plain_trailing_too(self):
"""반대 방향도 막혀야 한다 — 대기 예약이 있으면 일반 트레일링을 새로 걸 수 없다."""
t.register_arm(account='일반', symbol='005930', symbol_name='삼성전자', qty=10,
trigger_price=90000, steps=t.normalize_steps(STEPS))
res = handler.propose_trade(account='일반', side='SELL', symbol='005930',
symbol_name='삼성전자', qty=100,
order_type='TRAILING_STOP', trail_steps=STEPS)
self._assert_rejected(res, 'TRAIL_DUPLICATE')
def test_holiday_now_actually_blocks(self):
"""guards.is_today_holiday 의 dict 미인식 버그를 고친 뒤라야 이게 막힌다."""
self.collect.return_value = dict(_md(), is_holiday=True)
res = self._propose()
self.assertFalse(res['ok'], msg=res.get('message'))
self.assertIn('HOLIDAY', res['message'])
class ArmRegisterTests(unittest.TestCase):
"""PIN 승인 → 대기 예약 등록. 키움 호출이 0건이어야 한다."""
def setUp(self):
self.tmp = Path(tempfile.mkdtemp()) / 'trailing_stops.json'
self._orig = (t.STATE_FILE, t._LOCK_FILE)
t.STATE_FILE = self.tmp
t._LOCK_FILE = self.tmp.with_suffix('.json.lock')
mock.patch.object(handler.ledger, 'append', return_value=None).start()
self.submit = mock.patch.object(handler.kiwoom_order, 'submit').start()
self.addCleanup(mock.patch.stopall)
self.card = mock.MagicMock()
self.card.card_id = 'AB3D'
self.card.payload = {
'account': '일반', 'side': 'SELL', 'symbol': '005930', 'symbol_name': '삼성전자',
'qty': 100, 'price': None, 'order_type': 'TRAILING_ARM', 'routing_suffix': '_AL',
'trigger_price': 80000, 'trail_steps': t.normalize_steps(STEPS),
'trail_preview': [], 'min_sell_price': 60000,
}
def tearDown(self):
t.STATE_FILE, t._LOCK_FILE = self._orig
shutil.rmtree(self.tmp.parent, ignore_errors=True)
def test_register_places_no_order(self):
res = handler._register_trailing_arm(self.card, dry_run=False)
self.assertTrue(res['ok'], msg=res.get('message'))
self.submit.assert_not_called()
arms = t.list_arms()
self.assertEqual(len(arms), 1)
self.assertEqual((arms[0]['trigger_price'], arms[0]['card_id']), (80000, 'AB3D'))
self.assertIn('발동가에 닿으면 PIN 재확인 없이', res['message'])
def test_dry_run_writes_nothing(self):
res = handler._register_trailing_arm(self.card, dry_run=True)
self.assertTrue(res['ok'])
self.assertEqual(t.list_arms(), [])
self.submit.assert_not_called()
def test_cancel_removes_arm(self):
handler._register_trailing_arm(self.card, dry_run=False)
arm_id = t.list_arms()[0]['id']
with mock.patch.object(handler.sidecar, 'guard_or_raise', return_value=None):
res = handler.cancel_trailing_arm(arm_id)
self.assertTrue(res['ok'])
self.assertEqual(t.list_arms(), [])
def test_cancel_warns_about_orphan_orders(self):
"""발동 중 접수된 주문이 있으면 자동 취소하지 않고 주문번호를 알려준다."""
handler._register_trailing_arm(self.card, dry_run=False)
arm_id = t.list_arms()[0]['id']
t.mark_arm_firing(arm_id)
t.record_arm_leg(arm_id, {'n': 1, 'qty': 20, 'cond_uv': 72000, 'ord_uv': 71900,
'ord_no': '0374065'})
with mock.patch.object(handler.sidecar, 'guard_or_raise', return_value=None):
res = handler.cancel_trailing_arm(arm_id)
self.assertTrue(res['ok'])
self.assertIn('0374065', res['message'])
self.assertIn('별도로 취소', res['message'])
if __name__ == '__main__':
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
unittest.main(verbosity=2)
@@ -98,3 +98,46 @@ class SubmitResponseTests(unittest.TestCase):
if __name__ == '__main__':
unittest.main(verbosity=2)
class IdempotencyMatchTests(unittest.TestCase):
"""멱등성은 '이중 발주 위험이 실재하는' 이벤트만 본다.
2026-09-21: SOR 스톱 거부 직후 같은 주문을 다시 냈더니 IDEMPOTENCY_DUP 으로 또 막혔다.
키움이 거부한 주문은 접수되지 않은 게 확정이라 재시도를 막을 이유가 없다.
"""
def setUp(self):
import tempfile
from pathlib import Path
self.tmp = Path(tempfile.mkdtemp()) / 'order_log.jsonl'
mock.patch.object(ledger, '_ledger_path', return_value=self.tmp).start()
self.addCleanup(mock.patch.stopall)
def _write(self, event: str, h: str = 'HASH1'):
import json
from datetime import datetime
with self.tmp.open('a', encoding='utf-8') as f:
f.write(json.dumps({'ts': datetime.now(ledger.KST).isoformat(),
'event': event, 'payload': {'idem_hash': h}}) + '\n')
def test_submitted_blocks(self):
self._write('submitted')
self.assertIsNotNone(ledger.find_recent_idempotency('HASH1'))
def test_network_failure_blocks(self):
"""나갔는지 모르는 상태 — 가장 위험하므로 반드시 차단."""
self._write('failed')
self.assertIsNotNone(ledger.find_recent_idempotency('HASH1'))
def test_broker_reject_does_not_block(self):
self._write('rejected')
self.assertIsNone(ledger.find_recent_idempotency('HASH1'))
def test_dryrun_does_not_block(self):
self._write('dryrun')
self.assertIsNone(ledger.find_recent_idempotency('HASH1'))
def test_other_hash_ignored(self):
self._write('submitted', h='OTHER')
self.assertIsNone(ledger.find_recent_idempotency('HASH1'))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,803 @@
"""트레일링 스톱 예약 상태 관리 (계단식 분할 매도).
키움에 REST 트레일링 주문 TR 이 없어서, 스톱지정가(trde_tp=28) 주문을 실제로 걸어두고
고점이 갱신될 때마다 조건단가를 kt10002 정정으로 올려 트레일링을 구현한다.
**예약 1건 = 계단(레그) N개 = 키움 스톱주문 N건.** 고점 대비 하락률이 깊어질수록 더 많이
파는 계단식 청산이다 (예: −10% 20% / −20% 50% / −30% 전량). 계단별로 수량과 조건단가가
다른 스톱주문을 동시에 걸어두고, 고점이 오르면 살아있는 레그를 전부 상향 정정한다.
주문이 키움 서버에 있으므로 감시 루프가 죽어도 계단별 손절선은 살아있다.
감시 루프가 하는 일은 "이미 승인된 주문의 조건단가 상향"뿐 — 신규 발주·수량 변경 없음.
⚠️ 계단 비중은 관리자님이 **누적**으로 입력하고(20/50/100 = 전량), 내부에는 계단별
**추가분**(20/30/50)으로 환산해 저장한다. 주문 수량이 곧 추가분이기 때문이다.
⚠️ 한 계단이 체결된 뒤 반등해 신고점을 찍어도 **남은 계단만 새 고점 기준으로 따라 올린다**.
남은 수량을 계단 비율대로 재배치하려면 기존 주문 취소 + 신규 발주가 필요한데, 그러면
감시 루프에 자동 발주 경로가 생긴다(매매 자동 트리거 금지). 2026-07-31 관리자님 결정.
⚠️ kt10002 정정 응답의 ord_no 는 신규 주문번호다. 정정할 때마다 ord_no 를 갱신하지 않으면
두 번째 정정부터 orig_ord_no 가 틀려서 전부 실패한다. commit_step_modify 가 이걸 담당.
상태는 파일(state/trailing_stops.json)에 저장 — 웹·감시 루프가 각각 다른 프로세스라
같은 예약 목록을 봐야 한다. 동시성은 fcntl flock 으로 직렬화 (pin.py 와 같은 패턴).
── 대기 예약 (arms) — 예약 트레일링, 2026-08-25 ──────────────────────────────
같은 파일에 `arms` 키가 하나 더 있다. **발동가에 닿으면 그때 트레일링을 시작하는 예약**으로,
`reservations`(이미 키움에 주문이 나간 것)와 성질이 정반대다:
reservations : 주문이 키움에 있음 → 감시 루프가 죽어도 손절선은 살아있음, 장 마감에 소멸
arms : 주문이 없고 이 파일에만 있음 → 감시가 죽으면 발동 안 함, **당일만 유효**
⚠️ **arm 도 스톱주문과 같은 수명(등록일 당일)이다** — 2026-08-25 관리자님 결정으로 무기한에서
바뀌었다. 거래소가 지워주는 스톱주문과 달리 arm 은 우리 파일에만 있으니 우리가 지운다:
`is_arm_expired` 판정 + 감시 루프의 다음 거래일 첫 사이클 정리.
여기에 세 번째 키 `recent` 가 더 있다 — 장 마감 소멸·만료된 예약의 설정을 짧게 보관해
자산웹이 "같은 설정으로 다시 등록" 버튼을 띄울 수 있게 하는 **UI 편의용 링버퍼**다.
감사 기록이 아니다(그쪽은 state/order_log.jsonl, 무한 append).
⚠️ **두 갈래를 한 파일에 둔 이유는 발동이 "arm 제거 + 예약 등록"이라는 하나의 원자적
전이여야 해서다.** promote_arm 이 락 하나 안에서 os.replace 1회로 끝낸다. 파일을 쪼개면
락을 중첩해야 하고(데드락), 중간에 죽으면 둘 다 존재하거나 둘 다 사라진다.
⚠️ 그 대가로 **_write 는 None 인 쪽을 디스크에서 다시 읽어 보존**한다. 이 계약이 깨지면
한쪽 쓰기가 나머지 두 갈래를 조용히 전멸시킨다 — reservations 쪽 쓰기만 6곳이다.
`_read_doc` 이 세 키를 하드 정규화하므로 **키를 늘릴 때 _read_doc 과 _write 를 반드시
같이** 고쳐야 한다 (테스트로 고정해 둠).
⚠️ arm 의 steps 에는 **가격·주수를 저장하지 않는다**(정의 n/pct/cum/weight 만). routing_suffix
도 저장하지 않는다. 갭 상승으로 발동가를 훌쩍 넘겨 열릴 수 있고, 무기한 대기라 등록 시점
세션(KRX)과 발동 시점 세션(NXT)이 달라 옛 suffix 로 발주하면 거부된다. 둘 다 발동 시점에
plan_arm_fire / guards.determine_routing 으로 새로 계산한다.
"""
from __future__ import annotations
import fcntl
import json
import os
import secrets
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional
from .guards import after_trading_day, tick_size
WORKSPACE_ROOT = Path(__file__).resolve().parent.parent.parent
STATE_FILE = WORKSPACE_ROOT / 'state' / 'trailing_stops.json'
KST = timezone(timedelta(hours=9))
# 트레일 폭 허용 범위 (%). 너무 좁으면 정상 출렁임에 즉시 털리고, 너무 넓으면 스톱 의미가 없다.
# ⚠️ 상한을 바꾸면 **세 곳**이 같이 움직여야 한다 — 여기(서버 검증) /
# behive_web 트레일 폭 드롭다운 옵션 / behive_web `trailBlockReason` 의 범위 체크.
# 하나만 고치면 드롭다운에서 고를 수 있는데 서버가 거부하거나 그 반대가 된다.
MIN_TRAIL_PCT = 0.5
MAX_TRAIL_PCT = 30.0
# 지정가(ord_uv) = 조건단가(cond_uv) 보다 이 틱수만큼 아래.
# 조건단가와 같게 두면 발동 후 그 가격 아래로는 안 팔려 미체결로 남는다.
# 반대로 너무 벌리면 급락 시 헐값 매도가 되니 2틱.
ORD_UV_GAP_TICKS = 2
# 계단 개수 상한. 고점이 오를 때마다 살아있는 레그를 전부 정정하므로 정정 콜이 계단 수에
# 비례한다 (예약 3건 × 5계단 = 분당 15콜). 키움 rate limit 여유를 보고 5로 둔다.
MAX_STEPS = 5
# 혼동되는 글자(0/O, 1/I/l)를 뺀 영숫자 — 사람이 눈으로 읽고 옮겨 적는 id 라서.
_ID_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
def _now_iso() -> str:
return datetime.now(KST).isoformat(timespec='seconds')
def _new_id(prefix: str) -> str:
return prefix + ''.join(secrets.choice(_ID_CHARS) for _ in range(4))
def floor_to_tick(price: int) -> int:
"""호가단위로 내림. 매도 조건단가·지정가는 내림이 안전(더 빨리 발동/체결)."""
t = tick_size(price)
return price - (price % t)
def compute_levels(peak: int, trail_pct: float, min_sell_price: Optional[int] = None) -> dict:
"""고점·트레일 폭(·최저 매도가) → 조건단가(트리거)·지정가(체결가).
cond_uv = max(peak × (1 − pct/100), min_sell_price) 를 호가단위 내림
ord_uv = cond_uv − ORD_UV_GAP_TICKS 틱
min_sell_price(최저 매도가) = 손절선의 하한. 트레일 폭을 넓게 잡아도 이 가격 아래로는
손절선이 내려가지 않는다 — 초기 구간 손실 제한용. 고점이 올라 트레일 손절선이 이 값을
추월하면 그 뒤로는 트레일이 지배한다(최저가는 자연히 무의미해짐).
"""
if peak <= 0:
raise ValueError(f'peak must be > 0 (got {peak})')
if not (MIN_TRAIL_PCT <= trail_pct <= MAX_TRAIL_PCT):
raise ValueError(f'trail_pct out of range: {trail_pct}')
# round(_, 6) 은 부동소수점 잡음만 걷어낸다 — 5500×(1−30/100) 이 3849.9999999999995 로
# 나오는 탓에 int() 가 3849 로 깎고 호가단위 내림이 3845 까지 끌어내리던 버그가 있었다
# (의도는 3850). 잡음을 없앤 뒤 내림하므로 "정확히 그 폭 아래" 성격은 그대로다.
cond_uv = floor_to_tick(int(round(peak * (1 - trail_pct / 100), 6)))
floor_applied = False
if min_sell_price:
if min_sell_price <= 0:
raise ValueError(f'min_sell_price must be > 0 (got {min_sell_price})')
floor_uv = floor_to_tick(int(min_sell_price))
if floor_uv > cond_uv:
cond_uv = floor_uv
floor_applied = True
ord_uv = floor_to_tick(cond_uv - ORD_UV_GAP_TICKS * tick_size(cond_uv))
if cond_uv <= 0 or ord_uv <= 0:
raise ValueError(f'computed level <= 0 (peak={peak}, pct={trail_pct})')
return {'cond_uv': cond_uv, 'ord_uv': ord_uv, 'floor_applied': floor_applied}
def normalize_steps(raw: list) -> list:
"""입력한 계단 정의(누적 비중)를 내부 표현(추가 비중)으로 환산·검증한다.
입력 [{'pct': 10, 'cum': 20}, {'pct': 20, 'cum': 50}, {'pct': 30, 'cum': 100}]
출력 [{'n': 1, 'pct': 10.0, 'cum': 20.0, 'weight': 20.0}, ... weight 30, 50]
cum(누적 비중)은 "이 하락률에 닿았을 때 최초 보유의 몇 %가 팔려 있어야 하는가"다.
주문 수량은 계단마다의 추가분이므로 weight = cum − 직전 cum 으로 환산한다.
마지막 cum 이 100 미만이어도 허용한다(일부만 계단 청산하고 나머지는 계속 보유).
남는 수량은 스톱이 걸리지 않으므로 호출측이 화면에 표시해 준다.
"""
if not raw:
raise ValueError('계단이 비어 있음')
if len(raw) > MAX_STEPS:
raise ValueError(f'계단은 최대 {MAX_STEPS}개 (입력 {len(raw)}개)')
steps = []
prev_pct = 0.0
prev_cum = 0.0
for i, s in enumerate(raw, start=1):
pct = float(s['pct'])
cum = float(s['cum'])
if not (MIN_TRAIL_PCT <= pct <= MAX_TRAIL_PCT):
raise ValueError(f'{i}단계 하락률 {pct}% 는 {MIN_TRAIL_PCT}~{MAX_TRAIL_PCT}% 범위를 벗어남')
if pct <= prev_pct:
raise ValueError(f'{i}단계 하락률 {pct}% 가 앞 계단({prev_pct}%)보다 깊지 않음')
if cum <= prev_cum:
raise ValueError(f'{i}단계 누적 비중 {cum}% 가 앞 계단({prev_cum}%)보다 크지 않음')
if cum > 100:
raise ValueError(f'{i}단계 누적 비중 {cum}% 가 100% 를 넘음')
steps.append({'n': i, 'pct': pct, 'cum': cum, 'weight': cum - prev_cum})
prev_pct, prev_cum = pct, cum
return steps
def allocate_step_qty(total_qty: int, weights: list) -> list:
"""계단별 추가 비중(%) → 정수 주수. 최대잔여법(Hare quota).
내림으로 나눈 뒤 남는 주를 소수부가 큰 계단부터 1주씩 배분한다.
"내림 후 마지막 계단에 몰아주기" 보다 얕은 계단이 0주로 죽는 일이 적다
(2주 · 20/30/50 → 최대잔여 [0,1,1] 2계단 생존 / 몰아주기 [0,0,2] 1계단).
수량이 적어 0주가 되는 계단은 그대로 0 을 반환한다 — 발주할 수 없으므로
호출측이 걸러내고 "몇 계단으로 줄었는지" 를 사용자에게 알린다.
"""
if total_qty <= 0 or not weights:
return [0] * len(weights)
exact = [total_qty * w / 100.0 for w in weights]
base = [int(x) for x in exact]
remain = int(round(sum(exact))) - sum(base)
order = sorted(range(len(weights)), key=lambda i: exact[i] - base[i], reverse=True)
for i in order[:max(0, remain)]:
base[i] += 1
return base
def compute_step_levels(peak: int, steps: list, min_sell_price: Optional[int] = None) -> list:
"""고점 + 계단 정의 → 계단별 조건단가·지정가.
최저 매도가는 모든 계단에 같은 하한으로 걸린다. 그래서 초기에는 여러 계단이 같은
가격으로 뭉칠 수 있다(전부 최저 매도가). 그래도 주문을 병합하지 않는 이유는, 고점이
올라 트레일 손절선이 최저 매도가를 추월하면 각 계단이 제 하락률대로 다시 벌어지기
때문이다 — 병합해 버리면 정정만으로는 다시 쪼갤 수 없다(신규 발주가 필요해진다).
"""
out = []
for s in steps:
lv = compute_levels(peak, s['pct'], min_sell_price)
out.append({**s, 'cond_uv': lv['cond_uv'], 'ord_uv': lv['ord_uv'],
'floor_applied': lv['floor_applied']})
return out
def next_step_levels(res: dict, cur_price: int) -> Optional[dict]:
"""현재가가 고점을 넘었으면 새 고점과 상향 대상 레그를 반환 (순수 함수 — 단위테스트 대상).
반환 {'peak': 새 고점, 'steps': [{'n', 'cond_uv', 'ord_uv'}, ...]}.
고점이 안 올랐으면 None.
steps 가 빈 리스트일 수 있다 — 고점은 올랐지만 호가단위 내림이나 최저 매도가 지배로
조건단가가 하나도 안 올라간 경우다. 이때 호출측은 정정 API 없이 고점만 갱신한다
(불필요한 콜 0, 화면의 고점 표시는 정확하게 유지).
"""
if cur_price <= 0 or cur_price <= res['peak']:
return None
ups = []
for s in res.get('steps') or []:
lv = compute_levels(cur_price, s['pct'], res.get('min_sell_price'))
if lv['cond_uv'] > s['cond_uv']:
ups.append({'n': s['n'], 'cond_uv': lv['cond_uv'], 'ord_uv': lv['ord_uv']})
return {'peak': cur_price, 'steps': ups}
class _FileLock:
def __init__(self, path: Path):
self.path = path
self._fp = None
def __enter__(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
self._fp = open(self.path, 'w')
fcntl.flock(self._fp, fcntl.LOCK_EX)
return self
def __exit__(self, *args):
try:
fcntl.flock(self._fp, fcntl.LOCK_UN)
finally:
self._fp.close()
_LOCK_FILE = STATE_FILE.with_suffix(STATE_FILE.suffix + '.lock')
def _read_doc() -> dict:
"""상태파일 전체. `reservations`(키움에 주문이 나가 있는 예약) 와
`arms`(발동가 도달을 기다리는 대기 예약) 두 갈래가 한 파일에 들어 있다.
한 파일에 둔 이유는 발동이 "arm 제거 + 예약 등록" 이라는 하나의 원자적 전이이기
때문이다. 파일이 둘이면 락을 중첩해야 하고, 중간에 죽으면 둘 다 존재하거나 둘 다
사라진다.
"""
empty = {'reservations': [], 'arms': [], 'recent': []}
if not STATE_FILE.exists():
return empty
try:
data = json.loads(STATE_FILE.read_text(encoding='utf-8'))
except (OSError, ValueError):
return empty
if not isinstance(data, dict):
return empty
return {'reservations': data.get('reservations') or [],
'arms': data.get('arms') or [],
'recent': data.get('recent') or []}
def _read() -> list:
return _read_doc()['reservations']
def _read_arms() -> list:
return _read_doc()['arms']
def _read_recent() -> list:
return _read_doc()['recent']
def _write(reservations: Optional[list] = None, arms: Optional[list] = None,
recent: Optional[list] = None) -> None:
"""None 인 쪽은 디스크의 현재 값을 유지한다.
⚠️ 세 갈래가 한 파일에 있으므로 한쪽만 쓰는 호출이 다른 쪽을 지우면 안 된다.
반드시 _FileLock 안에서 부를 것 — 여기서 다시 읽어 병합한다.
"""
doc = _read_doc()
if reservations is not None:
doc['reservations'] = reservations
if arms is not None:
doc['arms'] = arms
if recent is not None:
doc['recent'] = recent
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = STATE_FILE.with_suffix(STATE_FILE.suffix + '.tmp')
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding='utf-8')
os.replace(tmp, STATE_FILE)
def list_active() -> list:
"""등록된 예약 전체 (락 없이 읽기 — 감시 루프의 스냅샷용)."""
return _read()
def get(res_id: str) -> Optional[dict]:
for r in _read():
if r['id'] == res_id:
return r
return None
def find_by_symbol(account: str, symbol: str) -> Optional[dict]:
"""같은 계좌·종목의 기존 예약. 중복 등록 차단용."""
for r in _read():
if r['account'] == account and r['symbol'] == symbol:
return r
return None
def _build_reservation(account: str, symbol: str, symbol_name: str, total_qty: int,
peak: int, steps: list, routing_suffix: str,
card_id: Optional[str], min_sell_price: Optional[int]) -> dict:
"""접수된 레그 목록 → 예약 dict (파일 접근 없는 순수 조립).
register_steps(등록 즉시 트레일링)와 promote_arm(대기 예약 발동)이 공유한다.
"""
if not steps:
raise ValueError('등록할 계단이 없음 — 접수 성공한 레그가 하나도 없다')
for s in steps:
if not s.get('ord_no'):
raise ValueError(f'{s.get("n")}단계 ord_no 없음 — 접수 확인된 주문만 등록')
return {
'id': _new_id('TRL-'),
'account': account,
'symbol': symbol,
'symbol_name': symbol_name,
'qty': total_qty,
'min_sell_price': min_sell_price,
'peak': peak,
'steps': [{'n': s['n'], 'pct': s['pct'], 'cum': s['cum'], 'weight': s['weight'],
'qty': s['qty'], 'ord_no': s['ord_no'],
'cond_uv': s['cond_uv'], 'ord_uv': s['ord_uv'],
'entry_cond_uv': s['cond_uv'], 'modify_count': 0} for s in steps],
# 등록 시점 계단 **정의**(가격·주수 없음). steps 는 레그가 체결·소멸할 때마다 줄어들고
# 마지막 레그가 빠지면 비어버려서, 재등록용 원본이 여기 없으면 계단을 복원할 수 없다.
'entry_steps': [{'n': s['n'], 'pct': s['pct'], 'cum': s['cum'], 'weight': s['weight']}
for s in steps],
'routing_suffix': routing_suffix,
'card_id': card_id,
'created_at': _now_iso(),
'updated_at': _now_iso(),
'entry_peak': peak,
}
def register_steps(account: str, symbol: str, symbol_name: str, total_qty: int,
peak: int, steps: list, routing_suffix: str = '',
card_id: Optional[str] = None,
min_sell_price: Optional[int] = None) -> dict:
"""키움에 계단별 스톱주문이 접수된 직후 호출 — 레그 목록을 예약 1건으로 등록.
steps 각 항목은 접수 결과까지 채워져 있어야 한다:
{'n', 'pct', 'cum', 'weight', 'qty', 'ord_no', 'cond_uv', 'ord_uv'}
접수에 실패한 레그는 호출측이 빼고 넘긴다 — 여기 등록된 것은 전부 키움에 살아있는
주문이어야 감시 루프의 생존 판정이 성립한다.
초기 고점(peak)은 등록 시점 현재가다. 과거 고점(52주·매수후) 기준 옵션은 손절선이
현재가 위로 올라가 즉시 발동하는 구조라 2026-07-30 제거됐다.
"""
res = _build_reservation(account, symbol, symbol_name, total_qty, peak, steps,
routing_suffix, card_id, min_sell_price)
with _FileLock(_LOCK_FILE):
reservations = _read()
reservations.append(res)
_write(reservations)
return res
def commit_peak(res_id: str, peak: int) -> Optional[dict]:
"""고점만 갱신 (정정할 레그가 없었던 경우). 화면의 고점 표시를 정확하게 유지한다."""
with _FileLock(_LOCK_FILE):
reservations = _read()
for r in reservations:
if r['id'] == res_id:
r['peak'] = peak
r['updated_at'] = _now_iso()
_write(reservations)
return r
return None
def commit_step_modify(res_id: str, step_n: int, new_ord_no: str, peak: int,
cond_uv: int, ord_uv: int) -> Optional[dict]:
"""레그 정정 성공 후 상태 갱신. new_ord_no 는 kt10002 응답의 신규 주문번호.
⚠️ ord_no 갱신이 이 함수의 핵심 — 안 하면 그 레그의 다음 정정이 전부 실패한다.
고점은 예약 단위라 레그마다 같은 값으로 덮어써도 무해하다.
"""
if not new_ord_no:
raise ValueError('new_ord_no required')
with _FileLock(_LOCK_FILE):
reservations = _read()
for r in reservations:
if r['id'] != res_id:
continue
for s in r.get('steps') or []:
if s['n'] != step_n:
continue
s['ord_no'] = new_ord_no
s['cond_uv'] = cond_uv
s['ord_uv'] = ord_uv
s['modify_count'] = s.get('modify_count', 0) + 1
r['peak'] = peak
r['updated_at'] = _now_iso()
_write(reservations)
return r
return None
return None
def remove_step(res_id: str, step_n: int, reason: str = '') -> Optional[dict]:
"""레그 하나를 예약에서 제거 (체결·취소·소멸). 마지막 레그면 예약 자체를 지운다.
반환 {'step': 제거된 레그, 'reservation': 예약 스냅샷, 'remaining': 남은 레그 수,
'reservation_removed': bool} — 알림 메시지 조립용.
"""
with _FileLock(_LOCK_FILE):
reservations = _read()
for i, r in enumerate(reservations):
if r['id'] != res_id:
continue
steps = r.get('steps') or []
hit = next((s for s in steps if s['n'] == step_n), None)
if hit is None:
return None
steps.remove(hit)
hit['removed_reason'] = reason
snapshot = json.loads(json.dumps(r))
if not steps:
reservations.pop(i)
_write(reservations)
return {'step': hit, 'reservation': snapshot, 'remaining': 0,
'reservation_removed': True}
r['updated_at'] = _now_iso()
_write(reservations)
return {'step': hit, 'reservation': snapshot, 'remaining': len(steps),
'reservation_removed': False}
return None
def should_notify_failure(res_id: str, now_ts: float, cooldown_sec: int) -> bool:
"""정정 실패 알림을 보낼지. 보낸다고 판단하면 그 시점을 기록한다 (락 안에서 원자적).
NXT 시간대에 KRX/SOR 원주문 정정이 거부되면 매 사이클 실패가 반복된다 —
쿨다운 없이 알리면 20:00까지 매분 텔레그램이 쏟아진다.
"""
with _FileLock(_LOCK_FILE):
reservations = _read()
for r in reservations:
if r['id'] != res_id:
continue
last = r.get('last_fail_notice_ts') or 0
if now_ts - last < cooldown_sec:
return False
r['last_fail_notice_ts'] = now_ts
_write(reservations)
return True
return False
def remove(res_id: str, reason: str = '') -> Optional[dict]:
"""예약 종료 (체결·취소·소멸). 반환값은 제거된 예약 — 알림 메시지용."""
with _FileLock(_LOCK_FILE):
reservations = _read()
for i, r in enumerate(reservations):
if r['id'] == res_id:
gone = reservations.pop(i)
_write(reservations)
gone['removed_reason'] = reason
return gone
return None
# ---------------------------------------------------------------------------
# 대기 예약 (arm) — 발동가에 닿으면 그때 트레일링을 등록한다
#
# 트레일링 스톱은 등록 즉시 계단별 스톱주문이 키움에 나가고 그 순간의 현재가가 고점이
# 된다. 아직 오르지 않은 종목엔 그게 불리해서, 발동가를 미리 정해두고 현재가가 거기에
# 올라와 닿는 순간 그때의 현재가를 고점으로 트레일링을 시작하는 갈래를 둔다.
#
# 대기 중인 예약은 키움이 아니라 이 파일에만 있다 — 거래소가 지워주지 않으므로 **우리가**
# 스톱주문과 같은 수명(등록일 당일)을 강제한다(is_arm_expired). 2026-08-25 관리자님 결정.
# 대신 감시 프로세스가 죽어 있으면 발동하지 않는다 — 이건 스톱주문과 정반대 성질이다.
# ---------------------------------------------------------------------------
def list_arms() -> list:
"""대기 예약 전체 (락 없이 읽기 — 감시 루프의 스냅샷용)."""
return _read_arms()
def get_arm(arm_id: str) -> Optional[dict]:
for a in _read_arms():
if a['id'] == arm_id:
return a
return None
def find_arm_by_symbol(account: str, symbol: str) -> Optional[dict]:
"""같은 계좌·종목의 대기 예약. 중복 등록 차단용."""
for a in _read_arms():
if a['account'] == account and a['symbol'] == symbol:
return a
return None
def register_arm(account: str, symbol: str, symbol_name: str, qty: int,
trigger_price: int, steps: list,
min_sell_price: Optional[int] = None,
card_id: Optional[str] = None) -> dict:
"""PIN 승인 직후 호출 — 발동가 도달을 기다리는 예약 1건 등록. 키움 주문은 아직 없다.
steps 는 normalize_steps 결과, 즉 **정의만**(n/pct/cum/weight)이다. 조건단가·지정가·
계단별 주수는 저장하지 않는다 — 갭 상승으로 발동가를 훌쩍 넘겨 열릴 수 있고 그 사이
보유수량이 줄었을 수도 있어서, 발동 시점의 현재가와 매도가능 수량으로 다시 계산해야
실제 시세와 맞는다.
routing_suffix 도 저장하지 않는다. 당일 안에서도 등록 시점 세션(정규장)과 발동 시점
세션(NXT 애프터)이 다를 수 있고, 옛 suffix 로 발주하면 거부된다.
"""
if not steps:
raise ValueError('계단이 비어 있음')
if trigger_price <= 0:
raise ValueError('발동가가 0 이하')
arm = {
'id': _new_id('ARM-'),
'account': account,
'symbol': symbol,
'symbol_name': symbol_name,
'qty': qty,
'trigger_price': trigger_price,
'min_sell_price': min_sell_price,
'steps': [{'n': s['n'], 'pct': s['pct'], 'cum': s['cum'], 'weight': s['weight']}
for s in steps],
'card_id': card_id,
'created_at': _now_iso(),
'updated_at': _now_iso(),
'firing_at': None,
}
with _FileLock(_LOCK_FILE):
arms = _read_arms()
arms.append(arm)
_write(arms=arms)
return arm
def remove_arm(arm_id: str, reason: str = '') -> Optional[dict]:
"""대기 예약 제거 (취소·수량 소멸·계단 소멸). 반환값은 제거된 arm — 알림 메시지용."""
with _FileLock(_LOCK_FILE):
arms = _read_arms()
for i, a in enumerate(arms):
if a['id'] == arm_id:
gone = arms.pop(i)
_write(arms=arms)
gone['removed_reason'] = reason
return gone
return None
def mark_arm_firing(arm_id: str) -> bool:
"""발주 직전 마킹. 이미 마킹돼 있으면 False (재발동 차단).
키움 발주가 성공한 뒤 promote_arm 전에 프로세스가 죽으면 다음 사이클이 같은 종목을
또 판다. 창은 1초 남짓이지만 결과가 이중 매도라, 발주를 시작했다는 사실을 먼저
디스크에 남긴다.
⚠️ 마킹이 남아있는 arm 은 **자동으로 재시도하지 않는다** — 주문이 나갔는지 알 수 없는
상태에서 다시 내는 게 바로 이중 매도다. 감시 루프는 알림만 보내고, 관리자님이 실제
미체결을 확인한 뒤 자산웹에서 취소한다.
"""
with _FileLock(_LOCK_FILE):
arms = _read_arms()
for a in arms:
if a['id'] != arm_id:
continue
if a.get('firing_at'):
return False
a['firing_at'] = _now_iso()
a['updated_at'] = _now_iso()
_write(arms=arms)
return True
return False
# ---------------------------------------------------------------------------
# 최근 종료 (recent) — 재등록 버튼용 단기 캐시
#
# 트레일링 스톱은 장 마감에 거래소가 지우고, 대기 예약은 등록일 당일로 우리가 지운다. 둘 다
# 사라지는 순간 계좌·수량·계단이 함께 없어져서, 다시 걸려면 처음부터 입력해야 했다.
# 종료 시점의 설정을 짧게 보관해 자산웹이 "이전 설정 그대로 채워진 거래 모달" 을 열 수 있게 한다.
#
# ⚠️ 이건 **UI 편의용 단기 캐시이지 감사 기록이 아니다.** 감사 원장은 state/order_log.jsonl 이고
# 그쪽은 무한 append 로 그대로 둔다. 여기는 링버퍼라 오래된 건 지워진다.
# ⚠️ 재등록은 **사람이 버튼을 누르고 PIN 을 다시 거친다** — 감시 루프가 스스로 되살리는 경로는
# 없다. "자동 재등록 안 함"(2026-07-30 결정)은 그대로 유효하다.
# ---------------------------------------------------------------------------
RECENT_MAX = 20
RECENT_DAYS = 7
def list_recent() -> list:
"""최근 종료 목록 (최신이 앞). 락 없이 읽기 — 웹 렌더용."""
return _read_recent()
def get_recent(key: str) -> Optional[dict]:
for e in _read_recent():
if e.get('key') == key:
return e
return None
def push_recent(entry: dict) -> dict:
"""종료된 예약 1건을 최근 목록 맨 앞에 넣는다 (링버퍼).
⚠️ 제거(remove_step/remove_arm) 와 **같은 락에 묶지 않는다.** 레그 소멸은 레그마다 제거가
끝난 **뒤에야** "예약이 죽었다" 를 알 수 있어 애초에 한 락에 못 넣기 때문이다. 그 사이에
죽으면 항목 하나를 잃는데, 순서를 뒤집으면(기록 먼저) 같은 자리에서 중복이 남는다 —
편의 기능이라 유실이 중복보다 낫다.
"""
entry = dict(entry)
entry.setdefault('ended_at', _now_iso())
entry['key'] = _new_id('RC-')
cutoff = (datetime.now(KST) - timedelta(days=RECENT_DAYS)).isoformat(timespec='seconds')
with _FileLock(_LOCK_FILE):
recent = [e for e in _read_recent() if (e.get('ended_at') or '') >= cutoff]
recent.insert(0, entry)
_write(recent=recent[:RECENT_MAX])
return entry
def make_recent_entry(kind: str, reason: str, src: dict, steps: list) -> dict:
"""예약/대기예약 스냅샷 → 재등록에 필요한 것만 뽑은 항목.
steps 는 정의만(n/pct/cum/weight) — 가격·주수는 재등록 시점에 다시 계산된다.
"""
return {
'kind': kind, # 'trailing' | 'arm' → 재등록 order_type 을 가른다
'reason': reason,
'account': src['account'],
'symbol': src['symbol'],
'symbol_name': src.get('symbol_name') or src['symbol'],
'qty': src.get('qty') or 0,
'steps': [{'n': s['n'], 'pct': s['pct'], 'cum': s['cum'], 'weight': s.get('weight')}
for s in steps],
'min_sell_price': src.get('min_sell_price'),
'trigger_price': src.get('trigger_price'), # arm 만. trailing 은 None
}
def is_arm_expired(arm: dict, now: Optional[datetime] = None) -> bool:
"""등록일의 거래 세션이 끝났으면 만료 (2026-08-25 관리자님 결정으로 무기한 → 당일).
트레일링 스톱주문이 장 마감에 소멸하는 것과 **같은 수명**을 갖게 한 것이다. 대기 예약은
키움이 아니라 우리 파일에만 있어 거래소가 지워주지 않으므로, 우리가 지워야 한다.
판정은 두 갈래:
· 등록일이 오늘보다 이전 → 만료 (밤새 넘긴 것·주말 넘긴 것)
· 등록일이 오늘이고 이미 20:00 지남 → 만료
⚠️ 감시 루프는 08:00~19:59 에만 도므로 **20:00 직후에 지워지지는 않는다.** 실제 정리는
다음 거래일 첫 사이클이다. 그 사이(장외)에 자산웹을 열면 만료 상태로 표시된다 —
화면이 거짓말하지 않게 웹도 같은 판정을 쓴다.
"""
now = now or datetime.now(KST)
created = (arm.get('created_at') or '')[:10]
if not created:
return False # 날짜를 모르면 함부로 지우지 않는다
today = now.strftime('%Y-%m-%d')
if created < today:
return True
return created == today and after_trading_day(now)
def record_arm_leg(arm_id: str, leg: dict) -> bool:
"""발동 중 레그 하나가 접수될 때마다 즉시 기록 (write-ahead).
firing_at 만으로는 중간에 죽었을 때 "몇 단이 실제로 나갔는지" 를 알 수 없다. 키움엔 우리
주문을 식별하는 client order id 가 없어 미체결 목록만으로는 관리자님이 직접 낸 스톱주문과
구분되지 않는다 — 그래서 주문번호를 우리가 적어둔다.
"""
with _FileLock(_LOCK_FILE):
arms = _read_arms()
for a in arms:
if a['id'] != arm_id:
continue
a.setdefault('fired_legs', []).append(
{'n': leg['n'], 'qty': leg['qty'], 'cond_uv': leg['cond_uv'],
'ord_uv': leg['ord_uv'], 'ord_no': leg.get('ord_no', '')})
a['updated_at'] = _now_iso()
_write(arms=arms)
return True
return False
def plan_arm_fire(arm: dict, cur_price: int, sellable_qty: int) -> dict:
"""대기 예약 + 현재가 + 매도가능 수량 → 발주 계획 (순수 함수 — 단위테스트 대상).
반환 {'action', 'reason', 'peak', 'qty_used', 'reduced', 'legs'}
fire : legs 를 그대로 발주한다
skip : 아직 아니다 (미도달·시세 없음) — 아무것도 하지 않는다
drop : 발동할 수 없다 (매도가능 0주·수량 부족) → 예약 삭제 + 알림
고점(peak)은 발동 시점 현재가다. 발동가가 아니라 현재가를 쓰는 이유는 갭 상승으로 발동가를
훌쩍 넘겨 열릴 수 있어서다 — 현재가 >= 발동가 이므로 손절선은 승인 시점 미리보기보다 항상
같거나 높다(관리자님에게 불리해지지 않는 방향).
수량은 min(예약 수량, 매도가능)이다. 대기 중 관리자님이 직접 팔았으면 그만큼 줄여 재배분한다
— 대기 예약이 보유분을 잠그지 않기로 한 결정의 뒷수습이 여기다.
"""
def _out(action, reason):
return {'action': action, 'reason': reason, 'peak': cur_price,
'qty_used': 0, 'reduced': False, 'legs': []}
if cur_price <= 0:
return _out('skip', 'no_price')
if cur_price < arm['trigger_price']:
return _out('skip', 'not_reached')
if sellable_qty <= 0:
return _out('drop', 'no_sellable')
qty_used = min(arm['qty'], sellable_qty)
steps = arm.get('steps') or []
leveled = compute_step_levels(cur_price, steps, arm.get('min_sell_price'))
qtys = allocate_step_qty(qty_used, [s['weight'] for s in steps])
# 0주가 된 계단은 발주할 수 없으니 떨어낸다. 단계 번호(n)는 유지해 몇 단계가 빠졌는지 보인다.
legs = [dict(s, qty=q) for s, q in zip(leveled, qtys) if q > 0]
if not legs:
return _out('drop', 'qty_too_small')
# 실주문 직전 마지막 안전망. propose 가 발동가 기준으로 이미 걸렀고 현재가 >= 발동가,
# 최저 매도가는 승인 후 불변이라 정상 경로에선 도달하지 않는다. 그래도 남기는 이유는
# 여기를 지나면 되돌릴 수 없는 매도 주문이 나가기 때문이다.
if legs[0]['cond_uv'] >= cur_price:
return _out('skip', 'immediate')
return {'action': 'fire', 'reason': '', 'peak': cur_price, 'qty_used': qty_used,
'reduced': qty_used < arm['qty'], 'legs': legs}
def promote_arm(arm_id: str, peak: int, steps: list,
routing_suffix: str = '') -> Optional[dict]:
"""발동 완료 — 대기 예약을 지우고 같은 내용의 트레일링 예약을 등록 (한 락 안에서 원자적).
steps 는 접수 결과까지 채워진 레그 목록(register_steps 와 같은 형식).
둘을 따로 하면 중간에 죽었을 때 arm 과 예약이 동시에 존재하거나 둘 다 사라진다.
반환 {'arm': 제거된 arm, 'reservation': 새 예약} — 알림 메시지 조립용.
"""
with _FileLock(_LOCK_FILE):
doc = _read_doc()
arms = doc['arms']
for i, a in enumerate(arms):
if a['id'] != arm_id:
continue
res = _build_reservation(
a['account'], a['symbol'], a['symbol_name'],
sum(s['qty'] for s in steps), peak, steps,
routing_suffix, a.get('card_id'), a.get('min_sell_price'))
arms.pop(i)
reservations = doc['reservations']
reservations.append(res)
_write(reservations=reservations, arms=arms)
return {'arm': a, 'reservation': res}
return None
def should_notify_arm_failure(arm_id: str, now_ts: float, cooldown_sec: int) -> bool:
"""발동 실패 알림을 보낼지. 보낸다고 판단하면 그 시점을 기록한다 (락 안에서 원자적).
firing_at 이 걸린 채 남은 arm 이나 사이드카 ON 상태는 매 사이클 같은 판정이 반복된다 —
쿨다운 없이 알리면 19:59까지 매분 텔레그램이 쏟아진다. should_notify_failure 와 같은 패턴.
"""
with _FileLock(_LOCK_FILE):
arms = _read_arms()
for a in arms:
if a['id'] != arm_id:
continue
last = a.get('last_fail_notice_ts') or 0
if now_ts - last < cooldown_sec:
return False
a['last_fail_notice_ts'] = now_ts
_write(arms=arms)
return True
return False
@@ -345,6 +345,7 @@ class RealtimeHub:
'open': _abs_int(vals.get('16')), # 시가
'high': _abs_int(vals.get('17')), # 고가
'low': _abs_int(vals.get('18')), # 저가
'volume': _abs_int(vals.get('13')), # 누적거래량 (RVOL 배지용)
'exchange': (vals.get('9081') or '').strip(),
'ts': datetime.now(KST).isoformat(),
}
@@ -1,7 +1,8 @@
#!/usr/bin/env python3
"""매월 1일 04:30 — 본인 계좌 잔액을 골디 inbox에 envelope로 떨어뜨린다.
"""매월 1일 04:30 — 본인+가희 계좌 잔액을 골디 inbox에 envelope로 떨어뜨린다.
가희 계좌는 제외 (label prefix '가희_'). 본인 계좌별 평가액(kt00018)·예수금(kt00001)·총자산을 집계.
소유자별(본인 self / 가희 gahee, label prefix '가희_'로 판별)로 평가액(kt00018)·예수금(kt00001)·
총자산을 집계해 payload.by_owner에 담는다. 골디는 self→증권(효원), gahee→증권(가희)로 reconcile.
LLM 불필요 — launchd로 실행. 실패 시 레이 텔레그램으로 자가 알림.
"""
from __future__ import annotations
@@ -26,7 +27,7 @@ INBOX_DIR = Path('/Users/snowoyh/.openclaw/agents/budget/inbox/incoming')
CONFIG_PATH = Path('/Users/snowoyh/.openclaw/openclaw.json')
TELEGRAM_ACCOUNT = 'stock'
TOPIC = 'securities_balance'
SCHEMA_VERSION = 1
SCHEMA_VERSION = 2
GAHEE_PREFIX = '가희_'
@@ -60,13 +61,12 @@ def send_telegram(text: str) -> bool:
return ok
def collect_owner_accounts() -> list[dict]:
def collect_accounts() -> list[dict]:
accounts = kw.list_accounts()
out = []
for a in accounts:
label = a['label']
if label.startswith(GAHEE_PREFIX):
continue
owner = 'gahee' if label.startswith(GAHEE_PREFIX) else 'self'
balance = kw.get_balance(label)
positions = kw.get_positions(label)
eval_amount = sum(p.get('evlt_amt', 0) for p in positions)
@@ -75,6 +75,7 @@ def collect_owner_accounts() -> list[dict]:
deposit = balance.get('d2_entra', 0)
out.append({
'label': label,
'owner': owner,
'account_no': a.get('account_no', ''),
'deposit': deposit,
'eval_amount': eval_amount,
@@ -86,6 +87,12 @@ def collect_owner_accounts() -> list[dict]:
def build_message(accounts: list[dict]) -> dict:
now = datetime.now(KST)
by_owner: dict[str, dict] = {}
for a in accounts:
o = by_owner.setdefault(a['owner'], {'deposit': 0, 'eval_amount': 0, 'total': 0})
o['deposit'] += a['deposit']
o['eval_amount'] += a['eval_amount']
o['total'] += a['total']
return {
'message_id': str(uuid.uuid4()),
'from': 'stock',
@@ -95,8 +102,9 @@ def build_message(accounts: list[dict]) -> dict:
'schema_version': SCHEMA_VERSION,
'payload': {
'as_of': now.strftime('%Y-%m-%d'),
'owner_scope': 'self_only',
'owner_scope': 'self_and_gahee',
'accounts': accounts,
'by_owner': by_owner,
'totals': {
'deposit': sum(a['deposit'] for a in accounts),
'eval_amount': sum(a['eval_amount'] for a in accounts),
@@ -117,9 +125,9 @@ def write_message(msg: dict) -> Path:
def main() -> int:
try:
accounts = collect_owner_accounts()
accounts = collect_accounts()
if not accounts:
raise RuntimeError('본인 계좌 0개 — 자격증명 또는 prefix 필터 점검 필요')
raise RuntimeError('계좌 0개 — 자격증명 점검 필요')
msg = build_message(accounts)
path = write_message(msg)
total = msg['payload']['totals']['total']
+252 -47
View File
@@ -18,6 +18,7 @@ from __future__ import annotations
import json
import os
import re
import sys
import subprocess
import threading
@@ -47,7 +48,7 @@ REPORTS_DIR.mkdir(parents=True, exist_ok=True)
PROMPT_FILE = WORKSPACE / 'prompts' / 'stock_analysis.md'
ALIAS_FILE = WORKSPACE / 'prompts' / 'name_aliases.json'
LLM_MODEL = 'openai-codex/gpt-5.5'
LLM_MODEL = 'openai/gpt-5.5'
LLM_TIMEOUT_SEC = 120
@@ -61,6 +62,59 @@ def now_kst() -> datetime:
return datetime.now()
def session_vol_frac(candles_asc: list[dict]) -> float | None:
"""마지막 봉이 오늘·정규장(09:00~15:30) 진행형이면 세션경과 비율(0.15~1.0), 아니면 None.
장중 누적 거래량을 하루치로 환산(RVOL·회전율)하는 분모. 휴장/주말은 오늘 봉이 없어 자연 배제.
NXT(15:30~) 시간대는 KRX 마감 후라 완성봉 취급(None)."""
if not candles_asc:
return None
now = now_kst()
if candles_asc[-1].get('date') != now.strftime('%Y%m%d'):
return None
hm = now.hour * 60 + now.minute
if not (9 * 60 <= hm < 15 * 60 + 30):
return None
return min(1.0, max(0.15, (hm - 9 * 60) / 390.0)) # 09:00~15:30 = 390분
def turnover_today(candles_asc: list[dict], vol_time_frac: float | None = None) -> tuple[float | None, bool]:
"""오늘 회전율(%)과 장중환산 여부. 마지막 봉 turnover_rate 우선, 없으면 최근 완성봉에서
상장주식수 역산 → 오늘 거래량으로 산출. vol_time_frac 있으면 오늘 누적을 하루치로 투영('예상').
데이터 없으면 (None, scaled)."""
if not candles_asc:
return None, False
scaled = bool(vol_time_frac and 0 < vol_time_frac < 1.0)
vols = [c.get('volume') or 0 for c in candles_asc]
today_v = vols[-1] if vols else 0
proj_v = (today_v / vol_time_frac) if scaled and today_v else today_v
last_c = candles_asc[-1]
if last_c.get('turnover_rate') and not scaled:
return last_c['turnover_rate'], scaled
for c in reversed(candles_asc[:-1]):
tr, cv = c.get('turnover_rate'), c.get('volume')
if tr and cv:
listed = cv / (tr / 100.0)
return (proj_v / listed * 100.0, scaled) if listed > 0 else (None, scaled)
return None, scaled
def rvol_today(candles_asc: list[dict], vol_time_frac: float | None = None) -> tuple[float | None, bool]:
"""상대거래량(RVOL) = 오늘 거래량 ÷ 직전 20거래일 평균, 장중환산 여부.
vol_time_frac 있으면 오늘 누적을 하루치로 투영('예상'). 데이터<21봉이면 (None, scaled)."""
if not candles_asc:
return None, False
scaled = bool(vol_time_frac and 0 < vol_time_frac < 1.0)
vols = [c.get('volume') or 0 for c in candles_asc]
if len(vols) < 21:
return None, scaled
avg20 = sum(vols[-21:-1]) / 20.0
if avg20 <= 0:
return None, scaled
today_v = vols[-1]
proj_v = (today_v / vol_time_frac) if scaled and today_v else today_v
return proj_v / avg20, scaled
def ts_for_filename() -> str:
return now_kst().strftime('%Y-%m-%d_%H-%M-%S')
@@ -117,6 +171,7 @@ def collect_snapshot(code: str, holding: dict | None = None) -> dict:
sma5 = sma(closes, 5)
sma20 = sma(closes, 20)
sma60 = sma(closes, 60)
sma120 = sma(closes, 120)
flow = kc.get_investor_flow(code, days=20)
flow_summary = _summarize_flow(flow)
@@ -137,6 +192,7 @@ def collect_snapshot(code: str, holding: dict | None = None) -> dict:
'sma5': sma5,
'sma20': sma20,
'sma60': sma60,
'sma120': sma120,
'flow': flow,
'flow_summary': flow_summary,
'holding': holding,
@@ -529,7 +585,7 @@ def remove_peer(stock_code: str, peer_code: str) -> bool:
# ============================================================================
CHART_W = 1000
CHART_H = 720 # crosshair 박스 영역(상단) + X축 날짜 라벨 영역 확보
CHART_H = 660 # X축 날짜 라벨 아래 여백 최소화 (라벨 baseline VOL_BOT+26=642)
CHART_PAD_L = 20 # 좌측 라벨 없음 (가격 라벨 우측으로 이동)
CHART_PAD_R = 140 # 우측 가격·거래량 라벨 영역 (28px 폰트 = 6자리+쉼표 약 140px)
CHART_PAD_T = 60 # SMA legend 영역 (y=4~50). OHLC 팝업은 차트 안 floating tooltip.
@@ -545,12 +601,19 @@ VOL_BOT = VOL_TOP + VOL_PANEL_H
CHART_RANGE_DAYS = {'1Y': 250, '6M': 120, '3M': 63, '1M': 21}
# 사용자 수평선 색상 팔레트 — 정렬(내림차순) 인덱스로 순환. SVG·칩 UI가 같은 매핑 사용.
# 모두 밝은 톤이라 태그 위 어두운 글자(#1a0f1f) 가독성 확보.
HLINE_COLORS = ['#e879f9', '#38bdf8', '#fbbf24', '#4ade80', '#fb7185', '#a78bfa', '#22d3ee', '#facc15']
def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None = None, intraday: bool = False) -> str:
"""캔들 + SMA 3선 + 거래량 막대 인라인 SVG.
def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None = None,
intraday: bool = False, hlines: list | None = None) -> str:
"""캔들 + SMA 3선 + 거래량 막대(+일봉 20일 거래량 이동평균선) 인라인 SVG.
range_key: '1Y' (250일) | '6M' (120일) | '3M' (63일) | '1M' (21일). 같은 snapshot 데이터에서 슬라이스만.
ref_price: 기준가(전일종가 등) — 지정 시 노란 점선 기준선 + 마지막 종가의 대비 등락률 표시 (분봉용).
hlines: 사용자가 등록한 수평선 가격 리스트(지지/저항 등). 표시 구간 안에 드는 값만 그린다(스케일 미확장).
회전율 값은 차트에 그리지 않고 caller가 turnover_today()로 별도 표시 (차트 아래).
snap: collect_snapshot 결과. candles_asc·sma5/20/60 사용.
외부 JS 의존성 0. CSS는 inline.
"""
@@ -563,12 +626,13 @@ def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None
sma5 = (snap.get('sma5') or [])[-n_take:]
sma20 = (snap.get('sma20') or [])[-n_take:]
sma60 = (snap.get('sma60') or [])[-n_take:]
sma120 = (snap.get('sma120') or [])[-n_take:]
# ---- 스케일 ----
highs = [c['high'] for c in candles]
lows = [c['low'] for c in candles]
# SMA 값도 가격축 범위에 포함 (선이 잘리지 않도록)
sma_vals = [v for v in (sma5 + sma20 + sma60) if v is not None]
sma_vals = [v for v in (sma5 + sma20 + sma60 + sma120) if v is not None]
y_max = max(max(highs), max(sma_vals) if sma_vals else 0)
y_min = min(min(lows), min(sma_vals) if sma_vals else float('inf'))
if ref_price:
@@ -701,6 +765,7 @@ def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None
parts.append(sma_path(sma5, '#fbbf24', 'SMA5')) # 노랑
parts.append(sma_path(sma20, '#34d399', 'SMA20')) # 초록
parts.append(sma_path(sma60, '#a78bfa', 'SMA60')) # 보라
parts.append(sma_path(sma120, '#22d3ee', 'SMA120')) # 청록
# ---- 전일종가 기준선 + 대비 등락률 (ref_price 지정 시 — 분봉) ----
if ref_price:
@@ -716,16 +781,16 @@ def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None
# SMA 범례
legend_y = PRICE_TOP + 14
legend_items = [('SMA5', '#fbbf24'), ('SMA20', '#34d399'), ('SMA60', '#a78bfa')]
legend_items = [('5일', '#fbbf24'), ('20일', '#34d399'), ('60일', '#a78bfa'), ('120일', '#22d3ee')]
lx = CHART_PAD_L + 8
for name, color in legend_items:
parts.append(
f'<rect x="{lx}" y="{legend_y-8}" width="10" height="3" fill="{color}"/>'
)
parts.append(
f'<text x="{lx+14}" y="{legend_y-2}" fill="#cfd5df">{name}</text>'
f'<text x="{lx+14}" y="{legend_y-2}" fill="#cfd5df" font-size="20">{name}</text>'
)
lx += 70
lx += 96
# ---- 거래량 패널 배경 ----
parts.append(
@@ -746,12 +811,30 @@ def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None
f'<rect x="{x-body_w/2:.2f}" y="{bar_top:.2f}" width="{body_w:.2f}" height="{bar_h:.2f}" '
f'fill="{color}" opacity="0.7"/>'
)
# 거래량 20일 이동평균선 — 일봉에서만 (분봉은 '20분 평균'이 돼 의미 없음)
vols_full = [c['volume'] for c in candles_full]
if not intraday:
vma = (sma(vols_full, 20))[-n_take:]
vma_pts = [f'{slot_x(i):.2f},{vol_y(v):.2f}' for i, v in enumerate(vma) if v is not None]
if len(vma_pts) >= 2:
parts.append(
f'<polyline points="{" ".join(vma_pts)}" fill="none" stroke="#f5a623" '
f'stroke-width="2" opacity="0.9"/>'
)
# 거래량 Y 레이블 (max 만) — 우측
parts.append(
f'<text x="{CHART_W - CHART_PAD_R + 6}" y="{VOL_TOP+22}" text-anchor="start" fill="#7a8493" font-size="28">'
f'{_fmt_vol(v_max)}</text>'
)
# 20MA 범례 (거래량 패널 하단 좌측) — 일봉 전용. RVOL·회전율 텍스트는 차트에 안 그림(차트 아래 표시).
if not intraday:
parts.append(
f'<text x="{CHART_PAD_L+8}" y="{VOL_BOT-8}" text-anchor="start" fill="#f5a623" font-size="24">'
f'— 거래량 20일평균</text>'
)
# ---- X축 날짜 (시작·중간·끝) ----
for i in (0, n // 2, n - 1):
x = slot_x(i)
@@ -762,6 +845,64 @@ def render_svg_chart(snap: dict, range_key: str = '1Y', ref_price: float | None
f'{_fmt_date(candles[i]["date"])}</text>'
)
# ---- 구간 고점선 + 현재가 태그 (고점·현재가 강조) ----
# 고점: 표시 구간 내 최고 고가에 노란 점선 + 우측 '고 N' 라벨.
hi_price = max(highs)
hy = price_y(hi_price)
parts.append(
f'<line x1="{CHART_PAD_L}" y1="{hy:.1f}" x2="{CHART_W-CHART_PAD_R}" y2="{hy:.1f}" '
f'stroke="#fbbf24" stroke-width="1" stroke-dasharray="4 4" opacity="0.55"/>'
)
# 고가 금액 — 가격패널 좌하단 안 (구간별 최고 고가). 상단 우측 대신 아래로 이동.
parts.append(
f'<text x="{CHART_PAD_L + 8}" y="{PRICE_BOT - 10:.1f}" text-anchor="start" fill="#fbbf24" '
f'font-size="26" font-weight="600">고가 {int(round(hi_price)):,}</text>'
)
# 현재가(마지막 종가): 얇은 기준선 + 우측 축 강조 태그(상승=빨강/하락=파랑).
cur_price = candles[-1]['close']
cy = price_y(cur_price)
last_up = candles[-1]['close'] >= candles[-1]['open']
cur_color = '#ef4444' if last_up else '#3b82f6'
parts.append(
f'<line x1="{CHART_PAD_L}" y1="{cy:.1f}" x2="{CHART_W-CHART_PAD_R}" y2="{cy:.1f}" '
f'stroke="{cur_color}" stroke-width="1" stroke-dasharray="2 3" opacity="0.5"/>'
)
tag_cy = min(max(cy, PRICE_TOP + 15), PRICE_BOT - 15)
parts.append(
f'<rect x="{CHART_W - CHART_PAD_R + 2}" y="{tag_cy-15:.1f}" width="{CHART_PAD_R-6}" height="30" '
f'fill="{cur_color}" rx="3"/>'
)
parts.append(
f'<text x="{CHART_W - CHART_PAD_R + 8}" y="{tag_cy+8:.1f}" text-anchor="start" fill="#fff" '
f'font-size="26" font-weight="700">{int(round(cur_price)):,}</text>'
)
# ---- 사용자 수평선 (hlines) — 지지/저항 등 수동 등록 가격선 ----
# 표시 구간(y_min~y_max) 안에 드는 값만 그린다. 스케일은 확장하지 않음(캔들 가독성 우선).
# 좌측에 분홍 가격 태그를 둔다(우측은 가격축·현재가 태그가 이미 점유).
for _hi, hp in enumerate(hlines or []):
try:
hp = float(hp)
except (TypeError, ValueError):
continue
if hp < y_min or hp > y_max:
continue
hcolor = HLINE_COLORS[_hi % len(HLINE_COLORS)]
hly = price_y(hp)
parts.append(
f'<line x1="{CHART_PAD_L}" y1="{hly:.1f}" x2="{CHART_W-CHART_PAD_R}" y2="{hly:.1f}" '
f'stroke="{hcolor}" stroke-width="1.6" stroke-dasharray="6 4" opacity="0.9"/>'
)
htag_y = min(max(hly, PRICE_TOP + 15), PRICE_BOT - 15)
parts.append(
f'<rect x="{CHART_PAD_L+2}" y="{htag_y-15:.1f}" width="122" height="30" '
f'fill="{hcolor}" rx="3" opacity="0.95"/>'
)
parts.append(
f'<text x="{CHART_PAD_L+8}" y="{htag_y+8:.1f}" text-anchor="start" fill="#1a0f1f" '
f'font-size="26" font-weight="700">{int(round(hp)):,}</text>'
)
parts.append('</svg>')
return ''.join(parts)
@@ -859,7 +1000,7 @@ def _build_data_block(snap: dict) -> str:
lines.append(f'### 종목: {snap["name"]} ({snap["code"]})')
lines.append(f'- 현재가: {b["price"]:,}원 ({b["change"]:+,} / {b["change_pct"]:+.2f}%)')
lines.append(f'- 오늘 시고저: 시 {b["open"]:,} / 고 {b["high"]:,} / 저 {b["low"]:,}')
lines.append(f'- 시가총액: {b["market_cap"]:,} (단위 키움원본) · 상장주식수: {b["listed_shares"]:,}주')
lines.append(f'- 시가총액: {_fmt_market_cap(b["market_cap"])} · 상장주식수: {b["listed_shares"]:,}주')
lines.append(f'- 52주 고저: 고 {b["high_52w"]:,}원 ({_fmt_date(b["high_52w_dt"])}) / 저 {b["low_52w"]:,}원 ({_fmt_date(b["low_52w_dt"])})')
lines.append(f'- 회전율(오늘): {b["turnover_rate"]:.2f}% · 외국인 보유비율: {b["foreign_holding_pct"]:.2f}%')
lines.append(f'- PER {b["per"]} · PBR {b["pbr"]} · ROE {b["roe"]} · EPS {b["eps"]:,} · BPS {b["bps"]:,}')
@@ -1121,11 +1262,50 @@ def _fmt_pct_signed(v: float) -> str:
return f'{v:+.2f}%'
_EM_RE = re.compile(r'\*\*(.+?)\*\*')
def _emphasize(escaped: str) -> str:
"""이스케이프된 텍스트의 **강조** → 붉은 강조 글씨. 입력은 이미 _esc 처리된 상태."""
return _EM_RE.sub(r'<span class="rpt-em">\1</span>', escaped)
def _fmt_eok_jo(eok) -> str:
"""억 단위 금액 → 조 단위. 1,997,447억 → 199.7조. 1조 미만은 소수 둘째자리. 비숫자는 —."""
if not isinstance(eok, (int, float)):
return '—'
jo = eok / 10000
return f'{jo:,.2f}조' if abs(jo) < 1 else f'{jo:,.1f}조'
def _fmt_market_cap(eok) -> str:
"""키움 mac(억원) → 사람이 읽는 '2,084조 1,983억원'. 0/None은 '—'."""
if not isinstance(eok, (int, float)) or eok <= 0:
return '—'
eok = int(eok)
jo, rem = divmod(eok, 10000)
if jo and rem:
return f'{jo:,}조 {rem:,}억원'
if jo:
return f'{jo:,}조원'
return f'{rem:,}억원'
def _fmt_date(s) -> str:
"""20260519 → 2026-05-19. 형식 다르면 원본 반환."""
"""날짜를 yy/mm/dd로. 20260519 · 2026-05-19 · 2026/05/19 → 26/05/19. 형식 다르면 원본."""
s = str(s or '').strip()
if len(s) == 8 and s.isdigit():
return f'{s[:4]}-{s[4:6]}-{s[6:]}'
d = s.replace('-', '').replace('/', '').replace('.', '')
if len(d) == 8 and d.isdigit():
return f'{d[2:4]}/{d[4:6]}/{d[6:]}'
return s
def _fmt_period(s) -> str:
"""회계기간 2021/12 · 2021.12 · 202112 → 21/12. 형식 다르면 원본."""
s = str(s or '').strip()
d = s.replace('-', '').replace('/', '').replace('.', '')
if len(d) == 6 and d.isdigit():
return f'{d[2:4]}/{d[4:6]}'
return s
@@ -1158,9 +1338,9 @@ _KV_HINTS = {
'BPS': '주당순자산. 1주당 회사가 가진 순자산 가치(원).',
'외국인 보유비율': '전체 발행 주식 중 외국인이 보유한 비율(%).',
# ── 성장성·펀더멘털 (FnGuide) ──
'매출액(억)': '회사가 1년에 물건·서비스를 팔아 벌어들인 총액(억원). 회사 덩치가 커지는지 보는 지표.',
'매출액(조)': '회사가 1년에 물건·서비스를 팔아 벌어들인 총액(조원). 회사 덩치가 커지는지 보는 지표.',
'매출증가율': '작년보다 매출이 몇 % 늘었는지. +면 성장, −면 역성장. 꾸준히 +면 좋아요.',
'영업이익(억)': '본업으로 번 이익(억원). 매출에서 원가·판관비를 뺀 것. 회사가 장사를 잘하는지.',
'영업이익(조)': '본업으로 번 이익(조원). 매출에서 원가·판관비를 뺀 것. 회사가 장사를 잘하는지.',
'EPS(원)': '주당순이익. 1주가 1년에 번 순이익(원). 높을수록, 그리고 늘어날수록 좋아요.',
'EPS증가율': '작년보다 주당순이익이 몇 % 늘었는지. 이익이 실제로 커지는지 보는 핵심 지표.',
'목표주가': '증권사들이 "이 정도까진 오를 만하다"고 본 가격의 평균. 현재가보다 높으면 상승 기대.',
@@ -1173,7 +1353,7 @@ _KV_HINTS = {
'목표주가 리비전': '최근 3개월간 증권사 목표주가 평균이 오르고(+) 있는지 내리고(−) 있는지. 오르는 중이면 시장 기대가 좋아지는 신호.',
'추정치 리비전': '최근 3개월간 예상 실적(EPS 등)이 상향(+)/하향(−)되는 중인지. 상향이면 전망이 밝아지는 거예요.',
'서프라이즈': '실제 발표 실적이 직전 예상치보다 잘 나왔으면(+) 어닝 서프라이즈, 못 나왔으면(−) 어닝 쇼크.',
'실적(억)': '실제로 확정된 작년 실적 금액(억원). 예상치가 아니라 진짜 나온 숫자.',
'실적(조)': '실제로 확정된 작년 실적 금액(조원). 예상치가 아니라 진짜 나온 숫자.',
}
@@ -1265,23 +1445,23 @@ def _render_fundamentals_html(fund: dict | None) -> str:
roe = r.get('roe')
body.append(
'<tr>'
f'<td>{_esc(r.get("period",""))}</td>'
f'<td style="text-align:right">{_fmt_num(round(sales)) if isinstance(sales,(int,float)) else "—"}</td>'
f'<td>{_esc(_fmt_period(r.get("period","")))}</td>'
f'<td style="text-align:right">{_fmt_eok_jo(sales)}</td>'
f'<td style="text-align:right">{_fmt_growth(r.get("sales_growth"))}</td>'
f'<td style="text-align:right">{_fmt_num(round(oper)) if isinstance(oper,(int,float)) else "—"}</td>'
f'<td style="text-align:right">{_fmt_eok_jo(oper)}</td>'
f'<td style="text-align:right">{_fmt_num(round(eps)) if isinstance(eps,(int,float)) else "—"}</td>'
f'<td style="text-align:right">{_fmt_growth(r.get("eps_growth"))}</td>'
f'<td style="text-align:right">{(str(roe)+"%") if isinstance(roe,(int,float)) else "—"}</td>'
'</tr>'
)
table_html = (
'<table class="flowtable">'
'<div class="tbl-scroll"><table class="flowtable">'
'<thead><tr>'
f'<th>기간</th><th>{_lbl("매출액(억)")}</th><th>{_lbl("매출증가율")}</th>'
f'<th>{_lbl("영업이익(억)")}</th><th>{_lbl("EPS(원)")}</th>'
f'<th>기간</th><th>{_lbl("매출액(조)")}</th><th>{_lbl("매출증가율")}</th>'
f'<th>{_lbl("영업이익(조)")}</th><th>{_lbl("EPS(원)")}</th>'
f'<th>{_lbl("EPS증가율")}</th><th>{_lbl("ROE")}</th>'
'</tr></thead>'
'<tbody>' + ''.join(body) + '</tbody></table>'
'<tbody>' + ''.join(body) + '</tbody></table></div>'
)
# ---- 컨센서스 카드 ----
@@ -1307,7 +1487,7 @@ def _render_fundamentals_html(fund: dict | None) -> str:
parts.append(f'<tr><th>{_lbl("참여 기관수")}</th><td>{int(oc)}개</td></tr>')
if parts:
cdate = consensus.get('date')
date_html = (f'<span style="color:#7a8493;font-size:11px"> · 기준 {_esc(cdate)}</span>'
date_html = (f'<span style="color:#7a8493;font-size:11px"> · 기준 {_esc(_fmt_date(cdate))}</span>'
if cdate else '')
cons_html = (
f'<h4 style="margin:10px 0 4px">📋 컨센서스{date_html}</h4>'
@@ -1372,7 +1552,7 @@ def _render_consensus_html(cons: dict | None) -> str:
sup_rows.append(
'<tr>'
f'<td>{_esc(label)}</td>'
f'<td style="text-align:right">{_fmt_num(round(act)) if isinstance(act,(int,float)) else "—"}</td>'
f'<td style="text-align:right">{_fmt_eok_jo(act)}</td>'
f'<td style="text-align:right">{_fmt_growth(sp)}</td>'
'</tr>'
)
@@ -1382,7 +1562,7 @@ def _render_consensus_html(cons: dict | None) -> str:
f'<h4 style="margin:10px 0 4px">🎯 어닝 서프라이즈 '
f'<span style="color:#7a8493;font-size:11px">({_esc(any_year)} 실적 vs 직전 컨센서스)</span></h4>'
'<table class="flowtable">'
f'<thead><tr><th>항목</th><th>{_lbl("실적(억)")}</th><th>{_lbl("서프라이즈")}</th></tr></thead>'
f'<thead><tr><th>항목</th><th>{_lbl("실적(조)")}</th><th>{_lbl("서프라이즈")}</th></tr></thead>'
'<tbody>' + ''.join(sup_rows) + '</tbody></table>'
)
@@ -1396,10 +1576,10 @@ def _render_consensus_html(cons: dict | None) -> str:
sales = r.get('sales'); op = r.get('op'); eps = r.get('eps'); roe = r.get('roe')
body.append(
'<tr' + (' style="opacity:.85"' if est else '') + '>'
f'<td>{_esc(r.get("period",""))}{tag}</td>'
f'<td style="text-align:right">{_fmt_num(round(sales)) if isinstance(sales,(int,float)) else "—"}</td>'
f'<td>{_esc(_fmt_period(r.get("period","")))}{tag}</td>'
f'<td style="text-align:right">{_fmt_eok_jo(sales)}</td>'
f'<td style="text-align:right">{_fmt_growth(r.get("sales_yoy"))}</td>'
f'<td style="text-align:right">{_fmt_num(round(op)) if isinstance(op,(int,float)) else "—"}</td>'
f'<td style="text-align:right">{_fmt_eok_jo(op)}</td>'
f'<td style="text-align:right">{_fmt_num(round(eps)) if isinstance(eps,(int,float)) else "—"}</td>'
f'<td style="text-align:right">{(str(roe)+"%") if isinstance(roe,(int,float)) else "—"}</td>'
'</tr>'
@@ -1407,10 +1587,10 @@ def _render_consensus_html(cons: dict | None) -> str:
parts.append(
'<h4 style="margin:10px 0 4px">📅 연도별 추정 재무 '
'<span style="color:#7a8493;font-size:11px">(IFRS연결 · A 실적 / E 추정)</span></h4>'
'<table class="flowtable">'
f'<thead><tr><th>기간</th><th>{_lbl("매출액(억)")}</th><th>{_lbl("YoY")}</th>'
f'<th>{_lbl("영업이익(억)")}</th><th>{_lbl("EPS(원)")}</th><th>{_lbl("ROE")}</th></tr></thead>'
'<tbody>' + ''.join(body) + '</tbody></table>'
'<div class="tbl-scroll"><table class="flowtable">'
f'<thead><tr><th>기간</th><th>{_lbl("매출액(조)")}</th><th>{_lbl("YoY")}</th>'
f'<th>{_lbl("영업이익(조)")}</th><th>{_lbl("EPS(원)")}</th><th>{_lbl("ROE")}</th></tr></thead>'
'<tbody>' + ''.join(body) + '</tbody></table></div>'
)
if not parts:
@@ -1477,6 +1657,12 @@ def build_report_html(snap: dict, parsed: dict) -> str:
v = p.get(field)
return f'{v:,}{suffix}' if isinstance(v, (int, float)) else '—'
def _peer_mac(p: dict) -> str:
if p.get('error'):
return '<span style="color:#ef4444;font-size:11px">조회 실패</span>'
v = p.get('market_cap')
return _fmt_market_cap(v) if isinstance(v, (int, float)) else '—'
def _peer_pct(p: dict, field: str) -> str:
if p.get('error'):
return '<span style="color:#ef4444;font-size:11px">조회 실패</span>'
@@ -1501,8 +1687,7 @@ def build_report_html(snap: dict, parsed: dict) -> str:
('오늘 시가', f'{b["open"]:,}원', lambda p: _peer_int(p, 'open', '원')),
('오늘 고가', f'{b["high"]:,}원', lambda p: _peer_int(p, 'high', '원')),
('오늘 저가', f'{b["low"]:,}원', lambda p: _peer_int(p, 'low', '원')),
('시가총액', f'{b["market_cap"]:,} <span style="color:#7a8493;font-size:11px">(키움 원본 단위)</span>',
lambda p: _peer_int(p, 'market_cap', '')),
('시가총액', _fmt_market_cap(b["market_cap"]), _peer_mac),
('상장주식수', f'{b["listed_shares"]:,}주', lambda p: _peer_int(p, 'listed_shares', '주')),
('52주 최고', f'{b["high_52w"]:,}원 <span style="color:#7a8493;font-size:11px">({_fmt_date(b["high_52w_dt"])})</span>',
lambda p: _peer_52w(p, 'high_52w', 'high_52w_dt')),
@@ -1552,6 +1737,20 @@ def build_report_html(snap: dict, parsed: dict) -> str:
chart_1y = render_svg_chart(snap, '1Y')
chart_6m = render_svg_chart(snap, '6M')
chart_1m = render_svg_chart(snap, '1M')
# 오늘 회전율·RVOL 값 — 차트 아래 표시. 장중 생성 시 종가 기준 환산('예상').
_vfrac = session_vol_frac(snap.get('candles_asc') or [])
_cands = snap.get('candles_asc') or []
_turn, _tscaled = turnover_today(_cands, _vfrac)
_rvol, _rscaled = rvol_today(_cands, _vfrac)
_cap_bits = []
if _rvol is not None:
_cap_bits.append(f'<b>RVOL</b> x{_rvol:.1f}')
if _turn is not None:
_cap_bits.append(f'<b>회전율</b> {_turn:.2f}%')
_turn_cap = (
f'<div class="chart-cap">{" · ".join(_cap_bits)}{" (예상)" if (_tscaled or _rscaled) else ""}</div>'
if _cap_bits else ''
)
chart_block = (
'<div class="chart-tabs">'
'<button type="button" data-range="1Y" class="active">1년</button>'
@@ -1561,6 +1760,7 @@ def build_report_html(snap: dict, parsed: dict) -> str:
f'<div class="chart-panel active" data-range="1Y">{chart_1y}</div>'
f'<div class="chart-panel" data-range="6M">{chart_6m}</div>'
f'<div class="chart-panel" data-range="1M">{chart_1m}</div>'
+ _turn_cap
)
# ---- 외국인/기관 수급 표 (최근 20일) ----
@@ -1583,11 +1783,11 @@ def build_report_html(snap: dict, parsed: dict) -> str:
'<thead><tr><th>일자</th><th>종가</th><th>외국인</th><th>기관</th><th>개인</th></tr></thead>'
'<tbody>' + ''.join(flow_rows) + '</tbody>'
'</table>'
f'<p class="flowsum">20일 누적 — '
f'<p class="flowsum"><b>20일 누적</b><br>'
f'<b style="color:{"#ef4444" if fs["foreign_net"]>0 else "#3b82f6"}">외국인 {fs["foreign_net"]*1000:+,}주</b> '
f'(매수일 {fs["foreign_buy_days"]}/20) · '
f'(매수일 {fs["foreign_buy_days"]}/20)<br>'
f'<b style="color:{"#ef4444" if fs["institution_net"]>0 else "#3b82f6"}">기관 {fs["institution_net"]*1000:+,}주</b> '
f'(매수일 {fs["institution_buy_days"]}/20) · '
f'(매수일 {fs["institution_buy_days"]}/20)<br>'
f'개인 {fs["individual_net"]*1000:+,}주</p>'
)
@@ -1616,7 +1816,7 @@ def build_report_html(snap: dict, parsed: dict) -> str:
paras = [p.strip() for p in s['body'].split('\n\n') if p.strip()]
if not paras:
paras = [s['body']]
body_html = ''.join(f'<p>{_esc(para).replace(chr(10), "<br>")}</p>' for para in paras)
body_html = ''.join(f'<p>{_emphasize(_esc(para)).replace(chr(10), "<br>")}</p>' for para in paras)
comment_html_parts.append(
f'<div class="rpt-llm-block"><h4>{title}</h4>{body_html}</div>'
)
@@ -1786,16 +1986,19 @@ body{background:#0f1419;color:#cfd5df;font-family:-apple-system,BlinkMacSystemFo
/* 보조 섹션 2단(넓으면 다단) 그리드 — 한 화면에 모이게 */
.rpt-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px;align-items:start;margin:14px 0}
.rpt-cols>.rpt-section{margin:0;height:100%}
.kvtable{border-collapse:collapse;width:100%}
.kvtable th{text-align:left;padding:7px 10px;color:#7a8493;font-weight:500;width:160px;border-bottom:1px solid #1f2937;font-size:13px}
.kvtable td{padding:7px 10px;border-bottom:1px solid #1f2937}
.flowtable{border-collapse:collapse;width:100%;font-size:13px}
.flowtable th{padding:6px 10px;color:#7a8493;border-bottom:1px solid #2a2f3a;text-align:left}
.flowtable td{padding:5px 10px;border-bottom:1px solid #1a1f28}
.flowsum{margin:10px 0 0;padding:10px;background:#0f1419;border-radius:6px;font-size:13px}
.kvtable{border-collapse:collapse;width:100%;font-size:12px}
.kvtable th{text-align:left;padding:6px 9px;color:#7a8493;font-weight:500;width:160px;border-bottom:1px solid #1f2937;font-size:12px}
.kvtable td{padding:6px 9px;border-bottom:1px solid #1f2937}
.tbl-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.tbl-scroll>table{min-width:100%;width:max-content}
.flowtable{border-collapse:collapse;width:100%;font-size:12px}
.flowtable th{padding:5px 9px;color:#7a8493;border-bottom:1px solid #2a2f3a;text-align:left;white-space:nowrap}
.flowtable td{padding:5px 9px;border-bottom:1px solid #1a1f28;white-space:nowrap}
.flowsum{margin:10px 0 0;padding:10px;background:#0f1419;border-radius:6px;font-size:13px;line-height:1.8}
.rpt-llm-block{margin:16px 0;padding:14px;background:#0f1419;border-radius:6px}
.rpt-llm-block h4{margin:0 0 10px;color:#fbbf24;font-size:14px}
.rpt-llm-block p{margin:8px 0;text-align:justify}
.rpt-em{color:#ff6b6b;font-weight:600}
.rpt-verdict{background:linear-gradient(135deg,#1f2937 0%,#161b22 100%)}
.verdict-line{font-size:16px;font-weight:600;margin:10px 0 6px;color:#fff}
.rpt-disclaimer{color:#7a8493;font-size:12px;margin:6px 0 0}
@@ -1806,6 +2009,8 @@ body{background:#0f1419;color:#cfd5df;font-family:-apple-system,BlinkMacSystemFo
.chart-tabs button.active{background:rgba(251,191,36,0.12);color:#fbbf24;border-color:#fbbf24}
.chart-panel{display:none}
.chart-panel.active{display:block}
.chart-cap{margin:8px 0 4px;padding:8px 10px 0;font-size:9px;line-height:1.5;color:#7a8493;border-top:1px solid #1d222b}
.chart-cap b{color:#9aa4b2;font-weight:600}
.kvtable th{vertical-align:top}
.info-label{cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;border-bottom:1px dotted #5b6473;display:inline-flex;align-items:center;gap:4px;color:#7a8493;padding:1px 2px}
.info-label::after{content:'ⓘ';font-size:11px;color:#5b6473;font-weight:normal}
@@ -1836,8 +2041,8 @@ body{background:#0f1419;color:#cfd5df;font-family:-apple-system,BlinkMacSystemFo
.peertable th:not(:first-child){text-align:right}
.peertable td{padding:7px 10px;border-bottom:1px solid #1a1f28}
.peers-note{margin:10px 0 0;color:#7a8493;font-size:11px}
.basicpeertable{border-collapse:collapse;width:100%;font-size:13px;table-layout:fixed}
.basicpeertable th.lbl{text-align:left;padding:7px 10px;color:#7a8493;font-weight:500;width:140px;border-bottom:1px solid #1f2937;font-size:13px;vertical-align:top}
.basicpeertable{border-collapse:collapse;width:100%;font-size:12px;table-layout:fixed}
.basicpeertable th.lbl{text-align:left;padding:6px 9px;color:#7a8493;font-weight:500;width:130px;border-bottom:1px solid #1f2937;font-size:12px;vertical-align:top}
.basicpeertable thead th{padding:8px 10px;color:#cfd5df;border-bottom:1px solid #2a2f3a;font-weight:600;text-align:left;background:#0f1419}
.basicpeertable thead th:nth-child(2){color:#fbbf24}
.basicpeertable td{padding:7px 10px;border-bottom:1px solid #1f2937;vertical-align:top;word-break:break-word}
@@ -37,6 +37,7 @@ from kiwoom_client import ( # noqa: E402
KST = ZoneInfo('Asia/Seoul')
STATE_DIR = WORKSPACE / 'state'
SNAPSHOT_FILE = STATE_DIR / 'portfolio_daily_snapshot.json'
STOCK_CODES_FILE = STATE_DIR / 'stock_codes.json'
RECIPIENT = 'mini.snowoyh@gmail.com'
CONFIG_PATH = Path('/Users/snowoyh/.openclaw/openclaw.json')
TELEGRAM_ACCOUNT = 'stock' # 레이 봇
@@ -161,6 +162,61 @@ def apply_ka10001_corrections(rows: list[dict]) -> int:
return applied
def correct_nxt_inactive_rows(rows: list[dict]) -> int:
"""NXT 미거래 보유종목을 KRX 시세로 보정한다.
kt00018 NXT 잔고 조회는 NXT 미거래 종목에서 KRX 최신가 대신 이전 거래일의
stale 가격을 반환할 수 있다. 종목 메타의 ``nxt_enable=false`` 인 코드만 KRX
ka10095 배치시세로 교체해 리포트와 일일 스냅샷에 같은 오류가 저장되지 않게 한다.
"""
codes = {r.get('code') for r in rows if r.get('code')}
if not codes:
return 0
cache = load_json(STOCK_CODES_FILE, {})
stocks = cache.get('stocks', {}) if isinstance(cache, dict) else {}
inactive = {
info.get('code')
for info in stocks.values()
if isinstance(info, dict)
and info.get('code') in codes
and info.get('nxt_enable') is False
}
if not inactive:
return 0
try:
quotes = get_watchlist_quotes(sorted(inactive), exchange='KRX')
except Exception as e:
print(f'[nxt-inactive-krx] fetch failed: {e}', file=sys.stderr)
return 0
corrected = 0
for r in rows:
if r.get('code') not in inactive:
continue
q = quotes.get(r['code']) or {}
price = q.get('price', 0) or 0
if price <= 0:
continue
change = q.get('change', 0) or 0
qty = r.get('qty', 0) or 0
buy_amount = r.get('buy_amount', 0) or 0
eval_value = price * qty
profit = eval_value - buy_amount
r.update({
'price': price,
'pred_close': price - change,
'day_change': change,
'day_change_pct': q.get('change_pct', 0.0) or 0.0,
'eval_value': eval_value,
'profit': profit,
'profit_rate': (profit / buy_amount * 100) if buy_amount else 0.0,
})
corrected += 1
return corrected
def collect_rows() -> tuple[list[dict], dict, dict]:
"""계좌별 포지션 → 평탄화된 row 리스트 + 계좌 잔고 + 계좌별 당일매매일지.
@@ -192,6 +248,9 @@ def collect_rows() -> tuple[list[dict], dict, dict]:
'tdy_buyq': p['tdy_buyq'],
'tdy_sellq': p['tdy_sellq'],
})
corrected = correct_nxt_inactive_rows(rows)
if corrected:
print(f'[nxt-inactive-krx] {corrected}개 보유행 KRX 시세 보정', file=sys.stderr)
balances = {a['label']: get_balance(a['label']) for a in list_accounts()}
journal_by_label = get_trade_journal_all()
return rows, balances, journal_by_label
@@ -520,14 +579,34 @@ def _render_html_owner(idx: int, owner: str, d: dict, balances: dict, prev_key,
)
# behive_web _render_owner_kpi 와 동일한 순서·라벨로 통일.
# 1) 총평가 → 2) 총매입 → 3) 총평가손익 → 4) 당일평가손익 → 5) 예수금 → 6) 순자산 → 7) 보유 종목 → 8) 당일 매매
# '총'은 계좌 전체(현금 포함)일 때만: 총자산 = 평가금액 + 예수금.
# 총자산 → 매입금액 → 평가금액 → 평가손익 → 예수금 → [당일 입출금] → 보유종목/당일매매 → 당일 평가손익
held = [r for r in d['consolidated'] if not r.get('phantom')]
parts.append(kpi('총 평가금액', f'{d["total_value"]:,}원'))
parts.append(kpi('총 매입금액', f'{d["total_cost"]:,}원'))
parts.append(kpi('총자산', f'{d["total_net"]:,}원'))
parts.append(kpi('매입금액', f'{d["total_cost"]:,}원'))
parts.append(kpi('평가금액', f'{d["total_value"]:,}원'))
parts.append(kpi(
'총 평가손익',
'평가손익',
f'{_signed(d["total_profit"])} ({_signed_pct(d["total_profit_rate"])})'
))
parts.append(kpi('예수금', f'{d["deposit"]:,}원'))
# 당일 입출금은 예수금 바로 아래 (현금 잔액 ↔ 그날 현금 이동)
cash_in = d.get('cash_in', 0)
cash_out = d.get('cash_out', 0)
if cash_in or cash_out:
net = cash_in - cash_out
nsign = '+' if net >= 0 else ''
ncolor = '#0a7' if net > 0 else ('#d33' if net < 0 else '#666')
# 한쪽만 있으면 순액과 같은 값이라 생략 (둘 다 있을 때만 쪼갠다)
detail = ''
if cash_in and cash_out:
detail = (f' <span style="color:#888;font-size:13px;">'
f'· 입금 {cash_in:,}원 · 출금 {cash_out:,}원</span>')
parts.append(kpi(
'당일 입출금',
f'<span style="color:{ncolor};">{nsign}{net:,}원</span>{detail}'
))
parts.append(kpi('보유종목/당일매매', f'{len(held)}개 / {len(d["traded_today"])}개'))
day_pl_total = d.get('day_pl_total')
if day_pl_total is None:
if prev_key:
@@ -540,26 +619,6 @@ def _render_html_owner(idx: int, owner: str, d: dict, balances: dict, prev_key,
pct_html = f' ({_signed_pct(pct)})'
suffix = f' <span style="color:#888;font-size:13px;">· {prev_key} 대비</span>' if prev_key else ''
parts.append(kpi('당일 평가손익', f'{_signed(day_pl_total)}{pct_html}{suffix}'))
parts.append(kpi('예수금', f'{d["deposit"]:,}원'))
parts.append(kpi('순자산', f'{d["total_net"]:,}원'))
cash_in = d.get('cash_in', 0)
cash_out = d.get('cash_out', 0)
if cash_in or cash_out:
net = cash_in - cash_out
nsign = '+' if net >= 0 else ''
ncolor = '#0a7' if net > 0 else ('#d33' if net < 0 else '#666')
detail_parts = []
if cash_in:
detail_parts.append(f'입금 {cash_in:,}원')
if cash_out:
detail_parts.append(f'출금 {cash_out:,}원')
detail = f' <span style="color:#888;font-size:13px;">· {" · ".join(detail_parts)}</span>' if detail_parts else ''
parts.append(kpi(
'당일 입출금',
f'<span style="color:{ncolor};">{nsign}{net:,}원</span>{detail}'
))
parts.append(kpi('보유 종목', f'{len(held)}개'))
parts.append(kpi('당일 매매', f'{len(d["traded_today"])}개'))
realized_pl = d.get('realized_pl_total', 0)
realized_fees = d.get('realized_fees_total', 0)
if d.get('traded_today') and (realized_pl or realized_fees):
@@ -710,13 +769,29 @@ def _render_html_body(owner_data: dict, ordered: list, balances: dict, today_key
f'<td style="{S["kpi_value"]}">{value_html}</td></tr>'
)
# behive_web KPI 순서로 통일
parts.append(kpi('총 평가금액', f'{grand_value:,}원'))
parts.append(kpi('총 매입금액', f'{grand_cost:,}원'))
# behive_web KPI 순서·라벨로 통일 (블록 자체가 전체 합산이라 '총' 중복 접두 제거)
parts.append(kpi('총자산', f'{grand_net:,}원'))
parts.append(kpi('매입금액', f'{grand_cost:,}원'))
parts.append(kpi('평가금액', f'{grand_value:,}원'))
parts.append(kpi(
'총 평가손익',
'평가손익',
f'{_signed(grand_profit)} ({_signed_pct(grand_rate)})'
))
parts.append(kpi('예수금', f'{grand_deposit:,}원'))
# 당일 입출금은 예수금 바로 아래
if grand_cash_in or grand_cash_out:
gn = grand_net_cash_flow
gsign = '+' if gn >= 0 else ''
gcolor = '#0a7' if gn > 0 else ('#d33' if gn < 0 else '#666')
# 한쪽만 있으면 순액과 같은 값이라 생략
gdetail = ''
if grand_cash_in and grand_cash_out:
gdetail = (f' <span style="color:#888;font-size:13px;">'
f'· 입금 {grand_cash_in:,}원 · 출금 {grand_cash_out:,}원</span>')
parts.append(kpi(
'당일 입출금',
f'<span style="color:{gcolor};">{gsign}{gn:,}원</span>{gdetail}'
))
if grand_day_pl_total is not None:
pct_html = ''
if grand_prev_net:
@@ -726,22 +801,6 @@ def _render_html_body(owner_data: dict, ordered: list, balances: dict, today_key
parts.append(kpi('당일 평가손익', f'{_signed(grand_day_pl_total)}{pct_html}{suffix}'))
elif prev_key:
parts.append(kpi('당일 평가손익', '<span style="color:#888;">전날 스냅샷 없음</span>'))
parts.append(kpi('총 예수금', f'{grand_deposit:,}원'))
parts.append(kpi('총 순자산', f'{grand_net:,}원'))
if grand_cash_in or grand_cash_out:
gn = grand_net_cash_flow
gsign = '+' if gn >= 0 else ''
gcolor = '#0a7' if gn > 0 else ('#d33' if gn < 0 else '#666')
gd = []
if grand_cash_in:
gd.append(f'입금 {grand_cash_in:,}원')
if grand_cash_out:
gd.append(f'출금 {grand_cash_out:,}원')
gdetail = f' <span style="color:#888;font-size:13px;">· {" · ".join(gd)}</span>'
parts.append(kpi(
'당일 입출금',
f'<span style="color:{gcolor};">{gsign}{gn:,}원</span>{gdetail}'
))
grand_realized_pl = sum(d.get('realized_pl_total', 0) for d in owner_data.values())
grand_realized_fees = sum(d.get('realized_fees_total', 0) for d in owner_data.values())
if any(d.get('traded_today') for d in owner_data.values()) and (grand_realized_pl or grand_realized_fees):
@@ -767,6 +826,20 @@ def build_report(by_account: bool = False) -> tuple[str, str, str]:
raise RuntimeError('키움 REST 응답에 보유종목이 없습니다.')
today_key = datetime.now(KST).strftime('%Y-%m-%d')
# 당일 매수 동반 매도행의 pl을 '전일 보유 평단' 기준으로 재계산 (ka10170 round-trip 오표시 교정).
# 자산웹과 동일 기준 — 전일 평단은 파일 이력 재생(prev_avg_map, 당일분 제외)에서.
try:
import trade_journal as tj
_prev_avgs = tj.prev_avg_map(today_key)
for _lb, _trs in journal_by_label.items():
for _t in _trs:
_pa = _prev_avgs.get((_lb, _t.get('code')), 0)
if (_t.get('sell_qty') or 0) > 0 and (_t.get('buy_qty') or 0) > 0 and _pa > 0:
tj._apply_rebase(_t, float(_t.get('sell_avg') or 0), int(_t['sell_qty']), _pa)
except Exception as e:
print(f'[journal rebase] failed: {e}', file=sys.stderr)
all_labels = [a['label'] for a in list_accounts()]
owner_groups = group_by_owner(all_labels)
@@ -998,14 +1071,18 @@ def build_report(by_account: bool = False) -> tuple[str, str, str]:
title = OWNER_LABELS.get(owner, f'{owner} 자산')
labels_disp = ' + '.join(d['labels'])
held = [r for r in d['consolidated'] if not r.get('phantom')]
# behive_web KPI 순서·라벨로 통일
# behive_web KPI 순서·라벨로 통일. 라벨은 표시폭 17칸 정렬(한글 2칸 기준).
lines += [
'==============================',
f"{idx}) {title} ({labels_disp})",
'==============================',
f'총 평가금액 : {d["total_value"]:,}원',
f'총 매입금액 : {d["total_cost"]:,}원',
f'총 평가손익 : {d["total_profit"]:+,}원 ({d["total_profit_rate"]:+.2f}%)',
f'총자산 : {d["total_net"]:,}원',
f'매입금액 : {d["total_cost"]:,}원',
f'평가금액 : {d["total_value"]:,}원',
f'평가손익 : {d["total_profit"]:+,}원 ({d["total_profit_rate"]:+.2f}%)',
f'예수금 : {d["deposit"]:,}원',
# '보유종목/당일매매'는 표시폭이 정확히 17칸이라 패딩 없이 콜론이 맞는다.
f'보유종목/당일매매: {len(held)}개 / {len(d["traded_today"])}개',
]
day_pl = d.get('day_pl_total')
if day_pl is not None:
@@ -1015,10 +1092,6 @@ def build_report(by_account: bool = False) -> tuple[str, str, str]:
lines.append(f'당일 평가손익 : {day_pl:+,}원 ({pct:+.2f}%){suffix}')
elif prev_key:
lines.append(f'당일 평가손익 : 전날 스냅샷 없음 ({prev_key})')
lines.append(f'예수금 : {d["deposit"]:,}원')
lines.append(f'순자산 : {d["total_net"]:,}원')
lines.append(f'보유 종목 : {len(held)}개')
lines.append(f'당일 매매 : {len(d["traded_today"])}개')
if d.get('traded_today') and (d.get('realized_pl_total', 0) or d.get('realized_fees_total', 0)):
fee = d.get('realized_fees_total', 0)
fee_part = f' (수수료·세금 {fee:,}원)' if fee else ''
@@ -1112,7 +1185,7 @@ def build_report(by_account: bool = False) -> tuple[str, str, str]:
tg_lines = [f'📊 주식 리포트 | {today_key} 20:00']
tg_lines.append('')
tg_lines.append(f'평가 {grand_value:,}원 · 예수금 {grand_deposit:,}원')
tg_lines.append(f'순자산 {grand_net:,}원')
tg_lines.append(f'총자산 {grand_net:,}원')
tg_lines.append(f'{grand_profit:+,}원 ({grand_rate:+.2f}%)')
if grand_day_pl_total is not None:
tg_lines.append(f'당일 평가손익 {grand_day_pl_total:+,}원 ({prev_key} 대비)')
@@ -1121,7 +1194,10 @@ def build_report(by_account: bool = False) -> tuple[str, str, str]:
elif owner_data:
tg_lines.append('전날 이전 스냅샷 없음')
if grand_cash_in or grand_cash_out:
tg_lines.append(f'당일 입출금 {grand_net_cash_flow:+,}원 (입금 {grand_cash_in:,}원 · 출금 {grand_cash_out:,}원)')
# 한쪽만 있으면 순액과 같은 값이라 괄호 생략
_brk = (f' (입금 {grand_cash_in:,}원 · 출금 {grand_cash_out:,}원)'
if (grand_cash_in and grand_cash_out) else '')
tg_lines.append(f'당일 입출금 {grand_net_cash_flow:+,}원{_brk}')
grand_realized_pl = sum(d.get('realized_pl_total', 0) for d in owner_data.values())
if any(d.get('traded_today') for d in owner_data.values()) and grand_realized_pl:
tg_lines.append(f'당일 실현 {grand_realized_pl:+,}원')
@@ -1130,7 +1206,7 @@ def build_report(by_account: bool = False) -> tuple[str, str, str]:
for owner in ordered:
d = owner_data[owner]
title = OWNER_LABELS.get(owner, owner).replace(' 자산', '')
tg_lines.append(f'• {title} 순자산 {d["total_net"]:,}원')
tg_lines.append(f'• {title} 총자산 {d["total_net"]:,}원')
tg_lines.append(f' 평가 {d["total_value"]:,}원 · 예수금 {d["deposit"]:,}원')
tg_lines.append(f' {d["total_profit"]:+,}원 ({d["total_profit_rate"]:+.2f}%)')
if d.get('day_pl_total') is not None:
@@ -1263,9 +1339,12 @@ def main() -> int:
args = sys.argv[1:]
mode = 'run'
by_account = False
no_mail = False
for a in args:
if a == '--by-account':
by_account = True
elif a == '--no-mail':
no_mail = True
elif a in ('run', 'send'):
mode = a
else:
@@ -1274,7 +1353,7 @@ def main() -> int:
print(
f"[START] stock_portfolio_report mode={mode} by_account={by_account} "
f"ts={started_at.isoformat()} pid={os.getpid()}"
f"no_mail={no_mail} ts={started_at.isoformat()} pid={os.getpid()}"
)
# send 모드는 휴장일/주말이면 즉시 스킵 — 시세·잔고 무변동이라 메일이 어제와 동일.
@@ -1301,6 +1380,12 @@ def main() -> int:
subject = f"[주식 리포트] {datetime.now(KST):%Y-%m-%d} 20:00"
if mode == 'send':
# --no-mail: 스냅샷만 남기는 스케줄 실행용. 휴장일 self-skip·실패 알림은 그대로 유지해야
# 하므로 run 모드가 아니라 send 경로에서 메일 호출만 건너뛴다.
if no_mail:
print('MAIL_SKIPPED=no_mail_flag')
print(f"[END] stock_portfolio_report status=ok ts={datetime.now(KST).isoformat()}")
return 0
mail_ok = False
try:
send_result = send_mail(subject, html_body, html=True)
@@ -0,0 +1,597 @@
#!/usr/bin/env python3
"""토글 켜진 종목의 급등락 감시 → 레이 텔레그램 알림.
LLM을 깨우지 않음. 세 기준을 병행한다:
1. 거래소 상·하한가 도달 — 그날 갈 수 있는 끝까지 간 것. 가장 강한 신호라 무엇에도 가리지 않는다.
2. 거래소 VI 발동 — "이건 급등락이다"를 거래소가 공식 판정. 문턱을 우리가 정할 필요가 없다.
3. 자기 이력 분위수 — 장중 이탈폭(전일종가 대비)이 그 종목 과거 이탈폭의 p90 을 넘으면 알림.
고정 %를 쓰지 않는 이유 (실측 관심·감시 65종목 × 280거래일, 2026-08-04):
- 장중 이탈폭 중간값이 3.8% → ±3%는 급등락이 아니라 평범한 날
- 종목별 변동성이 22배 차(ATR14 1.0%~22.3%). ±5% 문턱이면 KODEX 미국S&P500 은 발생 0회,
SK이터닉스는 75회. 같은 5%가 한쪽엔 도달 불가, 한쪽엔 노이즈다.
⚠️ ATR 배수(ATR% × K)를 쓰지 않는 이유 — 2026-08-04 관리자님 지적으로 교체:
국내 주식 하루 가격제한폭이 ±30% 인데 ATR 배수는 상한이 없어 변동성 큰 종목의 문턱이
제한폭 밖으로 밀려난다. 실측: 1차(ATR×1.5)가 5/65 종목, 확대(ATR×3.0)가 31/65 종목(48%)에서
30% 초과 = **영원히 발동 불가**였다(ATR% 중간값이 10%라 확대는 절반이 죽는다).
분위수는 실제로 관측된 이탈폭이라 구조적으로 제한폭을 넘을 수 없다(p90~p99 전부 30% 초과 0종목).
덤으로 알림량이 정의상 (1-p) 비율로 확정된다 — p90 = 종목당 연 25회.
⚠️ 신규상장 첫날·정리매매는 제한폭 예외지만, 그런 종목은 이력이 없어 애초에 VI만 감시한다.
데이터원은 behive_web 의 /api/realtime/quotes — 현재가와 VI를 한 번에 주고 키움 호출이 0이다.
VI(1h)는 시장 전역 broadcast라 구독이 필요 없고, 현재가(0B)는 구독이 필요한데 behive_web 의
_rt_gather_codes() 가 이미 보유+관심+감시를 구독하므로 토글 종목은 그 부분집합이다.
구독 상한에 걸려 빠진 종목만 ka10095 배치 1콜로 폴백한다.
⚠️ 상·하한가·VI 가 걸려 있는 동안 분위수 트리거는 **보류**했다가 풀린 뒤에 내보낸다
(2026-08-06 관리자님 요청). 그전엔 조용히 버려져서, VI 중에 문턱을 넘었다가 해제 시점에
되밀린 움직임은 알림이 아예 없었다. 같은 사건을 두 번 알리지 않으면서 사실은 잃지 않기 위함.
state:
behive_surge_toggles.json — 감시 대상 (behive_web 의 🔔 토글이 씀)
surge_thresholds.json — {date, by_code: {code: {thr, thr_big, prev_close, n}}}. 하루 1회만 계산
surge_limits.json — {date, by_code: {code: {upl, lst}}}. 거래소 상·하한가, 하루 1콜
surge_alerts.json — {date: {code: [트리거키], __pending__: {code: {키: 보류정보}}}}
방향별 1회 + 확대단계 1회 + 상·하한가 1회, VI 는 발동 건마다 1회
Usage:
python3 surge_monitor.py check # 1회 감시 (launchd 용)
python3 surge_monitor.py check --force # 장외에도 실행 (테스트용)
python3 surge_monitor.py dry-run # 판정만 출력, 텔레그램 발송 없음
python3 surge_monitor.py list # 토글 종목의 문턱(%)·발동가 출력
"""
from __future__ import annotations
import fcntl
import json
import sys
import time
import urllib.parse
import urllib.request
from contextlib import contextmanager
from datetime import datetime, timezone, timedelta
from pathlib import Path
KST = timezone(timedelta(hours=9))
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
sys.path.insert(0, str(WORKSPACE / 'scripts'))
sys.path.insert(0, str(WORKSPACE))
import kiwoom_client as kc # noqa: E402
STATE_DIR = WORKSPACE / 'state'
TOGGLES = STATE_DIR / 'behive_surge_toggles.json'
THR_CACHE = STATE_DIR / 'surge_thresholds.json'
LIMITS_CACHE = STATE_DIR / 'surge_limits.json'
ALERTS_STATE = STATE_DIR / 'surge_alerts.json'
# 보류 트리거를 담는 예약 키. 종목코드는 6자리라 이 이름과 충돌하지 않는다.
PENDING_KEY = '__pending__'
CONFIG_PATH = Path('/Users/snowoyh/.openclaw/openclaw.json')
TELEGRAM_ACCOUNT = 'stock' # 레이 봇
# 1차 문턱 = 그 종목 과거 장중 이탈폭의 이 분위수. 통수를 정하는 유일한 손잡이 —
# 분위수라 알림량이 정의상 (1-p) 비율로 확정된다. 0.90 = 종목당 연 25회(≈2주 1번).
SURGE_PCTL = 0.90
# 확대 단계 — 1차 알림 후 여기까지 더 벌어지면 한 번 더. 종목당 하루 최대 2통.
SURGE_PCTL_BIG = 0.98
# 분위수 산출에 필요한 최소 관측일. 미달(신규상장 등)이면 VI만 감시한다.
MIN_HISTORY = 60
# 이력 조회 봉 수 — sqlite 캐시에서 읽는 양만 늘린다(추가 API 콜 없음).
HISTORY_COUNT = 400
# 문턱 산출 페이싱 — ka10081 유량이 초당 5건이라 캐시가 stale 한 종목이 많으면 429가 쏟아진다.
MAX_COMPUTE_PER_CYCLE = 12
COMPUTE_PACE_SEC = 0.35
# behive_web 실시간 엔드포인트. BIND_HOST='' 라 localhost 로 도달한다.
RT_URL = 'http://127.0.0.1:18790/api/realtime/quotes'
RT_TIMEOUT = 5
MARKET_OPEN = (9, 0)
# 20:00 = NXT 애프터마켓 + KRX 애프터마켓(2026-09-14 시행) 마감.
# ⚠️ 마감 후 버퍼를 두지 않는다 — 데이터원인 realtime_hub 가 20:00 에 종료되므로
# (`_market_open()` → `behive_web._market_phase_state()['active']`) 그 뒤 사이클은
# 시세·VI 를 못 받고 헛돈다. plist 도 19:59 까지만 발화한다.
# ⚠️ 확대가 성립하는 전제는 hub 구독이 `_AL`(통합) 이라는 것 — KRX 애프터마켓 체결이
# 그 구독으로 들어온다. bare 코드면 15:30 이후 시세가 끊겨 감시가 무의미하다.
MARKET_CLOSE = (20, 0)
def load_json(path: Path, default):
if path.exists():
try:
return json.loads(path.read_text())
except Exception:
return default
return default
def save_json(path: Path, data):
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + '.tmp')
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2))
tmp.replace(path)
@contextmanager
def alerts_lock():
"""surge_alerts.json 동시 쓰기 직렬화. watchlist_monitor 와 동일 패턴, 별도 lock 파일."""
lock_path = ALERTS_STATE.with_suffix(ALERTS_STATE.suffix + '.lock')
lock_path.parent.mkdir(parents=True, exist_ok=True)
f = open(lock_path, 'a')
try:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
yield
finally:
try:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
finally:
f.close()
def is_market_hours() -> bool:
now = datetime.now(KST)
if now.weekday() >= 5:
return False
# KRX 휴장일도 거래 없음. 데이터 파일 누락이나 import 실패 시엔 평소대로 진행.
try:
from holiday_sync import is_holiday_today
if is_holiday_today():
return False
except Exception:
pass
mins = now.hour * 60 + now.minute
return (MARKET_OPEN[0] * 60 + MARKET_OPEN[1]) <= mins <= (MARKET_CLOSE[0] * 60 + MARKET_CLOSE[1])
def send_telegram(text: str) -> bool:
cfg = json.loads(CONFIG_PATH.read_text())
acct = cfg['channels']['telegram']['accounts'][TELEGRAM_ACCOUNT]
token = acct['botToken']
chat_ids = acct.get('allowFrom') or []
if not chat_ids:
print('no telegram chat_ids', file=sys.stderr)
return False
url = f'https://api.telegram.org/bot{token}/sendMessage'
ok = True
for chat_id in chat_ids:
data = urllib.parse.urlencode({
'chat_id': chat_id,
'text': text[:4000],
'disable_web_page_preview': 'true',
}).encode()
try:
req = urllib.request.Request(url, data=data, method='POST')
with urllib.request.urlopen(req, timeout=15) as r:
if r.status != 200:
ok = False
print(f'telegram HTTP {r.status}', file=sys.stderr)
except Exception as e:
print(f'telegram error: {e}', file=sys.stderr)
ok = False
return ok
# ---------------- 감시 대상 ----------------
def load_toggles() -> dict[str, str]:
"""{code: name} — behive_web 의 🔔 토글이 켠 종목만."""
raw = load_json(TOGGLES, {})
by_code = raw.get('by_code') if isinstance(raw, dict) else None
if not isinstance(by_code, dict):
return {}
out: dict[str, str] = {}
for code, v in by_code.items():
cc = kc._clean_code(code)
if not cc or not v:
continue
# v 는 True(레거시) 또는 {'name': ...}
out[cc] = (v.get('name') or '') if isinstance(v, dict) else ''
return out
# ---------------- 데이터원 ----------------
def fetch_realtime(codes: list[str]) -> tuple[dict, dict, bool]:
"""behive_web 실시간 허브에서 (quotes, vi, ok). 키움 호출 0.
허브가 콜드하거나 behive_web 이 죽어 있으면 ok=False — 호출측이 폴백/경고를 결정한다.
"""
if not codes:
return {}, {}, False
url = f'{RT_URL}?codes={",".join(codes)}'
try:
with urllib.request.urlopen(url, timeout=RT_TIMEOUT) as r:
d = json.loads(r.read().decode())
except Exception as e:
print(f'[warn] behive_web 실시간 조회 실패 ({e}) — VI 감시 불가, ATR만 진행', file=sys.stderr)
return {}, {}, False
return (d.get('quotes') or {}), (d.get('vi') or {}), bool(d.get('connected'))
def fetch_quotes_fallback(codes: list[str]) -> dict:
"""구독 상한에 걸려 허브에 없는 종목만 ka10095 배치 1콜로 보충."""
if not codes:
return {}
try:
return kc.get_watchlist_quotes(codes) or {}
except Exception as e:
print(f'[warn] ka10095 배치 실패: {e}', file=sys.stderr)
return {}
# ---------------- 문턱 (자기 이력 분위수) ----------------
def _quantile(sorted_vals: list[float], p: float) -> float:
"""오름차순 리스트의 p 분위수. 실측 스크립트와 같은 식(nearest-rank)을 써야 값이 일치한다."""
return sorted_vals[min(len(sorted_vals) - 1, int(len(sorted_vals) * p))]
def _compute_thresholds(code: str) -> tuple[dict | None, bool]:
"""(문턱 dict 또는 None, 재시도해야 하는가).
장중 이탈폭 = max(|고가−전일종가|, |저가−전일종가|) / 전일종가 × 100.
종가 대비가 아니라 **전일종가 대비 장중 최대 이탈폭**을 쓰는 이유 — 실시간 감시가 보는 값이
`현재가 vs 전일종가`(키움 pct)라서 같은 축으로 비교해야 한다. 종가기준은 장중 움직임을
과소평가해(중간값 3.8% vs 종가기준 1.x%) 문턱이 너무 낮게 잡힌다.
⚠️ 두 번째 반환값이 필요한 이유 — 조회 실패(429·네트워크)를 이력부족과 같이 취급해
빈 dict 로 캐시하면 그 종목이 **하루 내내 조용히 VI만 감시**하게 된다. 실패는 재시도 대상.
"""
try:
import daily_candles_cache as dcc
candles = dcc.get_candles(code, HISTORY_COUNT)
except Exception as e:
print(f'[{code}] 일봉 조회 실패 (다음 사이클 재시도): {e}', file=sys.stderr)
return None, True
if not candles or len(candles) < MIN_HISTORY:
return None, False
exc: list[float] = []
for i in range(1, len(candles)):
pc = candles[i - 1]['close']
if not pc:
continue
exc.append(max(abs(candles[i]['high'] - pc), abs(candles[i]['low'] - pc)) / pc * 100)
if len(exc) < MIN_HISTORY:
return None, False
exc.sort()
prev_close = candles[-1]['close']
if not prev_close:
return None, False
return {
'thr': round(_quantile(exc, SURGE_PCTL), 2),
'thr_big': round(_quantile(exc, SURGE_PCTL_BIG), 2),
'prev_close': prev_close,
'n': len(exc),
}, False
def get_threshold_map(codes: list[str]) -> dict[str, dict]:
"""종목별 문턱 맵. 하루 1회만 계산하고 캐시한다.
⚠️ daily_candles_cache.get_candles 는 캐시 최신봉이 어제보다 오래되면 ka10081 을 때린다.
매 사이클(1분) × 종목수만큼 호출하면 폭주하므로 날짜가 바뀔 때와 새 토글이 생길 때만 계산.
⚠️ 분위수 파라미터를 바꿨으면 캐시가 옛 값을 들고 있으니 `pctl` 서명이 다르면 재계산한다.
⚠️ **ka10081 유량 제한이 초당 5건**이라 종목을 한꺼번에 돌리면 429가 쏟아진다(2026-08-04 실측:
70종목 일괄 산출 시 26종목 실패). 그래서 사이클당 `MAX_COMPUTE_PER_CYCLE` 개까지만,
`COMPUTE_PACE_SEC` 간격으로 계산한다. 남은 종목은 다음 사이클이 이어받는다(1분 간격 × 396회라
장 시작 몇 분 안에 전부 채워진다). 조회 실패는 캐시하지 않아 다음 사이클에 재시도된다.
"""
today = datetime.now(KST).strftime('%Y-%m-%d')
sig = f'{SURGE_PCTL}/{SURGE_PCTL_BIG}'
cache = load_json(THR_CACHE, {})
fresh = cache.get('date') == today and cache.get('pctl') == sig
by_code = cache.get('by_code') if fresh else {}
if not isinstance(by_code, dict):
by_code = {}
missing = [c for c in codes if c not in by_code][:MAX_COMPUTE_PER_CYCLE]
if missing:
for i, c in enumerate(missing):
if i:
time.sleep(COMPUTE_PACE_SEC)
r, retry = _compute_thresholds(c)
if retry:
continue # 실패는 기록하지 않는다 — 다음 사이클에 다시 시도
by_code[c] = r if r else {}
save_json(THR_CACHE, {'date': today, 'pctl': sig, 'by_code': by_code})
return {c: v for c, v in by_code.items() if v.get('thr')}
# ---------------- 상·하한가 (거래소 계산값) ----------------
def get_limit_map(codes: list[str]) -> dict[str, dict]:
"""{code: {'upl', 'lst'}} — 거래소가 계산한 가격제한폭. 하루 1콜(ka10095 배치).
기준가(전일종가)로 정해져 장중 불변이라 하루 1회면 충분하다. 분위수 문턱과 달리
일봉 이력이 필요 없어서 신규상장 등 이력 부족 종목도 이 기준으론 감시된다.
⚠️ 배치 호출 자체가 실패하면 캐시하지 않는다 — 문턱 캐시와 같은 이유로, 실패를
'제한가 없음'으로 굳히면 그 종목이 하루 내내 조용히 빠진다. 응답에 값이 없는
종목만 빈 dict 로 굳혀 매 사이클 재조회를 막는다.
"""
if not codes:
return {}
today = datetime.now(KST).strftime('%Y-%m-%d')
cache = load_json(LIMITS_CACHE, {})
by_code = cache.get('by_code') if cache.get('date') == today else {}
if not isinstance(by_code, dict):
by_code = {}
missing = [c for c in codes if c not in by_code]
if missing:
try:
rows = kc.get_watchlist_quotes(missing) or {}
except Exception as e:
print(f'[warn] 상하한가 조회 실패 (다음 사이클 재시도): {e}', file=sys.stderr)
return {c: v for c, v in by_code.items() if v.get('upl')}
for c in missing:
r = rows.get(c) or {}
upl, lst = r.get('upper_limit') or 0, r.get('lower_limit') or 0
by_code[c] = {'upl': upl, 'lst': lst} if (upl and lst) else {}
save_json(LIMITS_CACHE, {'date': today, 'by_code': by_code})
return {c: v for c, v in by_code.items() if v.get('upl')}
# ---------------- 판정 ----------------
def _limit_hit(price: int, limits: dict | None) -> str | None:
"""'up'(상한가) | 'down'(하한가) | None. 거래소 제한가와 현재가 비교."""
if not limits or not price:
return None
upl, lst = limits.get('upl') or 0, limits.get('lst') or 0
if upl and price >= upl:
return 'up'
if lst and price <= lst:
return 'down'
return None
def _quantile_triggers(pct: float, thr: dict | None) -> list[dict]:
"""분위수 트리거만 — [{key, reason, word}]."""
if not thr or not thr.get('thr'):
return []
direction = 'up' if pct > 0 else 'down'
word = '급등' if pct > 0 else '급락'
t1, t2 = thr['thr'], thr.get('thr_big') or 0
top1 = round((1 - SURGE_PCTL) * 100)
top2 = round((1 - SURGE_PCTL_BIG) * 100)
out: list[dict] = []
if t2 and abs(pct) >= t2:
out.append({'key': f'{direction}2', 'word': word,
'reason': f'{thr["n"]}일 중 상위 {top2}% 움직임 (문턱 {t2:.1f}%)'})
if abs(pct) >= t1:
out.append({'key': direction, 'word': word,
'reason': f'{thr["n"]}일 중 상위 {top1}% 움직임 (문턱 {t1:.1f}%)'})
return out
def evaluate(pct: float, price: int, thr: dict | None, vi: dict | None,
limits: dict | None) -> tuple[list[dict], list[dict], str]:
"""(즉시 알릴 트리거, 보류할 트리거, 가림사유).
거래소 판정(상·하한가·VI)이 걸려 있는 동안 분위수 트리거는 **보류**한다. 같은 사건을
두 번 알리지 않으면서도, 그 사이 문턱을 넘은 사실은 버리지 않고 풀린 뒤에 내보내기 위해서다.
가림사유가 빈 문자열이면 가린 것이 없다는 뜻 — 호출측은 이때 보류분을 방출한다.
⚠️ 가림 여부는 `defer` 가 비었는지로 판단하면 안 된다. VI 중에 주가가 문턱 아래로
되밀리면 defer 도 비는데, 그걸 '가림 해제'로 읽으면 VI 도중에 보류분이 새어나간다.
"""
now: list[dict] = []
hit = _limit_hit(price, limits)
if hit == 'up':
now.append({'key': 'limit_up', 'word': '상한가',
'reason': f'거래소 상한가 도달 ({(limits or {}).get("upl", 0):,}원)'})
elif hit == 'down':
now.append({'key': 'limit_down', 'word': '하한가',
'reason': f'거래소 하한가 도달 ({(limits or {}).get("lst", 0):,}원)'})
vi_on = bool(vi and vi.get('active'))
if vi_on:
t = (vi.get('trigger_time') or '').strip() or 'na'
kind = (vi.get('apply_kind') or '').strip() or 'VI'
now.append({'key': f'vi:{t}', 'word': None, 'reason': f'거래소 VI 발동 ({kind})'})
quantile = _quantile_triggers(pct, thr)
if hit or vi_on:
return now, quantile, ({'up': '상한가', 'down': '하한가'}.get(hit) or 'VI')
return now + quantile, [], ''
def build_message(records: list[dict]) -> str:
ts = datetime.now(KST).strftime('%m/%d %H:%M')
lines = [f'[급등락] {ts}', f'{len(records)}건 감지']
for r in records:
pct = r['pct']
# ⚠️ 아이콘은 pct 가 아니라 방향어에서 파생한다. 보류 방출 건은 돌파 시점 방향(급락)과
# 방출 시점 현재가 부호(+)가 어긋날 수 있어, pct 로 고르면 '🚀 급락'이 찍힌다.
word = r.get('word') or ('급등' if pct > 0 else '급락')
icon = {'상한가': '⏫', '하한가': '⏬', '급등': '🚀', '급락': '🔻'}.get(word, '🚀')
label = r['name'] or r['code']
lines.append('')
lines.append(f'{icon} #{label} {word} ({pct:+.2f}%)')
lines.append(f'• 현재가: {r["price"]:,}원')
lines.append(f'• 사유: {r["reason"]}')
if r.get('vi_price'):
lines.append(f'• VI 발동가: {r["vi_price"]:,}원')
return '\n'.join(lines)
# ---------------- 실행 ----------------
def _prune(state: dict, today: str) -> dict:
"""오늘·어제만 남긴다 (무한 증식 방지)."""
keep = {today, (datetime.now(KST) - timedelta(days=1)).strftime('%Y-%m-%d')}
return {k: v for k, v in state.items() if k in keep}
def run(dry: bool = False, force: bool = False) -> int:
if not force and not is_market_hours():
print(f'장외 시간 — skip ({datetime.now(KST).strftime("%Y-%m-%d %H:%M")})')
return 0
toggles = load_toggles()
if not toggles:
print('감시 대상 없음 — 자산웹에서 🔔 토글을 켜주세요')
return 0
codes = sorted(toggles)
quotes, vi_map, connected = fetch_realtime(codes)
missing = [c for c in codes if not (quotes.get(c) or {}).get('price')]
if missing:
for c, q in fetch_quotes_fallback(missing).items():
quotes[kc._clean_code(c)] = {'price': q.get('price'), 'pct': q.get('change_pct'), 'change': q.get('change')}
thr_map = get_threshold_map(codes)
limit_map = get_limit_map(codes)
today = datetime.now(KST).strftime('%Y-%m-%d')
day_state = load_json(ALERTS_STATE, {}).get(today) or {}
already = {f'{c}:{k}' for c, ks in day_state.items() if isinstance(ks, list) for k in ks}
# 보류분 — 감시에서 빠진 종목 것은 버린다(토글이 꺼졌으면 방출할 이유가 없다).
held = day_state.get(PENDING_KEY) or {}
pending = {c: v for c, v in held.items() if c in toggles and isinstance(v, dict)} if isinstance(held, dict) else {}
pending_sig = json.dumps(pending, sort_keys=True)
to_send: list[dict] = []
flushed: list[tuple[str, str]] = [] # (code, key) — 발송 성공 시에만 보류에서 지운다
def _rec(code: str, price: int, pct: float, trig: dict, vi: dict | None) -> dict:
r = {
'code': code,
'name': toggles.get(code) or '',
'price': price,
'pct': pct,
'key': trig['key'],
'word': trig.get('word'),
'reason': trig['reason'],
}
if vi and vi.get('active'):
r['vi_price'] = vi.get('trigger_price') or 0
return r
for code in codes:
q = quotes.get(code) or {}
if not q.get('price') or q.get('pct') is None:
continue
price, pct = int(q['price']), float(q['pct'])
vi = vi_map.get(code)
now_trigs, defer_trigs, blocked_by = evaluate(
pct, price, thr_map.get(code), vi, limit_map.get(code))
for trig in now_trigs:
if f'{code}:{trig["key"]}' in already:
continue
to_send.append(_rec(code, price, pct, trig, vi))
already.add(f'{code}:{trig["key"]}')
if blocked_by:
# 가림 중 — 문턱 돌파 사실만 적어둔다. 이탈폭이 가장 컸던 시점을 남긴다.
for trig in defer_trigs:
if f'{code}:{trig["key"]}' in already:
continue
cur = (pending.get(code) or {}).get(trig['key']) or {}
if abs(pct) > abs(cur.get('pct') or 0):
pending.setdefault(code, {})[trig['key']] = {
'pct': pct, 'word': trig.get('word'),
'reason': trig['reason'], 'via': blocked_by,
}
continue
# 가림 해제 — 보류분 방출. 같은 키를 위에서 이미 즉시 알렸으면 already 가 걸러낸다.
for key, d in sorted((pending.get(code) or {}).items()):
if f'{code}:{key}' in already:
flushed.append((code, key))
continue
trig = {
'key': key, 'word': d.get('word'),
'reason': f'{d.get("reason") or "문턱 돌파"} — {d.get("via") or "VI"} 중 최대 {d.get("pct", 0):+.2f}% 도달',
}
to_send.append(_rec(code, price, pct, trig, vi))
already.add(f'{code}:{key}')
flushed.append((code, key))
if dry:
print(f'감시 {len(codes)}종목 / 허브연결 {connected} / 문턱 {len(thr_map)}종목 / 제한가 {len(limit_map)}종목')
for code in codes:
q = quotes.get(code) or {}
t = thr_map.get(code) or {}
lm = limit_map.get(code) or {}
thr = f'{t["thr"]:.1f}% / 확대 {t["thr_big"]:.1f}%' if t.get('thr') else '— (이력부족, VI·제한가만)'
lim = f'{lm["upl"]:,}/{lm["lst"]:,}' if lm.get('upl') else '—'
print(f' {code} {toggles.get(code) or "":<12} 등락 {q.get("pct")}% / 문턱 {thr} / 상하한 {lim}'
f'{" / VI" if (vi_map.get(code) or {}).get("active") else ""}'
f'{" / 보류 " + ",".join(sorted(pending[code])) if pending.get(code) else ""}')
if to_send:
print('--- DRY ---')
print(build_message(to_send))
print(f'done. watched={len(codes)} triggered={len(to_send)} pending={sum(len(v) for v in pending.values())}')
return 0
ok = send_telegram(build_message(to_send)) if to_send else True
if ok:
for code, key in flushed:
bucket = pending.get(code) or {}
bucket.pop(key, None)
if not bucket:
pending.pop(code, None)
if not to_send and json.dumps(pending, sort_keys=True) == pending_sig:
print(f'done. watched={len(codes)} triggered=0')
return 0
with alerts_lock():
latest = _prune(load_json(ALERTS_STATE, {}), today)
day = latest.setdefault(today, {})
if ok:
for r in to_send:
bucket = day.setdefault(r['code'], [])
if r['key'] not in bucket:
bucket.append(r['key'])
print(f'alerted {r["code"]} {r["name"]}:{r["key"]} {r["pct"]:+.2f}% @ {r["price"]:,}원')
if pending:
day[PENDING_KEY] = pending
else:
day.pop(PENDING_KEY, None)
save_json(ALERTS_STATE, latest)
print(f'done. watched={len(codes)} triggered={len(to_send)} pending={sum(len(v) for v in pending.values())}')
return 0
def cmd_list() -> int:
toggles = load_toggles()
if not toggles:
print('감시 대상 없음 — 자산웹에서 🔔 토글을 켜주세요')
return 0
codes = sorted(toggles)
thr_map = get_threshold_map(codes)
limit_map = get_limit_map(codes)
top1 = round((1 - SURGE_PCTL) * 100)
top2 = round((1 - SURGE_PCTL_BIG) * 100)
print(f'감시 {len(codes)}종목 — 1차=자기이력 상위 {top1}% / 확대=상위 {top2}%')
print(f'{"종목":<16}{"전일종가":>10}{"1차":>7}{"급등가":>10}{"급락가":>10}{"확대":>7}'
f'{"상한가":>10}{"하한가":>10}')
for c in codes:
t = thr_map.get(c) or {}
lm = limit_map.get(c) or {}
name = (toggles[c] or c)[:15]
lim = f'{lm["upl"]:>10,}{lm["lst"]:>10,}' if lm.get('upl') else f'{"—":>10}{"—":>10}'
if not t.get('thr'):
print(f'{name:<16}{"이력 부족 — VI·제한가만":>30}{lim}')
continue
pv, t1, t2 = t['prev_close'], t['thr'], t['thr_big']
print(f'{name:<16}{pv:>10,}{t1:>6.1f}%{round(pv * (1 + t1 / 100)):>10,}'
f'{round(pv * (1 - t1 / 100)):>10,}{t2:>6.1f}%{lim}')
print(f'\n※ 상·하한가 도달은 문턱과 별개로 항상 알림 (거래소 계산값, 하루 1콜 캐시)')
print(f'※ VI 발동은 이 문턱과 별개로 먼저 알림 (정적VI 전일종가 ±10% 부근)')
print(f'※ 상·하한가·VI 중 문턱을 넘으면 보류했다가 풀린 뒤 알림 (사실을 버리지 않음)')
print(f'※ 문턱은 하루 가격제한폭 ±30% 안에 있음이 보장됨 (실제 관측된 이탈폭의 분위수)')
return 0
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else 'help'
force = '--force' in sys.argv[2:]
try:
if cmd == 'check':
return run(dry=False, force=force)
if cmd == 'dry-run':
return run(dry=True, force=True)
if cmd == 'list':
return cmd_list()
print(__doc__, file=sys.stderr)
return 2
except Exception as e:
import traceback
tb = traceback.format_exc()
print(f'[fatal] {e}\n{tb}', file=sys.stderr)
try:
send_telegram(f'⚠️ [surge_monitor] 실행 실패\n{type(e).__name__}: {e}')
except Exception:
pass
return 1
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,68 @@
import sys
import unittest
from datetime import date
from pathlib import Path
from unittest.mock import patch
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
import ipo_alert
class IpoAlertTests(unittest.TestCase):
def setUp(self):
self.row = {
'ipoCode': 'A250030',
'compName': '진코스텍',
'poStartDate': '2026-10-02',
'poEndDate': '2026-10-06',
'lcalDate': '2026-10-15',
'fixPubPrice': '23500',
'orgNm': '하나증권',
}
self.detail = {
'ipoInfo': {
'compName': '진코스텍',
'poStartDate': '2026-10-02',
'poEndDate': '2026-10-06',
'lcalDate': '2026-10-15',
},
'registrationStatement': {'sbscMrgnRatio': '50.00'},
'demandForecastingResult': {'fixPubPrice': '23500'},
'joinManagers': [{'orgNm': '하나증권', 'sbscMinNum': '10'}],
}
def test_subscription_format_uses_first_day_and_minimum_cash(self):
text = ipo_alert.format_alert('subscription', self.row, self.detail)
self.assertIn('종목명 : 진코스텍', text)
self.assertIn('증권사 : 하나증권', text)
self.assertIn('청약일 : 2026-10-02 ~ 2026-10-06', text)
self.assertIn('증거금 : 117,500원(최소 10주)', text)
self.assertNotIn('23,500원', text)
self.assertNotIn('50%', text)
self.assertIn('수수료 2,000원(온라인 일반 기준)', text)
def test_listing_uses_listing_date(self):
text = ipo_alert.format_alert('listing', self.row, self.detail)
self.assertIn('[신규 상장]', text)
self.assertIn('상장일 : 2026-10-15', text)
def test_collect_subscription_matches_only_first_day(self):
payload = {
'examinationList': [], 'demandForecastingList': [],
'forecastingCompleteList': [],
'subscriptionList': [self.row],
'subscriptionCompleteList': [], 'listingList': [],
}
with patch.object(ipo_alert, 'fetch', return_value=ipo_alert.json.dumps(payload)), patch.object(
ipo_alert, '_detail', return_value=self.detail
):
first = ipo_alert.collect_for_date('subscription', date(2026, 10, 2))
second = ipo_alert.collect_for_date('subscription', date(2026, 10, 3))
self.assertEqual(len(first), 1)
self.assertEqual(second, [])
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,160 @@
import sys
import unittest
from datetime import date
from pathlib import Path
from unittest.mock import patch
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
import ipo_calendar_sync as sync
class IpoParserTests(unittest.TestCase):
def test_naver_api_parser_includes_cutoff_date(self):
payload = {
'examinationList': [],
'demandForecastingList': [],
'forecastingCompleteList': [],
'subscriptionList': [{
'ipoCode': 'A0035S0',
'compName': '빅웨이브로보틱스',
'poStartDate': '2026-09-15',
'poEndDate': '2026-09-16',
'lcalDate': '2026-09-29',
'orgNm': '한국투자증권',
}],
'subscriptionCompleteList': [],
'listingList': [],
}
with patch.object(sync, 'fetch', return_value=sync.json.dumps(payload)):
events = sync.parse_naver_api_events_from(date(2026, 9, 29))
self.assertEqual([(e.kind, e.name) for e in events], [
('listing', '빅웨이브로보틱스'),
])
self.assertEqual(events[0].start_date, date(2026, 9, 29))
def test_38_parsers_read_subscription_and_listing_tables(self):
subscription_html = '''
<table summary="공모주 청약일정"><tbody>
<tr><th>종목명</th><th>공모주일정</th><th>확정공모가</th><th>희망공모가</th><th>경쟁률</th><th>주간사</th></tr>
<tr><td>넥스아이</td><td>2026.11.03~11.04</td><td>-</td><td>-</td><td></td><td>한국투자증권</td></tr>
</tbody></table>'''
listing_html = '''
<table summary="신규상장종목"><tbody>
<tr><th>기업명</th><th>신규상장일</th></tr>
<tr><td>빅웨이브로보틱스</td><td>2026/09/29</td></tr>
</tbody></table>'''
subscriptions = sync.parse_38_subscription_html(
subscription_html, date(2026, 9, 29)
)
listings = sync.parse_38_listing_html(listing_html, date(2026, 9, 29))
self.assertEqual(subscriptions[0].start_date, date(2026, 11, 3))
self.assertEqual(subscriptions[0].brokers, '한국투자증권')
self.assertEqual(listings[0].name, '빅웨이브로보틱스')
self.assertEqual(listings[0].start_date, date(2026, 9, 29))
def test_zero_primary_result_switches_to_38_fallback(self):
fallback = sync.EventSpec(
'listing', '빅웨이브로보틱스', date(2026, 9, 29),
date(2026, 9, 29), '', sync.LISTING_URL,
source_label='38커뮤니케이션 IPO',
)
with patch.object(sync, 'parse_naver_api_events_from', return_value=[]), patch.object(
sync, 'parse_38_events_from', return_value=[fallback]
):
events, parser_used, errors = sync.collect_ipo_events(date(2026, 9, 29))
self.assertEqual(parser_used, '38_fallback')
self.assertEqual(events, [fallback])
self.assertEqual(errors, ['naver_api: 수집 결과 0건'])
def test_all_parser_failures_raise_structured_error(self):
with patch.object(
sync, 'parse_naver_api_events_from', side_effect=ValueError('schema changed')
), patch.object(
sync, 'parse_38_events_from', side_effect=ValueError('table changed')
):
with self.assertRaises(sync.IpoParserError) as caught:
sync.collect_ipo_events(date(2026, 9, 29))
self.assertEqual(caught.exception.errors, [
'naver_api: schema changed',
'38_fallback: table changed',
])
def test_parser_alert_is_deduplicated_and_recovery_is_reported(self):
state = {}
errors = ['naver_api: 수집 결과 0건']
with patch.object(sync, 'send_telegram', return_value=True) as send:
sync.update_parser_health(state, 'degraded', errors, 12)
sync.update_parser_health(state, 'degraded', errors, 12)
sync.update_parser_health(state, 'healthy', [], 18)
self.assertEqual(send.call_count, 2)
self.assertIn('파서 일부 실패', send.call_args_list[0].args[0])
self.assertIn('파서 복구', send.call_args_list[1].args[0])
self.assertEqual(state['parser_alerted_status'], 'healthy')
def test_failed_alert_is_retried_on_next_run(self):
state = {}
errors = ['naver_api: timeout', '38_fallback: timeout']
with patch.object(sync, 'send_telegram', side_effect=[False, True]) as send:
sync.update_parser_health(state, 'failed', errors, 0)
sync.update_parser_health(state, 'failed', errors, 0)
self.assertEqual(send.call_count, 2)
self.assertEqual(state['parser_alerted_status'], 'failed')
def test_primary_parser_gets_same_day_listing_augmentation(self):
future = sync.EventSpec(
'subscription', '넥스아이', date(2026, 11, 3), date(2026, 11, 4),
'한국투자증권', 'https://stock.naver.com/ipo/A493410',
)
listing_html = '''
<table summary="신규상장종목"><tbody>
<tr><th>기업명</th><th>신규상장일</th></tr>
<tr><td>빅웨이브로보틱스</td><td>2026/09/29</td></tr>
</tbody></table>'''
with patch.object(sync, 'parse_naver_api_events_from', return_value=[future]), patch.object(
sync, 'fetch', return_value=listing_html
):
events, parser_used, errors = sync.collect_ipo_events(date(2026, 9, 29))
self.assertEqual(parser_used, 'naver_api')
self.assertEqual(errors, [])
self.assertEqual({e.state_key for e in events}, {
'subscription|넥스아이', 'listing|빅웨이브로보틱스',
})
def test_primary_parser_backfills_recent_non_spac_listing(self):
future = sync.EventSpec(
'subscription', '넥스아이', date(2026, 11, 3), date(2026, 11, 4),
'한국투자증권', 'https://stock.naver.com/ipo/A493410',
)
listing_html = '''
<table summary="신규상장종목"><tbody>
<tr><th>기업명</th><th>신규상장일</th></tr>
<tr><td>와이즈플래닛컴퍼니</td><td>2026/09/23</td></tr>
<tr><td>KB스팩34호</td><td>2026/09/22</td></tr>
<tr><td>덕산넵코어스(구.넵코어스)</td><td>2026/09/30</td></tr>
</tbody></table>'''
with patch.object(sync, 'parse_naver_api_events_from', return_value=[future]), patch.object(
sync, 'fetch', return_value=listing_html
):
events, parser_used, errors = sync.collect_ipo_events(date(2026, 9, 29))
self.assertEqual(parser_used, 'naver_api')
self.assertEqual(errors, [])
self.assertIn('listing|와이즈플래닛컴퍼니', {e.state_key for e in events})
self.assertNotIn('listing|KB스팩34호', {e.state_key for e in events})
self.assertNotIn(
'listing|덕산넵코어스(구.넵코어스)', {e.state_key for e in events}
)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,76 @@
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
import stock_portfolio_report as report
class NxtInactiveCorrectionTests(unittest.TestCase):
def test_corrects_only_nxt_inactive_rows_with_krx_quote(self):
rows = [
{
'code': '381620', 'qty': 10, 'price': 6290, 'pred_close': 6290,
'day_change': 0, 'day_change_pct': 0.0, 'buy_amount': 58000,
'eval_value': 62900, 'profit': 4900, 'profit_rate': 8.45,
},
{
'code': '005930', 'qty': 2, 'price': 281000, 'pred_close': 275000,
'day_change': 6000, 'day_change_pct': 2.18, 'buy_amount': 500000,
'eval_value': 562000, 'profit': 62000, 'profit_rate': 12.4,
},
]
cache = {
'stocks': {
'제닉스로보틱스': {'code': '381620', 'nxt_enable': False},
'삼성전자': {'code': '005930', 'nxt_enable': True},
}
}
with tempfile.TemporaryDirectory() as td:
cache_path = Path(td) / 'stock_codes.json'
cache_path.write_text(json.dumps(cache))
with patch.object(report, 'STOCK_CODES_FILE', cache_path), patch.object(
report,
'get_watchlist_quotes',
return_value={
'381620': {'price': 6190, 'change': 40, 'change_pct': 0.65},
},
) as quotes:
corrected = report.correct_nxt_inactive_rows(rows)
self.assertEqual(corrected, 1)
quotes.assert_called_once_with(['381620'], exchange='KRX')
self.assertEqual(rows[0]['price'], 6190)
self.assertEqual(rows[0]['pred_close'], 6150)
self.assertEqual(rows[0]['eval_value'], 61900)
self.assertEqual(rows[0]['profit'], 3900)
self.assertAlmostEqual(rows[0]['profit_rate'], 3900 / 58000 * 100)
self.assertEqual(rows[1]['price'], 281000)
def test_keeps_original_row_when_krx_quote_is_missing(self):
row = {
'code': '381620', 'qty': 10, 'price': 6290, 'pred_close': 6290,
'day_change': 0, 'day_change_pct': 0.0, 'buy_amount': 58000,
'eval_value': 62900, 'profit': 4900, 'profit_rate': 8.45,
}
cache = {'stocks': {'제닉스로보틱스': {'code': '381620', 'nxt_enable': False}}}
with tempfile.TemporaryDirectory() as td:
cache_path = Path(td) / 'stock_codes.json'
cache_path.write_text(json.dumps(cache))
with patch.object(report, 'STOCK_CODES_FILE', cache_path), patch.object(
report, 'get_watchlist_quotes', return_value={}
):
corrected = report.correct_nxt_inactive_rows([row])
self.assertEqual(corrected, 0)
self.assertEqual(row['price'], 6290)
self.assertEqual(row['eval_value'], 62900)
if __name__ == '__main__':
unittest.main()
@@ -67,6 +67,95 @@ def _save_all(rows: list[dict]) -> None:
tmp.replace(JOURNAL)
def _advance(qty: int, avg: float, buy_qty: int, sell_qty: int, buy_avg: float) -> tuple[int, float]:
"""running 보유수량·평단 1스텝 진행.
매도는 **전 보유분 우선 소진**(평단 유지) → 잔여 당일 매수분을 가중평균 블렌딩.
이 순서가 '전일 보유 평단' 정의의 핵심 — 당일 매수가 매도 원가에 섞이지 않고,
매도로 원물이 다 빠지면 남는 건 당일 매수분 평단이 된다.
"""
consumed = min(sell_qty, qty)
qty -= consumed
rem_buy = buy_qty - (sell_qty - consumed) # 당일매수분 중 매도로 안 빠진 잔량
if rem_buy > 0:
avg = (qty * avg + rem_buy * buy_avg) / (qty + rem_buy)
qty += rem_buy
qty = max(qty, 0)
if qty == 0:
avg = 0.0 # 전량 청산 → 평단 리셋 (이후 순수 데이트레이딩을 보유분으로 오인 방지)
return qty, avg
def _apply_rebase(row: dict, sell_avg: float, sell_qty: int, pre_avg: float) -> None:
"""row의 pl_amt/prft_rt를 '전일 보유 평단(pre_avg)' 기준으로 덮어쓴다 (in-place).
그로스 = (매도평단 - 전일평단) × 매도수량, pl_amt = 그로스 - 수수료·세금 (ka10170과 동일 net).
"""
gross = (sell_avg - pre_avg) * sell_qty
fee = int(row.get('cmsn_tax') or 0)
row['pl_amt'] = round(gross) - fee
cost = pre_avg * sell_qty
row['prft_rt'] = round(row['pl_amt'] / cost * 100, 2) if cost else 0.0
row['_pl_rebased'] = True
row['prev_avg'] = round(pre_avg)
def rebase_pl(rows: list[dict]) -> list[dict]:
"""매도 실현손익을 '전일 보유 평단' 기준으로 재계산해 **새 리스트** 반환 (파일 미변경, 표시 전용).
ka10170은 당일 매수+매도가 같이 있으면 당일 매수평단↔매도평단 round-trip으로 pl_amt를 매겨
어제까지 보유한 원물의 손익을 가린다 (2026-06-29 0193W0: 6/26 보유 62@24,550 +
당일 62@21,875 매수 후 62@21,885 매도 → ka10170 +220원, 실제 원물 손익 약 -165,630원).
(account, code)별로 날짜순 재생(_advance)하며, **당일 매수가 동반된 매도 행**만
당일 매수 전 보유 평단을 원가로 잡아 덮어쓴다. 순수 매도(당일 매수 없음)는 키움 값이
정확하므로 건드리지 않는다.
⚠️ 입력 rows는 각 (account, code)의 **전체 이력**을 포함해야 재생이 정확하다.
"""
out = [dict(r) for r in rows]
by_key: dict[tuple, list[dict]] = {}
for r in out:
by_key.setdefault((r.get('account'), r.get('code')), []).append(r)
for grp in by_key.values():
grp.sort(key=lambda r: (r.get('date', ''), r.get('collected_at', '')))
qty, avg = 0, 0.0
for r in grp:
bq = int(r.get('buy_qty') or 0)
sq = int(r.get('sell_qty') or 0)
ba = float(r.get('buy_avg') or 0)
sa = float(r.get('sell_avg') or 0)
pre_avg = avg # 당일 매수 반영 전 = 전일 보유 평단
if sq > 0 and bq > 0 and pre_avg > 0:
_apply_rebase(r, sa, sq, pre_avg)
qty, avg = _advance(qty, avg, bq, sq, ba)
return out
def prev_avg_map(before_iso: str) -> dict:
"""파일 이력 중 before_iso **미만**(전일까지)을 (account, code)별 재생해 시작 평단 맵 반환.
라이브 journal(당일분)의 round-trip 오염 행을 재계산할 때 원가(전일 보유 평단) 소스.
{(account, code): avg}. 보유 0이면 0.
"""
rows = [r for r in _load_all() if r.get('date', '') < before_iso]
by_key: dict[tuple, list[dict]] = {}
for r in rows:
by_key.setdefault((r.get('account'), r.get('code')), []).append(r)
out: dict[tuple, float] = {}
for key, grp in by_key.items():
grp.sort(key=lambda r: (r.get('date', ''), r.get('collected_at', '')))
qty, avg = 0, 0.0
for r in grp:
qty, avg = _advance(
qty, avg,
int(r.get('buy_qty') or 0), int(r.get('sell_qty') or 0),
float(r.get('buy_avg') or 0),
)
out[key] = avg
return out
def record_trades(all_trades: dict, *, base_dt: str | None = None, quiet: bool = False, tag: str = 'collect') -> dict:
"""이미 받은 ka10170 응답({label: [trades]})을 jsonl에 적재 (idempotent).
`(iso_date, account in all_trades)` 단위 재적재 — 다른 계좌는 영향 X.
@@ -0,0 +1,704 @@
#!/usr/bin/env python3
"""트레일링 스톱 감시 — 고점이 갱신되면 키움 스톱주문의 조건단가를 상향 정정한다.
**예약 1건 = 계단(레그) N개.** 고점 대비 하락률이 깊어질수록 더 많이 파는 계단식 청산이라
계단마다 스톱주문이 따로 걸려 있다. 고점이 오르면 살아있는 레그를 전부 상향 정정한다.
LLM을 깨우지 않는다. 하는 일은 세 가지:
1. 레그가 아직 미체결로 살아있는지 확인 (ka10075) — 사라졌으면 정리+알림
2. 현재가가 저장된 고점을 넘었으면 kt10002 정정으로 계단별 조건단가·지정가를 함께 올림
3. **예약 트레일링(TRAILING_ARM) 발동** — 현재가가 발동가에 닿으면 그 시점 현재가를
고점으로 계단별 스톱주문을 발주하고, 대기 예약을 트레일링 예약으로 승격 (2026-08-25)
⚠️ 3번이 **신규 발주 경로**다 (2026-08-25 관리자님 명시 승인). 그전까지 이 감시가 허용된
근거는 "modify_order 하나만 부른다" 였는데 이제는 다르다. 바뀐 근거는 **계좌·종목·수량·
계단·발동가가 PIN 승인 시점에 전부 확정되고, 감시는 발동가 도달 판정만 한다** 는 것이다.
수량 증가·방향 변경 경로는 여전히 없다 (매도가능 부족 시 **축소** 재배분만).
한 계단이 체결돼도 남은 계단을 재배치하지 않는다 — 재배치엔 취소+신규 발주가 필요해
감시 루프에 (승인 범위를 넘는) 자동 발주 경로가 생긴다(2026-07-31 관리자님 결정).
⚠️ arm 단계는 손절선 상향이 **끝난 뒤** 별도 try/except 로 돈다. 순서가 설계다 — 상향은
이미 나간 주문을 지키는 동작이라 arm 쪽 어떤 예외도 그걸 선점해선 안 된다.
⚠️ 발동은 2단계 마킹으로 보호한다. 발주 전에 firing_at 을 디스크에 남기고 레그마다
주문번호를 적는다. **firing_at 이 남은 예약은 자동 재발동하지 않는다** — 주문이 나갔는지
모르는 상태에서 다시 내는 게 곧 이중 매도다. 알림만 보내고 사람이 확인해 취소한다.
⚠️ 레그가 사라진 사유는 **주문번호 단위**인 kt00007 로 판정한다. 종목 단위 집계인
당일매매일지(ka10170)를 쓰면 한 계단이 체결된 날 나머지 계단이 장 마감으로 소멸했을 때
그 소멸분까지 '체결' 로 오판한다 — 같은 종목 매도 기록이 이미 남아 있기 때문이다.
⚠️ 정정하면 주문번호가 새로 발급된다 (kt10002 응답 ord_no). trailing.commit_step_modify 가
상태파일의 레그별 ord_no 를 갱신하지 않으면 그 레그의 다음 정정부터 전부 실패한다.
⚠️ 고점은 '현재가' 로만 갱신한다. ka10095 가 당일 고가(high)도 주지만, 등록 시점 이전에
찍힌 고가까지 반영되면 등록 직후 손절선이 현재가 위로 올라가 즉시 발동할 수 있다.
(예: 오전 15,000 → 14,000 일 때 3% 등록 → 당일고가 기준이면 조건 14,550 > 현재가)
감시 간격이 2분이라 현재가 샘플링으로 충분하다.
현재 launchd cadence 는 1분 1회 (`check`), 08:00~20:00 전 거래 세션.
상시 daemon 을 띄우지 않는 이유는 매매 API 를 호출할 수 있는 프로세스를
24시간 살려두지 않기 위함.
⚠️ NXT 시간대(프리 08:00~09:00 / 애프터 15:30~20:00) 감시는 2026-07-30 관리자님 요청으로 추가.
**정규장에 등록된 주문(SOR `_AL` 또는 KRX `''`)의 정정이 NXT 단독 시간대에 받아들여지는지
실증 안 됨.** 거부되면 고점 갱신마다 실패가 반복되므로 실패 알림에 30분 쿨다운을 걸었다
(로그에는 매번 남으니 진단은 logs/stock-trailing-monitor.log 로).
⚠️ 1분보다 짧게 가려면 --repeat/--gap 을 쓴다. launchd 는 StartCalendarInterval 이
분 단위가 최소라 30초 발화를 만들 수 없고, StartInterval=30 은 GUI 세션 idle 시
발화가 보류돼(timer coalescing) 장중에 안 도는 문제가 있어 못 쓴다
(sim-scan 이 같은 이유로 캘린더로 전환됨). 그래서 30초는 "1분 발화 + 한 프로세스가
30초 간격 2회 검사" 로만 가능하다 — plist ProgramArguments 에 인자를 붙이면 된다.
(2026-07-30 30초로 운영했다가 관리자님 지시로 1분 복귀. 옵션은 보존)
Usage:
python3 trailing_monitor.py check # 1회 감시 (현재 launchd 설정)
python3 trailing_monitor.py check --repeat 2 --gap 30 # 30초 간격 2회
python3 trailing_monitor.py check --force # 장외에도 실행 (테스트용)
python3 trailing_monitor.py check --dry-run # 정정 API 호출 없이 판단·body 만 출력
python3 trailing_monitor.py list # 현재 예약 목록
"""
from __future__ import annotations
import sys
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
KST = timezone(timedelta(hours=9))
WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace')
sys.path.insert(0, str(WORKSPACE / 'scripts'))
import kiwoom_client as kc # noqa: E402
from orders import card, datasource, guards, kiwoom_order, ledger, sidecar, trailing # noqa: E402
from orders import handler # noqa: E402
from orders.handler import send_telegram # noqa: E402
# 정정 실패 알림 쿨다운 — NXT 시간대에 KRX/SOR 원주문 정정이 거부되면 매 사이클 반복된다.
FAIL_NOTICE_COOLDOWN_SEC = 1800
# 사이클마다 mtime 만 갱신하는 생존 신호. 이 감시가 죽으면 예약 트레일링(ARM)이
# 아예 발동하지 않는데, 예약 0건이면 로그를 한 줄도 남기지 않아 바깥에서 죽었는지
# 구분할 방법이 없었다(2026-08-28 이후 13일간 로그 무갱신이 정상인지 장애인지 불명).
# 로그로 찍으면 분당 1줄 × 720회/일이 쌓여 실제 이벤트를 덮으므로 파일 mtime 만 쓴다.
# scripts/health_check.py 가 이 파일을 읽는다.
HEARTBEAT_FILE = WORKSPACE / 'state' / 'trailing_monitor_heartbeat'
def _log(msg: str) -> None:
print(f'[{datetime.now(KST).strftime("%Y-%m-%d %H:%M:%S")}] {msg}', flush=True)
def _heartbeat() -> None:
"""감시가 이번 사이클을 돌았다는 신호. 실패해도 감시를 막지 않는다."""
try:
HEARTBEAT_FILE.write_text(
datetime.now(KST).isoformat(timespec='seconds'), encoding='utf-8')
except Exception:
pass
def session_now() -> tuple[bool, str]:
"""(감시할 세션인가, 세션명). 주말·휴장일·거래시간 외는 False.
세션 판정은 orders/limits.json 을 단일 진실 소스로 쓰는 guards.session_at 재사용 —
NXT 프리(08:00~09:00) / 정규(09:00~15:20) / 종가단일가(15:20~15:30) /
NXT 애프터(15:30~20:00). 시간이 바뀌면 limits.json 만 고치면 된다.
"""
now = datetime.now(KST)
if now.weekday() >= 5:
return False, 'WEEKEND'
if guards.is_today_holiday(now):
return False, 'HOLIDAY'
sess = guards.session_at(now)
return sess != 'CLOSED', sess
def _pct_s(v) -> str:
"""10.0 → '10' — 계단 하락률 표기."""
return f'{float(v or 0):g}'
def _leg_fill_check(account: str, ord_no: str, cache: dict) -> tuple:
"""레그가 사라진 사유 판정 — kt00007 에서 **그 주문번호의** 체결수량을 본다.
⚠️ 미체결 조회에서 사라진 것만으로는 체결·취소·소멸을 구분할 수 없다(셋 다 목록에서 빠짐).
종목 단위인 당일매매일지(ka10170)로 판정하면 계단 하나가 체결된 날 나머지 계단이
장 마감으로 소멸했을 때 그 소멸분까지 '체결' 로 오판한다 — 같은 종목의 매도 기록이
이미 남아 있기 때문이다. 주문번호 단위인 kt00007 은 레그마다 정확히 갈린다.
반환: (체결행 dict 또는 None, 조회 성공 여부).
조회 실패와 '체결 안 됨' 을 반드시 구분해야 한다 — 실패를 '체결 안 됨' 으로 단정하면
실제로 팔렸는데 안 팔렸다고 알리게 된다.
"""
if account not in cache:
try:
cache[account] = kc.get_order_executions(account)
except Exception as e:
_log(f'주문체결내역 조회 실패 ({account}): {e!r}')
cache[account] = None
rows = cache[account]
if rows is None:
return None, False
for row in rows:
if (row.get('ord_no') or '').strip() == ord_no and (row.get('cntr_qty') or 0) > 0:
return row, True
return None, True
def _notify_gone_batch(events: list) -> None:
"""한 사이클에 사라진 레그들을 예약 단위로 묶어 한 번만 알린다.
장 마감이면 계단이 통째로 소멸하는데 레그마다 보내면 3~5통이 몰아친다.
체결 / 미체결 소멸(장 마감 취소 등)은 관리자님이 취해야 할 행동이 완전히 다르므로
계단별로 구분해 한 메시지 안에 나열한다.
2026-07-30 실측: 스톱주문은 장 마감 후 체결 없이 소멸한다(15:30~18:21 사이).
자동 재등록은 하지 않는다(관리자님 지시) — 다시 걸려면 자산웹에서 수동 등록.
"""
by_res: dict = {}
for ev in events:
by_res.setdefault(ev['res']['id'], []).append(ev)
for evs in by_res.values():
res = evs[0]['res']
remaining = min(e['remaining'] for e in evs)
any_filled = any(e['filled'] for e in evs)
unknown = any(not e['ok'] for e in evs)
# 재등록 버튼용 스냅샷 — 예약이 통째로 죽었고, 체결도 판정불가도 없을 때만.
# 체결분을 다시 걸면 이미 판 걸 또 파는 셈이고, 판정불가는 체결일 수도 있어 같은 위험이다.
# 판정 로직을 새로 쓰지 않고 여기서 같이 쓰는 이유 — 두 벌이 되면 한쪽만 고쳐진다.
if remaining == 0 and not any_filled and not unknown:
entry_steps = res.get('entry_steps')
# ⚠️ 이번 배치 이벤트 수 < 원래 계단 수 = **앞선 사이클에 이미 빠진 레그가 있다**
# (거의 항상 장중 체결). 이 배치엔 체결이 없어 위 조건을 통과하므로, 여기서
# 막지 않으면 "일부는 이미 판" 설정을 재등록 후보로 올리게 된다.
if entry_steps and len(evs) == len(entry_steps):
try:
trailing.push_recent(trailing.make_recent_entry(
'trailing', 'not_in_open_orders', res, entry_steps))
except Exception as e:
_log(f'재등록 기록 실패 {res["id"]}: {e!r}')
if any_filled:
icon = '✅'
word = '체결'
elif unknown:
icon = '🎯'
word = '종료'
else:
icon = '⚠️'
word = '소멸'
lines = [f'{icon} 트레일링 {word} — {res["symbol_name"]} ({res["symbol"]})',
f'계좌: {res["account"]} · 고점 {res["peak"]:,}원']
if res.get('min_sell_price'):
lines.append(f'최저 매도가 {res["min_sell_price"]:,}원')
for ev in sorted(evs, key=lambda e: e['step']['n']):
s = ev['step']
head = (f'{s["n"]}단계 −{_pct_s(s["pct"])}% {s["cond_uv"]:,}원 · {s["qty"]:,}주')
if ev['filled']:
f = ev['filled']
lines.append(f' ✅ {head} → 체결 {f.get("cntr_qty", 0):,}주 '
f'@ {f.get("cntr_uv", 0):,}원')
elif ev['ok']:
lines.append(f' ⚠️ {head} → 미체결 소멸 (보유수량 그대로)')
else:
lines.append(f' 🎯 {head} → 체결 여부 판정 불가 (체결내역 조회 실패)')
if remaining > 0:
lines.append(f'남은 계단 {remaining}개 — 감시 계속합니다.')
elif any_filled or unknown:
# 체결·판정불가는 재등록 목록에 올리지 않는다(_archive 조건과 동일) —
# 없는 목록으로 안내하면 안 되므로 여기서도 재등록 버튼을 언급하지 않는다.
lines.append('남은 계단 없음 — 계속 쓰시려면 자산웹에서 다시 등록해주세요 '
'(자동 재등록 안 함).')
else:
lines.append('남은 계단 없음 — 자산웹 [📋 진행중] → 최근 종료 에서 ↺ 재등록 '
'(같은 설정이 채워집니다 · 자동 재등록은 안 합니다).')
send_telegram('\n'.join(lines), parse_mode=None)
def _notify_modify_failed(res: dict, step: dict, reason: str) -> None:
send_telegram(
f'⚠️ 트레일링 손절선 상향 실패 — {res["symbol_name"]} ({res["symbol"]})\n'
f'계좌: {res["account"]} · {step["n"]}단계 −{_pct_s(step["pct"])}% '
f'· 주문번호 {step["ord_no"]}\n'
f'현재 손절선 {step["cond_uv"]:,}원은 그대로 유지됩니다.\n'
f'사유: {reason}',
parse_mode=None)
def _sellable_qty(account: str, symbol: str, open_rows) -> tuple:
"""(매도가능 수량, 조회성공). 조회 실패를 0 과 반드시 구분한다 — 눈감고 발주하면 안 되고,
반대로 실패를 0 으로 읽으면 살아있는 예약을 지운다.
trde_able_qty 가 미체결 매도를 이미 뺐는지 확실하지 않아 min 으로 이중차감을 피한다.
"""
try:
positions = kc.get_positions(account)
except Exception as e:
_log(f'보유 조회 실패 [{account}]: {e!r}')
return 0, False
pos = next((p for p in positions if p.get('code') == symbol), None)
if pos is None:
return 0, True
hold = pos.get('qty') or 0
able = pos.get('trde_able_qty') or 0
pending = sum((o.get('unfilled_qty') or 0) for o in (open_rows or [])
if o.get('code') == symbol and o.get('side') == 'SELL')
return max(0, min(able, hold - pending)), True
def _notify_arm_issue(arm: dict, text: str) -> None:
"""대기 예약 관련 경고 — 예약 단위 쿨다운(매 사이클 반복되는 사유가 많다)."""
if not trailing.should_notify_arm_failure(arm['id'], time.time(), FAIL_NOTICE_COOLDOWN_SEC):
_log(f' 알림 생략 (쿨다운 {FAIL_NOTICE_COOLDOWN_SEC}초 내 이미 발송)')
return
try:
send_telegram(
f'⚠️ 예약 트레일링 — {arm["symbol_name"]} ({arm["symbol"]})\n'
f'계좌: {arm["account"]} · 발동가 {arm["trigger_price"]:,}원 · {arm["qty"]:,}주\n'
f'{text}',
parse_mode=None)
except Exception as e:
_log(f'알림 실패: {e!r}')
def _fire_arm(arm: dict, plan: dict, sess: str, dry_run: bool) -> None:
"""발동 — 계단 발주 후 대기 예약을 트레일링 예약으로 승격. 여기부터 되돌릴 수 없다."""
symbol = arm['symbol']
# 라우팅은 등록 시점이 아니라 지금 정한다 — 같은 날 안에서도 정규장에 등록해 NXT
# 애프터에 발동할 수 있고, 옛 suffix 로 발주하면 거부된다.
try:
suffix = guards.determine_routing(datetime.now(KST), datasource.nxt_eligible(symbol),
None, 'STOP_LIMIT')
except ValueError as e:
_log(f'라우팅 결정 실패 {arm["id"]} (세션 {sess}): {e!r}')
return
if dry_run:
_log(f'[dry-run] 발동 {arm["id"]} {arm["symbol_name"]} '
f'발동가 {arm["trigger_price"]:,} ≤ 현재가 {plan["peak"]:,} · '
f'{plan["qty_used"]:,}주 · routing {suffix or "KRX"}')
for s in plan['legs']:
_log(f' {s["n"]}단계 −{s["pct"]}% 조건 {s["cond_uv"]:,} / 지정 {s["ord_uv"]:,} '
f'· {s["qty"]:,}주')
return
# write-ahead: 발주를 시작했다는 사실을 먼저 디스크에 남긴다. 이 마킹이 있으면 다음
# 사이클은 재발동하지 않는다 — 주문이 나갔는지 모르는 상태에서 다시 내는 게 이중 매도다.
if not trailing.mark_arm_firing(arm['id']):
_log(f'발동 마킹 실패 {arm["id"]} — 이미 발주 시도된 예약 (재발동 안 함)')
return
placed, failed, _details = handler.place_trailing_legs(
account=arm['account'], symbol=symbol, steps=plan['legs'],
routing_suffix=suffix, card_id=arm.get('card_id'), dry_run=False,
on_placed=lambda leg: trailing.record_arm_leg(arm['id'], leg))
if not placed:
reason = failed[0]['reason'] if failed else 'UNKNOWN'
detail = failed[0].get('error', '') if failed else ''
_log(f'발동 전량 실패 {arm["id"]} {arm["symbol_name"]}: {reason} {detail}')
ledger.append('arm_fire_failed', {'arm_id': arm['id'], 'account': arm['account'],
'symbol': symbol, 'reason': reason,
'error': str(detail), 'stage': 'submit'})
# firing 마킹은 그대로 둔다 — 정말 한 건도 안 나갔는지 우리가 단정할 수 없다.
_notify_arm_issue(arm, f'계단 {len(plan["legs"])}단 전부 접수 실패 ({reason}) {detail}\n'
f'예약은 발동 처리 중단 상태로 남습니다 — 미체결을 확인하고 '
f'자산웹에서 취소해 주세요.')
return
registerable = [s for s in placed if s.get('ord_no')]
promoted = None
if registerable:
try:
promoted = trailing.promote_arm(arm['id'], plan['peak'], registerable, suffix)
except Exception as e:
_log(f'⚠️ 승격 실패 {arm["id"]}: {e!r}')
info = {'card_id': arm.get('card_id'), 'symbol': symbol, 'symbol_name': arm['symbol_name'],
'account': arm['account'], 'total_qty': plan['qty_used'], 'arm_qty': arm['qty'],
'qty_reduced': plan['reduced'], 'trigger_price': arm['trigger_price'],
'peak': plan['peak'], 'min_sell_price': arm.get('min_sell_price'),
'placed': placed, 'failed': failed, 'dry_run': False,
'res_id': (promoted['reservation']['id'] if promoted else None),
'register_error': None if promoted else '감시 등록 실패 — 손절선이 따라 올라가지 않습니다'}
ledger.append('arm_fired', {'arm_id': arm['id'], 'account': arm['account'], 'symbol': symbol,
'peak': plan['peak'], 'qty': plan['qty_used'],
'legs': len(placed), 'res_id': info['res_id']})
_log(f'발동 {arm["id"]} → {info["res_id"]} {arm["symbol_name"]} '
f'{len(placed)}단 · {plan["qty_used"]:,}주 · 고점 {plan["peak"]:,}')
try:
send_telegram(card.format_arm_fired(info), parse_mode=None)
except Exception as e:
_log(f'알림 실패: {e!r}')
def _expire_arms(arms: list, dry_run: bool) -> list:
"""만료된 대기 예약을 지우고 살아있는 것만 돌려준다 (2026-08-25).
스톱주문은 장 마감에 거래소가 지워주지만 대기 예약은 우리 파일에만 있으니 우리가 지운다.
감시 루프가 08:00~19:59 에만 도므로 실제 정리 시점은 **다음 거래일 첫 사이클**이다.
알림은 한 통으로 묶는다 — 여러 종목을 걸어두고 하루가 지나면 예약마다 보내면 아침에
텔레그램이 몰아친다.
"""
alive, dead = [], []
for a in arms:
(dead if trailing.is_arm_expired(a) else alive).append(a)
if not dead:
return alive
for a in dead:
_log(f'대기 예약 만료 {a["id"]} {a["symbol_name"]} '
f'(등록 {a.get("created_at", "?")[:16]} · 발동가 {a["trigger_price"]:,})')
if not dry_run:
trailing.remove_arm(a['id'], 'expired_end_of_day')
# 재등록 버튼용 스냅샷. 삭제 뒤에 남긴다 — 앞에 남기고 죽으면 중복이 생긴다.
# ⚠️ firing_at 이 걸린 예약은 제외 — 주문이 나갔는지 모르는 상태라, 재등록을
# 권하면 이중 매도로 유도하는 셈이다(사람이 미체결부터 확인해야 한다).
if not a.get('firing_at'):
try:
trailing.push_recent(trailing.make_recent_entry(
'arm', 'expired_end_of_day', a, a.get('steps') or []))
except Exception as e:
_log(f'재등록 기록 실패 {a["id"]}: {e!r}')
if dry_run:
return alive
lines = [f'🗓 예약 트레일링 만료 취소 — {len(dead)}건',
'장 마감이 지나 자동 취소했습니다 (스톱주문과 같은 당일 유효).']
for a in dead:
lines.append(f'· {a["symbol_name"]} ({a["symbol"]}) [{a["account"]}] '
f'{a["qty"]:,}주 · 발동가 {a["trigger_price"]:,}원 — 미발동')
lines.append('계속 노리시려면 자산웹 [📋 진행중] → 최근 종료 에서 ↺ 재등록 을 누르세요 '
'(같은 설정이 채워집니다 · PIN 은 평소대로).')
try:
send_telegram('\n'.join(lines), parse_mode=None)
except Exception as e:
_log(f'알림 실패: {e!r}')
return alive
def _check_arms(arms: list, quotes: dict, open_by_acct: dict, sess: str, dry_run: bool) -> None:
"""대기 예약 발동 판정. 평시(미도달)엔 추가 API 콜 0 — 시세는 호출측 배치를 재사용한다.
만료분은 호출측(check)이 이미 걷어낸 상태로 넘어온다 — 시세 조회가 실패해도 정리는
돼야 하므로 순서상 앞에 둔다.
"""
# 사이드카는 마킹 전에 본다. 마킹 후 전 레그가 SidecarBlocked 로 죽으면 예약이 발동 처리
# 중단 상태로 갇힌다.
if sidecar.is_disabled():
reached = [a for a in arms
if ((quotes.get(a['symbol']) or {}).get('price') or 0) >= a['trigger_price']]
for a in reached:
_log(f'사이드카 ON — 발동 보류 {a["id"]} {a["symbol_name"]}')
_notify_arm_issue(a, '사이드카가 켜져 있어 발동을 보류했습니다 (매매 차단). '
'끄면 다음 사이클에 발동합니다.')
return
for arm in arms:
if arm.get('firing_at'):
_log(f'발동 처리 중단 상태 {arm["id"]} {arm["symbol_name"]} '
f'(firing_at={arm["firing_at"]}) — 재발동 안 함')
legs = arm.get('fired_legs') or []
nos = ', '.join(s.get('ord_no', '') for s in legs if s.get('ord_no')) or '없음'
_notify_arm_issue(arm, f'발동 처리가 중단된 상태입니다 (시작 {arm["firing_at"]}).\n'
f'접수된 주문: {nos}\n'
f'키움 미체결을 확인하고 자산웹에서 예약을 취소해 주세요. '
f'중복 매도를 막기 위해 자동 재시도하지 않습니다.')
continue
cur = (quotes.get(arm['symbol']) or {}).get('price') or 0
if cur < arm['trigger_price']:
continue # 평시 경로 — 여기서 끝나면 API 콜 0
# 미체결 조회가 실패한 계좌는 판단을 보류한다 — 예약 경로와 같은 규칙이다.
# 미체결 매도 수량을 모르는 채로 발주하면 과다 매도가 될 수 있다(0 으로 단정 금지).
open_rows = open_by_acct.get(arm['account'])
if open_rows is None:
_log(f'미체결 조회 실패로 발동 보류 {arm["id"]} {arm["symbol_name"]}')
continue
sellable, ok = _sellable_qty(arm['account'], arm['symbol'], open_rows)
if not ok:
_log(f'보유 조회 실패로 발동 보류 {arm["id"]} {arm["symbol_name"]}')
continue # 눈감고 발주하지 않는다. 다음 사이클에 다시 본다
plan = trailing.plan_arm_fire(arm, cur, sellable)
if plan['action'] == 'drop':
why = {'no_sellable': '보유 수량이 없어', 'qty_too_small': '수량이 부족해 계단을 나눌 수 없어'}
_log(f'발동 불가 {arm["id"]} {arm["symbol_name"]} — {plan["reason"]}')
if not dry_run:
trailing.remove_arm(arm['id'], plan['reason'])
send_telegram(
f'🗑 예약 트레일링 삭제 — {arm["symbol_name"]} ({arm["symbol"]})\n'
f'계좌: {arm["account"]} · 발동가 {arm["trigger_price"]:,}원 도달했지만 '
f'{why.get(plan["reason"], plan["reason"])} 발동하지 못했습니다.\n'
f'예약 {arm["qty"]:,}주 · 매도가능 {sellable:,}주',
parse_mode=None)
continue
if plan['action'] != 'fire':
_log(f'발동 보류 {arm["id"]} {arm["symbol_name"]} — {plan["reason"]}')
continue
_fire_arm(arm, plan, sess, dry_run)
def _worth_running(force: bool) -> bool:
"""볼 게 있고 거래 세션인가. 루프 제어와 단발 실행이 같은 기준을 쓰게 분리."""
if not trailing.list_active() and not trailing.list_arms():
return False
return force or session_now()[0]
def check(force: bool = False, dry_run: bool = False) -> int:
_heartbeat()
reservations = trailing.list_active()
arms = trailing.list_arms()
if not reservations and not arms:
return 0
ok_sess, sess = session_now()
if not force and not ok_sess:
_log(f'{sess} — skip (예약 {len(reservations)}건 · 대기 예약 {len(arms)}건)')
return 0
# NXT 단독 시간대엔 KRX 호가가 15:30 종가로 고정돼 현재가와 갭이 생긴다 → NXT 시세로 조회.
quote_exchange = 'NX' if sess in ('NXT_PRE', 'NXT_AFTER') else 'AL'
# 만료된 대기 예약은 시세보다 먼저 걷어낸다 — 시세 조회가 실패해도 정리는 돼야 하고,
# 어제 예약이 오늘 시세로 발동하는 일은 절대 없어야 한다.
if arms:
arms = _expire_arms(arms, dry_run)
if not reservations and not arms:
return 0
# 계좌별 미체결 스냅샷 (계좌당 1콜). 스톱주문도 조건 도달 전까지 미체결로 잡힌다.
open_by_acct: dict[str, list] = {}
for acct in sorted({r['account'] for r in reservations} | {a['account'] for a in arms}):
try:
open_by_acct[acct] = kc.get_open_orders(acct, side='sell')
except Exception as e:
# 한 계좌 조회 실패로 다른 계좌 감시를 멈추지 않는다. 생존 판정은 건너뛴다
# (없다고 단정하면 살아있는 예약을 지워버린다).
_log(f'미체결 조회 실패 [{acct}]: {e!r}')
open_by_acct[acct] = None
# 레그가 사라질 때만 kt00007 을 계좌당 1콜 추가로 부른다 (평시엔 호출 안 함).
exec_cache: dict = {}
gone_events = []
alive = []
for r in reservations:
rows = open_by_acct.get(r['account'])
if rows is None:
continue # 조회 실패 계좌 — 이번 사이클 판단 보류
live_steps = []
for s in list(r.get('steps') or []):
row = next((x for x in rows if x['ord_no'] == s['ord_no']), None)
if row is not None:
live_steps.append((s, row))
continue
filled, exec_ok = _leg_fill_check(r['account'], s['ord_no'], exec_cache)
gone = trailing.remove_step(r['id'], s['n'], 'not_in_open_orders')
if filled:
why = f'체결 {filled.get("cntr_qty", 0):,}주 @ {filled.get("cntr_uv", 0):,}'
elif exec_ok:
why = '미체결 소멸 (체결수량 0 — 장 마감 취소 추정)'
else:
why = '사유 판정 불가 (주문체결내역 조회 실패)'
_log(f'레그 종료 {r["id"]}/{s["n"]}단계 {r["symbol_name"]} — {why}')
if gone:
gone_events.append({'res': gone['reservation'], 'step': gone['step'],
'filled': filled, 'ok': exec_ok,
'remaining': gone['remaining']})
if live_steps:
alive.append((r, live_steps))
if gone_events and not dry_run:
try:
_notify_gone_batch(gone_events)
except Exception as e:
_log(f'알림 실패: {e!r}')
# 시세는 예약·대기 예약 심볼을 합쳐 한 번에 받는다 — ka10095 배치 1콜이라 대기 예약이
# 늘어도 API 콜이 추가되지 않는다. 실패하면 두 단계 모두 이번 사이클을 건너뛴다.
quote_codes = sorted({r['symbol'] for r, _ in alive} | {a['symbol'] for a in arms})
if not quote_codes:
return 0
try:
quotes = kc.get_watchlist_quotes(quote_codes, exchange=quote_exchange)
except Exception as e:
_log(f'시세 조회 실패 ({quote_exchange}): {e!r}')
return 1
raised = 0
for r, live_steps in alive:
cur = (quotes.get(r['symbol']) or {}).get('price') or 0
# 살아있는 레그만 담은 스냅샷으로 판단 — 이미 빠진 계단은 따라 올릴 대상이 아니다.
snap = {'peak': r['peak'], 'min_sell_price': r.get('min_sell_price'),
'steps': [s for s, _ in live_steps]}
nx = trailing.next_step_levels(snap, cur)
if not nx:
continue
if not nx['steps']:
# 고점은 올랐지만 호가단위·최저 매도가 때문에 올릴 조건단가가 없다 → 정정 API 콜 0.
if not dry_run:
trailing.commit_peak(r['id'], nx['peak'])
continue
for up in nx['steps']:
s, row = next(x for x in live_steps if x[0]['n'] == up['n'])
# 미체결 잔량으로 정정 — 부분체결됐으면 잔량만 남는다 (저장된 qty 는 최초 주문수량).
qty = row.get('unfilled_qty') or s['qty']
try:
res = kiwoom_order.modify_order(
account_label=r['account'],
orig_ord_no=s['ord_no'],
symbol=r['symbol'],
modify_qty=qty,
modify_price=up['ord_uv'],
routing_suffix=row.get('routing_suffix', ''),
dry_run=dry_run,
card_id=r.get('card_id'),
modify_cond_price=up['cond_uv'],
)
except Exception as e:
# sidecar 비활성(매매 차단) 포함. 손절선은 기존 값이 그대로 살아있다.
_log(f'정정 예외 {r["id"]}/{s["n"]}단계 {r["symbol_name"]}: {e!r}')
continue
if dry_run:
_log(f'[dry-run] {r["id"]}/{s["n"]}단계 {r["symbol_name"]} '
f'고점 {r["peak"]:,}→{nx["peak"]:,} · '
f'손절 {s["cond_uv"]:,}→{up["cond_uv"]:,} / 지정 {up["ord_uv"]:,} · '
f'body={res.get("body")}')
continue
if res.get('ok'):
new_ord_no = res.get('new_ord_no') or ''
updated = trailing.commit_step_modify(r['id'], s['n'], new_ord_no,
nx['peak'], up['cond_uv'], up['ord_uv'])
raised += 1
_log(f'손절선 상향 {r["id"]}/{s["n"]}단계 {r["symbol_name"]} '
f'{s["cond_uv"]:,}→{up["cond_uv"]:,}원 (고점 {nx["peak"]:,}) '
f'ord_no {s["ord_no"]}→{new_ord_no}')
if not updated:
# 상태 갱신 실패 = 다음 정정이 옛 ord_no 로 나가 실패한다. 반드시 알린다.
_log(f'⚠️ 상태 갱신 실패 {r["id"]}/{s["n"]}단계 — ord_no 불일치 위험')
try:
_notify_modify_failed(r, s, f'정정은 성공했지만 상태 갱신 실패 '
f'(새 주문번호 {new_ord_no})')
except Exception:
pass
else:
reason = res.get('reason', 'UNKNOWN')
detail = (res.get('response') or {}).get('return_msg') or res.get('error') or ''
_log(f'정정 실패 {r["id"]}/{s["n"]}단계 {r["symbol_name"]}: '
f'{reason} {detail} (세션 {sess})')
# NXT 시간대에 KRX/SOR 원주문 정정이 거부되면 매 사이클 반복된다 — 쿨다운으로
# 스팸 차단. 쿨다운은 예약 단위다(같은 원인으로 전 계단이 함께 실패하므로
# 계단마다 알리면 한 사이클에 3~5통이 된다). 로그에는 매번 남는다.
if trailing.should_notify_failure(r['id'], time.time(), FAIL_NOTICE_COOLDOWN_SEC):
try:
_notify_modify_failed(r, s, f'{reason} {detail}'.strip() + f' · 세션 {sess}')
except Exception as e:
_log(f'알림 실패: {e!r}')
else:
_log(f' 알림 생략 (쿨다운 {FAIL_NOTICE_COOLDOWN_SEC}초 내 이미 발송)')
# 대기 예약 발동은 손절선 상향이 **끝난 뒤에** 한다. 순서가 설계다 — 상향은 이미 나간
# 주문을 지키는 동작이라 arm 쪽 어떤 예외도 그걸 선점하면 안 된다. 그래서 통째로 감싼다.
# arm 이 0건이면 이 아래는 실행되지 않아 기존 동작과 동일하다.
if arms:
try:
_check_arms(arms, quotes, open_by_acct, sess, dry_run)
except Exception as e:
_log(f'대기 예약 단계 예외 (트레일링 감시는 정상 완료): {e!r}')
if raised:
_log(f'완료 — {raised}개 계단 상향 / 예약 {len(alive)}건')
return 0
def cmd_list() -> int:
reservations = trailing.list_active()
arms = trailing.list_arms()
if not reservations and not arms:
print('트레일링 예약 없음')
return 0
if arms:
print(f'── 대기 예약 (발동가 도달 시 시작) {len(arms)}건 ──')
for a in arms:
steps = a.get('steps') or []
floor_part = f' · 최저 {a["min_sell_price"]:,}' if a.get('min_sell_price') else ''
print(f'{a["id"]} · {a["symbol_name"]}({a["symbol"]}) · {a["account"]} · '
f'{a["qty"]:,}주 · {len(steps)}단')
print(f' 발동가 {a["trigger_price"]:,}{floor_part} · 등록 {a["created_at"]}')
for s in steps:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_pct_s(cum)}%'
print(f' {s["n"]}단계 −{_pct_s(s["pct"])}% · {cum_part} '
f'(주수는 발동 시점 매도가능 기준으로 배분)')
if a.get('firing_at'):
legs = a.get('fired_legs') or []
nos = ', '.join(s.get('ord_no', '') for s in legs if s.get('ord_no')) or '없음'
print(f' ⚠️ 발동 처리 중단 (시작 {a["firing_at"]}) · 접수된 주문: {nos}')
if reservations and arms:
print(f'── 트레일링 예약 {len(reservations)}건 ──')
for r in reservations:
steps = r.get('steps') or []
floor_part = f' · 최저 {r["min_sell_price"]:,}' if r.get('min_sell_price') else ''
entry_peak = r.get('entry_peak') or 0
peak_part = f'고점 {r["peak"]:,}'
if entry_peak and r['peak'] > entry_peak:
peak_part += f' (등록 시 {entry_peak:,})'
print(f'{r["id"]} · {r["symbol_name"]}({r["symbol"]}) · {r["account"]} · '
f'{r["qty"]:,}주 · {len(steps)}단')
print(f' {peak_part}{floor_part} · 등록 {r["created_at"]} · 갱신 {r["updated_at"]}')
for s in steps:
cum = s.get('cum', 0)
cum_part = '전량' if cum >= 100 else f'누적 {_pct_s(cum)}%'
print(f' {s["n"]}단계 −{_pct_s(s["pct"])}% · 손절 {s["cond_uv"]:,} / '
f'지정 {s["ord_uv"]:,} · {s["qty"]:,}주 ({cum_part}) · '
f'ord_no {s["ord_no"]} · 상향 {s.get("modify_count", 0)}회')
return 0
def _int_opt(argv: list[str], name: str, default: int) -> int:
if name not in argv:
return default
i = argv.index(name)
if i + 1 >= len(argv):
return default
try:
return int(argv[i + 1])
except ValueError:
return default
def main(argv: list[str]) -> int:
cmd = argv[1] if len(argv) > 1 else 'check'
if cmd == 'list':
return cmd_list()
if cmd == 'check':
force = '--force' in argv
dry_run = '--dry-run' in argv
repeat = max(1, _int_opt(argv, '--repeat', 1))
gap = max(1, _int_opt(argv, '--gap', 30))
rc = 0
for i in range(repeat):
rc = check(force=force, dry_run=dry_run) or rc
if i >= repeat - 1:
break
# 예약 0건이거나 장 마감이면 남은 sweep 을 버린다 — 빈 sleep 으로 프로세스가
# 살아있을 이유가 없다. sleep '전에' 확인해야 한다.
if not _worth_running(force):
break
time.sleep(gap)
return rc
print(__doc__, file=sys.stderr)
return 2
if __name__ == '__main__':
sys.exit(main(sys.argv))
+5
View File
@@ -119,6 +119,11 @@ def _cmd_variants(argv):
elif sub == 'clear':
variants.clear()
print('변이 정의·계좌 전부 삭제')
elif sub == 'philosophy':
res = variants.add_philosophy_presets()
print(f"철학별 전략 추가: 신규 {len(res['added'])}개 · 중복 {res['skipped']}개 · 총 {res['total']}개")
for v in res['added']:
print(' ', v['id'], v['name'])
else:
vs = variants.load_variants()
print(f'변이 {len(vs)}개:')
+2 -2
View File
@@ -182,7 +182,7 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
pos['last_add_price'] = fill
cash -= cost
nstop, ntarget = signals.compute_stop_target(pos['entry_price'], od['atr'], od['recent_low'])
pos['stop'] = max(pos['stop'], nstop)
pos['stop'] = nstop if od.get('add_kind') == 'down' else max(pos['stop'], nstop)
pos['target'] = ntarget
elif kind == 'buy':
if code in positions or len(positions) >= config.MAX_POSITIONS or exited.get(code) == day:
@@ -223,7 +223,7 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
if act in ('sell', 'scale_out'):
pending.append({'kind': act, 'code': code, 'frac': dec.get('sell_frac', 1.0)})
elif act == 'add':
pending.append({'kind': 'add', 'code': code,
pending.append({'kind': 'add', 'code': code, 'add_kind': dec.get('add_kind'),
'atr': ind['atr'], 'recent_low': ind['recent_low']})
# ---- 2. 신규 매수 판단 (당일 종가) → 내일 시가 주문 큐잉 ----
+5
View File
@@ -41,6 +41,9 @@ REL_STRENGTH_DAYS = 20 # 상대강도: 종목 N일 수익률 − 시
TREND_SLOPE_GATE = 1 # 추세 게이트 ① 장기선 우상향 요구 (1=요구/0=완화). 보수성 완화 변이 실험용
MID_ARRAY_GATE = 1 # 추세 게이트 ② 5일선>중기선(60) 요구 (1=요구/0=완화)
IMMEDIATE_ENTRY = 0 # 즉시매수: 조건 통과 종목이 돌파·눌림목 둘 다 아니어도 강세장이면 현재가 즉시 매수 (1=켜기). 꾸준한 상승 종목 놓침 방지 실험용
AVERAGING_ENTRY = 0 # 물타기(애버리징): 보유 중 직전매수가 대비 하락 시 손절 대신 추가매수로 평단↓ (1=켜기). 종목당 10% 한도 내, 급락·약세장 제외
AVERAGING_DROP_PCT = 0.10 # 물타기 트리거 — 직전 매수가 대비 이 비율 하락 시 추가 (0.10=-10%마다)
CRASH_DROP_PCT = 0.10 # 당일 단일 하락이 이 비율 이상이면 물타기 말고 손절 (그림 #3, 0.10=-10%↓)
ATR_PERIOD = 14
BREAKOUT_LOOKBACK = 20 # 돌파 판정용 최근 고점 구간
SWING_LOW_LOOKBACK = 10 # 손절선 후보 스윙 저점 구간
@@ -93,6 +96,8 @@ TUNABLE = [
{'key': 'TREND_SLOPE_GATE', 'label': '장기선 우상향 요구', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '추세'},
{'key': 'MID_ARRAY_GATE', 'label': '중기 정배열(5>60) 요구', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '추세'},
{'key': 'IMMEDIATE_ENTRY', 'label': '즉시매수(눌림 안 기다림)', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '매수'},
{'key': 'AVERAGING_ENTRY', 'label': '물타기(하락 시 추가매수)', 'type': 'int', 'min': 0, 'max': 1, 'step': 1, 'group': '매도'},
{'key': 'AVERAGING_DROP_PCT', 'label': '물타기 하락 트리거', 'type': 'float', 'min': 0.03, 'max': 0.3, 'step': 0.01, 'group': '매도'},
{'key': 'ATR_PERIOD', 'label': 'ATR 기간(일)', 'type': 'int', 'min': 5, 'max': 60, 'step': 1, 'group': '변동성·기간'},
{'key': 'BREAKOUT_LOOKBACK', 'label': '돌파 고점 구간(일)', 'type': 'int', 'min': 5, 'max': 120, 'step': 1, 'group': '변동성·기간'},
{'key': 'SWING_LOW_LOOKBACK', 'label': '스윙 저점 구간(일)', 'type': 'int', 'min': 3, 'max': 60, 'step': 1, 'group': '변동성·기간'},
+35 -4
View File
@@ -225,10 +225,33 @@ def _ind_key(code):
config.RSI_PERIOD, config.TREND_SLOPE_DAYS)
def _gather(extra_codes=None):
LAST_QUOTES_PATH = config.STATE_DIR / 'last_quotes.json'
def _save_last_quotes(quotes: dict) -> None:
"""직전 스캔이 모은 시세 — judge_view(관심종목 탭)가 라이브 재호출 없이 같은 값을 재사용."""
if not quotes: # 시세 수집 실패(빈 dict)면 직전 정상값 보존 — 덮어쓰지 않음
return
try:
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
LAST_QUOTES_PATH.write_text(json.dumps(quotes, ensure_ascii=False))
except Exception as e:
sys.stderr.write(f'[engine] last_quotes 저장 실패: {e}\n')
def _load_last_quotes() -> dict | None:
"""직전 스캔 시세. 없으면 None → 호출부가 라이브 폴백."""
try:
return json.loads(LAST_QUOTES_PATH.read_text())
except Exception:
return None
def _gather(extra_codes=None, quotes=None):
"""파라미터 무관한 라이브 데이터 1회 수집 (메인·변이 공유). 종목·시세·시장매핑.
extra_codes: universe에 없지만 청산 판단을 위해 포함해야 할 보유 코드(수동 삭제된 보유분).
quotes: 주어지면 라이브 시세 호출을 건너뛰고 그대로 사용 (judge_view 가 직전 스캔 시세 재사용).
"""
data.reset_book_memo() # 호가 공유 캐시 리셋 — 이번 스캔의 체결가를 전 선수가 공유
_SCAN_CANDLES.clear()
@@ -241,6 +264,7 @@ def _gather(extra_codes=None):
uni = uni + [{'code': c, 'name': '', 'sources': ['held']}]
codes.append(c)
seen.add(c)
if not quotes: # None(미지정) 또는 빈 dict(저장본 비었음)면 라이브 수집
quotes = data.batch_quotes(codes)
cmkt = universe.code_market_map()
return uni, quotes, cmkt
@@ -286,7 +310,8 @@ def _run_scan(pf, uni, quotes, cmkt, now, execute: bool = True) -> dict:
flow = data.flow_net(data.investor_flow(code))
anl = data.analyst(code, cur_price)
dec = signals.evaluate_holding(pf.positions[code], ind, flow, anl,
held_days=_held_days(pf.positions[code], now))
held_days=_held_days(pf.positions[code], now),
market_ok=mkt_ok.get(cmkt.get(code, ''), True))
pf.apply_position_update(code, dec['position_update'])
dec.update({k: pf.positions[code].get(k) for k in
('entry_price', 'qty', 'stop', 'target', 'trailing_on', 'entry_at')})
@@ -305,7 +330,9 @@ def _run_scan(pf, uni, quotes, cmkt, now, execute: bool = True) -> dict:
# 새 평단 기준 손절·목표 재산정 (손절은 위로만 래칫)
new_entry = pf.positions[code]['entry_price']
nstop, ntarget = signals.compute_stop_target(new_entry, ind['atr'], ind['recent_low'])
pf.positions[code]['stop'] = max(pf.positions[code]['stop'], nstop)
# 추격매수(up)=손절 위로만 래칫 / 물타기(down)=낮아진 평단 기준으로 손절 재설정
pf.positions[code]['stop'] = (nstop if dec.get('add_kind') == 'down'
else max(pf.positions[code]['stop'], nstop))
pf.positions[code]['target'] = ntarget
buys.append(rec)
dec.update({'fill_price': fill, 'entry_price': new_entry,
@@ -396,7 +423,9 @@ def judge_view(vid: str) -> dict | None:
pf = Portfolio.load(pp, tp)
try:
backtest.apply_params(params)
uni, quotes, cmkt = _gather(set(pf.positions))
# 직전 스캔이 저장한 시세 재사용 — 라이브 재호출 없이 보유종목 가격과 같은 시점으로 통일.
# 스캔 이력이 아직 없으면(None) _gather 가 라이브 폴백.
uni, quotes, cmkt = _gather(set(pf.positions), quotes=_load_last_quotes())
return _run_scan(pf, uni, quotes, cmkt, now, execute=False)
finally:
backtest.apply_params(config.load_params()) # 전역 config 원복 (웹 데몬 잔류 방지)
@@ -422,6 +451,7 @@ def scan(force: bool = False) -> dict:
return {'skipped': True, 'reason': '장외/휴장', 'at': now.isoformat()}
pf = Portfolio.load()
uni, quotes, cmkt = _gather(set(pf.positions)) # 보유분은 universe에서 빠져도 청산 위해 포함
_save_last_quotes(quotes)
snap = _run_scan(pf, uni, quotes, cmkt, now)
_attach_benchmark(snap, pf, now)
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
@@ -445,6 +475,7 @@ def scan_all(force: bool = False) -> dict:
for vpf in vpfs.values():
held_union |= set(vpf.positions)
uni, quotes, cmkt = _gather(held_union)
_save_last_quotes(quotes)
# 기준 판단 (params.json) — 2026-06-10 기준전략 폐지: 매매 없이 판단만(last_scan = 화면·상태점용).
# 실제 매매는 전부 변이들이 한다 (메인 계좌 동결).
+19 -1
View File
@@ -266,11 +266,12 @@ def evaluate_candidate(code, name, sources, ind, flow, analyst, market_ok) -> di
'reason': f'눌림목 대기 — 지정가 {limit:,}'}
def evaluate_holding(position, ind, flow, analyst, held_days=None) -> dict:
def evaluate_holding(position, ind, flow, analyst, held_days=None, market_ok=True) -> dict:
"""보유 종목 판단. 우선순위: 손절/추세이탈(전량) → 시간손절 → 분할익절(scale_out) → 추격매수(add) → 유지.
position 의 stop/peak/trailing/scaled_out 갱신값도 position_update 로 함께 반환.
held_days: 보유 경과 일수(달력). None이면 시간손절 미적용(엔진·백테스트가 산정해 전달).
market_ok: 시장 강세 여부(물타기 허용 판단용, 백테스트는 중립 True).
"""
price = ind['price']
entry = position['entry_price']
@@ -314,6 +315,23 @@ def evaluate_holding(position, ind, flow, analyst, held_days=None) -> dict:
'price': price, 'checks': checks, 'action': None, 'state': 'HOLD',
'position_update': upd, 'pnl_pct': pnl_pct}
# 0. 물타기(애버리징) — 켜진 변이만. 하락 시 손절 대신 추가매수로 평단↓ (한도·강세장·비급락 조건).
# 당일 단일 급락(-CRASH%↓)은 물타기 금지→손절 우선(그림 #3), 약세장도 금지(그림 #4).
avg_on = getattr(config, 'AVERAGING_ENTRY', 0)
if avg_on:
prev_close = ind.get('prev_close') or price
today_chg = (price / prev_close - 1) if prev_close else 0
crash = today_chg <= -getattr(config, 'CRASH_DROP_PCT', 0.10)
avg_trigger = last_add * (1 - getattr(config, 'AVERAGING_DROP_PCT', 0.10))
budget_left = tranches < config.ENTRY_TRANCHES
# 트레일링/익절 전 + 한도 남음 + 강세장 + 비급락 + 직전매수가 -X% 도달 → 물타기
if (budget_left and market_ok and not crash and not trailing_on and not scaled_out
and price <= avg_trigger):
return {**base, 'state': 'ADD', 'action': 'add', 'add_kind': 'down',
'reason': f'물타기 — {tranches + 1}/{config.ENTRY_TRANCHES}차 (평단↓, -{config.AVERAGING_DROP_PCT*100:.0f}%)'}
# 물타기 한도 소진(또는 급락/약세장)인데 손절선 깨짐 → 최종 손절
# (한도 남았는데 급락이면 아래 일반 손절로 자연 처리)
# 1. 손절·추세이탈 — 전량
if price <= stop:
reason = '트레일링 익절' if (trailing_on and price > entry) else '손절'
+119 -43
View File
@@ -68,25 +68,31 @@ PTR_HTML = """
# 전체 리로드여도 보던 화면이 유지되게 한다 (자동·당겨서·수동 새로고침 공통).
REFRESH_JS = """
<script>(function(){
var TABS=['lab','watch','market'],SUBS=['bt','cmp','day'];
var TABS=['lab','watch','market'],SUBS=['bt','cmp','day'],WSUBS=['mine','auto'];
var p=new URLSearchParams(location.search);
var t=p.get('t'),s=p.get('s'),vw=p.get('view');
var t=p.get('t'),s=p.get('s'),w=p.get('w'),vw=p.get('view');
if(SUBS.indexOf(t)>=0){s=t;t='lab';} /* 레거시 ?t=bt/cmp 하위호환 */
if(t==='acct'||t==='tune')t='lab'; /* 삭제된 탭 하위호환 */
if(t){var r=document.getElementById('tab-'+t); if(r)r.checked=true;}
if(s){var rs=document.getElementById('sub-'+s); if(rs)rs.checked=true;}
if(w){var rw=document.getElementById('wsub-'+w); if(rw)rw.checked=true;}
/* 탭 라디오 ↔ URL 동기화 — 어떤 새로고침(당겨서/브라우저/reload)이든 보던 탭으로 돌아오게 */
function simURL(){
var cur=TABS.find(function(x){var e=document.getElementById('tab-'+x);return e&&e.checked;});
var sub=SUBS.find(function(x){var e=document.getElementById('sub-'+x);return e&&e.checked;});
var u='/?t='+(cur||'lab')+(cur==='lab'?'&s='+(sub||'cmp'):'')+((cur==='watch'&&vw)?'&view='+encodeURIComponent(vw):'');
var u='/?t='+(cur||'lab')+(cur==='lab'?'&s='+(sub||'cmp'):'');
if(cur==='watch'){
var wc=WSUBS.find(function(x){var e=document.getElementById('wsub-'+x);return e&&e.checked;});
u+='&w='+(wc||'mine');
if(vw)u+='&view='+encodeURIComponent(vw);
}
if(cur==='lab'&&sub==='day'){var dd=new URLSearchParams(location.search).get('d');
if(dd)u+='&d='+encodeURIComponent(dd);} /* 거래목록 선택 날짜 보존 */
return u;
}
window._simURL=simURL;
function syncUrl(){try{history.replaceState(null,'',simURL());}catch(e){}}
document.querySelectorAll('input[name="tab"],input[name="subtab"]').forEach(function(r2){r2.addEventListener('change',syncUrl);});
document.querySelectorAll('input[name="tab"],input[name="subtab"],input[name="wsub"]').forEach(function(r2){r2.addEventListener('change',syncUrl);});
syncUrl();
function banner(msg,color){var b=document.createElement('div');b.className='saved-banner';
if(color)b.style.background=color;b.textContent=msg;document.body.appendChild(b);
@@ -125,9 +131,7 @@ REFRESH_JS = """
var ae=document.activeElement;
if(ae&&(ae.tagName==='INPUT'||ae.tagName==='TEXTAREA')) return; /* 입력 중 멈춤 */
saveState();
var cur=TABS.find(function(x){var e=document.getElementById('tab-'+x);return e&&e.checked;});
var sub=SUBS.find(function(x){var e=document.getElementById('sub-'+x);return e&&e.checked;});
location.href='/?t='+(cur||'lab')+(cur==='lab'?'&s='+(sub||'cmp'):'')+((cur==='watch'&&vw)?'&view='+encodeURIComponent(vw):'');
location.href=simURL();
},60000);
})();</script>
"""
@@ -866,6 +870,19 @@ def _immediate_chip_html(p) -> str:
f'data-detail="{_esc(desc)}">즉시</span>')
def _avg_chip_html(p) -> str:
"""물타기(애버리징) 칩 — 보유 중 하락 시 손절 대신 추가매수로 평단 낮추는 변이. 아니면 빈 문자열."""
if not int((p or {}).get('AVERAGING_ENTRY', 0)):
return ''
drop = (p or {}).get('AVERAGING_DROP_PCT', 0.10)
desc = ('물타기(애버리징) 변이 — 우량주(추세·정배열 통과)를 산 뒤 직전 매수가보다 '
f'-{drop*100:.0f}% 떨어지면, 손절하지 않고 추가매수해 평균단가를 낮춰요(종목당 10% 한도). '
'단 ①당일 -10%↓ 급락이면 물타기 말고 손절 ②약세장이면 물타기 금지 — 떨어지는 칼·하락장 물타기는 피해요. '
'(손절형 기존 전략과 라이브 비교 중)')
return (f'<span class="bic bic-avg" data-title="물타기(애버리징)" '
f'data-detail="{_esc(desc)}">물타기</span>')
def _backtest_panel() -> str:
"""백테스트 결과 — 유형별(세부 라벨) 대표(1위) 조합 요약. 비교군은 이 대표들로 자동 구성되므로
수동 추가/제거 없이 읽기 전용 요약만 보여준다."""
@@ -1122,8 +1139,8 @@ def _compare_panel() -> str:
bt_row = ''
rows += (
f'<details class="row" data-reg="{i}" data-rank="{rank_of[v["id"]]}" data-bt="{bt_attr}" '
f'data-held="{s.get("open_positions", 0)}" data-trades="{s.get("closed_trades", 0)}"><summary>'
f'<span class="vid">{rank_of[v["id"]]}위</span>{_bear_chip_html(mp)}{_relax_chip_html(mp)}{_immediate_chip_html(mp)} <span class="muted">{_vid_disp(vid)}</span> '
f'data-ret="{ret}" data-held="{s.get("open_positions", 0)}" data-trades="{s.get("closed_trades", 0)}"><summary>'
f'<span class="vid">{rank_of[v["id"]]}위</span>{_bear_chip_html(mp)}{_relax_chip_html(mp)}{_immediate_chip_html(mp)}{_avg_chip_html(mp)} <span class="muted">{_vid_disp(vid)}</span> '
f'<span class="nm">{_esc(_cat_name(mp))}</span>{tag}'
f'<span class="rt"><span class="muted">🛒{s.get("open_positions", 0)}</span> '
f'<b style="color:{rc}">{ret:+g}%</b></span></summary>'
@@ -1155,29 +1172,45 @@ def _compare_panel() -> str:
f'</div></details>'
)
sortbar = ('<div class="cmp-sortbar" id="cmp-sortbar">'
'<button type="button" data-s="rank">손익순</button>'
'<button type="button" data-s="bt">평가순</button>'
'<button type="button" data-s="held">보유순</button>'
'<button type="button" data-s="trades">거래순</button></div>')
'<button type="button" data-s="rank" data-attr="data-ret">손익순</button>'
'<button type="button" data-s="bt" data-attr="data-bt">평가순</button>'
'<button type="button" data-s="held" data-attr="data-held">보유순</button>'
'<button type="button" data-s="trades" data-attr="data-trades">거래순</button></div>')
# 각 버튼: 한 번 누를 때마다 오름/내림 토글. 기본 방향 — 손익/보유/거래=내림(desc), 평가(백테스트순위)=오름(asc)
sort_js = """
<script>(function(){
var bar=document.getElementById('cmp-sortbar'),box=document.getElementById('cmp-rows');
if(!bar||!box)return;
var ATTR={rank:'data-ret',bt:'data-bt',held:'data-held',trades:'data-trades'};
var LBL={rank:'손익순',bt:'평가순',held:'보유순',trades:'거래순'};
var DEFDIR={rank:'desc',bt:'asc',held:'desc',trades:'desc'}; // 기본 방향
function num(el,a){return parseFloat(el.getAttribute(a))||0;}
var CMP={rank:function(a,b){return num(a,'data-rank')-num(b,'data-rank');},
bt:function(a,b){return num(a,'data-bt')-num(b,'data-bt');},
held:function(a,b){return num(b,'data-held')-num(a,'data-held');},
trades:function(a,b){return num(b,'data-trades')-num(a,'data-trades');}};
function apply(m){
var f=CMP[m]; if(!f)return;
Array.prototype.slice.call(box.children).sort(f).forEach(function(r){box.appendChild(r);});
bar.querySelectorAll('button').forEach(function(b){b.classList.toggle('active',b.dataset.s===m);});
try{sessionStorage.setItem('cmpSort',m);}catch(e){}
function apply(m,dir){
var at=ATTR[m]; if(!at)return;
var mul=(dir==='asc')?1:-1;
Array.prototype.slice.call(box.children)
.sort(function(a,b){return mul*(num(a,at)-num(b,at));})
.forEach(function(r){box.appendChild(r);});
bar.querySelectorAll('button').forEach(function(b){
var on=b.dataset.s===m;
b.classList.toggle('active',on);
b.textContent=LBL[b.dataset.s]+(on?(dir==='asc'?' ↑':' ↓'):'');
});
try{sessionStorage.setItem('cmpSort',m);sessionStorage.setItem('cmpDir',dir);}catch(e){}
}
bar.addEventListener('click',function(e){var b=e.target.closest('button');if(b)apply(b.dataset.s);});
var m='rank';try{m=sessionStorage.getItem('cmpSort')||'rank';}catch(e){}
if(!CMP[m])m='rank';
apply(m);
bar.addEventListener('click',function(e){
var b=e.target.closest('button'); if(!b)return;
var m=b.dataset.s, cur=sessionStorage.getItem('cmpSort'), curdir=sessionStorage.getItem('cmpDir');
var dir;
if(cur===m){dir=(curdir==='asc')?'desc':'asc';} // 같은 버튼 재클릭 → 방향 토글
else{dir=DEFDIR[m];} // 다른 버튼 → 그 버튼 기본 방향
apply(m,dir);
});
var m='rank',dir;
try{m=sessionStorage.getItem('cmpSort')||'rank';dir=sessionStorage.getItem('cmpDir');}catch(e){}
if(!ATTR[m])m='rank';
if(!dir)dir=DEFDIR[m];
apply(m,dir);
})();</script>"""
fav_js = ("""
<script>(function(){
@@ -1294,7 +1327,7 @@ h1{font-size:17px;margin:10px 0 2px}h2{font-size:14px;margin:22px 0 8px;color:#c
.vid{background:#1e293b;color:#7dd3fc;border-radius:5px;padding:1px 6px;font-size:11px;font-weight:700}
.bic{border-radius:5px;padding:1px 5px;font-size:10px;font-weight:700;margin-left:4px}
.bic0{background:#334155;color:#cbd5e1}.bic1{background:#1e3a5f;color:#7dd3fc}.bic2{background:#4a2e12;color:#fbbf24}
.bic-relax{background:#3b1d4a;color:#d8b4fe}.bic-now{background:#0c3a2e;color:#6ee7b7}
.bic-relax{background:#3b1d4a;color:#d8b4fe}.bic-now{background:#0c3a2e;color:#6ee7b7}.bic-avg{background:#3a2a0c;color:#fcd34d}
.cmp-btlink{background:#0f2a3f;color:#7dd3fc;border:1px solid #1e3a5f;border-radius:6px;padding:3px 8px;font-size:11px;font-weight:600;display:inline-block}
.note{white-space:pre-wrap;line-height:1.65;font-size:13px;color:#cbd5e1}
ul.an{margin:6px 0;padding-left:18px;line-height:1.7;font-size:13px}
@@ -1425,7 +1458,7 @@ td:first-child,th:first-child{text-align:left}
<title>{_esc(vid)} 전략 상세</title>{style}</head><body><div id="page">
{back}
<h1><span class="vid">{rank or "—"}위</span> <span class="muted">{_vid_disp(vid)}</span> {_esc(_cat_name(params))}
<span class="bic bic{bm}">{_BEAR_WORD[bm]}</span>{_relax_chip_html(params)}{_immediate_chip_html(params)} {icon}</h1>
<span class="bic bic{bm}">{_BEAR_WORD[bm]}</span>{_relax_chip_html(params)}{_immediate_chip_html(params)}{_avg_chip_html(params)} {icon}</h1>
<div class="sub">{_esc(_market_fit_summary(params))} · 약세장대처: {_BEAR_NAME[bm]}</div>
<div class="card">{notes_html}</div>
@@ -1825,7 +1858,8 @@ def _decision_card(d, watch_n: int = 0, held_n: int = 0, acc_detail: str = '', b
def render_html(view: str | None = None, day_date: str | None = None,
tab: str | None = None, sub: str | None = None) -> str:
tab: str | None = None, sub: str | None = None,
wsub: str | None = None) -> str:
snap = _load(config.LAST_SCAN_PATH, {})
from . import variants as vmod
_cv = [c for c in (_load(vmod.COMPARE_PATH, {}) or {}).get('variants', [])
@@ -1916,7 +1950,12 @@ def render_html(view: str | None = None, day_date: str | None = None,
# 상태 그룹 = 전체 전략(메인+변이) 합산 — 한 계좌라도 보유→보유 / 매수직전→매수 / 대기→대기.
# 아무 계좌도 안 보면 기준전략(M) 판정. 카드 체크리스트·사유는 기준전략 시각 유지(합산 정보는 사유 앞에 표시).
by_state = {}
# 출처(origin) 맵 — auto(자동편입) 종목은 별도 섹션으로 분리
_wl0 = umod._load_watchlist()
origin_map = {c: e.get('origin', '') for c, e in _wl0.items()}
by_state = {} # 내 종목(워치·관심·보유·수동)
by_state_auto = {} # 자동편입(auto) — 하단 별도
excluded = [] # 매매 안 하는 종목(ETF/ETN 등) → 하단 제외목록
for d in decisions:
if umod.is_etf(d.get('name', '')):
@@ -1940,18 +1979,30 @@ def render_html(view: str | None = None, day_date: str | None = None,
elif watch_n.get(code):
bits.append(f'{watch_n[code]}개 전략 매수 대기')
d = {**d, 'reason': ' · '.join(bits) + f' — {view_label}: {d.get("reason", "")}'}
by_state.setdefault(st, []).append(d)
# 출처대로만 분류 — auto(자동편입)는 변이가 페이퍼로 보유 중이어도 자동편입 탭에 (보유는 그 안 '보유' 그룹에 표시)
tgt = by_state_auto if origin_map.get(code) == 'auto' else by_state
tgt.setdefault(st, []).append(d)
judge_html = ''
def _state_blocks(bucket):
# 상태별 접이식 — 액션 상태(보유·추격·매수·매도·대기)는 펼침, 제외·데이터없음은 접힘(노이즈)
_open = {'HOLD', 'ADD', 'BUY', 'SELL', 'WAIT'}
out = ''
for st in STATE_ORDER:
items = by_state.get(st, [])
items = bucket.get(st, [])
if not items:
continue
label, color = STATE_BADGE.get(st, (st, '#888'))
judge_html += f'<h3 class="grp"><span class="badge" style="background:{color}">{label}</span> {len(items)}</h3>'
for d in items:
judge_html += _decision_card(d, watch_n.get(d.get('code'), 0), held_n.get(d.get('code'), 0),
cards = ''.join(_decision_card(d, watch_n.get(d.get('code'), 0), held_n.get(d.get('code'), 0),
_acc_detail(d.get('code')), buy_n.get(d.get('code'), 0))
for d in items)
op = ' open' if st in _open else ''
out += (f'<details class="row state-grp"{op}><summary>'
f'<span class="badge" style="background:{color}">{label}</span> {len(items)}</summary>'
f'<div class="body">{cards}</div></details>')
return out
mine_n = sum(len(v) for v in by_state.values())
judge_html = _state_blocks(by_state)
# 관찰목록 중 아직 스캔 안 된(방금 추가 등) 종목 → '스캔 대기'로 즉시 표시
wl = umod._load_watchlist()
@@ -1971,7 +2022,12 @@ def render_html(view: str | None = None, day_date: str | None = None,
f'<span class="price muted">다음 스캔 대기</span>{del_btn}</div></div>'
)
# 매매 안 하는 종목(ETF/ETN 등) → 하단 접이식 제외목록
# 자동편입(auto) 종목 → 상단 서브탭 '자동편입'으로 분리 (별도 HTML)
auto_n = sum(len(v) for v in by_state_auto.values())
judge_html_auto = (_state_blocks(by_state_auto) if auto_n else
'<div class="empty">자동편입된 종목이 없어요 (매일 장 마감 후 키움 순위정보로 편입)</div>')
# 매매 안 하는 종목(ETF/ETN 등) → 하단 접이식 제외목록 (내 종목 쪽에 둠)
if excluded:
judge_html += (
f'<details class="row" style="margin-top:16px"><summary>'
@@ -1994,11 +2050,13 @@ def render_html(view: str | None = None, day_date: str | None = None,
_vp = vv.get('params') or {}
_rx = ' 완화' if (not int(_vp.get('TREND_SLOPE_GATE', 1)) or not int(_vp.get('MID_ARRAY_GATE', 1))) else ''
_im = ' 즉시' if int(_vp.get('IMMEDIATE_ENTRY', 0)) else ''
_av = ' 물타기' if int(_vp.get('AVERAGING_ENTRY', 0)) else ''
opts += (f'<option value="{_esc(vv["id"])}"{_sel}>{_fav}{_rk(vv["id"])}{_vid_disp(vv["id"])} '
f'[{_BEAR_WORD[_bear_mode(_vp)]}{_rx}{_im}] · {_esc(_cat_name(_vp))}</option>')
f'[{_BEAR_WORD[_bear_mode(_vp)]}{_rx}{_im}{_av}] · {_esc(_cat_name(_vp))}</option>')
view_sel = (f'<div class="sub" style="margin:4px 0 8px">체크리스트 시각: '
f'<select id="judge-view" '
f'onchange="location.href=\'/?t=watch&view=\'+encodeURIComponent(this.value)">{opts}</select></div>')
f'onchange="var _w=document.getElementById(\'wsub-auto\').checked?\'auto\':\'mine\';'
f'location.href=\'/?t=watch&w=\'+_w+\'&view=\'+encodeURIComponent(this.value)">{opts}</select></div>')
mkt = snap.get('market_ok', {})
mkt_h = ' · '.join(f'{k} {"양호" if v else "약세"}' for k, v in mkt.items()) or '—'
@@ -2010,7 +2068,8 @@ def render_html(view: str | None = None, day_date: str | None = None,
# 활성 탭/서브탭을 서버에서 미리 checked — JS가 로드 후 바꾸며 생기던 깜빡임 제거.
tab = tab if tab in ('lab', 'watch', 'market') else 'lab'
sub = sub if sub in ('cmp', 'bt', 'day') else 'cmp'
ck = {f'tab-{tab}': ' checked', f'sub-{sub}': ' checked'}
wsub = wsub if wsub in ('mine', 'auto') else 'mine'
ck = {f'tab-{tab}': ' checked', f'sub-{sub}': ' checked', f'wsub-{wsub}': ' checked'}
def _ck(rid):
return ck.get(rid, '')
@@ -2028,6 +2087,12 @@ h1{{font-size:16px;margin:0 0 2px}} .sub{{color:#9ca3af;font-size:11px;margin:0
.kpi-sub{{color:#6b7280;font-size:10px;margin-top:2px}}
h2{{font-size:15px;margin:22px 0 8px;border-bottom:1px solid #1f2937;padding-bottom:6px}}
h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
.state-grp{{margin:6px 0;border-top:none}}
.state-grp>summary{{padding:8px 2px;font-size:13px;font-weight:600;list-style:none;display:flex;align-items:center;gap:6px}}
.state-grp>summary::-webkit-details-marker{{display:none}}
.state-grp>summary::before{{content:'▸';color:#6b7280;font-size:11px}}
.state-grp[open]>summary::before{{content:'▾'}}
.state-grp>.body{{padding:2px 0 4px}}
.badge{{color:#fff;border-radius:6px;padding:1px 8px;font-size:12px;font-weight:600}}
.row{{background:#141b24;border:1px solid #1f2937;border-radius:10px;margin-bottom:8px;overflow:hidden}}
.row>summary{{list-style:none;cursor:pointer;padding:10px 12px;display:flex;flex-wrap:nowrap;align-items:center;gap:8px;font-size:13px;overflow:hidden}}
@@ -2115,6 +2180,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
.bic2{{background:#4a2e12;color:#fbbf24}}
.bic-relax{{background:#3b1d4a;color:#d8b4fe}}
.bic-now{{background:#0c3a2e;color:#6ee7b7}}
.bic-avg{{background:#3a2a0c;color:#fcd34d}}
.cmp-sortbar{{display:flex;gap:6px;margin:0 0 10px}}
.cmp-sortbar button{{flex:1;background:#141b24;border:1px solid #1f2937;border-radius:8px;color:#9ca3af;font-size:11px;font-weight:600;padding:7px 4px;cursor:pointer}}
.cmp-sortbar button.active{{background:#15406e;color:#fff;border-color:#2952cc}}
@@ -2125,7 +2191,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
#chk-toast .ct-t{{font-size:15px;font-weight:700;color:#fff;margin-bottom:8px}}
#chk-toast .ct-d{{font-size:13px;line-height:1.6;color:#cbd5e1;white-space:pre-line}}
#chk-toast .ct-x{{margin-top:14px;width:100%;background:#1e3a8a;color:#fff;border:1px solid #2952cc;border-radius:9px;padding:10px;font-size:13px;font-weight:600;cursor:pointer}}
input[name="tab"],input[name="subtab"]{{display:none}}
input[name="tab"],input[name="subtab"],input[name="wsub"]{{display:none}}
.hd{{position:sticky;top:0;z-index:30;background:#0b0f14;padding:6px 0;border-bottom:1px solid #161e29}}
#page.dragging .hd{{position:static}}
.tabs{{display:flex;gap:6px;margin:0}}
@@ -2138,6 +2204,9 @@ input[name="tab"],input[name="subtab"]{{display:none}}
#sub-cmp:checked~#p-lab label[for="sub-cmp"],#sub-bt:checked~#p-lab label[for="sub-bt"]{{background:#15406e;color:#fff;border-color:#2952cc}}
#p-lab .subpanel{{display:none}}
#p-lab .subpanel h2{{margin-top:4px}}
#p-watch .subpanel{{display:none}}
#wsub-mine:checked~#p-watch #pw-mine,#wsub-auto:checked~#p-watch #pw-auto{{display:block}}
#wsub-mine:checked~#p-watch label[for="wsub-mine"],#wsub-auto:checked~#p-watch label[for="wsub-auto"]{{background:#15406e;color:#fff;border-color:#2952cc}}
.sortbar{{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}}
.sortbar button{{background:#141b24;border:1px solid #1f2937;border-radius:7px;color:#9ca3af;font-size:12px;font-weight:600;padding:7px 10px;cursor:pointer;font-variant-numeric:tabular-nums}}
.sortbar button.active{{background:#15406e;color:#fff;border-color:#2952cc}}
@@ -2208,6 +2277,8 @@ input[name="tab"],input[name="subtab"]{{display:none}}
<input type="radio" name="subtab" id="sub-cmp"{_ck('sub-cmp')}>
<input type="radio" name="subtab" id="sub-bt"{_ck('sub-bt')}>
<input type="radio" name="subtab" id="sub-day"{_ck('sub-day')}>
<input type="radio" name="wsub" id="wsub-mine"{_ck('wsub-mine')}>
<input type="radio" name="wsub" id="wsub-auto"{_ck('wsub-auto')}>
<header class="hd">
<h1>🪎 레이 자동매매 시뮬레이션 <span id="health" class="hdot {hcls}" data-detail="{_esc(hdetail)}" role="button" tabindex="0" title="시뮬 상태 보기"></span></h1>
<div class=sub>마지막 스캔 {scanned or '—'} · 시장 {mkt_h}</div>
@@ -2222,7 +2293,11 @@ input[name="tab"],input[name="subtab"]{{display:none}}
<div class="sub">상태 그룹은 전체 전략(메인+변이) 합산 — 한 전략이라도 보유/매수대기면 그 그룹으로 · 배지: 🟢매수준비 · 🟡대기 · 🛒보유 전략 수 (탭하면 어떤 전략인지)</div>
<div class="sub">종목명 일부만 입력해도 후보가 떠요 · 후보를 누르면 추가 · 자산(비하이브 워치·관심·보유)은 자동 편입 · 삭제는 카드의 ✕ (수동만) · 보유 중이면 삭제해도 청산까지 추적</div>
{view_sel}
{judge_html}
<nav class="subtabs" id="wsubtabs">
<label for="wsub-mine">📌 내 종목 ({mine_n})</label>
<label for="wsub-auto">🤖 자동편입 ({auto_n})</label></nav>
<div class="subpanel" id="pw-mine">{judge_html}</div>
<div class="subpanel" id="pw-auto">{judge_html_auto}</div>
</section>
<section class="panel" id="p-market">
<h2>시장 동향</h2>{market_html}
@@ -2278,12 +2353,13 @@ class Handler(BaseHTTPRequestHandler):
_dd = (_qs.get('d') or [None])[0]
_tab = (_qs.get('t') or [None])[0]
_sub = (_qs.get('s') or [None])[0]
_wsub = (_qs.get('w') or [None])[0]
# 레거시 ?t=bt/cmp/day (서브탭이 t로 옴) 하위호환 — REFRESH_JS와 동일 규칙
if _tab in ('bt', 'cmp', 'day'):
_sub, _tab = _tab, 'lab'
if _tab in ('acct', 'tune'):
_tab = 'lab'
body = render_html(view=_view, day_date=_dd, tab=_tab, sub=_sub).encode('utf-8')
body = render_html(view=_view, day_date=_dd, tab=_tab, sub=_sub, wsub=_wsub).encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
# 캐시 금지 — Safari 휴리스틱 캐싱이 옛 HTML/JS를 재사용해 탭 복원 로직이 구버전으로 돌던 문제 방지
+125 -2
View File
@@ -40,6 +40,116 @@ def _label(params: dict) -> str:
return '·'.join(f'{short.get(k, k)}{v:g}' for k, v in params.items())
def _philosophy_presets() -> list[dict]:
"""철학별 라이브 비교군 프리셋.
기존 sweep은 같은 규칙의 강약 조절이 많아 보유 종목이 겹치기 쉬웠다. 이 프리셋은
수급 기간, 애널 게이트, 시장 방어, 보유 종목 수, 진입 방식까지 같이 바꿔 라이브 행동을
더 벌리는 용도다. 주문 모듈과 무관한 페이퍼 시뮬 전용 정의다.
"""
base = {
'RSI_OVERBOUGHT': 75,
'RSI_OVERSOLD': 20,
'MAX_HOLD_DAYS': 20,
'BEAR_ENTRY_MODE': 1,
'TREND_SLOPE_GATE': 1,
'MID_ARRAY_GATE': 1,
'IMMEDIATE_ENTRY': 0,
'AVERAGING_ENTRY': 0,
'ENTRY_TRANCHES': 2,
'SCALE_OUT_FRAC': 0.0,
}
def p(name: str, strength: str, **params):
merged = dict(base)
merged.update(params)
return {'name': f'{name}·{strength}', 'params': merged}
return [
p('대형주 안정형', '보수', SMA_LONG=60, RR_RATIO=1.5, STOP_ATR_MULT=2.5,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.2,
TURNOVER_OVERHEAT_MULT=4.0, FLOW_DAYS=10, ANALYST_MIN_UPSIDE_PCT=3.0,
MAX_POSITIONS=12, RISK_PER_TRADE_PCT=0.005, BEAR_ENTRY_MODE=0),
p('대형주 안정형', '기본', SMA_LONG=60, RR_RATIO=2.0, STOP_ATR_MULT=2.2,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.5, TURNOVER_BREAKOUT_MULT=1.3,
TURNOVER_OVERHEAT_MULT=4.0, FLOW_DAYS=7, ANALYST_MIN_UPSIDE_PCT=5.0,
MAX_POSITIONS=10, RISK_PER_TRADE_PCT=0.0075, BEAR_ENTRY_MODE=1),
p('대형주 안정형', '공격', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=2.0,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.5, TURNOVER_BREAKOUT_MULT=1.5,
TURNOVER_OVERHEAT_MULT=4.5, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=5.0,
MAX_POSITIONS=8, RISK_PER_TRADE_PCT=0.01, BEAR_ENTRY_MODE=1),
p('수급 추종형', '보수', SMA_LONG=60, RR_RATIO=2.0, STOP_ATR_MULT=2.0,
PULLBACK_ATR_MULT=1.5, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.2,
FLOW_DAYS=10, ANALYST_MIN_UPSIDE_PCT=0.0, MAX_POSITIONS=8,
RISK_PER_TRADE_PCT=0.006, BEAR_ENTRY_MODE=0),
p('수급 추종형', '기본', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=1.8,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.3,
FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=0.0, MAX_POSITIONS=8,
RISK_PER_TRADE_PCT=0.01, BEAR_ENTRY_MODE=1),
p('수급 추종형', '공격', SMA_LONG=10, RR_RATIO=2.5, STOP_ATR_MULT=1.5,
PULLBACK_ATR_MULT=0.8, VOLUME_BREAKOUT_MULT=1.2, TURNOVER_BREAKOUT_MULT=1.2,
FLOW_DAYS=3, ANALYST_MIN_UPSIDE_PCT=-5.0, MAX_POSITIONS=6,
RISK_PER_TRADE_PCT=0.0125, BEAR_ENTRY_MODE=1, IMMEDIATE_ENTRY=1),
p('돌파 모멘텀형', '보수', SMA_LONG=60, BREAKOUT_LOOKBACK=40, RR_RATIO=3.0,
STOP_ATR_MULT=1.7, PULLBACK_ATR_MULT=0.0, VOLUME_BREAKOUT_MULT=2.5,
TURNOVER_BREAKOUT_MULT=2.0, TURNOVER_OVERHEAT_MULT=5.0, FLOW_DAYS=5,
ANALYST_MIN_UPSIDE_PCT=5.0, MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.006,
BEAR_ENTRY_MODE=0),
p('돌파 모멘텀형', '기본', SMA_LONG=20, BREAKOUT_LOOKBACK=20, RR_RATIO=3.0,
STOP_ATR_MULT=1.5, PULLBACK_ATR_MULT=0.0, VOLUME_BREAKOUT_MULT=2.0,
TURNOVER_BREAKOUT_MULT=1.8, TURNOVER_OVERHEAT_MULT=5.0, FLOW_DAYS=5,
ANALYST_MIN_UPSIDE_PCT=5.0, MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.01,
BEAR_ENTRY_MODE=1),
p('돌파 모멘텀형', '공격', SMA_LONG=10, BREAKOUT_LOOKBACK=10, RR_RATIO=3.0,
STOP_ATR_MULT=1.3, PULLBACK_ATR_MULT=0.0, VOLUME_BREAKOUT_MULT=1.7,
TURNOVER_BREAKOUT_MULT=1.5, TURNOVER_OVERHEAT_MULT=6.0, FLOW_DAYS=3,
ANALYST_MIN_UPSIDE_PCT=0.0, MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.0125,
BEAR_ENTRY_MODE=1, IMMEDIATE_ENTRY=1),
p('눌림 반등형', '보수', SMA_LONG=60, RR_RATIO=1.8, STOP_ATR_MULT=2.5,
PULLBACK_ATR_MULT=1.5, VOLUME_BREAKOUT_MULT=1.5, TURNOVER_BREAKOUT_MULT=1.2,
FLOW_DAYS=10, RSI_OVERSOLD=25, ANALYST_MIN_UPSIDE_PCT=5.0,
MAX_POSITIONS=10, RISK_PER_TRADE_PCT=0.006, BEAR_ENTRY_MODE=0),
p('눌림 반등형', '기본', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=2.2,
PULLBACK_ATR_MULT=1.5, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.2,
FLOW_DAYS=5, RSI_OVERSOLD=30, ANALYST_MIN_UPSIDE_PCT=5.0,
MAX_POSITIONS=8, RISK_PER_TRADE_PCT=0.01, BEAR_ENTRY_MODE=0),
p('눌림 반등형', '공격', SMA_LONG=20, RR_RATIO=2.5, STOP_ATR_MULT=2.0,
PULLBACK_ATR_MULT=2.0, VOLUME_BREAKOUT_MULT=1.2, TURNOVER_BREAKOUT_MULT=1.1,
FLOW_DAYS=3, RSI_OVERSOLD=35, ANALYST_MIN_UPSIDE_PCT=0.0,
MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.0125, BEAR_ENTRY_MODE=1),
p('소형 성장형', '보수', SMA_LONG=60, RR_RATIO=3.0, STOP_ATR_MULT=1.8,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.5, TURNOVER_BREAKOUT_MULT=2.0,
TURNOVER_OVERHEAT_MULT=6.0, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=10.0,
MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.004, BEAR_ENTRY_MODE=0),
p('소형 성장형', '기본', SMA_LONG=20, RR_RATIO=3.0, STOP_ATR_MULT=1.5,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.0, TURNOVER_BREAKOUT_MULT=1.8,
TURNOVER_OVERHEAT_MULT=6.0, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=10.0,
MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.006, BEAR_ENTRY_MODE=1),
p('소형 성장형', '공격', SMA_LONG=10, RR_RATIO=3.5, STOP_ATR_MULT=1.3,
PULLBACK_ATR_MULT=0.8, VOLUME_BREAKOUT_MULT=1.7, TURNOVER_BREAKOUT_MULT=1.5,
TURNOVER_OVERHEAT_MULT=7.0, FLOW_DAYS=3, ANALYST_MIN_UPSIDE_PCT=5.0,
MAX_POSITIONS=4, RISK_PER_TRADE_PCT=0.008, BEAR_ENTRY_MODE=1,
IMMEDIATE_ENTRY=1),
p('방어 현금형', '보수', SMA_LONG=60, RR_RATIO=1.5, STOP_ATR_MULT=2.5,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.5, TURNOVER_BREAKOUT_MULT=2.0,
TURNOVER_OVERHEAT_MULT=4.0, FLOW_DAYS=10, ANALYST_MIN_UPSIDE_PCT=12.0,
MAX_POSITIONS=4, RISK_PER_TRADE_PCT=0.003, BEAR_ENTRY_MODE=0),
p('방어 현금형', '기본', SMA_LONG=60, RR_RATIO=2.0, STOP_ATR_MULT=2.2,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.0, TURNOVER_BREAKOUT_MULT=1.8,
TURNOVER_OVERHEAT_MULT=4.5, FLOW_DAYS=7, ANALYST_MIN_UPSIDE_PCT=10.0,
MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.005, BEAR_ENTRY_MODE=0),
p('방어 현금형', '공격', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=2.0,
PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.8, TURNOVER_BREAKOUT_MULT=1.5,
TURNOVER_OVERHEAT_MULT=5.0, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=8.0,
MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.0075, BEAR_ENTRY_MODE=1),
]
def seed_from_sweep(n: int = 3) -> list[dict]:
"""backtest_results.json 검증 상위 n개 → 변이로 등록 (기존 계좌는 유지)."""
try:
@@ -103,7 +213,7 @@ def sync_label_tops(results: dict) -> dict:
'kept': len(current) - removed}
def add_variant(params: dict) -> dict | None:
def add_variant(params: dict, name: str | None = None) -> dict | None:
"""단일 params 조합을 변이로 추가 (백테스트 행 → 비교군). 이미 있으면 None."""
variants = load_variants()
seen = {json.dumps(v['params'], sort_keys=True) for v in variants}
@@ -111,12 +221,25 @@ def add_variant(params: dict) -> dict | None:
return None
nums = [int(v['id'][1:]) for v in variants if v['id'][1:].isdigit()]
vid = f'v{(max(nums) + 1) if nums else 1}'
v = {'id': vid, 'name': _label(params), 'params': params}
v = {'id': vid, 'name': name or _label(params), 'params': params}
variants.append(v)
save_variants(variants)
return v
def add_philosophy_presets() -> dict:
"""철학별 프리셋 18개를 비교군에 추가. 기존 계좌와 중복 조합은 유지."""
added = []
skipped = 0
for preset in _philosophy_presets():
v = add_variant(preset['params'], preset['name'])
if v:
added.append(v)
else:
skipped += 1
return {'added': added, 'skipped': skipped, 'total': len(load_variants())}
def remove_variant(vid: str):
"""변이 1개 삭제 (정의 + 계좌)."""
import shutil
@@ -111,4 +111,5 @@ python3 scripts/watchlist_monitor.py dry-run
- Stock code cache: `state/stock_codes.json` (키움 ka10099, lazy 갱신)
- Seen 기록: `state/behive_youtube_seen.json`
- Fetch 캐시: `state/behive_last_fetch.json`
- Cron: `비하이브 종목분석 요약` / `워치리스트 모니터링` — `cron/jobs.json`
- OpenClaw cron: 등록명 `비하이브 추천영상 요약` — `state/openclaw.sqlite` 의 `cron_jobs` 테이블 (조회·수정 `openclaw cron {list|show|edit}`)
- launchd: 워치리스트 모니터링 = `ai.openclaw.stock.watchlist-monitor` (평일 10·12·14시) — cron 아님
@@ -32,3 +32,159 @@
{"date": "2026-06-12", "market": "KOSPI", "market_label": "코스피", "rise": 756, "upper": 3, "fall": 144, "lower": 0, "steady": 18, "adr": 527.08, "personal": -43174, "foreign": 22041, "institutional": 22870, "captured_at": "2026-06-12T21:00:03.422609+09:00"}
{"date": "2026-06-15", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1010, "upper": 10, "fall": 662, "lower": 0, "steady": 62, "adr": 154.08, "personal": 6162, "foreign": -8164, "institutional": 2166, "captured_at": "2026-06-15T21:00:02.416077+09:00"}
{"date": "2026-06-15", "market": "KOSPI", "market_label": "코스피", "rise": 676, "upper": 3, "fall": 207, "lower": 0, "steady": 35, "adr": 328.02, "personal": -15124, "foreign": 10774, "institutional": 4462, "captured_at": "2026-06-15T21:00:02.416077+09:00"}
{"date": "2026-06-16", "market": "KOSDAQ", "market_label": "코스닥", "rise": 764, "upper": 6, "fall": 896, "lower": 1, "steady": 76, "adr": 85.84, "personal": 7907, "foreign": -3095, "institutional": -4630, "captured_at": "2026-06-16T21:00:02.050758+09:00"}
{"date": "2026-06-16", "market": "KOSPI", "market_label": "코스피", "rise": 519, "upper": 5, "fall": 360, "lower": 0, "steady": 38, "adr": 145.56, "personal": -21841, "foreign": 15353, "institutional": 7076, "captured_at": "2026-06-16T21:00:02.050758+09:00"}
{"date": "2026-06-17", "market": "KOSDAQ", "market_label": "코스닥", "rise": 957, "upper": 7, "fall": 681, "lower": 0, "steady": 96, "adr": 141.56, "personal": -335, "foreign": 400, "institutional": -101, "captured_at": "2026-06-17T21:00:02.638185+09:00"}
{"date": "2026-06-17", "market": "KOSPI", "market_label": "코스피", "rise": 349, "upper": 4, "fall": 526, "lower": 0, "steady": 42, "adr": 67.11, "personal": 5254, "foreign": -9952, "institutional": 5957, "captured_at": "2026-06-17T21:00:02.638185+09:00"}
{"date": "2026-06-18", "market": "KOSDAQ", "market_label": "코스닥", "rise": 245, "upper": 12, "fall": 1436, "lower": 0, "steady": 52, "adr": 17.9, "personal": 3842, "foreign": -1235, "institutional": -2659, "captured_at": "2026-06-18T21:00:02.338371+09:00"}
{"date": "2026-06-18", "market": "KOSPI", "market_label": "코스피", "rise": 109, "upper": 3, "fall": 791, "lower": 0, "steady": 17, "adr": 14.16, "personal": -4156, "foreign": 12776, "institutional": -7682, "captured_at": "2026-06-18T21:00:02.338371+09:00"}
{"date": "2026-06-19", "market": "KOSDAQ", "market_label": "코스닥", "rise": 200, "upper": 7, "fall": 1490, "lower": 1, "steady": 40, "adr": 13.88, "personal": 750, "foreign": 4977, "institutional": -5838, "captured_at": "2026-06-19T21:00:01.989022+09:00"}
{"date": "2026-06-19", "market": "KOSPI", "market_label": "코스피", "rise": 115, "upper": 2, "fall": 787, "lower": 0, "steady": 16, "adr": 14.87, "personal": 16478, "foreign": -3555, "institutional": -12244, "captured_at": "2026-06-19T21:00:01.989022+09:00"}
{"date": "2026-06-22", "market": "KOSDAQ", "market_label": "코스닥", "rise": 383, "upper": 8, "fall": 1297, "lower": 0, "steady": 53, "adr": 30.15, "personal": -4634, "foreign": 3152, "institutional": 1456, "captured_at": "2026-06-22T21:00:01.907533+09:00"}
{"date": "2026-06-22", "market": "KOSPI", "market_label": "코스피", "rise": 148, "upper": 3, "fall": 742, "lower": 1, "steady": 28, "adr": 20.32, "personal": 21506, "foreign": -25466, "institutional": 3034, "captured_at": "2026-06-22T21:00:01.907533+09:00"}
{"date": "2026-06-23", "market": "KOSDAQ", "market_label": "코스닥", "rise": 134, "upper": 8, "fall": 1574, "lower": 1, "steady": 26, "adr": 9.02, "personal": -3979, "foreign": 2746, "institutional": 1329, "captured_at": "2026-06-23T21:00:02.466967+09:00"}
{"date": "2026-06-23", "market": "KOSPI", "market_label": "코스피", "rise": 46, "upper": 4, "fall": 859, "lower": 0, "steady": 13, "adr": 5.82, "personal": 85910, "foreign": -42047, "institutional": -44760, "captured_at": "2026-06-23T21:00:02.466967+09:00"}
{"date": "2026-06-24", "market": "KOSDAQ", "market_label": "코스닥", "rise": 883, "upper": 12, "fall": 786, "lower": 1, "steady": 64, "adr": 113.72, "personal": -3184, "foreign": -321, "institutional": 3363, "captured_at": "2026-06-24T21:00:02.270251+09:00"}
{"date": "2026-06-24", "market": "KOSPI", "market_label": "코스피", "rise": 516, "upper": 6, "fall": 367, "lower": 0, "steady": 35, "adr": 142.23, "personal": 26312, "foreign": -46546, "institutional": 19095, "captured_at": "2026-06-24T21:00:02.270251+09:00"}
{"date": "2026-06-25", "market": "KOSDAQ", "market_label": "코스닥", "rise": 400, "upper": 8, "fall": 1280, "lower": 4, "steady": 57, "adr": 31.78, "personal": 1452, "foreign": 212, "institutional": -1707, "captured_at": "2026-06-25T21:00:01.814263+09:00"}
{"date": "2026-06-25", "market": "KOSPI", "market_label": "코스피", "rise": 291, "upper": 3, "fall": 589, "lower": 3, "steady": 36, "adr": 49.66, "personal": -24136, "foreign": -8754, "institutional": 33244, "captured_at": "2026-06-25T21:00:01.814263+09:00"}
{"date": "2026-06-26", "market": "KOSDAQ", "market_label": "코스닥", "rise": 220, "upper": 7, "fall": 1470, "lower": 2, "steady": 50, "adr": 15.42, "personal": -6688, "foreign": 3510, "institutional": 3084, "captured_at": "2026-06-26T21:00:02.514312+09:00"}
{"date": "2026-06-26", "market": "KOSPI", "market_label": "코스피", "rise": 111, "upper": 4, "fall": 780, "lower": 2, "steady": 24, "adr": 14.71, "personal": 81919, "foreign": -42906, "institutional": -41224, "captured_at": "2026-06-26T21:00:02.514312+09:00"}
{"date": "2026-06-29", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1555, "upper": 14, "fall": 157, "lower": 1, "steady": 30, "adr": 993.04, "personal": -5272, "foreign": 266, "institutional": 5043, "captured_at": "2026-06-29T21:00:03.315108+09:00"}
{"date": "2026-06-29", "market": "KOSPI", "market_label": "코스피", "rise": 820, "upper": 6, "fall": 88, "lower": 0, "steady": 9, "adr": 938.64, "personal": 45975, "foreign": -77332, "institutional": 29327, "captured_at": "2026-06-29T21:00:03.315108+09:00"}
{"date": "2026-06-30", "market": "KOSDAQ", "market_label": "코스닥", "rise": 718, "upper": 12, "fall": 974, "lower": 1, "steady": 53, "adr": 74.87, "personal": 3909, "foreign": -2464, "institutional": -1428, "captured_at": "2026-06-30T21:00:01.974835+09:00"}
{"date": "2026-06-30", "market": "KOSPI", "market_label": "코스피", "rise": 262, "upper": 4, "fall": 623, "lower": 0, "steady": 32, "adr": 42.7, "personal": 8401, "foreign": -37992, "institutional": 29332, "captured_at": "2026-06-30T21:00:01.974835+09:00"}
{"date": "2026-07-01", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1193, "upper": 17, "fall": 491, "lower": 1, "steady": 55, "adr": 245.93, "personal": -1082, "foreign": 2472, "institutional": -1255, "captured_at": "2026-07-01T21:00:01.866864+09:00"}
{"date": "2026-07-01", "market": "KOSPI", "market_label": "코스피", "rise": 700, "upper": 11, "fall": 193, "lower": 1, "steady": 25, "adr": 366.49, "personal": 17370, "foreign": -17028, "institutional": -700, "captured_at": "2026-07-01T21:00:01.866864+09:00"}
{"date": "2026-07-02", "market": "KOSDAQ", "market_label": "코스닥", "rise": 318, "upper": 8, "fall": 1355, "lower": 3, "steady": 66, "adr": 24.01, "personal": 4859, "foreign": -1942, "institutional": -3091, "captured_at": "2026-07-02T21:00:05.425991+09:00"}
{"date": "2026-07-02", "market": "KOSPI", "market_label": "코스피", "rise": 280, "upper": 4, "fall": 616, "lower": 1, "steady": 20, "adr": 46.03, "personal": 62413, "foreign": -43706, "institutional": -20825, "captured_at": "2026-07-02T21:00:05.425991+09:00"}
{"date": "2026-07-03", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1094, "upper": 8, "fall": 566, "lower": 0, "steady": 78, "adr": 194.7, "personal": 1075, "foreign": -202, "institutional": -1037, "captured_at": "2026-07-03T21:00:02.137974+09:00"}
{"date": "2026-07-03", "market": "KOSPI", "market_label": "코스피", "rise": 589, "upper": 2, "fall": 297, "lower": 1, "steady": 27, "adr": 198.32, "personal": -22942, "foreign": -21750, "institutional": 44079, "captured_at": "2026-07-03T21:00:02.137974+09:00"}
{"date": "2026-07-06", "market": "KOSDAQ", "market_label": "코스닥", "rise": 683, "upper": 6, "fall": 982, "lower": 0, "steady": 74, "adr": 70.16, "personal": 2591, "foreign": -447, "institutional": -2244, "captured_at": "2026-07-06T21:00:03.092182+09:00"}
{"date": "2026-07-06", "market": "KOSPI", "market_label": "코스피", "rise": 464, "upper": 8, "fall": 412, "lower": 0, "steady": 37, "adr": 114.56, "personal": 26829, "foreign": -13144, "institutional": -14618, "captured_at": "2026-07-06T21:00:03.092182+09:00"}
{"date": "2026-07-07", "market": "KOSDAQ", "market_label": "코스닥", "rise": 484, "upper": 6, "fall": 1194, "lower": 1, "steady": 61, "adr": 41.0, "personal": -3616, "foreign": 3742, "institutional": -100, "captured_at": "2026-07-07T21:00:04.611682+09:00"}
{"date": "2026-07-07", "market": "KOSPI", "market_label": "코스피", "rise": 358, "upper": 1, "fall": 512, "lower": 0, "steady": 42, "adr": 70.12, "personal": 31359, "foreign": -29172, "institutional": -3108, "captured_at": "2026-07-07T21:00:04.611682+09:00"}
{"date": "2026-07-08", "market": "KOSDAQ", "market_label": "코스닥", "rise": 246, "upper": 4, "fall": 1445, "lower": 1, "steady": 48, "adr": 17.29, "personal": -1959, "foreign": 3376, "institutional": -1453, "captured_at": "2026-07-08T21:00:03.487551+09:00"}
{"date": "2026-07-08", "market": "KOSPI", "market_label": "코스피", "rise": 125, "upper": 3, "fall": 765, "lower": 0, "steady": 22, "adr": 16.73, "personal": -358, "foreign": 3437, "institutional": -3552, "captured_at": "2026-07-08T21:00:03.487551+09:00"}
{"date": "2026-07-09", "market": "KOSDAQ", "market_label": "코스닥", "rise": 775, "upper": 8, "fall": 887, "lower": 0, "steady": 77, "adr": 88.28, "personal": -3217, "foreign": 221, "institutional": 3081, "captured_at": "2026-07-09T21:00:02.779248+09:00"}
{"date": "2026-07-09", "market": "KOSPI", "market_label": "코스피", "rise": 237, "upper": 1, "fall": 652, "lower": 0, "steady": 24, "adr": 36.5, "personal": -13278, "foreign": 1343, "institutional": 12885, "captured_at": "2026-07-09T21:00:02.779248+09:00"}
{"date": "2026-07-10", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1488, "upper": 4, "fall": 204, "lower": 5, "steady": 43, "adr": 713.88, "personal": -4213, "foreign": -1563, "institutional": 5825, "captured_at": "2026-07-10T21:00:02.641644+09:00"}
{"date": "2026-07-10", "market": "KOSPI", "market_label": "코스피", "rise": 802, "upper": 6, "fall": 92, "lower": 0, "steady": 19, "adr": 878.26, "personal": -7805, "foreign": -3228, "institutional": 11314, "captured_at": "2026-07-10T21:00:02.641644+09:00"}
{"date": "2026-07-13", "market": "KOSDAQ", "market_label": "코스닥", "rise": 221, "upper": 4, "fall": 1460, "lower": 2, "steady": 57, "adr": 15.39, "personal": 1833, "foreign": -3878, "institutional": 1843, "captured_at": "2026-07-13T21:00:01.940921+09:00"}
{"date": "2026-07-13", "market": "KOSPI", "market_label": "코스피", "rise": 179, "upper": 3, "fall": 716, "lower": 0, "steady": 18, "adr": 25.42, "personal": 38869, "foreign": -16705, "institutional": -22338, "captured_at": "2026-07-13T21:00:01.940921+09:00"}
{"date": "2026-07-14", "market": "KOSDAQ", "market_label": "코스닥", "rise": 445, "upper": 6, "fall": 1212, "lower": 0, "steady": 83, "adr": 37.21, "personal": 698, "foreign": -2471, "institutional": 1587, "captured_at": "2026-07-14T21:00:05.513305+09:00"}
{"date": "2026-07-14", "market": "KOSPI", "market_label": "코스피", "rise": 219, "upper": 5, "fall": 662, "lower": 0, "steady": 31, "adr": 33.84, "personal": -41411, "foreign": 9565, "institutional": 32117, "captured_at": "2026-07-14T21:00:05.513305+09:00"}
{"date": "2026-07-15", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1465, "upper": 14, "fall": 217, "lower": 1, "steady": 57, "adr": 678.44, "personal": -1542, "foreign": 231, "institutional": 1086, "captured_at": "2026-07-15T21:00:04.202161+09:00"}
{"date": "2026-07-15", "market": "KOSPI", "market_label": "코스피", "rise": 711, "upper": 7, "fall": 169, "lower": 0, "steady": 32, "adr": 424.85, "personal": -24666, "foreign": 23031, "institutional": 1992, "captured_at": "2026-07-15T21:00:04.202161+09:00"}
{"date": "2026-07-16", "market": "KOSDAQ", "market_label": "코스닥", "rise": 501, "upper": 11, "fall": 1182, "lower": 1, "steady": 56, "adr": 43.28, "personal": 4184, "foreign": -2919, "institutional": -1553, "captured_at": "2026-07-16T21:00:01.884912+09:00"}
{"date": "2026-07-16", "market": "KOSPI", "market_label": "코스피", "rise": 384, "upper": 6, "fall": 488, "lower": 0, "steady": 40, "adr": 79.92, "personal": 36647, "foreign": -13665, "institutional": -23831, "captured_at": "2026-07-16T21:00:01.884912+09:00"}
{"date": "2026-07-20", "market": "KOSDAQ", "market_label": "코스닥", "rise": 198, "upper": 10, "fall": 1487, "lower": 3, "steady": 54, "adr": 13.96, "personal": 1907, "foreign": -518, "institutional": -1330, "captured_at": "2026-07-20T21:00:02.383101+09:00"}
{"date": "2026-07-20", "market": "KOSPI", "market_label": "코스피", "rise": 97, "upper": 3, "fall": 799, "lower": 4, "steady": 18, "adr": 12.45, "personal": 3490, "foreign": 5198, "institutional": -9235, "captured_at": "2026-07-20T21:00:02.383101+09:00"}
{"date": "2026-07-21", "market": "KOSDAQ", "market_label": "코스닥", "rise": 983, "upper": 19, "fall": 657, "lower": 5, "steady": 96, "adr": 151.36, "personal": 1474, "foreign": -1360, "institutional": -129, "captured_at": "2026-07-21T21:00:02.218278+09:00"}
{"date": "2026-07-21", "market": "KOSPI", "market_label": "코스피", "rise": 485, "upper": 1, "fall": 382, "lower": 2, "steady": 47, "adr": 126.56, "personal": -16421, "foreign": 2952, "institutional": 13744, "captured_at": "2026-07-21T21:00:02.218278+09:00"}
{"date": "2026-07-22", "market": "KOSDAQ", "market_label": "코스닥", "rise": 839, "upper": 14, "fall": 793, "lower": 3, "steady": 103, "adr": 107.16, "personal": 2636, "foreign": -815, "institutional": -1914, "captured_at": "2026-07-22T21:00:02.077211+09:00"}
{"date": "2026-07-22", "market": "KOSPI", "market_label": "코스피", "rise": 469, "upper": 0, "fall": 399, "lower": 0, "steady": 47, "adr": 117.54, "personal": -12176, "foreign": 26211, "institutional": -13963, "captured_at": "2026-07-22T21:00:02.077211+09:00"}
{"date": "2026-07-23", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1442, "upper": 9, "fall": 252, "lower": 1, "steady": 42, "adr": 573.52, "personal": -2373, "foreign": 1675, "institutional": 643, "captured_at": "2026-07-23T21:00:03.478785+09:00"}
{"date": "2026-07-23", "market": "KOSPI", "market_label": "코스피", "rise": 831, "upper": 4, "fall": 74, "lower": 0, "steady": 10, "adr": 1128.38, "personal": -22077, "foreign": 21359, "institutional": 976, "captured_at": "2026-07-23T21:00:03.478785+09:00"}
{"date": "2026-07-24", "market": "KOSDAQ", "market_label": "코스닥", "rise": 371, "upper": 9, "fall": 1281, "lower": 1, "steady": 80, "adr": 29.64, "personal": 4881, "foreign": -1315, "institutional": -3580, "captured_at": "2026-07-24T21:00:02.378107+09:00"}
{"date": "2026-07-24", "market": "KOSPI", "market_label": "코스피", "rise": 301, "upper": 2, "fall": 585, "lower": 0, "steady": 30, "adr": 51.79, "personal": 51782, "foreign": -32683, "institutional": -19514, "captured_at": "2026-07-24T21:00:02.378107+09:00"}
{"date": "2026-07-27", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1007, "upper": 10, "fall": 628, "lower": 2, "steady": 99, "adr": 161.43, "personal": -1550, "foreign": -1422, "institutional": 2731, "captured_at": "2026-07-27T21:00:02.567797+09:00"}
{"date": "2026-07-27", "market": "KOSPI", "market_label": "코스피", "rise": 497, "upper": 0, "fall": 372, "lower": 2, "steady": 47, "adr": 132.89, "personal": 19788, "foreign": -28811, "institutional": 8595, "captured_at": "2026-07-27T21:00:02.567797+09:00"}
{"date": "2026-07-28", "market": "KOSDAQ", "market_label": "코스닥", "rise": 140, "upper": 11, "fall": 1557, "lower": 2, "steady": 38, "adr": 9.69, "personal": 480, "foreign": 860, "institutional": -1345, "captured_at": "2026-07-28T21:00:02.094342+09:00"}
{"date": "2026-07-28", "market": "KOSPI", "market_label": "코스피", "rise": 36, "upper": 3, "fall": 878, "lower": 2, "steady": 3, "adr": 4.43, "personal": 43152, "foreign": -45009, "institutional": 1830, "captured_at": "2026-07-28T21:00:02.094342+09:00"}
{"date": "2026-07-29", "market": "KOSDAQ", "market_label": "코스닥", "rise": 141, "upper": 4, "fall": 1554, "lower": 3, "steady": 35, "adr": 9.31, "personal": -4579, "foreign": 3082, "institutional": 1474, "captured_at": "2026-07-29T21:00:04.833116+09:00"}
{"date": "2026-07-29", "market": "KOSPI", "market_label": "코스피", "rise": 91, "upper": 1, "fall": 805, "lower": 1, "steady": 21, "adr": 11.41, "personal": -19701, "foreign": -12502, "institutional": 31769, "captured_at": "2026-07-29T21:00:04.833116+09:00"}
{"date": "2026-07-30", "market": "KOSDAQ", "market_label": "코스닥", "rise": 733, "upper": 5, "fall": 938, "lower": 0, "steady": 57, "adr": 78.68, "personal": -1108, "foreign": 2483, "institutional": -1435, "captured_at": "2026-07-30T21:00:02.501493+09:00"}
{"date": "2026-07-30", "market": "KOSPI", "market_label": "코스피", "rise": 609, "upper": 4, "fall": 277, "lower": 0, "steady": 30, "adr": 221.3, "personal": -14309, "foreign": 13280, "institutional": 805, "captured_at": "2026-07-30T21:00:02.501493+09:00"}
{"date": "2026-07-31", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1557, "upper": 14, "fall": 141, "lower": 0, "steady": 30, "adr": 1114.18, "personal": -4715, "foreign": 1697, "institutional": 2985, "captured_at": "2026-07-31T21:00:02.143730+09:00"}
{"date": "2026-07-31", "market": "KOSPI", "market_label": "코스피", "rise": 775, "upper": 12, "fall": 127, "lower": 0, "steady": 15, "adr": 619.69, "personal": -82840, "foreign": 72410, "institutional": 11503, "captured_at": "2026-07-31T21:00:02.143730+09:00"}
{"date": "2026-08-03", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1170, "upper": 11, "fall": 468, "lower": 0, "steady": 89, "adr": 252.35, "personal": 302, "foreign": -2092, "institutional": 1669, "captured_at": "2026-08-03T21:00:05.036019+09:00"}
{"date": "2026-08-03", "market": "KOSPI", "market_label": "코스피", "rise": 455, "upper": 3, "fall": 419, "lower": 0, "steady": 43, "adr": 109.31, "personal": 46523, "foreign": -28220, "institutional": -19516, "captured_at": "2026-08-03T21:00:05.036019+09:00"}
{"date": "2026-08-04", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1505, "upper": 11, "fall": 175, "lower": 0, "steady": 47, "adr": 866.29, "personal": -2587, "foreign": -2775, "institutional": 5438, "captured_at": "2026-08-04T21:00:01.937958+09:00"}
{"date": "2026-08-04", "market": "KOSPI", "market_label": "코스피", "rise": 788, "upper": 1, "fall": 96, "lower": 0, "steady": 31, "adr": 821.88, "personal": 8187, "foreign": -3716, "institutional": -5385, "captured_at": "2026-08-04T21:00:01.937958+09:00"}
{"date": "2026-08-05", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1346, "upper": 11, "fall": 301, "lower": 0, "steady": 78, "adr": 450.83, "personal": 3941, "foreign": -2979, "institutional": -1101, "captured_at": "2026-08-05T21:00:05.552836+09:00"}
{"date": "2026-08-05", "market": "KOSPI", "market_label": "코스피", "rise": 675, "upper": 0, "fall": 197, "lower": 0, "steady": 43, "adr": 342.64, "personal": -11844, "foreign": 14464, "institutional": -2838, "captured_at": "2026-08-05T21:00:05.552836+09:00"}
{"date": "2026-08-06", "market": "KOSDAQ", "market_label": "코스닥", "rise": 736, "upper": 7, "fall": 893, "lower": 0, "steady": 95, "adr": 83.2, "personal": 863, "foreign": -2481, "institutional": 1615, "captured_at": "2026-08-06T21:00:02.122175+09:00"}
{"date": "2026-08-06", "market": "KOSPI", "market_label": "코스피", "rise": 490, "upper": 3, "fall": 381, "lower": 0, "steady": 44, "adr": 129.4, "personal": 33367, "foreign": -32893, "institutional": -1214, "captured_at": "2026-08-06T21:00:02.122175+09:00"}
{"date": "2026-08-07", "market": "KOSDAQ", "market_label": "코스닥", "rise": 808, "upper": 6, "fall": 829, "lower": 0, "steady": 84, "adr": 98.19, "personal": 3402, "foreign": -2522, "institutional": -1044, "captured_at": "2026-08-07T21:00:06.627011+09:00"}
{"date": "2026-08-07", "market": "KOSPI", "market_label": "코스피", "rise": 554, "upper": 3, "fall": 322, "lower": 0, "steady": 36, "adr": 172.98, "personal": 2675, "foreign": -8651, "institutional": 5854, "captured_at": "2026-08-07T21:00:06.627011+09:00"}
{"date": "2026-08-10", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1431, "upper": 11, "fall": 236, "lower": 0, "steady": 52, "adr": 611.02, "personal": -6729, "foreign": 1027, "institutional": 5667, "captured_at": "2026-08-10T21:00:01.810609+09:00"}
{"date": "2026-08-10", "market": "KOSPI", "market_label": "코스피", "rise": 697, "upper": 0, "fall": 183, "lower": 0, "steady": 31, "adr": 380.87, "personal": 9003, "foreign": -14909, "institutional": 5692, "captured_at": "2026-08-10T21:00:01.810609+09:00"}
{"date": "2026-08-11", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1097, "upper": 8, "fall": 547, "lower": 0, "steady": 76, "adr": 202.01, "personal": 4351, "foreign": -3562, "institutional": -954, "captured_at": "2026-08-11T21:00:02.471962+09:00"}
{"date": "2026-08-11", "market": "KOSPI", "market_label": "코스피", "rise": 563, "upper": 4, "fall": 309, "lower": 0, "steady": 38, "adr": 183.5, "personal": -708, "foreign": 535, "institutional": 212, "captured_at": "2026-08-11T21:00:02.471962+09:00"}
{"date": "2026-08-12", "market": "KOSDAQ", "market_label": "코스닥", "rise": 886, "upper": 8, "fall": 753, "lower": 0, "steady": 83, "adr": 118.73, "personal": 3127, "foreign": -1623, "institutional": -1459, "captured_at": "2026-08-12T21:00:02.259243+09:00"}
{"date": "2026-08-12", "market": "KOSPI", "market_label": "코스피", "rise": 380, "upper": 3, "fall": 477, "lower": 0, "steady": 53, "adr": 80.29, "personal": -31911, "foreign": 28354, "institutional": 5279, "captured_at": "2026-08-12T21:00:02.259243+09:00"}
{"date": "2026-08-13", "market": "KOSDAQ", "market_label": "코스닥", "rise": 634, "upper": 9, "fall": 1009, "lower": 1, "steady": 81, "adr": 63.66, "personal": 2565, "foreign": -1362, "institutional": -1132, "captured_at": "2026-08-13T21:00:04.775938+09:00"}
{"date": "2026-08-13", "market": "KOSPI", "market_label": "코스피", "rise": 333, "upper": 1, "fall": 533, "lower": 0, "steady": 35, "adr": 62.66, "personal": -27410, "foreign": 21102, "institutional": 6834, "captured_at": "2026-08-13T21:00:04.775938+09:00"}
{"date": "2026-08-14", "market": "KOSDAQ", "market_label": "코스닥", "rise": 904, "upper": 9, "fall": 741, "lower": 0, "steady": 79, "adr": 123.21, "personal": 3145, "foreign": -659, "institutional": -2524, "captured_at": "2026-08-14T21:00:02.471174+09:00"}
{"date": "2026-08-14", "market": "KOSPI", "market_label": "코스피", "rise": 677, "upper": 3, "fall": 204, "lower": 0, "steady": 25, "adr": 333.33, "personal": -19820, "foreign": 30387, "institutional": -10298, "captured_at": "2026-08-14T21:00:02.471174+09:00"}
{"date": "2026-08-18", "market": "KOSDAQ", "market_label": "코스닥", "rise": 372, "upper": 8, "fall": 1288, "lower": 1, "steady": 68, "adr": 29.48, "personal": 3905, "foreign": 366, "institutional": -4176, "captured_at": "2026-08-18T21:00:02.165948+09:00"}
{"date": "2026-08-18", "market": "KOSPI", "market_label": "코스피", "rise": 195, "upper": 5, "fall": 682, "lower": 0, "steady": 29, "adr": 29.33, "personal": 7420, "foreign": 914, "institutional": -7951, "captured_at": "2026-08-18T21:00:02.165948+09:00"}
{"date": "2026-08-19", "market": "KOSDAQ", "market_label": "코스닥", "rise": 396, "upper": 6, "fall": 1275, "lower": 3, "steady": 58, "adr": 31.46, "personal": -664, "foreign": 147, "institutional": 500, "captured_at": "2026-08-19T21:00:06.595526+09:00"}
{"date": "2026-08-19", "market": "KOSPI", "market_label": "코스피", "rise": 171, "upper": 3, "fall": 703, "lower": 0, "steady": 32, "adr": 24.75, "personal": 46357, "foreign": -34726, "institutional": -13390, "captured_at": "2026-08-19T21:00:06.595526+09:00"}
{"date": "2026-08-20", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1064, "upper": 5, "fall": 576, "lower": 1, "steady": 86, "adr": 185.27, "personal": 16, "foreign": -1328, "institutional": 1194, "captured_at": "2026-08-20T21:00:05.687686+09:00"}
{"date": "2026-08-20", "market": "KOSPI", "market_label": "코스피", "rise": 521, "upper": 4, "fall": 338, "lower": 0, "steady": 46, "adr": 155.33, "personal": -22712, "foreign": 17068, "institutional": -4895, "captured_at": "2026-08-20T21:00:05.687686+09:00"}
{"date": "2026-08-21", "market": "KOSDAQ", "market_label": "코스닥", "rise": 282, "upper": 5, "fall": 1378, "lower": 3, "steady": 63, "adr": 20.78, "personal": 6236, "foreign": -2837, "institutional": -3462, "captured_at": "2026-08-21T21:00:04.974633+09:00"}
{"date": "2026-08-21", "market": "KOSPI", "market_label": "코스피", "rise": 193, "upper": 0, "fall": 683, "lower": 0, "steady": 28, "adr": 28.26, "personal": -11652, "foreign": -1760, "institutional": 2481, "captured_at": "2026-08-21T21:00:04.974633+09:00"}
{"date": "2026-08-24", "market": "KOSDAQ", "market_label": "코스닥", "rise": 921, "upper": 5, "fall": 706, "lower": 2, "steady": 96, "adr": 130.79, "personal": -2604, "foreign": 2416, "institutional": 249, "captured_at": "2026-08-24T21:00:02.141569+09:00"}
{"date": "2026-08-24", "market": "KOSPI", "market_label": "코스피", "rise": 579, "upper": 3, "fall": 286, "lower": 0, "steady": 38, "adr": 203.5, "personal": 33217, "foreign": -36691, "institutional": -12901, "captured_at": "2026-08-24T21:00:02.141569+09:00"}
{"date": "2026-08-25", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1186, "upper": 4, "fall": 466, "lower": 0, "steady": 74, "adr": 255.36, "personal": -1375, "foreign": 1327, "institutional": 156, "captured_at": "2026-08-25T21:00:03.869052+09:00"}
{"date": "2026-08-25", "market": "KOSPI", "market_label": "코스피", "rise": 647, "upper": 2, "fall": 226, "lower": 1, "steady": 34, "adr": 285.9, "personal": 10509, "foreign": -38140, "institutional": 11711, "captured_at": "2026-08-25T21:00:03.869052+09:00"}
{"date": "2026-08-26", "market": "KOSDAQ", "market_label": "코스닥", "rise": 907, "upper": 8, "fall": 708, "lower": 0, "steady": 111, "adr": 129.24, "personal": 1931, "foreign": -918, "institutional": -1054, "captured_at": "2026-08-26T21:00:04.234334+09:00"}
{"date": "2026-08-26", "market": "KOSPI", "market_label": "코스피", "rise": 585, "upper": 2, "fall": 275, "lower": 0, "steady": 47, "adr": 213.45, "personal": -22461, "foreign": -1148, "institutional": 7615, "captured_at": "2026-08-26T21:00:04.234334+09:00"}
{"date": "2026-08-27", "market": "KOSDAQ", "market_label": "코스닥", "rise": 840, "upper": 7, "fall": 794, "lower": 0, "steady": 96, "adr": 106.68, "personal": -1752, "foreign": 2955, "institutional": -1192, "captured_at": "2026-08-27T21:00:03.016599+09:00"}
{"date": "2026-08-27", "market": "KOSPI", "market_label": "코스피", "rise": 317, "upper": 0, "fall": 539, "lower": 0, "steady": 50, "adr": 58.81, "personal": -19116, "foreign": 1333, "institutional": 1835, "captured_at": "2026-08-27T21:00:03.016599+09:00"}
{"date": "2026-08-28", "market": "KOSDAQ", "market_label": "코스닥", "rise": 957, "upper": 6, "fall": 690, "lower": 0, "steady": 83, "adr": 139.57, "personal": 979, "foreign": -942, "institutional": -30, "captured_at": "2026-08-28T21:00:04.731356+09:00"}
{"date": "2026-08-28", "market": "KOSPI", "market_label": "코스피", "rise": 640, "upper": 2, "fall": 238, "lower": 0, "steady": 32, "adr": 269.75, "personal": 4194, "foreign": -8525, "institutional": -11876, "captured_at": "2026-08-28T21:00:04.731356+09:00"}
{"date": "2026-08-31", "market": "KOSDAQ", "market_label": "코스닥", "rise": 741, "upper": 8, "fall": 889, "lower": 0, "steady": 105, "adr": 84.25, "personal": 2530, "foreign": -552, "institutional": -2005, "captured_at": "2026-08-31T21:00:05.779900+09:00"}
{"date": "2026-08-31", "market": "KOSPI", "market_label": "코스피", "rise": 366, "upper": 0, "fall": 489, "lower": 1, "steady": 54, "adr": 74.69, "personal": -1464, "foreign": -6434, "institutional": -7891, "captured_at": "2026-08-31T21:00:05.779900+09:00"}
{"date": "2026-09-01", "market": "KOSDAQ", "market_label": "코스닥", "rise": 686, "upper": 3, "fall": 950, "lower": 1, "steady": 98, "adr": 72.45, "personal": 4389, "foreign": -1531, "institutional": -2787, "captured_at": "2026-09-01T21:00:02.657470+09:00"}
{"date": "2026-09-01", "market": "KOSPI", "market_label": "코스피", "rise": 446, "upper": 4, "fall": 422, "lower": 0, "steady": 40, "adr": 106.64, "personal": -5402, "foreign": -4968, "institutional": -6286, "captured_at": "2026-09-01T21:00:02.657470+09:00"}
{"date": "2026-09-02", "market": "KOSDAQ", "market_label": "코스닥", "rise": 359, "upper": 7, "fall": 1298, "lower": 4, "steady": 76, "adr": 28.11, "personal": 1727, "foreign": 618, "institutional": -2322, "captured_at": "2026-09-02T21:00:04.146664+09:00"}
{"date": "2026-09-02", "market": "KOSPI", "market_label": "코스피", "rise": 139, "upper": 1, "fall": 735, "lower": 0, "steady": 37, "adr": 19.05, "personal": 23029, "foreign": -19173, "institutional": -20361, "captured_at": "2026-09-02T21:00:04.146664+09:00"}
{"date": "2026-09-03", "market": "KOSDAQ", "market_label": "코스닥", "rise": 530, "upper": 3, "fall": 1108, "lower": 1, "steady": 93, "adr": 48.06, "personal": 2950, "foreign": -1082, "institutional": -1838, "captured_at": "2026-09-03T21:00:03.846475+09:00"}
{"date": "2026-09-03", "market": "KOSPI", "market_label": "코스피", "rise": 424, "upper": 0, "fall": 433, "lower": 0, "steady": 52, "adr": 97.92, "personal": -9552, "foreign": 414, "institutional": 9143, "captured_at": "2026-09-03T21:00:03.846475+09:00"}
{"date": "2026-09-04", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1199, "upper": 4, "fall": 436, "lower": 1, "steady": 97, "adr": 275.29, "personal": -4125, "foreign": 2599, "institutional": 1609, "captured_at": "2026-09-04T21:00:05.467425+09:00"}
{"date": "2026-09-04", "market": "KOSPI", "market_label": "코스피", "rise": 549, "upper": 1, "fall": 309, "lower": 0, "steady": 51, "adr": 177.99, "personal": -37229, "foreign": 4796, "institutional": 16694, "captured_at": "2026-09-04T21:00:05.467425+09:00"}
{"date": "2026-09-07", "market": "KOSDAQ", "market_label": "코스닥", "rise": 833, "upper": 9, "fall": 807, "lower": 1, "steady": 95, "adr": 104.21, "personal": 1896, "foreign": -717, "institutional": -1292, "captured_at": "2026-09-07T21:00:02.700353+09:00"}
{"date": "2026-09-07", "market": "KOSPI", "market_label": "코스피", "rise": 478, "upper": 1, "fall": 381, "lower": 0, "steady": 52, "adr": 125.72, "personal": -68374, "foreign": 25532, "institutional": 26491, "captured_at": "2026-09-07T21:00:02.700353+09:00"}
{"date": "2026-09-08", "market": "KOSDAQ", "market_label": "코스닥", "rise": 506, "upper": 6, "fall": 1141, "lower": 0, "steady": 90, "adr": 44.87, "personal": 479, "foreign": 1583, "institutional": -2135, "captured_at": "2026-09-08T21:00:05.322694+09:00"}
{"date": "2026-09-08", "market": "KOSPI", "market_label": "코스피", "rise": 231, "upper": 0, "fall": 634, "lower": 0, "steady": 45, "adr": 36.44, "personal": -30329, "foreign": 6316, "institutional": 6427, "captured_at": "2026-09-08T21:00:05.322694+09:00"}
{"date": "2026-09-09", "market": "KOSDAQ", "market_label": "코스닥", "rise": 962, "upper": 6, "fall": 672, "lower": 0, "steady": 99, "adr": 144.05, "personal": -2548, "foreign": 2646, "institutional": -190, "captured_at": "2026-09-09T21:00:02.199305+09:00"}
{"date": "2026-09-09", "market": "KOSPI", "market_label": "코스피", "rise": 463, "upper": 0, "fall": 400, "lower": 0, "steady": 50, "adr": 115.75, "personal": -22879, "foreign": -4348, "institutional": 9422, "captured_at": "2026-09-09T21:00:02.199305+09:00"}
{"date": "2026-09-10", "market": "KOSDAQ", "market_label": "코스닥", "rise": 827, "upper": 3, "fall": 796, "lower": 1, "steady": 115, "adr": 104.14, "personal": -2698, "foreign": -11253, "institutional": 13996, "captured_at": "2026-09-10T21:00:02.740511+09:00"}
{"date": "2026-09-10", "market": "KOSPI", "market_label": "코스피", "rise": 419, "upper": 3, "fall": 443, "lower": 0, "steady": 53, "adr": 95.26, "personal": 3802, "foreign": -26217, "institutional": 5744, "captured_at": "2026-09-10T21:00:02.740511+09:00"}
{"date": "2026-09-11", "market": "KOSDAQ", "market_label": "코스닥", "rise": 607, "upper": 11, "fall": 1004, "lower": 0, "steady": 124, "adr": 61.55, "personal": 5120, "foreign": -3123, "institutional": -2159, "captured_at": "2026-09-11T21:00:06.064390+09:00"}
{"date": "2026-09-11", "market": "KOSPI", "market_label": "코스피", "rise": 369, "upper": 1, "fall": 475, "lower": 0, "steady": 73, "adr": 77.89, "personal": 18675, "foreign": -22984, "institutional": -12184, "captured_at": "2026-09-11T21:00:06.064390+09:00"}
{"date": "2026-09-14", "market": "KOSDAQ", "market_label": "코스닥", "rise": 545, "upper": 6, "fall": 1115, "lower": 0, "steady": 77, "adr": 49.42, "personal": 1029, "foreign": -923, "institutional": -238, "captured_at": "2026-09-14T21:00:06.345847+09:00"}
{"date": "2026-09-14", "market": "KOSPI", "market_label": "코스피", "rise": 309, "upper": 2, "fall": 563, "lower": 0, "steady": 45, "adr": 55.24, "personal": 30351, "foreign": -33363, "institutional": -11869, "captured_at": "2026-09-14T21:00:06.345847+09:00"}
{"date": "2026-09-15", "market": "KOSDAQ", "market_label": "코스닥", "rise": 788, "upper": 8, "fall": 842, "lower": 0, "steady": 101, "adr": 94.54, "personal": -1478, "foreign": 272, "institutional": 1149, "captured_at": "2026-09-15T21:00:06.045245+09:00"}
{"date": "2026-09-15", "market": "KOSPI", "market_label": "코스피", "rise": 254, "upper": 1, "fall": 623, "lower": 0, "steady": 40, "adr": 40.93, "personal": 8596, "foreign": -15458, "institutional": -9585, "captured_at": "2026-09-15T21:00:06.045245+09:00"}
{"date": "2026-09-16", "market": "KOSDAQ", "market_label": "코스닥", "rise": 739, "upper": 7, "fall": 893, "lower": 0, "steady": 99, "adr": 83.54, "personal": 62, "foreign": -272, "institutional": 222, "captured_at": "2026-09-16T21:00:05.119909+09:00"}
{"date": "2026-09-16", "market": "KOSPI", "market_label": "코스피", "rise": 304, "upper": 0, "fall": 578, "lower": 1, "steady": 35, "adr": 52.5, "personal": -12061, "foreign": -16726, "institutional": 12251, "captured_at": "2026-09-16T21:00:05.119909+09:00"}
{"date": "2026-09-17", "market": "KOSDAQ", "market_label": "코스닥", "rise": 945, "upper": 6, "fall": 662, "lower": 0, "steady": 123, "adr": 143.66, "personal": -250, "foreign": -329, "institutional": 455, "captured_at": "2026-09-17T21:00:06.412999+09:00"}
{"date": "2026-09-17", "market": "KOSPI", "market_label": "코스피", "rise": 453, "upper": 1, "fall": 398, "lower": 0, "steady": 62, "adr": 114.07, "personal": 3020, "foreign": -22546, "institutional": 2480, "captured_at": "2026-09-17T21:00:06.412999+09:00"}
{"date": "2026-09-18", "market": "KOSDAQ", "market_label": "코스닥", "rise": 919, "upper": 3, "fall": 698, "lower": 2, "steady": 116, "adr": 131.71, "personal": 162, "foreign": -511, "institutional": 250, "captured_at": "2026-09-18T21:00:02.289980+09:00"}
{"date": "2026-09-18", "market": "KOSPI", "market_label": "코스피", "rise": 407, "upper": 5, "fall": 465, "lower": 1, "steady": 41, "adr": 88.41, "personal": -36359, "foreign": 4486, "institutional": 15322, "captured_at": "2026-09-18T21:00:02.289980+09:00"}
{"date": "2026-09-21", "market": "KOSDAQ", "market_label": "코스닥", "rise": 628, "upper": 5, "fall": 1023, "lower": 0, "steady": 82, "adr": 61.88, "personal": 1416, "foreign": -995, "institutional": -285, "captured_at": "2026-09-21T21:00:05.395370+09:00"}
{"date": "2026-09-21", "market": "KOSPI", "market_label": "코스피", "rise": 310, "upper": 0, "fall": 552, "lower": 0, "steady": 52, "adr": 56.16, "personal": -31850, "foreign": 117, "institutional": 15162, "captured_at": "2026-09-21T21:00:05.395370+09:00"}
{"date": "2026-09-22", "market": "KOSDAQ", "market_label": "코스닥", "rise": 625, "upper": 6, "fall": 1002, "lower": 2, "steady": 107, "adr": 62.85, "personal": 1548, "foreign": -545, "institutional": -953, "captured_at": "2026-09-22T21:00:06.024212+09:00"}
{"date": "2026-09-22", "market": "KOSPI", "market_label": "코스피", "rise": 383, "upper": 0, "fall": 463, "lower": 0, "steady": 68, "adr": 82.72, "personal": -15643, "foreign": 494, "institutional": -1295, "captured_at": "2026-09-22T21:00:06.024212+09:00"}
{"date": "2026-09-23", "market": "KOSDAQ", "market_label": "코스닥", "rise": 856, "upper": 4, "fall": 786, "lower": 0, "steady": 94, "adr": 109.41, "personal": 275, "foreign": -462, "institutional": 76, "captured_at": "2026-09-23T21:00:06.222401+09:00"}
{"date": "2026-09-23", "market": "KOSPI", "market_label": "코스피", "rise": 311, "upper": 0, "fall": 548, "lower": 0, "steady": 55, "adr": 56.75, "personal": -14649, "foreign": -4942, "institutional": 3189, "captured_at": "2026-09-23T21:00:06.222401+09:00"}
{"date": "2026-09-28", "market": "KOSDAQ", "market_label": "코스닥", "rise": 967, "upper": 11, "fall": 681, "lower": 3, "steady": 88, "adr": 142.98, "personal": 223, "foreign": 469, "institutional": -747, "captured_at": "2026-09-28T21:00:06.010555+09:00"}
{"date": "2026-09-28", "market": "KOSPI", "market_label": "코스피", "rise": 611, "upper": 1, "fall": 249, "lower": 0, "steady": 54, "adr": 245.78, "personal": 26467, "foreign": -32682, "institutional": -10169, "captured_at": "2026-09-28T21:00:06.010555+09:00"}
{"date": "2026-09-29", "market": "KOSDAQ", "market_label": "코스닥", "rise": 571, "upper": 4, "fall": 1084, "lower": 5, "steady": 87, "adr": 52.8, "personal": 1469, "foreign": -1305, "institutional": 66, "captured_at": "2026-09-29T21:00:05.779795+09:00"}
{"date": "2026-09-29", "market": "KOSPI", "market_label": "코스피", "rise": 232, "upper": 0, "fall": 624, "lower": 0, "steady": 58, "adr": 37.18, "personal": 12447, "foreign": -29922, "institutional": 1041, "captured_at": "2026-09-29T21:00:05.779795+09:00"}
{"date": "2026-09-30", "market": "KOSDAQ", "market_label": "코스닥", "rise": 804, "upper": 3, "fall": 845, "lower": 2, "steady": 94, "adr": 95.28, "personal": 188, "foreign": -713, "institutional": 482, "captured_at": "2026-09-30T21:00:02.664980+09:00"}
{"date": "2026-09-30", "market": "KOSPI", "market_label": "코스피", "rise": 352, "upper": 1, "fall": 513, "lower": 0, "steady": 49, "adr": 68.81, "personal": 12109, "foreign": -20646, "institutional": -7993, "captured_at": "2026-09-30T21:00:02.664980+09:00"}
{"date": "2026-10-01", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1230, "upper": 2, "fall": 423, "lower": 0, "steady": 89, "adr": 291.25, "personal": -7770, "foreign": 2280, "institutional": 5684, "captured_at": "2026-10-01T21:00:02.087388+09:00"}
{"date": "2026-10-01", "market": "KOSPI", "market_label": "코스피", "rise": 542, "upper": 0, "fall": 332, "lower": 0, "steady": 42, "adr": 163.25, "personal": -14102, "foreign": -5558, "institutional": 3239, "captured_at": "2026-10-01T21:00:02.087388+09:00"}
{"date": "2026-10-02", "market": "KOSDAQ", "market_label": "코스닥", "rise": 958, "upper": 4, "fall": 681, "lower": 0, "steady": 102, "adr": 141.26, "personal": 841, "foreign": -1767, "institutional": 936, "captured_at": "2026-10-02T21:00:06.325959+09:00"}
{"date": "2026-10-02", "market": "KOSPI", "market_label": "코스피", "rise": 500, "upper": 1, "fall": 362, "lower": 0, "steady": 56, "adr": 138.4, "personal": -17897, "foreign": -1080, "institutional": 4155, "captured_at": "2026-10-02T21:00:06.325959+09:00"}
{"date": "2026-10-06", "market": "KOSDAQ", "market_label": "코스닥", "rise": 1012, "upper": 6, "fall": 645, "lower": 0, "steady": 81, "adr": 157.83, "personal": -2111, "foreign": 1073, "institutional": 1047, "captured_at": "2026-10-06T21:00:05.141855+09:00"}
{"date": "2026-10-06", "market": "KOSPI", "market_label": "코스피", "rise": 425, "upper": 0, "fall": 461, "lower": 0, "steady": 32, "adr": 92.19, "personal": 7441, "foreign": -17575, "institutional": -39, "captured_at": "2026-10-06T21:00:05.141855+09:00"}
{"date": "2026-10-07", "market": "KOSDAQ", "market_label": "코스닥", "rise": 596, "upper": 3, "fall": 1060, "lower": 0, "steady": 85, "adr": 56.51, "personal": 4871, "foreign": -2495, "institutional": -2102, "captured_at": "2026-10-07T21:00:04.754118+09:00"}
{"date": "2026-10-07", "market": "KOSPI", "market_label": "코스피", "rise": 333, "upper": 3, "fall": 527, "lower": 0, "steady": 57, "adr": 63.76, "personal": 26183, "foreign": -26188, "institutional": -7101, "captured_at": "2026-10-07T21:00:04.754118+09:00"}
{"date": "2026-10-08", "market": "KOSDAQ", "market_label": "코스닥", "rise": 618, "upper": 13, "fall": 1031, "lower": 1, "steady": 88, "adr": 61.14, "personal": 483, "foreign": 625, "institutional": -1075, "captured_at": "2026-10-08T21:00:05.577237+09:00"}
{"date": "2026-10-08", "market": "KOSPI", "market_label": "코스피", "rise": 252, "upper": 3, "fall": 625, "lower": 0, "steady": 40, "adr": 40.8, "personal": 29972, "foreign": -20704, "institutional": -16789, "captured_at": "2026-10-08T21:00:05.577237+09:00"}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"👀": "v1"}
{"👀": "v1", "⭐": "v4", "🔥": "v300", "💎": "v40", "🚀": "v319", "🎯": "v321"}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,204 @@
{
"cash": 100000000,
"cash": 11759121.887999972,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943184+09:00",
"updated_at": "2026-06-15T15:25:02.113024+09:00"
"positions": {
"010950": {
"code": "010950",
"name": "S-Oil",
"sources": [
"auto"
],
"qty": 86,
"entry_price": 128395.3488372093,
"entry_at": "2026-07-08T09:16:01.874405+09:00",
"stop": 112596,
"target": 175793,
"peak": 173500,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 169000,
"tranches": 2,
"tranche_value": 5572914.42378125,
"last_add_price": 130800
},
"194370": {
"code": "194370",
"name": "제이에스코퍼레이션",
"sources": [
"auto"
],
"qty": 728,
"entry_price": 13160.096153846154,
"entry_at": "2026-08-03T12:00:10.581409+09:00",
"stop": 11445,
"target": 18305,
"peak": 14150,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 12200,
"tranches": 2,
"tranche_value": 4803063.4110624995,
"last_add_price": 13420
},
"041520": {
"code": "041520",
"name": "이엘씨",
"sources": [
"auto"
],
"qty": 479,
"entry_price": 11636.137787056368,
"entry_at": "2026-09-03T09:02:19.208652+09:00",
"stop": 10093,
"target": 16265,
"peak": 13200,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 11,601 도달",
"cur_price": 12790,
"tranches": 2,
"tranche_value": 4801549.203874999,
"last_add_price": 11930
},
"252990": {
"code": "252990",
"name": "샘씨엔에스",
"sources": [
"auto"
],
"qty": 550,
"entry_price": 13428.945454545454,
"entry_at": "2026-09-04T10:32:21.056829+09:00",
"stop": 10340,
"target": 22696,
"peak": 20700,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 13,168 도달",
"cur_price": 18340,
"tranches": 2,
"tranche_value": 3703169.8275951464,
"last_add_price": 13820
},
"088350": {
"code": "088350",
"name": "한화생명",
"sources": [
"auto"
],
"qty": 817,
"entry_price": 5740,
"entry_at": "2026-09-22T09:32:35.675924+09:00",
"stop": 5129,
"target": 7573,
"peak": 5900,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 5190,
"tranches": 1,
"tranche_value": 4695809.036093748,
"last_add_price": 5740
},
"161890": {
"code": "161890",
"name": "한국콜마",
"sources": [
"auto"
],
"qty": 34,
"entry_price": 136600,
"entry_at": "2026-10-02T14:48:37.320234+09:00",
"stop": 116526,
"target": 196823,
"peak": 148900,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 138400,
"tranches": 1,
"tranche_value": 4696049.632218748,
"last_add_price": 136600
},
"108490": {
"code": "108490",
"name": "로보티즈",
"sources": [
"interest"
],
"qty": 14,
"entry_price": 316500,
"entry_at": "2026-10-06T13:48:38.139356+09:00",
"stop": 268844,
"target": 459469,
"peak": 323500,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 299500,
"tranches": 1,
"tranche_value": 4645722.400968748,
"last_add_price": 316500
},
"295310": {
"code": "295310",
"name": "에이치브이엠",
"sources": [
"watch"
],
"qty": 127,
"entry_price": 72011.02362204724,
"entry_at": "2026-10-08T09:06:38.453273+09:00",
"stop": 62774,
"target": 99722,
"peak": 76700,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 74200,
"tranches": 2,
"tranche_value": 4640247.106124998,
"last_add_price": 73700
}
},
"realized_pnl": -31739844.228000037,
"closed_trades": 35,
"wins": 1,
"peak_equity": 100102462.48,
"max_drawdown": 0.2711544112406138,
"last_exit": {
"030000": "2026-08-06",
"086790": "2026-07-29",
"005930": "2026-06-23",
"298040": "2026-06-24",
"403870": "2026-07-07",
"240810": "2026-07-24",
"093370": "2026-07-20",
"214450": "2026-10-02",
"055550": "2026-07-20",
"024060": "2026-07-27",
"003680": "2026-08-24",
"252990": "2026-09-01",
"078930": "2026-09-22",
"090850": "2026-08-06",
"138040": "2026-08-14",
"257720": "2026-09-04",
"068270": "2026-09-04",
"327260": "2026-09-07",
"003550": "2026-09-15",
"000720": "2026-10-06",
"316140": "2026-10-08"
},
"created_at": "2026-06-11T09:00:01.943184+09:00",
"updated_at": "2026-10-08T15:28:36.335052+09:00"
}
@@ -0,0 +1,108 @@
{"ts": "2026-06-17T11:15:02.163140+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 130900, "qty": 47, "cost": 6153223, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 118039, "target": 169483, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "129,300 vs 118,950"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,400 / 118,950"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,400 / 118,307"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-13.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +371 · 기 +158 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+22%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.12% (환산) vs 평균 0.38% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 126,500"}, {"label": "거래량 급증", "ok": true, "detail": "3,094,534 (환산) vs 평균 1,031,725"}, {"label": "회전율 급증", "ok": true, "detail": "1.12% (환산) vs 평균 0.38%"}, {"label": "RSI", "ok": true, "detail": "60"}]}
{"ts": "2026-06-19T09:02:05.756730+09:00", "side": "BUY", "code": "005930", "name": "삼성전자", "price": 373500, "qty": 16, "cost": 5976896, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 331259, "target": 500222, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "373,000 vs 330,350"}, {"label": "정배열(5>10)", "ok": true, "detail": "352,400 / 330,350"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "352,400 / 258,005"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,427 · 기 +4,448 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+19%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.04% (환산) vs 평균 0.53% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 370,000"}, {"label": "거래량 급증", "ok": true, "detail": "60,950,213 (환산) vs 평균 31,088,390"}, {"label": "회전율 급증", "ok": true, "detail": "1.04% (환산) vs 평균 0.53%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-06-19T09:02:06.178128+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19160, "qty": 325, "cost": 6227934, "path": "pullback", "reason": "눌림목 지정가 19,690 도달", "stop": 17760, "target": 23360, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,150 vs 18,963"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,616 / 18,963"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,616 / 19,361"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-31.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,162 · 기 +2,080 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "37,673 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,690 (현재 19,150)"}]}
{"ts": "2026-06-23T09:00:07.147411+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18820, "qty": 325, "proceeds": 6104573, "entry_price": 19160, "pnl": -123361, "pnl_pct": -1.77, "hold_from": "2026-06-19T09:02:06.178112+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,710)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 23,360"}, {"label": "추세(10일선)", "ok": false, "detail": "18,919"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -318 · 기 -227"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,528"}]}
{"ts": "2026-06-23T09:06:02.261681+09:00", "side": "BUY", "code": "298040", "name": "효성중공업", "price": 4130000, "qty": 1, "cost": 4130620, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 3559830, "target": 5840510, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "4,125,000 vs 3,535,900"}, {"label": "정배열(5>10)", "ok": true, "detail": "3,678,200 / 3,535,900"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "3,678,200 / 3,411,683"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-6.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +16 · 기 -7 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.82% (환산) vs 평균 0.63% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 4,085,000"}, {"label": "거래량 급증", "ok": true, "detail": "170,653 (환산) vs 평균 58,834"}, {"label": "회전율 급증", "ok": true, "detail": "1.82% (환산) vs 평균 0.63%"}, {"label": "RSI", "ok": true, "detail": "62"}]}
{"ts": "2026-06-23T12:04:03.036279+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 117700, "qty": 47, "proceeds": 5521113, "entry_price": 130900, "pnl": -632110, "pnl_pct": -10.08, "hold_from": "2026-06-17T11:15:02.163131+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "118,039 (현재 117,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 169,483"}, {"label": "추세(10일선)", "ok": false, "detail": "119,330"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +332 · 기 -364"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 134,300"}]}
{"ts": "2026-06-23T12:32:03.300082+09:00", "side": "SELL", "code": "005930", "name": "삼성전자", "price": 330000, "qty": 16, "proceeds": 5269704, "entry_price": 373500, "pnl": -707192, "pnl_pct": -11.65, "hold_from": "2026-06-19T09:02:05.756719+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "331,259 (현재 330,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 500,222"}, {"label": "추세(10일선)", "ok": false, "detail": "335,050"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -3,792 · 기 +1,417"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 384,779"}]}
{"ts": "2026-06-24T09:02:01.876083+09:00", "side": "SELL", "code": "298040", "name": "효성중공업", "price": 3499000, "qty": 1, "proceeds": 3492177, "entry_price": 4130000, "pnl": -638443, "pnl_pct": -15.28, "hold_from": "2026-06-23T09:06:02.261677+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "3,559,830 (현재 3,489,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 5,840,510"}, {"label": "추세(10일선)", "ok": true, "detail": "3,472,300"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +19 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 4,274,721"}]}
{"ts": "2026-06-24T09:02:02.294346+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 53200, "qty": 97, "cost": 5161174, "path": "pullback", "reason": "눌림목 지정가 60,114 도달", "stop": 43200, "target": 83200, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,200 vs 52,550"}, {"label": "정배열(5>10)", "ok": true, "detail": "57,960 / 52,550"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "57,960 / 49,671"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +711 · 기 -2,060 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+29%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.34% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "1,102,733 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "1.34% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 60,114 (현재 53,200)"}]}
{"ts": "2026-06-24T09:08:00.922591+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 155400, "qty": 26, "cost": 4041006, "path": "pullback", "reason": "눌림목 지정가 155,963 도달", "stop": 118951, "target": 264748, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "155,400 vs 128,090"}, {"label": "정배열(5>10)", "ok": true, "detail": "143,100 / 128,090"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "143,100 / 121,690"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+45.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +143 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "6.50% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,192,593 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "6.50% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 155,963 (현재 155,400)"}]}
{"ts": "2026-06-26T09:00:03.477186+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 50000, "qty": 97, "proceeds": 4840542, "entry_price": 53200, "pnl": -320632, "pnl_pct": -6.02, "hold_from": "2026-06-24T09:02:02.294329+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "43,200 (현재 51,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 83,200"}, {"label": "추세(10일선)", "ok": false, "detail": "52,330"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -311 · 기 -267"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 57,056"}]}
{"ts": "2026-06-26T09:04:03.833814+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 164900, "qty": 25, "cost": 4123118, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 118951, "target": 264748, "signals": [{"label": "손절선", "ok": true, "detail": "118,951 (현재 164,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 264,748"}, {"label": "추세(10일선)", "ok": true, "detail": "129,020"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -140 · 기 +323"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 164,359"}]}
{"ts": "2026-06-29T09:00:12.226221+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 339500, "qty": 18, "cost": 6111917, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 300291, "target": 457126, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "337,000 vs 305,050"}, {"label": "정배열(5>10)", "ok": true, "detail": "313,600 / 305,050"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "313,600 / 305,367"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+37.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -22 · 기 +97 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+43%"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.50% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "155,347 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.50% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-06-29T09:40:01.364512+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 351000, "qty": 17, "cost": 5967895, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 300291, "target": 457126, "signals": [{"label": "손절선", "ok": true, "detail": "300,291 (현재 351,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 457,126"}, {"label": "추세(10일선)", "ok": true, "detail": "306,450"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -22 · 기 +97"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 349,802"}]}
{"ts": "2026-06-30T09:00:03.224282+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 55300, "qty": 80, "cost": 4424664, "path": "pullback", "reason": "눌림목 지정가 60,222 도달", "stop": 43200, "target": 91600, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,700 vs 52,600"}, {"label": "정배열(5>10)", "ok": true, "detail": "58,060 / 52,600"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "58,060 / 49,679"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +222 · 기 -821 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+28%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "827 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 60,222 (현재 53,700)"}]}
{"ts": "2026-07-02T10:18:02.949133+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 15980, "qty": 274, "cost": 4379177, "path": "pullback", "reason": "눌림목 지정가 16,358 도달", "stop": 12444, "target": 26587, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "15,980 vs 13,620"}, {"label": "정배열(5>10)", "ok": true, "detail": "16,104 / 13,620"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,104 / 11,478"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+25.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,833 · 기 +437 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+25%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "9.98% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "10,701,165 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "9.98% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,358 (현재 15,980)"}]}
{"ts": "2026-07-02T10:40:02.874484+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18960, "qty": 319, "cost": 6049147, "path": "pullback", "reason": "눌림목 지정가 19,580 도달", "stop": 17760, "target": 22560, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,960 vs 18,944"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,578 / 18,944"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,578 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -339 · 기 +110 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.86% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": true, "detail": "986,112 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.86% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,580 (현재 18,960)"}]}
{"ts": "2026-07-02T11:20:01.970932+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 22550, "qty": 268, "cost": 6044307, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 22560, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 19,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,560"}, {"label": "추세(10일선)", "ok": true, "detail": "18,983"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,350"}]}
{"ts": "2026-07-02T11:24:06.075978+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18860, "qty": 587, "proceeds": 11049232, "entry_price": 20599, "pnl": -1044222, "pnl_pct": -8.44, "hold_from": "2026-07-02T10:40:02.874471+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,039 (현재 18,890)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 25,279"}, {"label": "추세(10일선)", "ok": false, "detail": "18,937"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T09:44:02.531757+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 123600, "qty": 51, "proceeds": 6291308, "entry_price": 160057, "pnl": -1872816, "pnl_pct": -22.78, "hold_from": "2026-06-24T09:08:00.922587+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "124,222 (현재 123,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 267,562"}, {"label": "추세(10일선)", "ok": false, "detail": "124,920"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +674 · 기 -2,170"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T10:24:02.696490+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19040, "qty": 306, "cost": 5827114, "path": "pullback", "reason": "눌림목 지정가 19,631 도달", "stop": 17760, "target": 22880, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,030 vs 18,951"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,592 / 18,951"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,592 / 19,359"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -35 · 기 +168 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.54% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "619,193 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.54% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,631 (현재 19,030)"}]}
{"ts": "2026-07-06T09:00:05.367354+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 130600, "qty": 27, "cost": 3526729, "path": "pullback", "reason": "눌림목 지정가 154,452 도달", "stop": 96000, "target": 234400, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "133,100 vs 125,860"}, {"label": "정배열(5>10)", "ok": true, "detail": "138,640 / 125,860"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "138,640 / 121,318"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +782 · 기 -1,979 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.01% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": false, "detail": "4,080 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": false, "detail": "0.01% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 154,452 (현재 133,100)"}]}
{"ts": "2026-07-06T09:34:01.582045+09:00", "side": "BUY", "code": "055550", "name": "신한지주", "price": 110000, "qty": 52, "cost": 5720858, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 99202, "target": 142394, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "110,300 vs 99,990"}, {"label": "정배열(5>10)", "ok": true, "detail": "101,240 / 99,990"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "101,240 / 96,630"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -717 · 기 +1,343 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.55% (환산) vs 평균 0.35% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 109,800"}, {"label": "거래량 급증", "ok": true, "detail": "2,617,853 (환산) vs 평균 1,653,581"}, {"label": "회전율 급증", "ok": true, "detail": "0.55% (환산) vs 평균 0.35%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-07-07T09:48:07.302373+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 43000, "qty": 80, "proceeds": 3433292, "entry_price": 55300, "pnl": -991372, "pnl_pct": -22.24, "hold_from": "2026-06-30T09:00:03.224267+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "43,200 (현재 43,050)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 91,600"}, {"label": "추세(10일선)", "ok": false, "detail": "51,535"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +1,424 · 기 -5,706"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 59,449"}]}
{"ts": "2026-07-07T10:28:06.382153+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 12410, "qty": 274, "proceeds": 3393709, "entry_price": 15980, "pnl": -985467, "pnl_pct": -22.34, "hold_from": "2026-07-02T10:18:02.949119+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "12,444 (현재 12,430)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 26,587"}, {"label": "추세(10일선)", "ok": false, "detail": "13,265"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,543 · 기 +15"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 16,975"}]}
{"ts": "2026-07-07T14:06:20.089623+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 183300, "qty": 19, "cost": 3483222, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 96000, "target": 234400, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 183,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 234,400"}, {"label": "추세(10일선)", "ok": true, "detail": "123,150"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +263 · 기 -1,543"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 139,025"}]}
{"ts": "2026-07-07T14:06:22.687123+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 20750, "qty": 281, "cost": 5831625, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 22880, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,880"}, {"label": "추세(10일선)", "ok": true, "detail": "18,901"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 +113"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,358"}]}
{"ts": "2026-07-08T09:00:10.790205+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 108300, "qty": 46, "proceeds": 4972085, "entry_price": 152367, "pnl": -2037866, "pnl_pct": -28.92, "hold_from": "2026-07-06T09:00:05.367339+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "118,668 (현재 112,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 253,466"}, {"label": "추세(10일선)", "ok": false, "detail": "123,820"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +243 · 기 -857"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:16:01.874414+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 126100, "qty": 44, "cost": 5549232, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 110301, "target": 173498, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "126,100 vs 108,080"}, {"label": "정배열(5>10)", "ok": true, "detail": "114,400 / 108,080"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "114,400 / 113,132"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+16.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -164 · 기 +736 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+18%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.48% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "544,247 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.48% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-08T09:34:04.862709+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 130800, "qty": 42, "cost": 5494424, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 110301, "target": 173498, "signals": [{"label": "손절선", "ok": true, "detail": "110,301 (현재 130,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 173,498"}, {"label": "추세(10일선)", "ok": true, "detail": "108,520"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -164 · 기 +736"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 130,050"}]}
{"ts": "2026-07-10T09:26:06.360793+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 130900, "qty": 42, "cost": 5498625, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 117686, "target": 170541, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "130,800 vs 120,640"}, {"label": "정배열(5>10)", "ok": true, "detail": "122,760 / 120,640"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "122,760 / 118,578"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+18.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -427 · 기 +555 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+23%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.60% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": true, "detail": "1,646,513 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": true, "detail": "0.60% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "61"}]}
{"ts": "2026-07-10T11:06:06.387602+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 13660, "qty": 271, "cost": 3702415, "path": "pullback", "reason": "눌림목 지정가 15,971 도달", "stop": 10360, "target": 23560, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,630 vs 13,385"}, {"label": "정배열(5>10)", "ok": true, "detail": "15,634 / 13,385"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,634 / 11,439"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +239 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+47%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "14.23% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "15,267,605 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "14.23% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 15,971 (현재 13,630)"}]}
{"ts": "2026-07-10T12:14:02.591560+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 126200, "qty": 29, "cost": 3660349, "path": "pullback", "reason": "눌림목 지정가 151,752 도달", "stop": 96000, "target": 216800, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "126,200 vs 125,170"}, {"label": "정배열(5>10)", "ok": true, "detail": "137,260 / 125,170"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "137,260 / 121,203"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +483 · 기 -241 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "7.53% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,695,762 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "7.53% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 151,752 (현재 126,200)"}]}
{"ts": "2026-07-13T09:06:06.491483+09:00", "side": "BUY", "code": "055550", "name": "신한지주", "price": 112800, "qty": 50, "cost": 5640846, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 99202, "target": 142394, "signals": [{"label": "손절선", "ok": true, "detail": "99,202 (현재 112,800)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 142,394"}, {"label": "추세(10일선)", "ok": true, "detail": "100,240"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -358 · 기 +991"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 112,792"}]}
{"ts": "2026-07-13T09:08:05.274334+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 134300, "qty": 41, "cost": 5507126, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 117686, "target": 170541, "signals": [{"label": "손절선", "ok": true, "detail": "117,686 (현재 134,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 170,541"}, {"label": "추세(10일선)", "ok": true, "detail": "120,980"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -277 · 기 +460"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 134,118"}]}
{"ts": "2026-07-13T09:26:03.332430+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 139100, "qty": 27, "cost": 3756263, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 96000, "target": 216800, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 139,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 216,800"}, {"label": "추세(10일선)", "ok": true, "detail": "126,460"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -213 · 기 +772"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 136,162"}]}
{"ts": "2026-07-14T09:40:04.963062+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 5910, "qty": 931, "cost": 5503035, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5447, "target": 7298, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "5,900 vs 5,543"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,646 / 5,543"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,646 / 5,614"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+33.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +107 · 기 +5 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.34% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "74,367 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.34% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "65"}]}
{"ts": "2026-07-15T10:30:01.562473+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 303500, "qty": 35, "proceeds": 10601786, "entry_price": 345086, "pnl": -1478026, "pnl_pct": -12.05, "hold_from": "2026-06-29T09:00:12.226210+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "303,877 (현재 303,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 468,711"}, {"label": "추세(10일선)", "ok": true, "detail": "301,700"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -25 · 기 +35"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-15T13:56:01.759138+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6030, "qty": 912, "cost": 5500185, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5447, "target": 7298, "signals": [{"label": "손절선", "ok": true, "detail": "5,447 (현재 6,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,298"}, {"label": "추세(10일선)", "ok": true, "detail": "5,556"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +147 · 기 -11"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,028"}]}
{"ts": "2026-07-15T14:32:01.808579+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18570, "qty": 587, "proceeds": 10879334, "entry_price": 19859, "pnl": -779405, "pnl_pct": -6.49, "hold_from": "2026-07-03T10:24:02.696479+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "18,586 (현재 18,570)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 23,675"}, {"label": "추세(10일선)", "ok": false, "detail": "18,905"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -214 · 기 +240"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-16T09:04:02.184872+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 341500, "qty": 16, "cost": 5464820, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 301363, "target": 461911, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "340,500 vs 305,400"}, {"label": "정배열(5>10)", "ok": true, "detail": "314,300 / 305,400"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "314,300 / 305,425"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+34.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -33 · 기 +28 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.18% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "226,533 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "2.18% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-07-16T09:28:03.406225+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 353000, "qty": 15, "cost": 5295794, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 301363, "target": 461911, "signals": [{"label": "손절선", "ok": true, "detail": "301,363 (현재 352,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 461,911"}, {"label": "추세(10일선)", "ok": true, "detail": "306,600"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -33 · 기 +28"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 351,963"}]}
{"ts": "2026-07-20T12:28:02.248848+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 10310, "qty": 271, "proceeds": 2788562, "entry_price": 13660, "pnl": -913854, "pnl_pct": -24.52, "hold_from": "2026-07-10T11:06:06.387588+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,360 (현재 10,310)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 23,560"}, {"label": "추세(10일선)", "ok": false, "detail": "12,013"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -887 · 기 +515"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,626"}]}
{"ts": "2026-07-20T14:12:02.314100+09:00", "side": "SELL", "code": "055550", "name": "신한지주", "price": 100100, "qty": 102, "proceeds": 10190290, "entry_price": 111373, "pnl": -1171414, "pnl_pct": -10.12, "hold_from": "2026-07-06T09:34:01.582032+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "100,203 (현재 100,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 144,881"}, {"label": "추세(10일선)", "ok": true, "detail": "98,970"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -168 · 기 +257"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-23T09:46:06.299582+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 88400, "qty": 61, "cost": 5393209, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 77778, "target": 120266, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "88,300 vs 80,910"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,020 / 80,910"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,020 / 75,060"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+47.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -150 · 기 +299 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.80% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "743,133 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.80% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-23T10:30:02.401768+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 14140, "qty": 356, "cost": 5034595, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11706, "target": 21442, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,180 vs 12,429"}, {"label": "정배열(5>10)", "ok": true, "detail": "13,154 / 12,429"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "13,154 / 12,687"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+55.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +109 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "84.26% (환산) vs 평균 31.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 13,990"}, {"label": "거래량 급증", "ok": true, "detail": "12,639,979 (환산) vs 평균 4,720,532"}, {"label": "회전율 급증", "ok": true, "detail": "84.26% (환산) vs 평균 31.47%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-23T13:06:04.378524+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 91400, "qty": 59, "cost": 5393409, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 77778, "target": 120266, "signals": [{"label": "손절선", "ok": true, "detail": "77,778 (현재 91,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 120,266"}, {"label": "추세(10일선)", "ok": true, "detail": "81,220"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -169 · 기 +311"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 91,159"}]}
{"ts": "2026-07-24T09:00:02.274051+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 111800, "qty": 56, "proceeds": 6248591, "entry_price": 132420, "pnl": -1168021, "pnl_pct": -15.57, "hold_from": "2026-07-10T12:14:02.591547+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 113,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 241,679"}, {"label": "추세(10일선)", "ok": false, "detail": "123,880"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -480 · 기 -206"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-24T09:02:03.981423+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 15060, "qty": 334, "cost": 5030795, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11706, "target": 21442, "signals": [{"label": "손절선", "ok": true, "detail": "11,706 (현재 15,020)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,442"}, {"label": "추세(10일선)", "ok": true, "detail": "12,513"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +208 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,766"}]}
{"ts": "2026-07-24T10:02:04.270583+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 292, "cost": 5534230, "path": "pullback", "reason": "눌림목 지정가 19,643 도달", "stop": 17760, "target": 22520, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,950 vs 18,943"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,576 / 18,943"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,576 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+19.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +2 · 기 -5 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.39% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "454,069 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.39% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,643 (현재 18,950)"}]}
{"ts": "2026-07-27T09:02:05.306008+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7470, "qty": 731, "cost": 5461389, "path": "pullback", "reason": "눌림목 지정가 7,635 도달", "stop": 6370, "target": 10769, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "7,470 vs 5,392"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,452 / 5,392"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,452 / 4,889"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+82.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +49 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.79% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "49,327 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "0.79% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "74"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,635 (현재 7,470)"}]}
{"ts": "2026-07-27T09:08:02.026931+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7910, "qty": 690, "cost": 5458719, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 6370, "target": 10769, "signals": [{"label": "손절선", "ok": true, "detail": "6,370 (현재 7,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 10,769"}, {"label": "추세(10일선)", "ok": true, "detail": "5,434"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +49 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,747"}]}
{"ts": "2026-07-27T09:10:00.979478+09:00", "side": "SELL", "code": "024060", "name": "흥구석유", "price": 12030, "qty": 690, "proceeds": 8284514, "entry_price": 14585, "pnl": -1780876, "pnl_pct": -17.52, "hold_from": "2026-07-23T10:30:02.401758+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "12,081 (현재 12,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,097"}, {"label": "추세(10일선)", "ok": false, "detail": "12,214"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -85 · 기 +2"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-27T09:12:01.035449+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13150, "qty": 306, "cost": 4024504, "path": "pullback", "reason": "눌림목 지정가 13,180 도달", "stop": 10340, "target": 21580, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,150 vs 12,888"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,378 / 12,888"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,378 / 13,268"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+1.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +27 · 기 +15 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.46% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "274,967 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.46% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,180 (현재 13,150)"}]}
{"ts": "2026-07-28T11:22:00.688753+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6630, "qty": 1421, "proceeds": 9402859, "entry_price": 7684, "pnl": -1517249, "pnl_pct": -13.71, "hold_from": "2026-07-27T09:02:05.306004+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "6,577 (현재 6,380)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 11,004"}, {"label": "추세(10일선)", "ok": true, "detail": "5,282"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +96 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:02:01.303424+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6540, "qty": 654, "cost": 4277802, "path": "pullback", "reason": "눌림목 지정가 7,526 도달", "stop": 5295, "target": 10276, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,540 vs 5,298"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,266 / 5,298"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,266 / 4,873"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+70.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +188 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.87% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "116,047 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "1.87% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "63"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,526 (현재 6,540)"}]}
{"ts": "2026-07-29T09:24:03.681936+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 292, "cost": 5534230, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 22520, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,520"}, {"label": "추세(10일선)", "ok": true, "detail": "18,901"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +99 · 기 +138"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,268"}]}
{"ts": "2026-07-29T09:24:04.035867+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6710, "qty": 637, "cost": 4274911, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5295, "target": 10276, "signals": [{"label": "손절선", "ok": true, "detail": "5,295 (현재 8,460)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 10,276"}, {"label": "추세(10일선)", "ok": true, "detail": "5,064"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +188 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,796"}]}
{"ts": "2026-07-29T09:24:04.447212+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 11310, "qty": 317, "cost": 3585808, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 21580, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 15,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,580"}, {"label": "추세(10일선)", "ok": true, "detail": "12,768"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +242 · 기 -182"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,887"}]}
{"ts": "2026-07-29T10:52:02.292514+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 10340, "qty": 623, "proceeds": 6429258, "entry_price": 12214, "pnl": -1181053, "pnl_pct": -15.34, "hold_from": "2026-07-27T09:12:01.035446+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,340 (현재 10,340)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,835"}, {"label": "추세(10일선)", "ok": false, "detail": "12,607"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +239 · 기 -181"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T12:04:01.628867+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 119200, "qty": 83, "proceeds": 9874307, "entry_price": 132580, "pnl": -1131443, "pnl_pct": -10.09, "hold_from": "2026-07-10T09:26:06.360780+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "119,709 (현재 119,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 171,192"}, {"label": "추세(10일선)", "ok": false, "detail": "119,490"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -11 · 기 -51"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T13:14:05.383429+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5580, "qty": 1291, "proceeds": 7189733, "entry_price": 6624, "pnl": -1362980, "pnl_pct": -15.76, "hold_from": "2026-07-29T09:02:01.303420+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,600 (현재 5,590)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,697"}, {"label": "추세(10일선)", "ok": true, "detail": "5,204"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +185 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-30T09:04:05.277102+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6130, "qty": 589, "cost": 3611112, "path": "pullback", "reason": "눌림목 지정가 7,457 도달", "stop": 4793, "target": 10140, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,120 vs 5,256"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,182 / 5,256"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,182 / 4,866"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+65.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +234 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.31% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "267,687 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "4.31% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,457 (현재 6,120)"}]}
{"ts": "2026-07-30T09:16:09.704313+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 124100, "qty": 40, "cost": 4964745, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 113673, "target": 155380, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "124,000 vs 117,010"}, {"label": "정배열(5>10)", "ok": true, "detail": "118,260 / 117,010"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "118,260 / 110,085"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+54.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +9 · 기 +389 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.31% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": true, "detail": "518,220 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": true, "detail": "0.31% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "68"}]}
{"ts": "2026-07-30T10:10:06.142998+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 127700, "qty": 39, "cost": 4981047, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 113673, "target": 155380, "signals": [{"label": "손절선", "ok": true, "detail": "113,673 (현재 127,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 155,380"}, {"label": "추세(10일선)", "ok": true, "detail": "117,370"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +35 · 기 +392"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 126,835"}]}
{"ts": "2026-07-31T09:06:05.825941+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6550, "qty": 551, "cost": 3609591, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 4793, "target": 10140, "signals": [{"label": "손절선", "ok": true, "detail": "4,793 (현재 6,640)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 10,140"}, {"label": "추세(10일선)", "ok": true, "detail": "5,308"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +355 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,453"}]}
{"ts": "2026-07-31T09:06:06.330737+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 77700, "qty": 120, "proceeds": 9305818, "entry_price": 89875, "pnl": -1480800, "pnl_pct": -13.55, "hold_from": "2026-07-23T09:46:06.299578+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "78,839 (현재 78,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 122,984"}, {"label": "추세(10일선)", "ok": false, "detail": "79,880"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -179 · 기 +325"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-03T12:00:10.581416+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 12910, "qty": 371, "cost": 4790328, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11314, "target": 17699, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "12,930 vs 12,003"}, {"label": "정배열(5>10)", "ok": true, "detail": "12,138 / 12,003"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "12,138 / 11,924"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +7 · 기 +10 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+74%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.72% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 12,870"}, {"label": "거래량 급증", "ok": true, "detail": "208,994 (환산) vs 평균 99,303"}, {"label": "회전율 급증", "ok": true, "detail": "0.72% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-03T12:02:08.464738+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 13420, "qty": 357, "cost": 4791659, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11314, "target": 17699, "signals": [{"label": "손절선", "ok": true, "detail": "11,314 (현재 13,450)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,699"}, {"label": "추세(10일선)", "ok": true, "detail": "12,055"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +7 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,339"}]}
{"ts": "2026-08-04T09:34:08.150391+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90000, "qty": 53, "cost": 4770716, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 79135, "target": 122595, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "89,800 vs 81,060"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,320 / 81,060"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,320 / 75,085"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+55.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +68 · 기 +117 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+15%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.64% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "595,733 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.64% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-08-04T10:32:11.011486+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 93500, "qty": 13, "cost": 1215682, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 79135, "target": 122595, "signals": [{"label": "손절선", "ok": true, "detail": "79,135 (현재 93,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 122,595"}, {"label": "추세(10일선)", "ok": true, "detail": "81,420"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +57 · 기 +117"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 92,841"}]}
{"ts": "2026-08-06T09:00:13.271931+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18730, "qty": 584, "proceeds": 10916990, "entry_price": 18950, "pnl": -151470, "pnl_pct": -1.16, "hold_from": "2026-07-24T10:02:04.270573+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,520"}, {"label": "추세(10일선)", "ok": false, "detail": "18,923"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -203 · 기 -147"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-06T09:06:08.044853+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5470, "qty": 1843, "proceeds": 10061552, "entry_price": 5969, "pnl": -941669, "pnl_pct": -8.37, "hold_from": "2026-07-14T09:40:04.963052+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,498 (현재 5,490)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,383"}, {"label": "추세(10일선)", "ok": false, "detail": "5,502"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -30 · 기 -23"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-06T09:12:11.568337+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13110, "qty": 277, "cost": 3632015, "path": "pullback", "reason": "눌림목 지정가 13,160 도달", "stop": 10340, "target": 21420, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,090 vs 12,882"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,366 / 12,882"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,366 / 13,268"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-12.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -352 · 기 +499 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.38% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "229,113 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.38% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,160 (현재 13,090)"}]}
{"ts": "2026-08-06T09:14:08.213833+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 193600, "qty": 24, "cost": 4647097, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 176944, "target": 243568, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "193,500 vs 180,840"}, {"label": "정배열(5>10)", "ok": true, "detail": "186,480 / 180,840"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "186,480 / 176,699"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +255 · 기 -319 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+42%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.49% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 191,700"}, {"label": "거래량 급증", "ok": true, "detail": "1,163,480 (환산) vs 평균 738,783"}, {"label": "회전율 급증", "ok": true, "detail": "0.49% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-08-07T09:02:10.128383+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14290, "qty": 254, "cost": 3630204, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 21420, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 14,240)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,420"}, {"label": "추세(10일선)", "ok": true, "detail": "12,997"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -216 · 기 +313"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,856"}]}
{"ts": "2026-08-07T09:34:06.949454+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 198600, "qty": 24, "cost": 4767115, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 176944, "target": 243568, "signals": [{"label": "손절선", "ok": true, "detail": "176,944 (현재 198,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 243,568"}, {"label": "추세(10일선)", "ok": true, "detail": "181,350"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +338 · 기 +73"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 197,939"}]}
{"ts": "2026-08-14T13:32:13.603290+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 114900, "qty": 79, "proceeds": 9059400, "entry_price": 125877, "pnl": -886392, "pnl_pct": -8.72, "hold_from": "2026-07-30T09:16:09.704305+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "114,936 (현재 114,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 158,700"}, {"label": "추세(10일선)", "ok": false, "detail": "116,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +29 · 기 +11"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-14T13:34:15.676314+09:00", "side": "BUY", "code": "003550", "name": "LG", "price": 118900, "qty": 42, "cost": 4994549, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 106512, "target": 156063, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "118,800 vs 106,650"}, {"label": "정배열(5>10)", "ok": true, "detail": "110,480 / 106,650"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "110,480 / 107,793"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+14.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +36 · 기 +116 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.43% (환산) vs 평균 0.19% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 111,900"}, {"label": "거래량 급증", "ok": true, "detail": "662,849 (환산) vs 평균 280,124"}, {"label": "회전율 급증", "ok": true, "detail": "0.43% (환산) vs 평균 0.19%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-08-24T13:12:17.476505+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5030, "qty": 1140, "proceeds": 5723018, "entry_price": 6333, "pnl": -1497685, "pnl_pct": -20.57, "hold_from": "2026-07-30T09:04:05.277095+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,039 (현재 5,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 10,215"}, {"label": "추세(10일선)", "ok": false, "detail": "5,148"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -31 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-24T13:14:17.289907+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 32950, "qty": 73, "cost": 2405711, "path": "pullback", "reason": "눌림목 지정가 33,047 도달", "stop": 22546, "target": 64161, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "33,000 vs 28,622"}, {"label": "정배열(5>10)", "ok": true, "detail": "35,100 / 28,622"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "35,100 / 31,286"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+24.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -62 · 기 +65 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+106%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.32% (환산) vs 평균 3.74% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 45,300"}, {"label": "거래량 급증", "ok": false, "detail": "298,285 (환산) vs 평균 409,191"}, {"label": "회전율 급증", "ok": false, "detail": "3.32% (환산) vs 평균 3.74%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 33,047 (현재 33,000)"}]}
{"ts": "2026-08-25T13:44:19.077912+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 35650, "qty": 68, "cost": 2424564, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 22546, "target": 64161, "signals": [{"label": "손절선", "ok": true, "detail": "22,546 (현재 35,650)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 64,161"}, {"label": "추세(10일선)", "ok": true, "detail": "28,887"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -118 · 기 +90"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 35,565"}]}
{"ts": "2026-09-01T09:02:21.139404+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 12660, "qty": 531, "proceeds": 6709351, "entry_price": 13674, "pnl": -552868, "pnl_pct": -7.42, "hold_from": "2026-08-06T09:12:11.568330+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "10,691 (현재 12,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,624"}, {"label": "추세(10일선)", "ok": false, "detail": "12,846"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -53 · 기 -347"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-01T09:08:18.803509+09:00", "side": "BUY", "code": "257720", "name": "실리콘투", "price": 55500, "qty": 90, "cost": 4995749, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 47628, "target": 79116, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "55,400 vs 41,585"}, {"label": "정배열(5>10)", "ok": true, "detail": "44,870 / 41,585"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "44,870 / 36,364"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+48.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +3,572 · 기 -3,450 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.74% (환산) vs 평균 1.56% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 48,250"}, {"label": "거래량 급증", "ok": true, "detail": "1,796,567 (환산) vs 평균 1,023,767"}, {"label": "회전율 급증", "ok": true, "detail": "2.74% (환산) vs 평균 1.56%"}, {"label": "RSI", "ok": true, "detail": "74"}]}
{"ts": "2026-09-03T09:00:26.653781+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 119500, "qty": 66, "proceeds": 7871620, "entry_price": 90689, "pnl": 1885223, "pnl_pct": 31.77, "hold_from": "2026-08-04T09:34:08.150378+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "119,430 (현재 119,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "83,990"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 -35"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-03T09:02:19.208669+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11590, "qty": 414, "cost": 4798980, "path": "pullback", "reason": "눌림목 지정가 11,601 도달", "stop": 10071, "target": 16146, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "11,590 vs 11,327"}, {"label": "정배열(5>10)", "ok": true, "detail": "11,748 / 11,327"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "11,748 / 9,426"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+28.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +17 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 0.30% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 13,450"}, {"label": "거래량 급증", "ok": false, "detail": "7 (환산) vs 평균 36,789"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 0.30%"}, {"label": "RSI", "ok": true, "detail": "62"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 11,601 (현재 11,590)"}]}
{"ts": "2026-09-03T09:56:23.117947+09:00", "side": "BUY", "code": "003550", "name": "LG", "price": 122000, "qty": 41, "cost": 5002750, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 106512, "target": 156063, "signals": [{"label": "손절선", "ok": true, "detail": "106,512 (현재 122,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 156,063"}, {"label": "추세(10일선)", "ok": true, "detail": "106,970"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -119 · 기 +22"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 121,979"}]}
{"ts": "2026-09-04T10:30:24.211910+09:00", "side": "SELL", "code": "257720", "name": "실리콘투", "price": 47550, "qty": 90, "proceeds": 4271155, "entry_price": 55500, "pnl": -724594, "pnl_pct": -14.32, "hold_from": "2026-09-01T09:08:18.803493+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "47,628 (현재 47,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 79,116"}, {"label": "추세(10일선)", "ok": true, "detail": "40,805"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +476 · 기 -207"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 57,250"}]}
{"ts": "2026-09-04T10:32:21.056845+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13060, "qty": 283, "cost": 3696534, "path": "pullback", "reason": "눌림목 지정가 13,168 도달", "stop": 10340, "target": 21220, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "12,980 vs 12,871"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,344 / 12,871"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,344 / 13,266"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-10.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -81 · 기 +73 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+73%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.03% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "620,178 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "1.03% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,168 (현재 12,980)"}]}
{"ts": "2026-09-04T12:34:21.536359+09:00", "side": "SELL", "code": "068270", "name": "셀트리온", "price": 187800, "qty": 48, "proceeds": 8996822, "entry_price": 196100, "pnl": -417390, "pnl_pct": -4.23, "hold_from": "2026-08-06T09:14:08.213823+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "178,744 (현재 187,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 248,168"}, {"label": "추세(10일선)", "ok": false, "detail": "189,720"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -128 · 기 -47"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-04T12:34:21.902524+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 124600, "qty": 38, "cost": 4735510, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 108235, "target": 173695, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "124,500 vs 84,530"}, {"label": "정배열(5>10)", "ok": true, "detail": "90,260 / 84,530"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "90,260 / 75,663"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+78.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +301 · 기 -97 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.78% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "724,799 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.78% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "84"}]}
{"ts": "2026-09-04T13:34:23.898414+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13820, "qty": 267, "cost": 3690493, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 21220, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 13,840)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,220"}, {"label": "추세(10일선)", "ok": true, "detail": "12,957"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -81 · 기 +73"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,837"}]}
{"ts": "2026-09-07T15:06:30.232287+09:00", "side": "SELL", "code": "327260", "name": "RF머트리얼즈", "price": 33650, "qty": 141, "proceeds": 4735398, "entry_price": 34252, "pnl": -94876, "pnl_pct": -1.76, "hold_from": "2026-08-24T13:14:17.289893+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "23,791 (현재 33,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 65,634"}, {"label": "추세(10일선)", "ok": false, "detail": "35,015"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -281 · 기 -15"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-07T15:06:30.638671+09:00", "side": "BUY", "code": "000720", "name": "현대건설", "price": 128400, "qty": 37, "cost": 4751513, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 110463, "target": 182212, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "127,900 vs 114,410"}, {"label": "정배열(5>10)", "ok": true, "detail": "114,840 / 114,410"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "114,840 / 113,722"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+24.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +8 · 기 -24 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+30%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.12% (환산) vs 평균 0.58% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 122,600"}, {"label": "거래량 급증", "ok": true, "detail": "1,249,244 (환산) vs 평균 648,608"}, {"label": "회전율 급증", "ok": true, "detail": "1.12% (환산) vs 평균 0.58%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-09-08T09:04:22.326266+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 129800, "qty": 37, "cost": 4803320, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 108235, "target": 173695, "signals": [{"label": "손절선", "ok": true, "detail": "108,235 (현재 129,800)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 173,695"}, {"label": "추세(10일선)", "ok": true, "detail": "85,060"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +257 · 기 -87"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 128,770"}]}
{"ts": "2026-09-08T09:42:26.307124+09:00", "side": "BUY", "code": "000720", "name": "현대건설", "price": 133000, "qty": 35, "cost": 4655698, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 110463, "target": 182212, "signals": [{"label": "손절선", "ok": true, "detail": "110,463 (현재 133,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 182,212"}, {"label": "추세(10일선)", "ok": true, "detail": "114,930"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -109 · 기 +175"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 132,920"}]}
{"ts": "2026-09-11T11:44:24.674261+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11930, "qty": 65, "cost": 775566, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10071, "target": 16146, "signals": [{"label": "손절선", "ok": true, "detail": "10,071 (현재 11,980)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 16,146"}, {"label": "추세(10일선)", "ok": true, "detail": "11,366"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +5 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 11,976"}]}
{"ts": "2026-09-15T09:00:49.345239+09:00", "side": "SELL", "code": "003550", "name": "LG", "price": 113400, "qty": 83, "proceeds": 9393846, "entry_price": 120431, "pnl": -603453, "pnl_pct": -5.84, "hold_from": "2026-08-14T13:34:15.676304+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "108,115 (현재 111,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 157,380"}, {"label": "추세(10일선)", "ok": false, "detail": "118,280"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -16 · 기 -177"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-15T09:10:30.136504+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 36450, "qty": 127, "cost": 4629844, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 32978, "target": 46866, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "36,200 vs 32,230"}, {"label": "정배열(5>10)", "ok": true, "detail": "33,260 / 32,230"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "33,260 / 30,948"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +3,281 · 기 -786 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+21%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.54% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 33,850"}, {"label": "거래량 급증", "ok": true, "detail": "3,898,453 (환산) vs 평균 2,489,243"}, {"label": "회전율 급증", "ok": true, "detail": "0.54% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "72"}]}
{"ts": "2026-09-22T09:32:35.251487+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 109700, "qty": 75, "proceeds": 8211456, "entry_price": 127165, "pnl": -1327374, "pnl_pct": -13.73, "hold_from": "2026-09-04T12:34:21.902508+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "110,486 (현재 110,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 177,203"}, {"label": "추세(10일선)", "ok": true, "detail": "83,080"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +266 · 기 -141"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-22T09:32:35.675940+09:00", "side": "BUY", "code": "088350", "name": "한화생명", "price": 5740, "qty": 817, "cost": 4690283, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5129, "target": 7573, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "5,720 vs 4,822"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,161 / 4,822"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,161 / 4,734"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+26.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -544 · 기 +443 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+11%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.68% (환산) vs 평균 0.43% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,630"}, {"label": "거래량 급증", "ok": true, "detail": "5,933,633 (환산) vs 평균 3,731,411"}, {"label": "회전율 급증", "ok": true, "detail": "0.68% (환산) vs 평균 0.43%"}, {"label": "RSI", "ok": true, "detail": "74"}]}
{"ts": "2026-10-02T14:48:36.881112+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 304500, "qty": 31, "proceeds": 9421093, "entry_price": 347065, "pnl": -1339521, "pnl_pct": -12.26, "hold_from": "2026-07-16T09:04:02.184863+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "305,213 (현재 305,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 472,619"}, {"label": "추세(10일선)", "ok": true, "detail": "301,850"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +84 · 기 -122"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-02T14:48:37.320243+09:00", "side": "BUY", "code": "161890", "name": "한국콜마", "price": 136600, "qty": 34, "cost": 4645097, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 116526, "target": 196823, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "136,300 vs 112,690"}, {"label": "정배열(5>10)", "ok": true, "detail": "114,560 / 112,690"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "114,560 / 92,538"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+50.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +32 · 기 -67 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+27%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.28% (환산) vs 평균 1.76% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 124,600"}, {"label": "거래량 급증", "ok": true, "detail": "1,006,968 (환산) vs 평균 415,172"}, {"label": "회전율 급증", "ok": true, "detail": "4.28% (환산) vs 평균 1.76%"}, {"label": "RSI", "ok": true, "detail": "76"}]}
{"ts": "2026-10-06T13:48:37.716813+09:00", "side": "SELL", "code": "000720", "name": "현대건설", "price": 112600, "qty": 72, "proceeds": 8091391, "entry_price": 130636, "pnl": -1315820, "pnl_pct": -13.81, "hold_from": "2026-09-07T15:06:30.638654+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "112,556 (현재 112,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 184,877"}, {"label": "추세(10일선)", "ok": false, "detail": "112,890"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -101 · 기 -533"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-06T13:48:38.139374+09:00", "side": "BUY", "code": "108490", "name": "로보티즈", "price": 316500, "qty": 14, "cost": 4431665, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 268844, "target": 459469, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "316,000 vs 263,000"}, {"label": "정배열(5>10)", "ok": true, "detail": "274,000 / 263,000"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "274,000 / 242,227"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+22.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +82 · 기 +24 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.91% (환산) vs 평균 1.94% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 315,000"}, {"label": "거래량 급증", "ok": true, "detail": "574,160 (환산) vs 평균 284,061"}, {"label": "회전율 급증", "ok": true, "detail": "3.91% (환산) vs 평균 1.94%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-10-08T09:06:38.026073+09:00", "side": "SELL", "code": "316140", "name": "우리금융지주", "price": 32950, "qty": 127, "proceeds": 4176490, "entry_price": 36450, "pnl": -453354, "pnl_pct": -9.6, "hold_from": "2026-09-15T09:10:30.136492+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "32,978 (현재 32,950)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 46,866"}, {"label": "추세(10일선)", "ok": true, "detail": "31,905"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +17,844 · 기 +1,402"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 37,223"}]}
{"ts": "2026-10-08T09:06:38.453290+09:00", "side": "BUY", "code": "295310", "name": "에이치브이엠", "price": 70400, "qty": 65, "cost": 4576686, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 61192, "target": 98025, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "70,700 vs 55,755"}, {"label": "정배열(5>10)", "ok": true, "detail": "61,380 / 55,755"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "61,380 / 47,905"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+42.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -73 · 기 +228 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+19%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.50% (환산) vs 평균 0.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 61,900"}, {"label": "거래량 급증", "ok": true, "detail": "185,780 (환산) vs 평균 120,414"}, {"label": "회전율 급증", "ok": true, "detail": "1.50% (환산) vs 평균 0.97%"}, {"label": "RSI", "ok": true, "detail": "75"}]}
{"ts": "2026-10-08T09:20:39.679141+09:00", "side": "BUY", "code": "295310", "name": "에이치브이엠", "price": 73700, "qty": 62, "cost": 4570085, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 61192, "target": 98025, "signals": [{"label": "손절선", "ok": true, "detail": "61,192 (현재 73,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 98,025"}, {"label": "추세(10일선)", "ok": true, "detail": "56,025"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -73 · 기 +228"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 72,709"}]}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.943289+09:00",
"updated_at": "2026-06-15T15:25:02.148135+09:00"
}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944222+09:00",
"updated_at": "2026-06-15T15:25:02.276302+09:00"
}
@@ -1,13 +1,204 @@
{
"cash": 100000000,
"cash": 39593090.37300002,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944232+09:00",
"updated_at": "2026-06-15T15:25:02.277525+09:00"
"positions": {
"194370": {
"code": "194370",
"name": "제이에스코퍼레이션",
"sources": [
"auto"
],
"qty": 532,
"entry_price": 13160.206766917294,
"entry_at": "2026-08-03T12:00:11.186392+09:00",
"stop": 11445,
"target": 15733,
"peak": 14150,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 12200,
"tranches": 2,
"tranche_value": 3504336.4241500003,
"last_add_price": 13420
},
"041520": {
"code": "041520",
"name": "이엘씨",
"sources": [
"auto"
],
"qty": 566,
"entry_price": 11757.597173144877,
"entry_at": "2026-09-03T09:02:24.974496+09:00",
"stop": 10215,
"target": 14072,
"peak": 13200,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 11,601 도달",
"cur_price": 12790,
"tranches": 2,
"tranche_value": 3334430.645615085,
"last_add_price": 11930
},
"316140": {
"code": "316140",
"name": "우리금융지주",
"sources": [
"auto"
],
"qty": 265,
"entry_price": 35694.90566037736,
"entry_at": "2026-09-03T09:30:28.212011+09:00",
"stop": 32337,
"target": 40732,
"peak": 36900,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 32500,
"tranches": 2,
"tranche_value": 4741640.48120924,
"last_add_price": 36150
},
"252990": {
"code": "252990",
"name": "샘씨엔에스",
"sources": [
"auto"
],
"qty": 304,
"entry_price": 13496.184210526315,
"entry_at": "2026-09-07T15:06:35.044589+09:00",
"stop": 18268,
"target": 18110,
"peak": 20700,
"trailing_on": true,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 13,160 도달",
"cur_price": 18340,
"tranches": 2,
"tranche_value": 2059070.58259455,
"last_add_price": 13930
},
"004370": {
"code": "004370",
"name": "농심",
"sources": [
"auto"
],
"qty": 12,
"entry_price": 406000,
"entry_at": "2026-10-01T09:02:45.301394+09:00",
"stop": 372500,
"target": 456250,
"peak": 410000,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 410,925 도달",
"cur_price": 398000,
"tranches": 1,
"tranche_value": 5205421.720847539,
"last_add_price": 406000
},
"093370": {
"code": "093370",
"name": "후성",
"sources": [
"interest"
],
"qty": 412,
"entry_price": 15444.368932038835,
"entry_at": "2026-10-01T09:12:47.860612+09:00",
"stop": 13324,
"target": 18625,
"peak": 16450,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 15150,
"tranches": 2,
"tranche_value": 3193742.1412302153,
"last_add_price": 15740
},
"122640": {
"code": "122640",
"name": "예스티",
"sources": [
"interest"
],
"qty": 90,
"entry_price": 34500,
"entry_at": "2026-10-01T10:38:41.833335+09:00",
"stop": 29749,
"target": 41627,
"peak": 34750,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 32400,
"tranches": 1,
"tranche_value": 3118790.5134803206,
"last_add_price": 34500
},
"456010": {
"code": "456010",
"name": "아이씨티케이",
"sources": [
"watch"
],
"qty": 122,
"entry_price": 24250,
"entry_at": "2026-10-08T09:02:53.389847+09:00",
"stop": 20800,
"target": 29425,
"peak": 24650,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 24,970 도달",
"cur_price": 24100,
"tranches": 1,
"tranche_value": 2973963.562447827,
"last_add_price": 24250
}
},
"realized_pnl": -15883632.137000024,
"closed_trades": 41,
"wins": 4,
"peak_equity": 100172467.23000002,
"max_drawdown": 0.1585967411636447,
"last_exit": {
"030000": "2026-09-04",
"086790": "2026-07-28",
"403870": "2026-07-07",
"240810": "2026-07-24",
"214450": "2026-09-10",
"093370": "2026-07-20",
"055550": "2026-07-20",
"024060": "2026-07-27",
"003680": "2026-08-24",
"138040": "2026-08-19",
"010950": "2026-09-03",
"252990": "2026-09-01",
"090850": "2026-08-05",
"078930": "2026-09-03",
"068270": "2026-09-04",
"327260": "2026-09-07",
"004370": "2026-09-21",
"005930": "2026-10-01",
"088350": "2026-10-01",
"001790": "2026-10-01",
"079650": "2026-10-07"
},
"created_at": "2026-06-11T09:00:01.944232+09:00",
"updated_at": "2026-10-08T15:28:47.138392+09:00"
}
@@ -0,0 +1,117 @@
{"ts": "2026-06-17T11:15:02.299761+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 130900, "qty": 38, "cost": 4974946, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 118039, "target": 150191, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "129,300 vs 118,950"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,400 / 118,950"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,400 / 118,307"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-13.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +371 · 기 +158 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+22%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.12% (환산) vs 평균 0.38% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 126,500"}, {"label": "거래량 급증", "ok": true, "detail": "3,094,534 (환산) vs 평균 1,031,725"}, {"label": "회전율 급증", "ok": true, "detail": "1.12% (환산) vs 평균 0.38%"}, {"label": "RSI", "ok": true, "detail": "60"}]}
{"ts": "2026-06-19T09:02:06.341563+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19160, "qty": 325, "cost": 6227934, "path": "pullback", "reason": "눌림목 지정가 19,690 도달", "stop": 17760, "target": 21260, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,150 vs 18,963"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,616 / 18,963"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,616 / 19,361"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-31.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,162 · 기 +2,080 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "37,673 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,690 (현재 19,150)"}]}
{"ts": "2026-06-23T09:00:09.238138+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18820, "qty": 325, "proceeds": 6104573, "entry_price": 19160, "pnl": -123361, "pnl_pct": -1.77, "hold_from": "2026-06-19T09:02:06.341561+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,710)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,260"}, {"label": "추세(10일선)", "ok": false, "detail": "18,919"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -318 · 기 -227"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,528"}]}
{"ts": "2026-06-23T12:04:04.077319+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 117700, "qty": 38, "proceeds": 4463878, "entry_price": 130900, "pnl": -511068, "pnl_pct": -10.08, "hold_from": "2026-06-17T11:15:02.299760+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "118,039 (현재 117,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 150,191"}, {"label": "추세(10일선)", "ok": false, "detail": "119,330"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +332 · 기 -364"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 134,300"}]}
{"ts": "2026-06-24T09:02:02.369304+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 53200, "qty": 49, "cost": 2607191, "path": "pullback", "reason": "눌림목 지정가 60,114 도달", "stop": 43200, "target": 68200, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,200 vs 52,550"}, {"label": "정배열(5>10)", "ok": true, "detail": "57,960 / 52,550"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "57,960 / 49,671"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +711 · 기 -2,060 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+29%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.34% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "1,102,733 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "1.34% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 60,114 (현재 53,200)"}]}
{"ts": "2026-06-24T09:08:00.981981+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 155400, "qty": 13, "cost": 2020503, "path": "pullback", "reason": "눌림목 지정가 155,963 도달", "stop": 118951, "target": 210074, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "155,400 vs 128,090"}, {"label": "정배열(5>10)", "ok": true, "detail": "143,100 / 128,090"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "143,100 / 121,690"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+45.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +143 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "6.50% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,192,593 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "6.50% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 155,963 (현재 155,400)"}]}
{"ts": "2026-06-26T09:00:05.483093+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 50000, "qty": 49, "proceeds": 2445222, "entry_price": 53200, "pnl": -161969, "pnl_pct": -6.02, "hold_from": "2026-06-24T09:02:02.369303+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "43,200 (현재 51,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 68,200"}, {"label": "추세(10일선)", "ok": false, "detail": "52,330"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -311 · 기 -267"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 57,056"}]}
{"ts": "2026-06-26T09:04:03.892266+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 164900, "qty": 12, "cost": 1979097, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 118951, "target": 210074, "signals": [{"label": "손절선", "ok": true, "detail": "118,951 (현재 164,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 210,074"}, {"label": "추세(10일선)", "ok": true, "detail": "129,020"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -140 · 기 +323"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 164,359"}]}
{"ts": "2026-06-30T09:00:05.439867+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 55300, "qty": 40, "cost": 2212332, "path": "pullback", "reason": "눌림목 지정가 60,222 도달", "stop": 43200, "target": 73450, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,700 vs 52,600"}, {"label": "정배열(5>10)", "ok": true, "detail": "58,060 / 52,600"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "58,060 / 49,679"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +222 · 기 -821 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+28%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "827 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 60,222 (현재 53,700)"}]}
{"ts": "2026-07-01T09:06:03.204343+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 357500, "qty": 11, "cost": 3933090, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 314220, "target": 422420, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "357,000 vs 307,050"}, {"label": "정배열(5>10)", "ok": true, "detail": "317,600 / 307,050"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "317,600 / 305,700"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -75 · 기 +221 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+35%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.89% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "196,247 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.89% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "68"}]}
{"ts": "2026-07-01T09:24:02.305227+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 372000, "qty": 11, "cost": 4092614, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 314220, "target": 422420, "signals": [{"label": "손절선", "ok": true, "detail": "314,220 (현재 372,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 422,420"}, {"label": "추세(10일선)", "ok": true, "detail": "308,550"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -75 · 기 +221"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 368,499"}]}
{"ts": "2026-07-02T10:18:03.823440+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 15980, "qty": 139, "cost": 2221553, "path": "pullback", "reason": "눌림목 지정가 16,358 도달", "stop": 12444, "target": 21284, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "15,980 vs 13,620"}, {"label": "정배열(5>10)", "ok": true, "detail": "16,104 / 13,620"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,104 / 11,478"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+25.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,833 · 기 +437 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+25%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "9.98% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "10,701,165 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "9.98% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,358 (현재 15,980)"}]}
{"ts": "2026-07-02T10:40:02.946612+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18960, "qty": 324, "cost": 6143961, "path": "pullback", "reason": "눌림목 지정가 19,580 도달", "stop": 17760, "target": 20760, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,960 vs 18,944"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,578 / 18,944"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,578 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -339 · 기 +110 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.86% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "986,112 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.86% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,580 (현재 18,960)"}]}
{"ts": "2026-07-02T11:20:02.049404+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 22550, "qty": 273, "cost": 6157073, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 20760, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 19,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,760"}, {"label": "추세(10일선)", "ok": true, "detail": "18,983"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,350"}]}
{"ts": "2026-07-02T11:24:06.141326+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18860, "qty": 597, "proceeds": 11237464, "entry_price": 20602, "pnl": -1063571, "pnl_pct": -8.45, "hold_from": "2026-07-02T10:40:02.946610+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,042 (현재 18,890)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,942"}, {"label": "추세(10일선)", "ok": false, "detail": "18,937"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T09:44:02.602967+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 123600, "qty": 25, "proceeds": 3083974, "entry_price": 159960, "pnl": -915625, "pnl_pct": -22.73, "hold_from": "2026-06-24T09:08:00.981980+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "124,125 (현재 123,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 213,713"}, {"label": "추세(10일선)", "ok": false, "detail": "124,920"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +674 · 기 -2,170"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T10:24:02.766124+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19040, "qty": 315, "cost": 5998500, "path": "pullback", "reason": "눌림목 지정가 19,631 도달", "stop": 17760, "target": 20960, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,030 vs 18,951"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,592 / 18,951"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,592 / 19,359"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -35 · 기 +168 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.54% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "619,193 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.54% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,631 (현재 19,030)"}]}
{"ts": "2026-07-06T09:00:07.944661+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 130600, "qty": 13, "cost": 1698055, "path": "pullback", "reason": "눌림목 지정가 154,452 도달", "stop": 96000, "target": 182500, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "133,100 vs 125,860"}, {"label": "정배열(5>10)", "ok": true, "detail": "138,640 / 125,860"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "138,640 / 121,318"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +782 · 기 -1,979 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.01% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": false, "detail": "4,080 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": false, "detail": "0.01% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 154,452 (현재 133,100)"}]}
{"ts": "2026-07-06T09:46:01.641444+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 320500, "qty": 22, "proceeds": 7037251, "entry_price": 364750, "pnl": -988453, "pnl_pct": -12.13, "hold_from": "2026-07-01T09:06:03.204341+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "320,756 (현재 320,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 430,741"}, {"label": "추세(10일선)", "ok": true, "detail": "303,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -111 · 기 +227"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-07T09:48:07.376376+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 43000, "qty": 40, "proceeds": 1716646, "entry_price": 55300, "pnl": -495686, "pnl_pct": -22.24, "hold_from": "2026-06-30T09:00:05.439858+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "43,200 (현재 43,050)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 73,450"}, {"label": "추세(10일선)", "ok": false, "detail": "51,535"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +1,424 · 기 -5,706"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 59,449"}]}
{"ts": "2026-07-07T10:28:06.455126+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 12410, "qty": 139, "proceeds": 1721626, "entry_price": 15980, "pnl": -499927, "pnl_pct": -22.34, "hold_from": "2026-07-02T10:18:03.823437+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "12,444 (현재 12,430)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,284"}, {"label": "추세(10일선)", "ok": false, "detail": "13,265"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,543 · 기 +15"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 16,975"}]}
{"ts": "2026-07-07T14:08:19.509937+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 20750, "qty": 289, "cost": 5997650, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 20960, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,960"}, {"label": "추세(10일선)", "ok": true, "detail": "18,901"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 +113"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,358"}]}
{"ts": "2026-07-08T09:00:16.934111+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 108300, "qty": 13, "proceeds": 1405155, "entry_price": 130600, "pnl": -292900, "pnl_pct": -17.08, "hold_from": "2026-07-06T09:00:07.944654+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "166,450 (현재 112,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": false, "detail": "123,820"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +243 · 기 -857"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-09T09:36:03.447933+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 132000, "qty": 31, "cost": 4092614, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 117015, "target": 154477, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "131,900 vs 108,660"}, {"label": "정배열(5>10)", "ok": true, "detail": "115,560 / 108,660"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,560 / 113,228"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+22.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -184 · 기 +964 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.91% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "1,026,867 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.91% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-10T11:06:06.461063+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 13660, "qty": 143, "cost": 1953673, "path": "pullback", "reason": "눌림목 지정가 15,971 도달", "stop": 10360, "target": 18610, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,630 vs 13,385"}, {"label": "정배열(5>10)", "ok": true, "detail": "15,634 / 13,385"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,634 / 11,439"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +239 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+47%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "14.23% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "15,267,605 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "14.23% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 15,971 (현재 13,630)"}]}
{"ts": "2026-07-10T12:14:02.662405+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 126200, "qty": 15, "cost": 1893284, "path": "pullback", "reason": "눌림목 지정가 151,752 도달", "stop": 96000, "target": 171500, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "126,200 vs 125,170"}, {"label": "정배열(5>10)", "ok": true, "detail": "137,260 / 125,170"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "137,260 / 121,203"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +483 · 기 -241 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "7.53% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,695,762 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "7.53% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 151,752 (현재 126,200)"}]}
{"ts": "2026-07-13T09:00:09.684184+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 136900, "qty": 30, "cost": 4107616, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 117015, "target": 154477, "signals": [{"label": "손절선", "ok": true, "detail": "117,015 (현재 136,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,477"}, {"label": "추세(10일선)", "ok": true, "detail": "109,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -64 · 기 +919"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,864"}]}
{"ts": "2026-07-13T09:12:04.289323+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 341500, "qty": 11, "cost": 3757063, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 300363, "target": 403206, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "341,000 vs 305,450"}, {"label": "정배열(5>10)", "ok": true, "detail": "314,400 / 305,450"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "314,400 / 305,433"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -63 · 기 +144 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.90% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "197,687 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.90% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-07-13T09:26:03.401224+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 139100, "qty": 14, "cost": 1947692, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 96000, "target": 171500, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 139,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 171,500"}, {"label": "추세(10일선)", "ok": true, "detail": "126,460"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -213 · 기 +772"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 136,162"}]}
{"ts": "2026-07-13T09:48:02.326780+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 135500, "qty": 36, "cost": 4878732, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 122472, "target": 155042, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "135,500 vs 121,110"}, {"label": "정배열(5>10)", "ok": true, "detail": "123,700 / 121,110"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "123,700 / 118,657"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -277 · 기 +460 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+18%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.77% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": true, "detail": "2,103,900 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": true, "detail": "0.77% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "65"}]}
{"ts": "2026-07-13T10:04:06.402387+09:00", "side": "BUY", "code": "055550", "name": "신한지주", "price": 113100, "qty": 42, "cost": 4750913, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 101931, "target": 129854, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "113,050 vs 100,265"}, {"label": "정배열(5>10)", "ok": true, "detail": "101,790 / 100,265"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "101,790 / 96,676"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+31.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -368 · 기 +1,005 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.71% (환산) vs 평균 0.35% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 109,800"}, {"label": "거래량 급증", "ok": true, "detail": "3,366,381 (환산) vs 평균 1,653,581"}, {"label": "회전율 급증", "ok": true, "detail": "0.71% (환산) vs 평균 0.35%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-15T11:56:05.674537+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 138900, "qty": 35, "cost": 4862229, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 122472, "target": 155042, "signals": [{"label": "손절선", "ok": true, "detail": "122,472 (현재 138,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 155,042"}, {"label": "추세(10일선)", "ok": true, "detail": "121,450"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -40 · 기 +527"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 138,828"}]}
{"ts": "2026-07-15T14:32:01.874159+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18570, "qty": 604, "proceeds": 11194408, "entry_price": 19858, "pnl": -801741, "pnl_pct": -6.49, "hold_from": "2026-07-03T10:24:02.766122+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "18,586 (현재 18,570)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,767"}, {"label": "추세(10일선)", "ok": false, "detail": "18,905"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -214 · 기 +240"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-16T09:28:03.472538+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 353000, "qty": 11, "cost": 3883582, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 300363, "target": 403206, "signals": [{"label": "손절선", "ok": true, "detail": "300,363 (현재 352,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 403,206"}, {"label": "추세(10일선)", "ok": true, "detail": "306,600"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -33 · 기 +28"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 351,963"}]}
{"ts": "2026-07-20T11:28:01.181290+09:00", "side": "SELL", "code": "055550", "name": "신한지주", "price": 101900, "qty": 42, "proceeds": 4271454, "entry_price": 113100, "pnl": -479458, "pnl_pct": -9.9, "hold_from": "2026-07-13T10:04:06.402386+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "101,931 (현재 101,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 129,854"}, {"label": "추세(10일선)", "ok": true, "detail": "99,150"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -156 · 기 +275"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 115,714"}]}
{"ts": "2026-07-20T12:28:02.306152+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 10310, "qty": 143, "proceeds": 1471455, "entry_price": 13660, "pnl": -482218, "pnl_pct": -24.52, "hold_from": "2026-07-10T11:06:06.461061+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,360 (현재 10,310)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,610"}, {"label": "추세(10일선)", "ok": false, "detail": "12,013"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -887 · 기 +515"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,626"}]}
{"ts": "2026-07-23T09:50:01.505017+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 88200, "qty": 43, "cost": 3793169, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 77564, "target": 104155, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "88,200 vs 80,900"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,000 / 80,900"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,000 / 75,058"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+47.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -150 · 기 +299 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.85% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "785,587 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.85% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-23T10:30:02.474411+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 14140, "qty": 190, "cost": 2687003, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11706, "target": 17791, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,180 vs 12,429"}, {"label": "정배열(5>10)", "ok": true, "detail": "13,154 / 12,429"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "13,154 / 12,687"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+55.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +109 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "84.26% (환산) vs 평균 31.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 13,990"}, {"label": "거래량 급증", "ok": true, "detail": "12,639,979 (환산) vs 평균 4,720,532"}, {"label": "회전율 급증", "ok": true, "detail": "84.26% (환산) vs 평균 31.47%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-23T12:54:04.487607+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90900, "qty": 42, "cost": 3818373, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 77564, "target": 104155, "signals": [{"label": "손절선", "ok": true, "detail": "77,564 (현재 91,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 104,155"}, {"label": "추세(10일선)", "ok": true, "detail": "81,180"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -169 · 기 +311"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 90,945"}]}
{"ts": "2026-07-24T09:00:05.421727+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 111800, "qty": 29, "proceeds": 3235878, "entry_price": 132428, "pnl": -605098, "pnl_pct": -15.58, "hold_from": "2026-07-10T12:14:02.662404+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 113,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 187,069"}, {"label": "추세(10일선)", "ok": false, "detail": "123,880"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -480 · 기 -206"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-24T09:02:04.034477+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 15060, "qty": 179, "cost": 2696144, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11706, "target": 17791, "signals": [{"label": "손절선", "ok": true, "detail": "11,706 (현재 15,020)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,791"}, {"label": "추세(10일선)", "ok": true, "detail": "12,513"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +208 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,766"}]}
{"ts": "2026-07-24T10:02:04.389259+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 310, "cost": 5875381, "path": "pullback", "reason": "눌림목 지정가 19,643 도달", "stop": 17760, "target": 20735, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,950 vs 18,943"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,576 / 18,943"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,576 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+19.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +2 · 기 -5 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.39% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "454,069 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.39% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,643 (현재 18,950)"}]}
{"ts": "2026-07-27T09:02:06.217883+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7470, "qty": 423, "cost": 3160284, "path": "pullback", "reason": "눌림목 지정가 7,635 도달", "stop": 6370, "target": 9119, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "7,470 vs 5,392"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,452 / 5,392"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,452 / 4,889"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+82.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +49 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.79% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "49,327 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "0.79% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "74"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,635 (현재 7,470)"}]}
{"ts": "2026-07-27T09:08:02.078866+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7910, "qty": 400, "cost": 3164475, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 6370, "target": 9119, "signals": [{"label": "손절선", "ok": true, "detail": "6,370 (현재 7,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,119"}, {"label": "추세(10일선)", "ok": true, "detail": "5,434"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +49 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,747"}]}
{"ts": "2026-07-27T09:10:01.033059+09:00", "side": "SELL", "code": "024060", "name": "흥구석유", "price": 12030, "qty": 369, "proceeds": 4430414, "entry_price": 14586, "pnl": -952734, "pnl_pct": -17.53, "hold_from": "2026-07-23T10:30:02.474409+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "12,082 (현재 12,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,342"}, {"label": "추세(10일선)", "ok": false, "detail": "12,214"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -85 · 기 +2"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-27T09:12:01.497852+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13150, "qty": 164, "cost": 2156923, "path": "pullback", "reason": "눌림목 지정가 13,180 도달", "stop": 10340, "target": 17365, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,150 vs 12,888"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,378 / 12,888"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,378 / 13,268"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+1.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +27 · 기 +15 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.46% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "274,967 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.46% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,180 (현재 13,150)"}]}
{"ts": "2026-07-27T09:32:01.372128+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 127900, "qty": 45, "cost": 5756363, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 117645, "target": 143283, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "127,900 vs 117,400"}, {"label": "정배열(5>10)", "ok": true, "detail": "119,040 / 117,400"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "119,040 / 110,150"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +106 · 기 +291 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+16%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.38% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": true, "detail": "643,340 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": true, "detail": "0.38% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "71"}]}
{"ts": "2026-07-28T11:22:00.740360+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6630, "qty": 823, "proceeds": 5445850, "entry_price": 7684, "pnl": -878909, "pnl_pct": -13.72, "hold_from": "2026-07-27T09:02:06.217881+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "6,577 (현재 6,380)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,344"}, {"label": "추세(10일선)", "ok": true, "detail": "5,282"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +96 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-28T12:56:02.869289+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 117400, "qty": 45, "proceeds": 5272698, "entry_price": 127900, "pnl": -483665, "pnl_pct": -8.21, "hold_from": "2026-07-27T09:32:01.372126+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "117,645 (현재 117,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 143,283"}, {"label": "추세(10일선)", "ok": true, "detail": "116,360"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +84 · 기 +324"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 130,553"}]}
{"ts": "2026-07-28T13:20:03.820316+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 123800, "qty": 71, "proceeds": 8772660, "entry_price": 137176, "pnl": -968301, "pnl_pct": -9.75, "hold_from": "2026-07-13T09:48:02.326778+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "123,862 (현재 123,800)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 157,147"}, {"label": "추세(10일선)", "ok": true, "detail": "119,940"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +122 · 기 -106"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:02:02.215048+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6540, "qty": 357, "cost": 2335130, "path": "pullback", "reason": "눌림목 지정가 7,526 도달", "stop": 5295, "target": 8408, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,540 vs 5,298"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,266 / 5,298"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,266 / 4,873"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+70.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +188 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.87% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "116,047 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "1.87% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "63"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,526 (현재 6,540)"}]}
{"ts": "2026-07-29T09:24:08.685814+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 128600, "qty": 61, "proceeds": 7829303, "entry_price": 134410, "pnl": -370927, "pnl_pct": -4.32, "hold_from": "2026-07-09T09:36:03.447931+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "118,954 (현재 116,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 157,594"}, {"label": "추세(10일선)", "ok": true, "detail": "105,620"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +245"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:24:08.686031+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 11310, "qty": 191, "cost": 2160534, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 17365, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 15,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,365"}, {"label": "추세(10일선)", "ok": true, "detail": "12,768"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +242 · 기 -182"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,887"}]}
{"ts": "2026-07-29T09:26:01.888464+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18940, "qty": 310, "proceeds": 5859951, "entry_price": 18950, "pnl": -15430, "pnl_pct": -0.05, "hold_from": "2026-07-24T10:02:04.389258+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,645 (현재 18,950)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "18,943"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +99 · 기 +138"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:26:01.888676+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6610, "qty": 357, "proceeds": 2355168, "entry_price": 6540, "pnl": 20038, "pnl_pct": 1.07, "hold_from": "2026-07-29T09:02:02.215046+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "7,526 (현재 6,610)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "5,306"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +188 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:28:01.375105+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6390, "qty": 848, "cost": 5419533, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5867, "target": 7174, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,380 vs 5,591"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,742 / 5,591"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,742 / 5,622"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +73 · 기 -3 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.62% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "134,413 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.62% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "73"}]}
{"ts": "2026-07-29T10:24:01.432673+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 123400, "qty": 42, "cost": 5183577, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 113016, "target": 138976, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "123,400 vs 116,950"}, {"label": "정배열(5>10)", "ok": true, "detail": "118,140 / 116,950"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "118,140 / 110,075"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+51.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +47 · 기 +310 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.38% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": true, "detail": "644,676 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": true, "detail": "0.38% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-29T10:52:02.342370+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 10340, "qty": 355, "proceeds": 3663542, "entry_price": 12160, "pnl": -653915, "pnl_pct": -14.97, "hold_from": "2026-07-27T09:12:01.497849+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,340 (현재 10,340)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 14,890"}, {"label": "추세(10일선)", "ok": false, "detail": "12,607"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +239 · 기 -181"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T13:34:03.579933+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5860, "qty": 848, "proceeds": 4959590, "entry_price": 6390, "pnl": -459943, "pnl_pct": -8.29, "hold_from": "2026-07-29T09:28:01.375104+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,867 (현재 5,860)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,174"}, {"label": "추세(10일선)", "ok": true, "detail": "5,539"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +73 · 기 -3"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,525"}]}
{"ts": "2026-07-30T09:04:06.858551+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19090, "qty": 288, "cost": 5498745, "path": "pullback", "reason": "눌림목 지정가 19,653 도달", "stop": 17760, "target": 21085, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,100 vs 18,958"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,606 / 18,958"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,606 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+29.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +2 · 기 +327 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.14% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "160,727 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.14% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,653 (현재 19,100)"}]}
{"ts": "2026-07-30T09:04:06.859148+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6130, "qty": 329, "cost": 2017073, "path": "pullback", "reason": "눌림목 지정가 7,457 도달", "stop": 4793, "target": 8135, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,120 vs 5,256"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,182 / 5,256"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,182 / 4,866"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+65.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +234 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.31% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "267,687 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "4.31% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,457 (현재 6,120)"}]}
{"ts": "2026-07-30T09:24:06.912152+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 131800, "qty": 29, "cost": 3822773, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 116987, "target": 154020, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "132,000 vs 108,670"}, {"label": "정배열(5>10)", "ok": true, "detail": "115,580 / 108,670"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,580 / 113,230"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+51.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -6 · 기 +178 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.66% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "738,813 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.66% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-30T09:26:06.863454+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6300, "qty": 867, "cost": 5462919, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5789, "target": 7067, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,280 vs 5,581"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,722 / 5,581"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,722 / 5,620"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +87 · 기 +6 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.51% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "109,080 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.51% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "71"}]}
{"ts": "2026-07-30T09:50:06.667546+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 126400, "qty": 41, "cost": 5183177, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 113016, "target": 138976, "signals": [{"label": "손절선", "ok": true, "detail": "113,016 (현재 126,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 138,976"}, {"label": "추세(10일선)", "ok": true, "detail": "117,230"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +9 · 기 +389"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 126,082"}]}
{"ts": "2026-07-31T09:06:06.408818+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6550, "qty": 308, "cost": 2017703, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 4793, "target": 8135, "signals": [{"label": "손절선", "ok": true, "detail": "4,793 (현재 6,640)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,135"}, {"label": "추세(10일선)", "ok": true, "detail": "5,308"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +355 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,453"}]}
{"ts": "2026-07-31T09:06:06.408934+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 77700, "qty": 85, "proceeds": 6591621, "entry_price": 89534, "pnl": -1019920, "pnl_pct": -13.22, "hold_from": "2026-07-23T09:50:01.505016+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "78,555 (현재 78,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 106,003"}, {"label": "추세(10일선)", "ok": false, "detail": "79,880"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -179 · 기 +325"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-03T12:00:11.186394+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 12910, "qty": 271, "cost": 3499135, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11314, "target": 15304, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "12,930 vs 12,003"}, {"label": "정배열(5>10)", "ok": true, "detail": "12,138 / 12,003"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "12,138 / 11,924"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +7 · 기 +10 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+74%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.72% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 12,870"}, {"label": "거래량 급증", "ok": true, "detail": "208,994 (환산) vs 평균 99,303"}, {"label": "회전율 급증", "ok": true, "detail": "0.72% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-03T12:02:09.141957+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 13420, "qty": 261, "cost": 3503145, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11314, "target": 15304, "signals": [{"label": "손절선", "ok": true, "detail": "11,314 (현재 13,450)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 15,304"}, {"label": "추세(10일선)", "ok": true, "detail": "12,055"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +7 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,339"}]}
{"ts": "2026-08-04T09:50:08.969522+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90400, "qty": 39, "cost": 3526129, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 79421, "target": 106869, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "90,300 vs 81,110"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,420 / 81,110"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,420 / 75,093"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+56.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +68 · 기 +117 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+14%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.87% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "805,693 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.87% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-08-04T10:32:12.149555+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 93500, "qty": 38, "cost": 3553533, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 79421, "target": 106869, "signals": [{"label": "손절선", "ok": true, "detail": "79,421 (현재 93,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 106,869"}, {"label": "추세(10일선)", "ok": true, "detail": "81,420"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +57 · 기 +117"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 93,241"}]}
{"ts": "2026-08-05T09:02:12.579251+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5700, "qty": 867, "proceeds": 4932263, "entry_price": 6300, "pnl": -530656, "pnl_pct": -9.52, "hold_from": "2026-07-30T09:26:06.863452+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,789 (현재 5,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,067"}, {"label": "추세(10일선)", "ok": true, "detail": "5,526"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +14 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,413"}]}
{"ts": "2026-08-05T09:02:12.579432+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13040, "qty": 160, "cost": 2086713, "path": "pullback", "reason": "눌림목 지정가 13,170 도달", "stop": 10340, "target": 17090, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,040 vs 12,877"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,356 / 12,877"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,356 / 13,267"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-6.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -72 · 기 +134 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.13% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "78,333 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.13% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,170 (현재 13,040)"}]}
{"ts": "2026-08-05T09:38:10.847019+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13830, "qty": 151, "cost": 2088643, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 17090, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 13,860)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,090"}, {"label": "추세(10일선)", "ok": true, "detail": "12,959"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -72 · 기 +134"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,817"}]}
{"ts": "2026-08-06T09:00:17.713628+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18730, "qty": 288, "proceeds": 5383721, "entry_price": 19090, "pnl": -115023, "pnl_pct": -1.89, "hold_from": "2026-07-30T09:04:06.858549+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,085"}, {"label": "추세(10일선)", "ok": false, "detail": "18,923"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -203 · 기 -147"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,459"}]}
{"ts": "2026-08-06T09:34:15.268992+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 192000, "qty": 26, "cost": 4992749, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 175344, "target": 216984, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "191,800 vs 180,670"}, {"label": "정배열(5>10)", "ok": true, "detail": "186,140 / 180,670"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "186,140 / 176,671"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +255 · 기 -319 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+44%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.67% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 191,700"}, {"label": "거래량 급증", "ok": true, "detail": "1,582,767 (환산) vs 평균 738,783"}, {"label": "회전율 급증", "ok": true, "detail": "0.67% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-07T09:12:15.071471+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 196400, "qty": 25, "cost": 4910736, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 175344, "target": 216984, "signals": [{"label": "손절선", "ok": true, "detail": "175,344 (현재 196,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 216,984"}, {"label": "추세(10일선)", "ok": true, "detail": "181,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +338 · 기 +73"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 196,264"}]}
{"ts": "2026-08-10T09:00:14.782704+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 136000, "qty": 29, "cost": 3944592, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 116987, "target": 154020, "signals": [{"label": "손절선", "ok": true, "detail": "116,987 (현재 137,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,020"}, {"label": "추세(10일선)", "ok": true, "detail": "109,170"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -253 · 기 +225"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,714"}]}
{"ts": "2026-08-12T09:30:11.931330+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 400000, "qty": 22, "proceeds": 8782840, "entry_price": 347250, "pnl": 1142194, "pnl_pct": 15.19, "hold_from": "2026-07-13T09:12:04.289321+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "399,647 (현재 399,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "311,300"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -6 · 기 +72"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-12T09:38:12.218719+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19050, "qty": 296, "cost": 5639646, "path": "pullback", "reason": "눌림목 지정가 19,679 도달", "stop": 17760, "target": 20985, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,040 vs 18,952"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,594 / 18,952"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,594 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +113 · 기 +20 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.22% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "250,373 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.22% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,679 (현재 19,040)"}]}
{"ts": "2026-08-13T09:00:30.556332+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18880, "qty": 296, "proceeds": 5577582, "entry_price": 19050, "pnl": -62063, "pnl_pct": -0.89, "hold_from": "2026-08-12T09:38:12.218718+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,930)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,985"}, {"label": "추세(10일선)", "ok": false, "detail": "18,941"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -50 · 기 -76"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,411"}]}
{"ts": "2026-08-13T09:08:16.771869+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 402500, "qty": 8, "cost": 3220483, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 352720, "target": 477170, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "402,000 vs 311,550"}, {"label": "정배열(5>10)", "ok": true, "detail": "326,600 / 311,550"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "326,600 / 306,450"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +27 · 기 +23 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+26%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.20% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "228,347 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "2.20% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "75"}]}
{"ts": "2026-08-13T14:02:16.525079+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 415500, "qty": 8, "cost": 3324499, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 352720, "target": 477170, "signals": [{"label": "손절선", "ok": true, "detail": "352,720 (현재 415,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 477,170"}, {"label": "추세(10일선)", "ok": true, "detail": "312,900"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +19"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 415,052"}]}
{"ts": "2026-08-19T09:16:16.546128+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 113700, "qty": 83, "proceeds": 9418698, "entry_price": 124882, "pnl": -948057, "pnl_pct": -8.95, "hold_from": "2026-07-29T10:24:01.432671+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "114,155 (현재 113,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 140,972"}, {"label": "추세(10일선)", "ok": false, "detail": "115,980"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +206 · 기 -264"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-20T09:02:17.441276+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 33400, "qty": 43, "cost": 1436415, "path": "pullback", "reason": "눌림목 지정가 33,138 도달", "stop": 23118, "target": 48823, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "33,000 vs 28,622"}, {"label": "정배열(5>10)", "ok": true, "detail": "35,100 / 28,622"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "35,100 / 31,286"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -71 · 기 +7 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+106%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.67% (환산) vs 평균 3.74% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 45,300"}, {"label": "거래량 급증", "ok": false, "detail": "60,673 (환산) vs 평균 409,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.67% (환산) vs 평균 3.74%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 33,138 (현재 33,000)"}]}
{"ts": "2026-08-20T09:16:21.637579+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 35800, "qty": 40, "cost": 1432215, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 23118, "target": 48823, "signals": [{"label": "손절선", "ok": true, "detail": "23,118 (현재 36,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 48,823"}, {"label": "추세(10일선)", "ok": true, "detail": "28,922"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -71 · 기 +7"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 35,971"}]}
{"ts": "2026-08-24T13:12:20.459313+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5030, "qty": 637, "proceeds": 3197862, "entry_price": 6333, "pnl": -836913, "pnl_pct": -20.58, "hold_from": "2026-07-30T09:04:06.859145+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,039 (현재 5,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,274"}, {"label": "추세(10일선)", "ok": false, "detail": "5,148"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -31 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-24T13:14:20.866192+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19000, "qty": 286, "cost": 5434815, "path": "pullback", "reason": "눌림목 지정가 19,655 도달", "stop": 17760, "target": 20860, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,000 vs 18,948"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,586 / 18,948"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,586 / 19,359"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-3.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +102 · 기 -149 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.29% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "331,827 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.29% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,655 (현재 19,000)"}]}
{"ts": "2026-09-01T09:02:25.875329+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 12660, "qty": 311, "proceeds": 3929582, "entry_price": 13424, "pnl": -245774, "pnl_pct": -5.69, "hold_from": "2026-08-05T09:02:12.579429+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 12,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,049"}, {"label": "추세(10일선)", "ok": false, "detail": "12,846"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -53 · 기 -347"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-01T09:32:25.166638+09:00", "side": "BUY", "code": "001790", "name": "대한제당", "price": 2945, "qty": 1897, "cost": 5587503, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 2767, "target": 3211, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "2,935 vs 2,826"}, {"label": "정배열(5>10)", "ok": true, "detail": "2,866 / 2,826"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "2,866 / 2,600"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +75 · 기 +164 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.46% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 2,905"}, {"label": "거래량 급증", "ok": true, "detail": "399,207 (환산) vs 평균 190,371"}, {"label": "회전율 급증", "ok": true, "detail": "0.46% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-09-03T09:00:32.822879+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 119500, "qty": 77, "proceeds": 9183557, "entry_price": 91930, "pnl": 2103895, "pnl_pct": 29.99, "hold_from": "2026-08-04T09:50:08.969520+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "119,430 (현재 119,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "83,990"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 -35"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-03T09:02:24.974500+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11590, "qty": 287, "cost": 3326829, "path": "pullback", "reason": "눌림목 지정가 11,601 도달", "stop": 10071, "target": 13868, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "11,590 vs 11,327"}, {"label": "정배열(5>10)", "ok": true, "detail": "11,748 / 11,327"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "11,748 / 9,426"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+28.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +17 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 0.30% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 13,450"}, {"label": "거래량 급증", "ok": false, "detail": "7 (환산) vs 평균 36,789"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 0.30%"}, {"label": "RSI", "ok": true, "detail": "62"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 11,601 (현재 11,590)"}]}
{"ts": "2026-09-03T09:10:25.790827+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 145600, "qty": 58, "proceeds": 8428333, "entry_price": 133900, "pnl": 660968, "pnl_pct": 8.74, "hold_from": "2026-07-30T09:24:06.912150+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "145,883 (현재 145,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "110,030"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -31 · 기 +240"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-03T09:30:28.212028+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 35250, "qty": 134, "cost": 4724209, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 32007, "target": 40115, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "35,200 vs 32,130"}, {"label": "정배열(5>10)", "ok": true, "detail": "33,060 / 32,130"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "33,060 / 30,932"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+14.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +562 · 기 +643 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.00% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 33,850"}, {"label": "거래량 급증", "ok": true, "detail": "7,197,153 (환산) vs 평균 2,489,243"}, {"label": "회전율 급증", "ok": true, "detail": "1.00% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-09-04T11:14:26.490576+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18930, "qty": 286, "proceeds": 5403423, "entry_price": 19000, "pnl": -31392, "pnl_pct": -0.37, "hold_from": "2026-08-24T13:14:20.866190+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,940)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,860"}, {"label": "추세(10일선)", "ok": false, "detail": "18,942"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -2 · 기 -39"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,363"}]}
{"ts": "2026-09-04T12:02:28.348864+09:00", "side": "BUY", "code": "079650", "name": "서산", "price": 6890, "qty": 239, "cost": 1646957, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5081, "target": 9603, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,910 vs 5,072"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,228 / 5,072"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,228 / 4,366"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+61.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +193 · 기 +5 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "46.65% (환산) vs 평균 23.23% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 6,760"}, {"label": "거래량 급증", "ok": true, "detail": "9,328,695 (환산) vs 평균 4,645,030"}, {"label": "회전율 급증", "ok": true, "detail": "46.65% (환산) vs 평균 23.23%"}, {"label": "RSI", "ok": true, "detail": "68"}]}
{"ts": "2026-09-04T12:34:27.129200+09:00", "side": "SELL", "code": "068270", "name": "셀트리온", "price": 187800, "qty": 51, "proceeds": 9559123, "entry_price": 194157, "pnl": -344362, "pnl_pct": -3.27, "hold_from": "2026-08-06T09:34:15.268990+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "177,101 (현재 187,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 219,741"}, {"label": "추세(10일선)", "ok": false, "detail": "189,720"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -128 · 기 -47"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-04T12:50:27.780788+09:00", "side": "BUY", "code": "088350", "name": "한화생명", "price": 5820, "qty": 674, "cost": 3923268, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5177, "target": 6784, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "5,810 vs 4,830"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,179 / 4,830"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,179 / 4,736"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+28.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -2,249 · 기 +1,219 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+9%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.09% (환산) vs 평균 0.43% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,630"}, {"label": "거래량 급증", "ok": true, "detail": "9,490,533 (환산) vs 평균 3,731,411"}, {"label": "회전율 급증", "ok": true, "detail": "1.09% (환산) vs 평균 0.43%"}, {"label": "RSI", "ok": true, "detail": "75"}]}
{"ts": "2026-09-07T15:06:35.044011+09:00", "side": "SELL", "code": "327260", "name": "RF머트리얼즈", "price": 33650, "qty": 83, "proceeds": 2787504, "entry_price": 34557, "pnl": -81126, "pnl_pct": -2.62, "hold_from": "2026-08-20T09:02:17.441272+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "24,275 (현재 33,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 49,980"}, {"label": "추세(10일선)", "ok": false, "detail": "35,015"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -281 · 기 -15"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-07T15:06:35.044591+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13090, "qty": 157, "cost": 2055438, "path": "pullback", "reason": "눌림목 지정가 13,160 도달", "stop": 10340, "target": 17215, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,100 vs 12,883"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,368 / 12,883"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,368 / 13,268"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-13.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -111 · 기 +407 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+72%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.25% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "754,723 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "1.25% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,160 (현재 13,100)"}]}
{"ts": "2026-09-08T09:02:32.216263+09:00", "side": "BUY", "code": "088350", "name": "한화생명", "price": 6010, "qty": 652, "cost": 3919108, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5177, "target": 6784, "signals": [{"label": "손절선", "ok": true, "detail": "5,177 (현재 6,010)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 6,784"}, {"label": "추세(10일선)", "ok": true, "detail": "4,850"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,497 · 기 +2,372"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 5,980"}]}
{"ts": "2026-09-08T12:32:31.602963+09:00", "side": "BUY", "code": "079650", "name": "서산", "price": 7390, "qty": 223, "cost": 1648217, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5081, "target": 9603, "signals": [{"label": "손절선", "ok": true, "detail": "5,081 (현재 7,380)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,603"}, {"label": "추세(10일선)", "ok": true, "detail": "5,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +103 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,329"}]}
{"ts": "2026-09-10T09:06:30.772066+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 356500, "qty": 16, "proceeds": 5692877, "entry_price": 409000, "pnl": -852104, "pnl_pct": -12.84, "hold_from": "2026-08-13T09:08:16.771867+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "358,791 (현재 358,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 484,313"}, {"label": "추세(10일선)", "ok": true, "detail": "307,150"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +106 · 기 -147"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-10T11:38:28.224555+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13930, "qty": 147, "cost": 2048017, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 17215, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 13,910)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,215"}, {"label": "추세(10일선)", "ok": true, "detail": "12,964"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -256 · 기 +80"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,859"}]}
{"ts": "2026-09-10T14:02:37.654102+09:00", "side": "BUY", "code": "004370", "name": "농심", "price": 410500, "qty": 12, "cost": 4926739, "path": "pullback", "reason": "눌림목 지정가 411,461 도달", "stop": 375781, "target": 462578, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "410,500 vs 396,800"}, {"label": "정배열(5>10)", "ok": true, "detail": "404,600 / 396,800"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "404,600 / 369,900"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +8 · 기 -29 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.27% (환산) vs 평균 0.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 440,000"}, {"label": "거래량 급증", "ok": false, "detail": "16,224 (환산) vs 평균 31,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.27% (환산) vs 평균 0.51%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 411,461 (현재 410,500)"}]}
{"ts": "2026-09-11T11:44:31.402107+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11930, "qty": 279, "cost": 3328969, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10071, "target": 13868, "signals": [{"label": "손절선", "ok": true, "detail": "10,071 (현재 11,980)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 13,868"}, {"label": "추세(10일선)", "ok": true, "detail": "11,366"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +5 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 11,976"}]}
{"ts": "2026-09-14T09:28:34.566240+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 36150, "qty": 131, "cost": 4736360, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 32007, "target": 40115, "signals": [{"label": "손절선", "ok": true, "detail": "32,007 (현재 36,150)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 40,115"}, {"label": "추세(10일선)", "ok": true, "detail": "32,225"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +2,045 · 기 -615"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 36,089"}]}
{"ts": "2026-09-21T15:08:43.617889+09:00", "side": "SELL", "code": "004370", "name": "농심", "price": 392500, "qty": 12, "proceeds": 4700816, "entry_price": 410500, "pnl": -225923, "pnl_pct": -4.38, "hold_from": "2026-09-10T14:02:37.654100+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "375,781 (현재 392,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 462,578"}, {"label": "추세(10일선)", "ok": false, "detail": "395,000"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -1 · 기 -5"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 419,823"}]}
{"ts": "2026-09-22T09:04:44.417679+09:00", "side": "BUY", "code": "005930", "name": "삼성전자", "price": 282500, "qty": 17, "cost": 4803220, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 257943, "target": 319336, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "282,000 vs 262,350"}, {"label": "정배열(5>10)", "ok": true, "detail": "265,300 / 262,350"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "265,300 / 262,833"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,624 · 기 +8,791 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+73%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.60% (환산) vs 평균 0.29% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 279,000"}, {"label": "거래량 급증", "ok": true, "detail": "35,127,313 (환산) vs 평균 16,802,321"}, {"label": "회전율 급증", "ok": true, "detail": "0.60% (환산) vs 평균 0.29%"}, {"label": "RSI", "ok": true, "detail": "60"}]}
{"ts": "2026-10-01T09:00:53.825503+09:00", "side": "SELL", "code": "005930", "name": "삼성전자", "price": 266500, "qty": 17, "proceeds": 4521666, "entry_price": 282500, "pnl": -281555, "pnl_pct": -5.66, "hold_from": "2026-09-22T09:04:44.417677+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "257,943 (현재 268,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 319,336"}, {"label": "추세(10일선)", "ok": false, "detail": "269,100"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -6,202 · 기 -2,419"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 288,311"}]}
{"ts": "2026-10-01T09:02:45.301402+09:00", "side": "BUY", "code": "004370", "name": "농심", "price": 406000, "qty": 12, "cost": 4872731, "path": "pullback", "reason": "눌림목 지정가 410,925 도달", "stop": 372500, "target": 456250, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "402,000 vs 395,950"}, {"label": "정배열(5>10)", "ok": true, "detail": "402,900 / 395,950"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "402,900 / 369,758"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+15.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +6 · 기 +5 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.06% (환산) vs 평균 0.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 440,000"}, {"label": "거래량 급증", "ok": false, "detail": "3,460 (환산) vs 평균 31,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.06% (환산) vs 평균 0.51%"}, {"label": "RSI", "ok": true, "detail": "57"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 410,925 (현재 402,000)"}]}
{"ts": "2026-10-01T09:10:41.764132+09:00", "side": "SELL", "code": "088350", "name": "한화생명", "price": 5280, "qty": 1326, "proceeds": 6987628, "entry_price": 5913, "pnl": -854749, "pnl_pct": -10.71, "hold_from": "2026-09-04T12:50:27.780786+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,274 (현재 5,270)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 6,873"}, {"label": "추세(10일선)", "ok": true, "detail": "4,776"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -609 · 기 -421"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-01T09:12:47.860614+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 15160, "qty": 210, "cost": 3184078, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 13113, "target": 18231, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "15,260 vs 13,101"}, {"label": "정배열(5>10)", "ok": true, "detail": "13,626 / 13,101"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "13,626 / 12,642"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+23.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +746 · 기 -275 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "6.79% (환산) vs 평균 2.72% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 14,740"}, {"label": "거래량 급증", "ok": true, "detail": "7,284,333 (환산) vs 평균 2,916,070"}, {"label": "회전율 급증", "ok": true, "detail": "6.79% (환산) vs 평균 2.72%"}, {"label": "RSI", "ok": true, "detail": "70"}]}
{"ts": "2026-10-01T09:38:41.381489+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 15740, "qty": 202, "cost": 3179957, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 13113, "target": 18231, "signals": [{"label": "손절선", "ok": true, "detail": "13,113 (현재 15,720)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,231"}, {"label": "추세(10일선)", "ok": true, "detail": "13,147"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +746 · 기 -275"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 15,690"}]}
{"ts": "2026-10-01T10:38:41.433153+09:00", "side": "SELL", "code": "001790", "name": "대한제당", "price": 2765, "qty": 1897, "proceeds": 5234977, "entry_price": 2945, "pnl": -352526, "pnl_pct": -6.11, "hold_from": "2026-09-01T09:32:25.166635+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "2,767 (현재 2,765)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 3,211"}, {"label": "추세(10일선)", "ok": false, "detail": "2,809"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -67 · 기 +86"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 2,988"}]}
{"ts": "2026-10-01T10:38:41.833351+09:00", "side": "BUY", "code": "122640", "name": "예스티", "price": 34500, "qty": 90, "cost": 3105466, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 29749, "target": 41627, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "34,400 vs 30,080"}, {"label": "정배열(5>10)", "ok": true, "detail": "30,150 / 30,080"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "30,150 / 27,595"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+14.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +49 · 기 +1 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+16%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.97% (환산) vs 평균 1.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 32,800"}, {"label": "거래량 급증", "ok": true, "detail": "860,787 (환산) vs 평균 365,437"}, {"label": "회전율 급증", "ok": true, "detail": "3.97% (환산) vs 평균 1.69%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-10-07T14:42:49.079809+09:00", "side": "SELL", "code": "079650", "name": "서산", "price": 5360, "qty": 462, "proceeds": 2471491, "entry_price": 7131, "pnl": -823683, "pnl_pct": -24.84, "hold_from": "2026-09-04T12:02:28.348862+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,375 (현재 5,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,765"}, {"label": "추세(10일선)", "ok": true, "detail": "4,916"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -89 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-08T09:02:53.389863+09:00", "side": "BUY", "code": "456010", "name": "아이씨티케이", "price": 24250, "qty": 122, "cost": 2958944, "path": "pullback", "reason": "눌림목 지정가 24,970 도달", "stop": 20800, "target": 29425, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "24,450 vs 24,440"}, {"label": "정배열(5>10)", "ok": true, "detail": "25,180 / 24,440"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "25,180 / 17,336"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+55.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +115 · 기 +6 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+145%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.80% (환산) vs 평균 8.21% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 30,350"}, {"label": "거래량 급증", "ok": false, "detail": "116,753 (환산) vs 평균 1,191,082"}, {"label": "회전율 급증", "ok": false, "detail": "0.80% (환산) vs 평균 8.21%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 24,970 (현재 24,450)"}]}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944241+09:00",
"updated_at": "2026-06-15T15:25:02.278741+09:00"
}
@@ -1,13 +1,205 @@
{
"cash": 100000000,
"cash": 44992945.30450006,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944251+09:00",
"updated_at": "2026-06-15T15:25:02.279953+09:00"
"positions": {
"194370": {
"code": "194370",
"name": "제이에스코퍼레이션",
"sources": [
"auto"
],
"qty": 530,
"entry_price": 13160.188679245282,
"entry_at": "2026-08-03T12:00:11.189297+09:00",
"stop": 11445,
"target": 15732,
"peak": 14150,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 12200,
"tranches": 2,
"tranche_value": 3497500.5799993444,
"last_add_price": 13420
},
"252990": {
"code": "252990",
"name": "샘씨엔에스",
"sources": [
"auto"
],
"qty": 272,
"entry_price": 14105.33088235294,
"entry_at": "2026-09-04T11:14:26.495104+09:00",
"stop": 18268,
"target": 18636,
"peak": 20700,
"trailing_on": true,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 13,946 도달",
"cur_price": 18340,
"tranches": 2,
"tranche_value": 1928587.8820308954,
"last_add_price": 14660
},
"041520": {
"code": "041520",
"name": "이엘씨",
"sources": [
"auto"
],
"qty": 562,
"entry_price": 11971.530249110321,
"entry_at": "2026-09-15T09:04:39.495077+09:00",
"stop": 10441,
"target": 14267,
"peak": 13200,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 11,977 도달",
"cur_price": 12790,
"tranches": 2,
"tranche_value": 3370314.5438456116,
"last_add_price": 12170
},
"095610": {
"code": "095610",
"name": "테스",
"sources": [
"auto"
],
"qty": 21,
"entry_price": 158552.38095238095,
"entry_at": "2026-09-22T09:02:46.938295+09:00",
"stop": 121395,
"target": 214288,
"peak": 187400,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 157,986 도달",
"cur_price": 164200,
"tranches": 2,
"tranche_value": 1773948.7536711907,
"last_add_price": 164000
},
"372170": {
"code": "372170",
"name": "윤성에프앤씨",
"sources": [
"auto"
],
"qty": 213,
"entry_price": 20702.816901408452,
"entry_at": "2026-09-23T12:32:47.146539+09:00",
"stop": 16947,
"target": 26336,
"peak": 25500,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 21,336 도달",
"cur_price": 23800,
"tranches": 2,
"tranche_value": 2212317.899773747,
"last_add_price": 21400
},
"025860": {
"code": "025860",
"name": "남해화학",
"sources": [
"watch"
],
"qty": 895,
"entry_price": 6050,
"entry_at": "2026-09-28T09:04:45.106987+09:00",
"stop": 5730,
"target": 6530,
"peak": 6180,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 6,148 도달",
"cur_price": 5940,
"tranches": 1,
"tranche_value": 5419734.413812503,
"last_add_price": 6050
},
"051900": {
"code": "051900",
"name": "LG생활건강",
"sources": [
"auto"
],
"qty": 14,
"entry_price": 271500,
"entry_at": "2026-10-01T09:02:45.598918+09:00",
"stop": 242017,
"target": 315725,
"peak": 277000,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 277,258 도달",
"cur_price": 269500,
"tranches": 1,
"tranche_value": 3971189.5843109195,
"last_add_price": 271500
},
"056360": {
"code": "056360",
"name": "코위버",
"sources": [
"watch"
],
"qty": 731,
"entry_price": 6360,
"entry_at": "2026-10-08T09:50:45.349011+09:00",
"stop": 5770,
"target": 7245,
"peak": 6390,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 6,523 도달",
"cur_price": 6210,
"tranches": 1,
"tranche_value": 4653764.676570003,
"last_add_price": 6360
}
},
"realized_pnl": -15857423.131500036,
"closed_trades": 45,
"wins": 7,
"peak_equity": 101529111.72700004,
"max_drawdown": 0.16624243648348033,
"last_exit": {
"030000": "2026-09-04",
"086790": "2026-09-23",
"403870": "2026-07-07",
"095610": "2026-07-29",
"093370": "2026-07-20",
"240810": "2026-07-24",
"214450": "2026-09-10",
"034730": "2026-07-07",
"055550": "2026-07-20",
"024060": "2026-07-27",
"003680": "2026-08-24",
"252990": "2026-09-01",
"010950": "2026-09-03",
"090850": "2026-08-05",
"078930": "2026-07-31",
"068270": "2026-09-04",
"138040": "2026-09-08",
"003550": "2026-09-15",
"004370": "2026-09-21",
"000720": "2026-09-28",
"316140": "2026-10-01",
"079650": "2026-10-08"
},
"created_at": "2026-06-11T09:00:01.944251+09:00",
"updated_at": "2026-10-08T15:28:47.141332+09:00"
}
@@ -0,0 +1,131 @@
{"ts": "2026-06-17T11:15:02.304318+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 130900, "qty": 38, "cost": 4974946, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 118039, "target": 150191, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "129,300 vs 118,950"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,400 / 118,950"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,400 / 118,307"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-13.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +371 · 기 +158 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+22%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.12% (환산) vs 평균 0.38% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 126,500"}, {"label": "거래량 급증", "ok": true, "detail": "3,094,534 (환산) vs 평균 1,031,725"}, {"label": "회전율 급증", "ok": true, "detail": "1.12% (환산) vs 평균 0.38%"}, {"label": "RSI", "ok": true, "detail": "60"}]}
{"ts": "2026-06-17T16:47:28.483754+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 62900, "qty": 35, "cost": 2201830, "path": "pullback", "reason": "눌림목 지정가 64,531 도달", "stop": 48962, "target": 83807, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "62,800 vs 53,510"}, {"label": "정배열(5>10)", "ok": true, "detail": "59,880 / 53,510"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "59,880 / 49,831"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -4,434 · 기 +5,040 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+9%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "11.31% vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "9,309,211 vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": true, "detail": "11.31% vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "58"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 64,531 (현재 62,800)"}]}
{"ts": "2026-06-19T09:02:06.344845+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19160, "qty": 325, "cost": 6227934, "path": "pullback", "reason": "눌림목 지정가 20,044 도달", "stop": 17760, "target": 21260, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,150 vs 18,963"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,616 / 18,963"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,616 / 19,361"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-31.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,162 · 기 +2,080 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "37,673 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,044 (현재 19,150)"}]}
{"ts": "2026-06-23T09:00:09.342852+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18820, "qty": 325, "proceeds": 6104573, "entry_price": 19160, "pnl": -123361, "pnl_pct": -1.77, "hold_from": "2026-06-19T09:02:06.344843+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,710)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,260"}, {"label": "추세(10일선)", "ok": false, "detail": "18,919"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -318 · 기 -227"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,528"}]}
{"ts": "2026-06-23T12:04:04.079542+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 117700, "qty": 38, "proceeds": 4463878, "entry_price": 130900, "pnl": -511068, "pnl_pct": -10.08, "hold_from": "2026-06-17T11:15:02.304317+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "118,039 (현재 117,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 150,191"}, {"label": "추세(10일선)", "ok": false, "detail": "119,330"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +332 · 기 -364"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 134,300"}]}
{"ts": "2026-06-24T09:04:06.111950+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 156700, "qty": 13, "cost": 2037406, "path": "pullback", "reason": "눌림목 지정가 165,075 도달", "stop": 120251, "target": 211374, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "156,200 vs 128,170"}, {"label": "정배열(5>10)", "ok": true, "detail": "143,260 / 128,170"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "143,260 / 121,703"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +143 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.83% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "2,860,073 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "5.83% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 165,075 (현재 156,200)"}]}
{"ts": "2026-06-24T09:12:05.347423+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 17270, "qty": 150, "cost": 2590889, "path": "pullback", "reason": "눌림목 지정가 17,366 도달", "stop": 13981, "target": 22203, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "17,280 vs 13,750"}, {"label": "정배열(5>10)", "ok": true, "detail": "16,364 / 13,750"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,364 / 11,500"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +1,740 · 기 -508 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+16%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.34% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": false, "detail": "2,509,340 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": false, "detail": "2.34% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "65"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 17,366 (현재 17,280)"}]}
{"ts": "2026-06-24T09:46:04.779355+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158200, "qty": 13, "cost": 2056908, "path": "pullback", "reason": "눌림목 지정가 158,264 도달", "stop": 120729, "target": 214407, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "158,000 vs 153,380"}, {"label": "정배열(5>10)", "ok": true, "detail": "172,240 / 153,380"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,240 / 99,750"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+56.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +41 · 기 +65 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+5%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.25% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "822,940 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "4.25% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,264 (현재 158,000)"}]}
{"ts": "2026-06-24T11:12:05.896210+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 18260, "qty": 142, "cost": 2593309, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 13981, "target": 22203, "signals": [{"label": "손절선", "ok": true, "detail": "13,981 (현재 18,250)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,203"}, {"label": "추세(10일선)", "ok": true, "detail": "13,847"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +1,742 · 기 -508"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 18,094"}]}
{"ts": "2026-06-25T09:00:23.474580+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 179100, "qty": 11, "cost": 1970396, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 120729, "target": 214407, "signals": [{"label": "손절선", "ok": true, "detail": "120,729 (현재 178,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 214,407"}, {"label": "추세(10일선)", "ok": true, "detail": "155,410"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -56 · 기 +134"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 167,275"}]}
{"ts": "2026-06-26T09:00:05.584610+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 50000, "qty": 35, "proceeds": 1746588, "entry_price": 62900, "pnl": -455243, "pnl_pct": -20.51, "hold_from": "2026-06-17T16:47:28.483745+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "48,962 (현재 51,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 83,807"}, {"label": "추세(10일선)", "ok": false, "detail": "52,330"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -311 · 기 -267"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 66,756"}]}
{"ts": "2026-06-26T10:14:04.057812+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 166900, "qty": 12, "cost": 2003100, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 120251, "target": 211374, "signals": [{"label": "손절선", "ok": true, "detail": "120,251 (현재 166,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 211,374"}, {"label": "추세(10일선)", "ok": true, "detail": "129,220"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -126 · 기 +320"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 165,659"}]}
{"ts": "2026-06-29T11:36:01.262629+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 745000, "qty": 3, "cost": 2235335, "path": "pullback", "reason": "눌림목 지정가 748,914 도달", "stop": 612829, "target": 943257, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "744,000 vs 735,600"}, {"label": "정배열(5>10)", "ok": true, "detail": "766,800 / 735,600"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "766,800 / 530,600"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -194 · 기 +152 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+8%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.35% (환산) vs 평균 0.45% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 883,000"}, {"label": "거래량 급증", "ok": true, "detail": "977,966 (환산) vs 평균 329,863"}, {"label": "회전율 급증", "ok": true, "detail": "1.35% (환산) vs 평균 0.45%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 748,914 (현재 744,000)"}]}
{"ts": "2026-06-29T14:18:01.482930+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 778000, "qty": 3, "cost": 2334350, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 612829, "target": 943257, "signals": [{"label": "손절선", "ok": true, "detail": "612,829 (현재 779,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 943,257"}, {"label": "추세(10일선)", "ok": true, "detail": "739,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -200 · 기 +143"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 778,043"}]}
{"ts": "2026-06-30T09:00:05.540133+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 55300, "qty": 41, "cost": 2267640, "path": "pullback", "reason": "눌림목 지정가 63,981 도달", "stop": 43200, "target": 73450, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,700 vs 52,600"}, {"label": "정배열(5>10)", "ok": true, "detail": "58,060 / 52,600"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "58,060 / 49,679"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +222 · 기 -821 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+28%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "827 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 63,981 (현재 53,700)"}]}
{"ts": "2026-07-01T09:06:03.206698+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 357500, "qty": 11, "cost": 3933090, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 314220, "target": 422420, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "357,000 vs 307,050"}, {"label": "정배열(5>10)", "ok": true, "detail": "317,600 / 307,050"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "317,600 / 305,700"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -75 · 기 +221 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+35%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.89% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "196,247 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.89% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "68"}]}
{"ts": "2026-07-01T09:24:02.307379+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 372000, "qty": 11, "cost": 4092614, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 314220, "target": 422420, "signals": [{"label": "손절선", "ok": true, "detail": "314,220 (현재 372,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 422,420"}, {"label": "추세(10일선)", "ok": true, "detail": "308,550"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -75 · 기 +221"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 368,499"}]}
{"ts": "2026-07-01T11:06:02.349355+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 212000, "qty": 24, "proceeds": 5078078, "entry_price": 167779, "pnl": 1050774, "pnl_pct": 26.36, "hold_from": "2026-06-24T09:46:04.779353+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "213,193 (현재 213,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "158,880"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -229 · 기 +300"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T10:18:03.827422+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 122200, "qty": 43, "cost": 5255388, "path": "pullback", "reason": "눌림목 지정가 122,215 도달", "stop": 111000, "target": 139000, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "122,000 vs 119,760"}, {"label": "정배열(5>10)", "ok": true, "detail": "121,000 / 119,760"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "121,000 / 118,432"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +22 · 기 -44 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+29%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.80% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": true, "detail": "2,199,199 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": true, "detail": "0.80% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 122,215 (현재 122,000)"}]}
{"ts": "2026-07-02T10:40:02.948677+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18960, "qty": 324, "cost": 6143961, "path": "pullback", "reason": "눌림목 지정가 19,970 도달", "stop": 17760, "target": 20760, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,960 vs 18,944"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,578 / 18,944"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,578 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -339 · 기 +110 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.86% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "986,112 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.86% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,970 (현재 18,960)"}]}
{"ts": "2026-07-02T11:20:02.051721+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 22550, "qty": 273, "cost": 6157073, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 20760, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 19,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,760"}, {"label": "추세(10일선)", "ok": true, "detail": "18,983"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,350"}]}
{"ts": "2026-07-02T11:24:06.143347+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18860, "qty": 597, "proceeds": 11237464, "entry_price": 20602, "pnl": -1063571, "pnl_pct": -8.45, "hold_from": "2026-07-02T10:40:02.948676+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,042 (현재 18,890)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,942"}, {"label": "추세(10일선)", "ok": false, "detail": "18,937"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T09:04:01.665799+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 14370, "qty": 292, "proceeds": 4187858, "entry_price": 17751, "pnl": -996340, "pnl_pct": -19.05, "hold_from": "2026-06-24T09:12:05.347421+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "14,457 (현재 14,390)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,693"}, {"label": "추세(10일선)", "ok": true, "detail": "13,461"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,591 · 기 +253"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T09:28:01.636551+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 125700, "qty": 25, "proceeds": 3136372, "entry_price": 161596, "pnl": -904134, "pnl_pct": -22.21, "hold_from": "2026-06-24T09:04:06.111949+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "125,761 (현재 125,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 215,349"}, {"label": "추세(10일선)", "ok": true, "detail": "125,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +674 · 기 -2,170"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T10:16:02.526460+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 126200, "qty": 42, "cost": 5301195, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111000, "target": 139000, "signals": [{"label": "손절선", "ok": true, "detail": "111,000 (현재 126,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 139,000"}, {"label": "추세(10일선)", "ok": true, "detail": "120,180"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -140 · 기 +278"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 125,493"}]}
{"ts": "2026-07-03T10:24:02.768194+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19040, "qty": 314, "cost": 5979457, "path": "pullback", "reason": "눌림목 지정가 20,004 도달", "stop": 17760, "target": 20960, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,030 vs 18,951"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,592 / 18,951"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,592 / 19,359"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -35 · 기 +168 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.54% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "619,193 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.54% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,004 (현재 19,030)"}]}
{"ts": "2026-07-06T09:00:08.046896+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 130600, "qty": 13, "cost": 1698055, "path": "pullback", "reason": "눌림목 지정가 164,068 도달", "stop": 96000, "target": 182500, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "133,100 vs 125,860"}, {"label": "정배열(5>10)", "ok": true, "detail": "138,640 / 125,860"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "138,640 / 121,318"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +782 · 기 -1,979 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.01% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": false, "detail": "4,080 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": false, "detail": "0.01% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 164,068 (현재 133,100)"}]}
{"ts": "2026-07-06T09:00:08.048351+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 14510, "qty": 130, "cost": 1886583, "path": "pullback", "reason": "눌림목 지정가 17,169 도달", "stop": 10827, "target": 20034, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,490 vs 13,471"}, {"label": "정배열(5>10)", "ok": true, "detail": "15,806 / 13,471"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,806 / 11,453"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+7.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,226 · 기 +134 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.02% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": false, "detail": "20,667 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": false, "detail": "0.02% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "54"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 17,169 (현재 14,490)"}]}
{"ts": "2026-07-06T09:46:01.643722+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 320500, "qty": 22, "proceeds": 7037251, "entry_price": 364750, "pnl": -988453, "pnl_pct": -12.13, "hold_from": "2026-07-01T09:06:03.206697+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "320,756 (현재 320,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 430,741"}, {"label": "추세(10일선)", "ok": true, "detail": "303,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -111 · 기 +227"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-07T09:12:07.261676+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 155900, "qty": 12, "cost": 1871081, "path": "pullback", "reason": "눌림목 지정가 158,314 도달", "stop": 118529, "target": 211957, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "155,800 vs 153,160"}, {"label": "정배열(5>10)", "ok": true, "detail": "171,800 / 153,160"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,800 / 99,713"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +80 · 기 -197 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+14%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.33% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "645,567 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "3.33% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,314 (현재 155,800)"}]}
{"ts": "2026-07-07T09:48:07.378539+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 43000, "qty": 41, "proceeds": 1759562, "entry_price": 55300, "pnl": -508078, "pnl_pct": -22.24, "hold_from": "2026-06-30T09:00:05.540126+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "43,200 (현재 43,050)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 73,450"}, {"label": "추세(10일선)", "ok": false, "detail": "51,535"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +1,424 · 기 -5,706"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 59,449"}]}
{"ts": "2026-07-07T10:30:07.357407+09:00", "side": "SELL", "code": "034730", "name": "SK", "price": 628000, "qty": 6, "proceeds": 3760652, "entry_price": 761500, "pnl": -809033, "pnl_pct": -17.53, "hold_from": "2026-06-29T11:36:01.262627+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "629,329 (현재 628,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 959,757"}, {"label": "추세(10일선)", "ok": false, "detail": "724,000"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -58 · 기 +6"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-07T12:02:08.175480+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 12230, "qty": 130, "proceeds": 1586800, "entry_price": 14510, "pnl": -299783, "pnl_pct": -15.71, "hold_from": "2026-07-06T09:00:08.048341+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "10,827 (현재 12,230)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,034"}, {"label": "추세(10일선)", "ok": false, "detail": "13,245"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -1,558 · 기 -4"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 15,512"}]}
{"ts": "2026-07-07T14:08:22.267417+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 177000, "qty": 11, "cost": 1947292, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 118529, "target": 211957, "signals": [{"label": "손절선", "ok": true, "detail": "118,529 (현재 177,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 211,957"}, {"label": "추세(10일선)", "ok": true, "detail": "148,710"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +152 · 기 -281"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 165,142"}]}
{"ts": "2026-07-07T14:08:22.269186+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 20750, "qty": 288, "cost": 5976896, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 20960, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,960"}, {"label": "추세(10일선)", "ok": true, "detail": "18,901"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 +113"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,358"}]}
{"ts": "2026-07-08T09:00:17.041286+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 108300, "qty": 13, "proceeds": 1405155, "entry_price": 130600, "pnl": -292900, "pnl_pct": -17.08, "hold_from": "2026-07-06T09:00:08.046892+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "166,450 (현재 112,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": false, "detail": "123,820"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +243 · 기 -857"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-09T09:36:03.450093+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 132000, "qty": 30, "cost": 3960594, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 117015, "target": 154477, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "131,900 vs 108,660"}, {"label": "정배열(5>10)", "ok": true, "detail": "115,560 / 108,660"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,560 / 113,228"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+22.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -184 · 기 +964 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.91% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "1,026,867 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.91% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-10T11:06:06.463233+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 13660, "qty": 143, "cost": 1953673, "path": "pullback", "reason": "눌림목 지정가 16,984 도달", "stop": 10360, "target": 18610, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,630 vs 13,385"}, {"label": "정배열(5>10)", "ok": true, "detail": "15,634 / 13,385"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,634 / 11,439"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +239 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+47%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "14.23% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "15,267,605 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "14.23% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,984 (현재 13,630)"}]}
{"ts": "2026-07-10T12:14:02.664485+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 126200, "qty": 15, "cost": 1893284, "path": "pullback", "reason": "눌림목 지정가 162,268 도달", "stop": 96000, "target": 171500, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "126,200 vs 125,170"}, {"label": "정배열(5>10)", "ok": true, "detail": "137,260 / 125,170"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "137,260 / 121,203"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +483 · 기 -241 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "7.53% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,695,762 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "7.53% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 162,268 (현재 126,200)"}]}
{"ts": "2026-07-13T09:00:09.784350+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 136900, "qty": 29, "cost": 3970696, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 117015, "target": 154477, "signals": [{"label": "손절선", "ok": true, "detail": "117,015 (현재 136,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,477"}, {"label": "추세(10일선)", "ok": true, "detail": "109,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -64 · 기 +919"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,864"}]}
{"ts": "2026-07-13T09:12:04.291897+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 341500, "qty": 11, "cost": 3757063, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 300363, "target": 403206, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "341,000 vs 305,450"}, {"label": "정배열(5>10)", "ok": true, "detail": "314,400 / 305,450"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "314,400 / 305,433"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -63 · 기 +144 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.90% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "197,687 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.90% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-07-13T09:26:03.403270+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 139100, "qty": 14, "cost": 1947692, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 96000, "target": 171500, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 139,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 171,500"}, {"label": "추세(10일선)", "ok": true, "detail": "126,460"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -213 · 기 +772"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 136,162"}]}
{"ts": "2026-07-13T10:04:06.404637+09:00", "side": "BUY", "code": "055550", "name": "신한지주", "price": 113100, "qty": 42, "cost": 4750913, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 101931, "target": 129854, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "113,050 vs 100,265"}, {"label": "정배열(5>10)", "ok": true, "detail": "101,790 / 100,265"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "101,790 / 96,676"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+31.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -368 · 기 +1,005 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.71% (환산) vs 평균 0.35% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 109,800"}, {"label": "거래량 급증", "ok": true, "detail": "3,366,381 (환산) vs 평균 1,653,581"}, {"label": "회전율 급증", "ok": true, "detail": "0.71% (환산) vs 평균 0.35%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-15T14:32:01.876272+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18570, "qty": 602, "proceeds": 11157341, "entry_price": 19858, "pnl": -799013, "pnl_pct": -6.49, "hold_from": "2026-07-03T10:24:02.768192+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "18,586 (현재 18,570)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,766"}, {"label": "추세(10일선)", "ok": false, "detail": "18,905"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -214 · 기 +240"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-16T09:00:08.142600+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 209000, "qty": 23, "proceeds": 4797626, "entry_price": 165991, "pnl": 979254, "pnl_pct": 25.91, "hold_from": "2026-07-07T09:12:07.261674+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "210,764 (현재 207,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "158,280"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -799 · 기 +891"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-16T09:28:03.474549+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 353000, "qty": 11, "cost": 3883582, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 300363, "target": 403206, "signals": [{"label": "손절선", "ok": true, "detail": "300,363 (현재 352,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 403,206"}, {"label": "추세(10일선)", "ok": true, "detail": "306,600"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -33 · 기 +28"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 351,963"}]}
{"ts": "2026-07-20T11:28:01.183314+09:00", "side": "SELL", "code": "055550", "name": "신한지주", "price": 101900, "qty": 42, "proceeds": 4271454, "entry_price": 113100, "pnl": -479458, "pnl_pct": -9.9, "hold_from": "2026-07-13T10:04:06.404635+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "101,931 (현재 101,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 129,854"}, {"label": "추세(10일선)", "ok": true, "detail": "99,150"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -156 · 기 +275"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 115,714"}]}
{"ts": "2026-07-20T12:28:02.308175+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 10310, "qty": 143, "proceeds": 1471455, "entry_price": 13660, "pnl": -482218, "pnl_pct": -24.52, "hold_from": "2026-07-10T11:06:06.463231+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,360 (현재 10,310)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,610"}, {"label": "추세(10일선)", "ok": false, "detail": "12,013"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -887 · 기 +515"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,626"}]}
{"ts": "2026-07-22T14:18:05.417998+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14030, "qty": 158, "cost": 2217073, "path": "pullback", "reason": "눌림목 지정가 14,023 도달", "stop": 11077, "target": 18460, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,020 vs 12,975"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,552 / 12,975"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,552 / 13,283"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,005 · 기 +1,723 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.97% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "585,754 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.97% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "53"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 14,023 (현재 14,020)"}]}
{"ts": "2026-07-22T15:04:00.988784+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158100, "qty": 12, "cost": 1897485, "path": "pullback", "reason": "눌림목 지정가 158,107 도달", "stop": 120314, "target": 214778, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "158,000 vs 153,380"}, {"label": "정배열(5>10)", "ok": true, "detail": "172,240 / 153,380"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,240 / 99,750"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+49.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -311 · 기 +22 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.02% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "971,360 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": true, "detail": "5.02% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,107 (현재 158,000)"}]}
{"ts": "2026-07-23T09:50:01.507037+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 88200, "qty": 44, "cost": 3881382, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 77564, "target": 104155, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "88,200 vs 80,900"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,000 / 80,900"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,000 / 75,058"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+47.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -150 · 기 +299 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.85% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "785,587 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.85% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-23T10:24:01.496618+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 152100, "qty": 12, "proceeds": 1821641, "entry_price": 158100, "pnl": -75844, "pnl_pct": -3.8, "hold_from": "2026-07-22T15:04:00.988783+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "120,314 (현재 152,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 214,778"}, {"label": "추세(10일선)", "ok": false, "detail": "152,780"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -101 · 기 -107"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 167,575"}]}
{"ts": "2026-07-23T10:30:02.476529+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 14140, "qty": 192, "cost": 2715287, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11706, "target": 17791, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,180 vs 12,429"}, {"label": "정배열(5>10)", "ok": true, "detail": "13,154 / 12,429"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "13,154 / 12,687"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+55.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +109 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "84.26% (환산) vs 평균 31.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 13,990"}, {"label": "거래량 급증", "ok": true, "detail": "12,639,979 (환산) vs 평균 4,720,532"}, {"label": "회전율 급증", "ok": true, "detail": "84.26% (환산) vs 평균 31.47%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-23T12:54:04.489474+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90900, "qty": 42, "cost": 3818373, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 77564, "target": 104155, "signals": [{"label": "손절선", "ok": true, "detail": "77,564 (현재 91,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 104,155"}, {"label": "추세(10일선)", "ok": true, "detail": "81,180"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -169 · 기 +311"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 90,945"}]}
{"ts": "2026-07-24T09:00:05.529199+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 111800, "qty": 29, "proceeds": 3235878, "entry_price": 132428, "pnl": -605098, "pnl_pct": -15.58, "hold_from": "2026-07-10T12:14:02.664484+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 113,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 187,069"}, {"label": "추세(10일선)", "ok": false, "detail": "123,880"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -480 · 기 -206"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-24T09:00:05.530375+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 155500, "qty": 12, "cost": 1866280, "path": "pullback", "reason": "눌림목 지정가 158,093 도달", "stop": 117686, "target": 212221, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "155,000 vs 153,080"}, {"label": "정배열(5>10)", "ok": true, "detail": "171,640 / 153,080"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,640 / 99,700"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+44.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +200 · 기 -454 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+27%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.77% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "730,327 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "3.77% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,093 (현재 155,000)"}]}
{"ts": "2026-07-24T09:02:04.036333+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 15060, "qty": 181, "cost": 2726269, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11706, "target": 17791, "signals": [{"label": "손절선", "ok": true, "detail": "11,706 (현재 15,020)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,791"}, {"label": "추세(10일선)", "ok": true, "detail": "12,513"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +208 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,766"}]}
{"ts": "2026-07-24T09:28:04.942689+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14510, "qty": 153, "cost": 2220363, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11077, "target": 18460, "signals": [{"label": "손절선", "ok": true, "detail": "11,077 (현재 15,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,460"}, {"label": "추세(10일선)", "ok": true, "detail": "13,113"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -857 · 기 +1,172"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,791"}]}
{"ts": "2026-07-24T10:02:04.391526+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 312, "cost": 5913287, "path": "pullback", "reason": "눌림목 지정가 20,012 도달", "stop": 17760, "target": 20735, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,950 vs 18,943"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,576 / 18,943"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,576 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+19.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +2 · 기 -5 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.39% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "454,069 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.39% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,012 (현재 18,950)"}]}
{"ts": "2026-07-27T09:10:01.034991+09:00", "side": "SELL", "code": "024060", "name": "흥구석유", "price": 12030, "qty": 373, "proceeds": 4478440, "entry_price": 14586, "pnl": -963116, "pnl_pct": -17.53, "hold_from": "2026-07-23T10:30:02.476527+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "12,083 (현재 12,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,342"}, {"label": "추세(10일선)", "ok": false, "detail": "12,214"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -85 · 기 +2"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-27T09:12:01.501479+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7850, "qty": 420, "cost": 3297495, "path": "pullback", "reason": "눌림목 지정가 7,907 도달", "stop": 6743, "target": 9510, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "7,840 vs 5,428"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,526 / 5,428"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,526 / 4,895"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+91.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +49 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "11.17% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "693,433 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": true, "detail": "11.17% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": false, "detail": "80"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,907 (현재 7,840)"}]}
{"ts": "2026-07-27T12:56:04.228682+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 8300, "qty": 397, "cost": 3295594, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 6743, "target": 9510, "signals": [{"label": "손절선", "ok": true, "detail": "6,743 (현재 8,310)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,510"}, {"label": "추세(10일선)", "ok": true, "detail": "5,476"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +50 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 8,127"}]}
{"ts": "2026-07-27T13:38:04.225254+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6880, "qty": 817, "proceeds": 5609999, "entry_price": 8069, "pnl": -983090, "pnl_pct": -14.73, "hold_from": "2026-07-27T09:12:01.501476+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "6,962 (현재 6,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,729"}, {"label": "추세(10일선)", "ok": true, "detail": "5,334"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +50 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-28T09:00:15.725820+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6780, "qty": 394, "cost": 2671721, "path": "pullback", "reason": "눌림목 지정가 7,884 도달", "stop": 5628, "target": 8509, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "7,050 vs 5,350"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,368 / 5,350"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,368 / 4,882"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+73.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +86 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.01% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "813 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "0.01% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "69"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,884 (현재 7,050)"}]}
{"ts": "2026-07-28T09:10:01.736195+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7100, "qty": 376, "cost": 2670000, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5628, "target": 8509, "signals": [{"label": "손절선", "ok": true, "detail": "5,628 (현재 7,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,509"}, {"label": "추세(10일선)", "ok": true, "detail": "5,354"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +86 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,079"}]}
{"ts": "2026-07-28T10:08:02.763925+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 11170, "qty": 311, "proceeds": 3467096, "entry_price": 14266, "pnl": -970340, "pnl_pct": -21.7, "hold_from": "2026-07-22T14:18:05.417997+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "11,223 (현재 11,220)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,831"}, {"label": "추세(10일선)", "ok": false, "detail": "12,695"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +106 · 기 -64"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:02:02.218183+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 117900, "qty": 47, "cost": 5542131, "path": "pullback", "reason": "눌림목 지정가 118,115 도달", "stop": 111200, "target": 127950, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "117,800 vs 116,390"}, {"label": "정배열(5>10)", "ok": true, "detail": "117,020 / 116,390"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "117,020 / 109,982"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+45.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +39 · 기 +307 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+26%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": false, "detail": "44,660 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "57"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 118,115 (현재 117,800)"}]}
{"ts": "2026-07-29T09:16:04.458945+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 120500, "qty": 46, "cost": 5543831, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111200, "target": 127950, "signals": [{"label": "손절선", "ok": true, "detail": "111,200 (현재 120,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 127,950"}, {"label": "추세(10일선)", "ok": true, "detail": "116,660"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +39 · 기 +307"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 120,492"}]}
{"ts": "2026-07-29T09:24:08.688483+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 122600, "qty": 15, "cost": 1839276, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 117686, "target": 212221, "signals": [{"label": "손절선", "ok": true, "detail": "117,686 (현재 177,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 212,221"}, {"label": "추세(10일선)", "ok": true, "detail": "148,710"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +453 · 기 -688"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 164,742"}]}
{"ts": "2026-07-29T09:24:08.688690+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 128600, "qty": 59, "proceeds": 7572605, "entry_price": 134408, "pnl": -358685, "pnl_pct": -4.32, "hold_from": "2026-07-09T09:36:03.450092+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "118,952 (현재 116,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 157,593"}, {"label": "추세(10일선)", "ok": true, "detail": "105,620"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +245"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:26:01.891082+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18940, "qty": 312, "proceeds": 5897757, "entry_price": 18950, "pnl": -15530, "pnl_pct": -0.05, "hold_from": "2026-07-24T10:02:04.391524+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,645 (현재 18,950)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "18,943"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +99 · 기 +138"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:28:01.377044+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6390, "qty": 850, "cost": 5432315, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5867, "target": 7174, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,380 vs 5,591"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,742 / 5,591"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,742 / 5,622"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +73 · 기 -3 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.62% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "134,413 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.62% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "73"}]}
{"ts": "2026-07-29T10:18:05.366579+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 116400, "qty": 27, "proceeds": 3136672, "entry_price": 137222, "pnl": -568884, "pnl_pct": -15.17, "hold_from": "2026-07-24T09:00:05.530369+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "117,686 (현재 115,800)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 192,006"}, {"label": "추세(10일선)", "ok": false, "detail": "149,160"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +445 · 기 -685"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T12:04:01.681905+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 119200, "qty": 85, "proceeds": 10112243, "entry_price": 124176, "pnl": -444341, "pnl_pct": -4.01, "hold_from": "2026-07-02T10:18:03.827419+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "111,006 (현재 119,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 143,933"}, {"label": "추세(10일선)", "ok": false, "detail": "119,490"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -11 · 기 -51"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T13:06:06.472442+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5690, "qty": 770, "proceeds": 4372756, "entry_price": 6936, "pnl": -968965, "pnl_pct": -17.97, "hold_from": "2026-07-28T09:00:15.725812+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,741 (현재 5,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,729"}, {"label": "추세(10일선)", "ok": true, "detail": "5,214"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +185 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T13:34:03.581955+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5860, "qty": 850, "proceeds": 4971287, "entry_price": 6390, "pnl": -461028, "pnl_pct": -8.29, "hold_from": "2026-07-29T09:28:01.377043+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,867 (현재 5,860)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,174"}, {"label": "추세(10일선)", "ok": true, "detail": "5,539"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +73 · 기 -3"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,525"}]}
{"ts": "2026-07-30T09:04:06.862580+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 121300, "qty": 42, "cost": 5095364, "path": "pullback", "reason": "눌림목 지정가 122,665 도달", "stop": 111000, "target": 136750, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "121,200 vs 119,680"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,840 / 119,680"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,840 / 118,418"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+34.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -214 · 기 +182 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+36%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.18% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": false, "detail": "494,880 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": false, "detail": "0.18% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 122,665 (현재 121,200)"}]}
{"ts": "2026-07-30T09:04:06.862756+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19090, "qty": 286, "cost": 5460559, "path": "pullback", "reason": "눌림목 지정가 20,019 도달", "stop": 17760, "target": 21085, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,100 vs 18,958"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,606 / 18,958"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,606 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+29.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +2 · 기 +327 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.14% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "160,727 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.14% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,019 (현재 19,100)"}]}
{"ts": "2026-07-30T09:04:06.863066+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6130, "qty": 326, "cost": 1998680, "path": "pullback", "reason": "눌림목 지정가 7,792 도달", "stop": 4793, "target": 8135, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,120 vs 5,256"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,182 / 5,256"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,182 / 4,866"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+65.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +234 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.31% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "267,687 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "4.31% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,792 (현재 6,120)"}]}
{"ts": "2026-07-30T09:16:11.313716+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 125200, "qty": 41, "cost": 5133970, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111000, "target": 136750, "signals": [{"label": "손절선", "ok": true, "detail": "111,000 (현재 125,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 136,750"}, {"label": "추세(10일선)", "ok": true, "detail": "120,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -214 · 기 +182"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 124,618"}]}
{"ts": "2026-07-30T09:24:06.914861+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 131800, "qty": 29, "cost": 3822773, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 116987, "target": 154020, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "132,000 vs 108,670"}, {"label": "정배열(5>10)", "ok": true, "detail": "115,580 / 108,670"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,580 / 113,230"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+51.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -6 · 기 +178 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.66% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "738,813 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.66% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-30T09:26:06.866302+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6300, "qty": 865, "cost": 5450317, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5789, "target": 7067, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,280 vs 5,581"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,722 / 5,581"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,722 / 5,620"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +87 · 기 +6 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.51% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "109,080 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.51% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "71"}]}
{"ts": "2026-07-31T09:06:06.410935+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6550, "qty": 305, "cost": 1998050, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 4793, "target": 8135, "signals": [{"label": "손절선", "ok": true, "detail": "4,793 (현재 6,640)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,135"}, {"label": "추세(10일선)", "ok": true, "detail": "5,308"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +355 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,453"}]}
{"ts": "2026-07-31T09:06:06.411045+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 77700, "qty": 86, "proceeds": 6669170, "entry_price": 89519, "pnl": -1030585, "pnl_pct": -13.2, "hold_from": "2026-07-23T09:50:01.507036+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "78,539 (현재 78,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 105,988"}, {"label": "추세(10일선)", "ok": false, "detail": "79,880"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -179 · 기 +325"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-03T12:00:11.189299+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 12910, "qty": 270, "cost": 3486223, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11314, "target": 15304, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "12,930 vs 12,003"}, {"label": "정배열(5>10)", "ok": true, "detail": "12,138 / 12,003"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "12,138 / 11,924"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +7 · 기 +10 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+74%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.72% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 12,870"}, {"label": "거래량 급증", "ok": true, "detail": "208,994 (환산) vs 평균 99,303"}, {"label": "회전율 급증", "ok": true, "detail": "0.72% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-03T12:02:09.144443+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 13420, "qty": 260, "cost": 3489723, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11314, "target": 15304, "signals": [{"label": "손절선", "ok": true, "detail": "11,314 (현재 13,450)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 15,304"}, {"label": "추세(10일선)", "ok": true, "detail": "12,055"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +7 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,339"}]}
{"ts": "2026-08-05T09:02:12.583140+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5700, "qty": 865, "proceeds": 4920886, "entry_price": 6300, "pnl": -529432, "pnl_pct": -9.52, "hold_from": "2026-07-30T09:26:06.866300+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,789 (현재 5,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,067"}, {"label": "추세(10일선)", "ok": true, "detail": "5,526"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +14 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,413"}]}
{"ts": "2026-08-05T09:02:12.583322+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13040, "qty": 160, "cost": 2086713, "path": "pullback", "reason": "눌림목 지정가 13,947 도달", "stop": 10340, "target": 17090, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,040 vs 12,877"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,356 / 12,877"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,356 / 13,267"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-6.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -72 · 기 +134 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.13% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "78,333 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.13% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,947 (현재 13,040)"}]}
{"ts": "2026-08-05T09:38:10.849723+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13830, "qty": 151, "cost": 2088643, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10340, "target": 17090, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 13,860)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,090"}, {"label": "추세(10일선)", "ok": true, "detail": "12,959"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -72 · 기 +134"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,817"}]}
{"ts": "2026-08-06T09:00:17.815534+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18730, "qty": 286, "proceeds": 5346334, "entry_price": 19090, "pnl": -114225, "pnl_pct": -1.89, "hold_from": "2026-07-30T09:04:06.862754+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,085"}, {"label": "추세(10일선)", "ok": false, "detail": "18,923"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -203 · 기 -147"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,459"}]}
{"ts": "2026-08-06T09:34:15.272349+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 192000, "qty": 26, "cost": 4992749, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 175344, "target": 216984, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "191,800 vs 180,670"}, {"label": "정배열(5>10)", "ok": true, "detail": "186,140 / 180,670"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "186,140 / 176,671"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +255 · 기 -319 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+44%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.67% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 191,700"}, {"label": "거래량 급증", "ok": true, "detail": "1,582,767 (환산) vs 평균 738,783"}, {"label": "회전율 급증", "ok": true, "detail": "0.67% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-07T09:12:15.074213+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 196400, "qty": 25, "cost": 4910736, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 175344, "target": 216984, "signals": [{"label": "손절선", "ok": true, "detail": "175,344 (현재 196,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 216,984"}, {"label": "추세(10일선)", "ok": true, "detail": "181,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +338 · 기 +73"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 196,264"}]}
{"ts": "2026-08-10T09:00:14.888891+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 136000, "qty": 28, "cost": 3808571, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 116987, "target": 154020, "signals": [{"label": "손절선", "ok": true, "detail": "116,987 (현재 137,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,020"}, {"label": "추세(10일선)", "ok": true, "detail": "109,170"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -253 · 기 +225"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,714"}]}
{"ts": "2026-08-12T09:30:11.933981+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 400000, "qty": 22, "proceeds": 8782840, "entry_price": 347250, "pnl": 1142194, "pnl_pct": 15.19, "hold_from": "2026-07-13T09:12:04.291895+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "399,647 (현재 399,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "311,300"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -6 · 기 +72"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-12T09:38:12.221096+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19050, "qty": 294, "cost": 5601540, "path": "pullback", "reason": "눌림목 지정가 20,036 도달", "stop": 17760, "target": 20985, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,040 vs 18,952"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,594 / 18,952"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,594 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +113 · 기 +20 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.22% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "250,373 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.22% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,036 (현재 19,040)"}]}
{"ts": "2026-08-13T09:00:30.658185+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18880, "qty": 294, "proceeds": 5539896, "entry_price": 19050, "pnl": -61644, "pnl_pct": -0.89, "hold_from": "2026-08-12T09:38:12.221094+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,930)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,985"}, {"label": "추세(10일선)", "ok": false, "detail": "18,941"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -50 · 기 -76"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,411"}]}
{"ts": "2026-08-13T09:08:16.774337+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 402500, "qty": 8, "cost": 3220483, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 352720, "target": 477170, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "402,000 vs 311,550"}, {"label": "정배열(5>10)", "ok": true, "detail": "326,600 / 311,550"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "326,600 / 306,450"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +27 · 기 +23 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+26%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.20% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "228,347 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "2.20% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "75"}]}
{"ts": "2026-08-13T14:02:16.527631+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 415500, "qty": 8, "cost": 3324499, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 352720, "target": 477170, "signals": [{"label": "손절선", "ok": true, "detail": "352,720 (현재 415,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 477,170"}, {"label": "추세(10일선)", "ok": true, "detail": "312,900"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +19"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 415,052"}]}
{"ts": "2026-08-24T13:12:20.462527+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5030, "qty": 631, "proceeds": 3167741, "entry_price": 6333, "pnl": -828989, "pnl_pct": -20.57, "hold_from": "2026-07-30T09:04:06.863063+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,039 (현재 5,030)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,274"}, {"label": "추세(10일선)", "ok": false, "detail": "5,148"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -31 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-24T13:12:20.462991+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 31900, "qty": 167, "cost": 5328099, "path": "pullback", "reason": "눌림목 지정가 31,939 도달", "stop": 29900, "target": 34900, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "31,850 vs 31,795"}, {"label": "정배열(5>10)", "ok": true, "detail": "32,390 / 31,795"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "32,390 / 30,876"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -892 · 기 +171 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+37%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.24% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 33,850"}, {"label": "거래량 급증", "ok": false, "detail": "1,768,231 (환산) vs 평균 2,489,243"}, {"label": "회전율 급증", "ok": false, "detail": "0.24% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "53"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 31,939 (현재 31,850)"}]}
{"ts": "2026-08-25T11:36:17.992170+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 32750, "qty": 162, "cost": 5306296, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 29900, "target": 34900, "signals": [{"label": "손절선", "ok": true, "detail": "29,900 (현재 32,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 34,900"}, {"label": "추세(10일선)", "ok": true, "detail": "31,885"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,322 · 기 +436"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 32,722"}]}
{"ts": "2026-09-01T09:02:25.877997+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 12660, "qty": 311, "proceeds": 3929582, "entry_price": 13424, "pnl": -245774, "pnl_pct": -5.69, "hold_from": "2026-08-05T09:02:12.583320+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "10,340 (현재 12,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,049"}, {"label": "추세(10일선)", "ok": false, "detail": "12,846"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -53 · 기 -347"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-01T09:10:27.212333+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19310, "qty": 288, "cost": 5562114, "path": "pullback", "reason": "눌림목 지정가 20,043 도달", "stop": 17896, "target": 21431, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,310 vs 18,979"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,648 / 18,979"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,648 / 19,364"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-9.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -11 · 기 +18 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+30%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.16% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "182,700 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.16% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,043 (현재 19,310)"}]}
{"ts": "2026-09-03T09:10:25.793603+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 145600, "qty": 57, "proceeds": 8283017, "entry_price": 133863, "pnl": 651672, "pnl_pct": 8.77, "hold_from": "2026-07-30T09:24:06.914859+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "145,883 (현재 145,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "110,030"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -31 · 기 +240"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-03T09:56:26.998125+09:00", "side": "BUY", "code": "003550", "name": "LG", "price": 122000, "qty": 36, "cost": 4392659, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 109684, "target": 140474, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "122,000 vs 106,970"}, {"label": "정배열(5>10)", "ok": true, "detail": "111,120 / 106,970"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "111,120 / 107,847"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+18.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -119 · 기 +22 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+14%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.37% (환산) vs 평균 0.19% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 111,900"}, {"label": "거래량 급증", "ok": true, "detail": "575,367 (환산) vs 평균 280,124"}, {"label": "회전율 급증", "ok": true, "detail": "0.37% (환산) vs 평균 0.19%"}, {"label": "RSI", "ok": true, "detail": "71"}]}
{"ts": "2026-09-04T11:14:26.494620+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18930, "qty": 288, "proceeds": 5441209, "entry_price": 19310, "pnl": -120905, "pnl_pct": -1.97, "hold_from": "2026-09-01T09:10:27.212331+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,896 (현재 18,940)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,431"}, {"label": "추세(10일선)", "ok": false, "detail": "18,942"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -2 · 기 -39"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,673"}]}
{"ts": "2026-09-04T11:14:26.495106+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13590, "qty": 141, "cost": 1916477, "path": "pullback", "reason": "눌림목 지정가 13,946 도달", "stop": 10481, "target": 18253, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,590 vs 12,932"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,466 / 12,932"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,466 / 13,276"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-7.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -81 · 기 +73 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+66%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.98% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "588,939 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.98% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,946 (현재 13,590)"}]}
{"ts": "2026-09-04T12:34:27.137943+09:00", "side": "SELL", "code": "068270", "name": "셀트리온", "price": 187800, "qty": 51, "proceeds": 9559123, "entry_price": 194157, "pnl": -344362, "pnl_pct": -3.27, "hold_from": "2026-08-06T09:34:15.272347+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "177,101 (현재 187,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 219,741"}, {"label": "추세(10일선)", "ok": false, "detail": "189,720"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -128 · 기 -47"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-04T12:36:28.825231+09:00", "side": "BUY", "code": "079650", "name": "서산", "price": 6800, "qty": 243, "cost": 1652648, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 4991, "target": 9513, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,780 vs 5,060"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,202 / 5,060"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,202 / 4,364"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+58.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +193 · 기 +5 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "47.40% (환산) vs 평균 23.23% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 6,760"}, {"label": "거래량 급증", "ok": true, "detail": "9,478,267 (환산) vs 평균 4,645,030"}, {"label": "회전율 급증", "ok": true, "detail": "47.40% (환산) vs 평균 23.23%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-09-08T12:32:31.605944+09:00", "side": "BUY", "code": "079650", "name": "서산", "price": 7390, "qty": 223, "cost": 1648217, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 4991, "target": 9513, "signals": [{"label": "손절선", "ok": true, "detail": "4,991 (현재 7,380)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,513"}, {"label": "추세(10일선)", "ok": true, "detail": "5,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +103 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,239"}]}
{"ts": "2026-09-08T15:20:32.428940+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 130000, "qty": 93, "proceeds": 12066424, "entry_price": 119186, "pnl": 980462, "pnl_pct": 9.07, "hold_from": "2026-07-29T09:02:02.218181+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "130,576 (현재 130,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "117,640"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -147 · 기 +423"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-09T09:02:32.227738+09:00", "side": "BUY", "code": "000720", "name": "현대건설", "price": 135800, "qty": 23, "cost": 3123869, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 117206, "target": 163692, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "135,800 vs 115,200"}, {"label": "정배열(5>10)", "ok": true, "detail": "116,420 / 115,200"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "116,420 / 113,853"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+34.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -280 · 기 +899 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+22%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.32% (환산) vs 평균 0.58% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 122,600"}, {"label": "거래량 급증", "ok": true, "detail": "1,470,387 (환산) vs 평균 648,608"}, {"label": "회전율 급증", "ok": true, "detail": "1.32% (환산) vs 평균 0.58%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-09-09T09:06:32.111702+09:00", "side": "BUY", "code": "000720", "name": "현대건설", "price": 141500, "qty": 22, "cost": 3113467, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 117206, "target": 163692, "signals": [{"label": "손절선", "ok": true, "detail": "117,206 (현재 141,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 163,692"}, {"label": "추세(10일선)", "ok": true, "detail": "115,740"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -280 · 기 +899"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 140,627"}]}
{"ts": "2026-09-10T09:06:30.775863+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 356500, "qty": 16, "proceeds": 5692877, "entry_price": 409000, "pnl": -852104, "pnl_pct": -12.84, "hold_from": "2026-08-13T09:08:16.774335+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "358,791 (현재 358,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 484,313"}, {"label": "추세(10일선)", "ok": true, "detail": "307,150"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +106 · 기 -147"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-10T14:02:37.657665+09:00", "side": "BUY", "code": "004370", "name": "농심", "price": 410500, "qty": 12, "cost": 4926739, "path": "pullback", "reason": "눌림목 지정가 420,141 도달", "stop": 375781, "target": 462578, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "410,500 vs 396,800"}, {"label": "정배열(5>10)", "ok": true, "detail": "404,600 / 396,800"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "404,600 / 369,900"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +8 · 기 -29 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.27% (환산) vs 평균 0.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 440,000"}, {"label": "거래량 급증", "ok": false, "detail": "16,224 (환산) vs 평균 31,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.27% (환산) vs 평균 0.51%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 420,141 (현재 410,500)"}]}
{"ts": "2026-09-11T09:04:34.978393+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14660, "qty": 131, "cost": 1920748, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10481, "target": 18253, "signals": [{"label": "손절선", "ok": true, "detail": "10,481 (현재 14,510)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,253"}, {"label": "추세(10일선)", "ok": true, "detail": "13,024"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -103 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,345"}]}
{"ts": "2026-09-15T09:01:02.932908+09:00", "side": "SELL", "code": "003550", "name": "LG", "price": 113400, "qty": 36, "proceeds": 4074439, "entry_price": 122000, "pnl": -318219, "pnl_pct": -7.05, "hold_from": "2026-09-03T09:56:26.998121+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "109,684 (현재 111,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 140,474"}, {"label": "추세(10일선)", "ok": false, "detail": "118,280"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -16 · 기 -177"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 124,558"}]}
{"ts": "2026-09-15T09:04:39.495078+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11780, "qty": 286, "cost": 3369585, "path": "pullback", "reason": "눌림목 지정가 11,977 도달", "stop": 10254, "target": 14069, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "11,790 vs 11,347"}, {"label": "정배열(5>10)", "ok": true, "detail": "11,788 / 11,347"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "11,788 / 9,429"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+35.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +26 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 0.30% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 13,450"}, {"label": "거래량 급증", "ok": false, "detail": "313 (환산) vs 평균 36,789"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 0.30%"}, {"label": "RSI", "ok": true, "detail": "65"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 11,977 (현재 11,790)"}]}
{"ts": "2026-09-17T13:50:40.410598+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 12170, "qty": 276, "cost": 3359424, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10254, "target": 14069, "signals": [{"label": "손절선", "ok": true, "detail": "10,254 (현재 12,170)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 14,069"}, {"label": "추세(10일선)", "ok": true, "detail": "11,385"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +16 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 12,163"}]}
{"ts": "2026-09-21T15:08:43.620924+09:00", "side": "SELL", "code": "004370", "name": "농심", "price": 392500, "qty": 12, "proceeds": 4700816, "entry_price": 410500, "pnl": -225923, "pnl_pct": -4.38, "hold_from": "2026-09-10T14:02:37.657664+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "375,781 (현재 392,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 462,578"}, {"label": "추세(10일선)", "ok": false, "detail": "395,000"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -1 · 기 -5"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 419,823"}]}
{"ts": "2026-09-22T09:02:46.938301+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 153600, "qty": 11, "cost": 1689853, "path": "pullback", "reason": "눌림목 지정가 157,986 도달", "stop": 115572, "target": 210643, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "153,000 vs 152,880"}, {"label": "정배열(5>10)", "ok": true, "detail": "171,240 / 152,880"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,240 / 99,667"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +116 · 기 -19 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+40%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.27% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "52,007 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "0.27% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "58"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 157,986 (현재 153,000)"}]}
{"ts": "2026-09-23T12:32:46.718334+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 132200, "qty": 83, "proceeds": 10951203, "entry_price": 123227, "pnl": 721869, "pnl_pct": 7.28, "hold_from": "2026-07-30T09:04:06.862578+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "132,275 (현재 132,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "120,780"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -350 · 기 +269"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-23T12:32:47.146555+09:00", "side": "BUY", "code": "372170", "name": "윤성에프앤씨", "price": 20050, "qty": 110, "cost": 2205831, "path": "pullback", "reason": "눌림목 지정가 21,336 도달", "stop": 16123, "target": 25941, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "20,000 vs 19,058"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,636 / 19,058"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,636 / 19,446"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+35.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +1 · 기 -1 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.06% (환산) vs 평균 0.56% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 23,400"}, {"label": "거래량 급증", "ok": false, "detail": "5,027 (환산) vs 평균 44,688"}, {"label": "회전율 급증", "ok": false, "detail": "0.06% (환산) vs 평균 0.56%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 21,336 (현재 20,000)"}]}
{"ts": "2026-09-28T09:04:45.105461+09:00", "side": "SELL", "code": "000720", "name": "현대건설", "price": 119200, "qty": 45, "proceeds": 5353540, "entry_price": 138587, "pnl": -883795, "pnl_pct": -13.99, "hold_from": "2026-09-09T09:02:32.227735+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "119,278 (현재 119,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 167,550"}, {"label": "추세(10일선)", "ok": true, "detail": "113,530"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -440 · 기 -358"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-28T09:04:45.106990+09:00", "side": "BUY", "code": "025860", "name": "남해화학", "price": 6050, "qty": 895, "cost": 5415562, "path": "pullback", "reason": "눌림목 지정가 6,148 도달", "stop": 5730, "target": 6530, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,060 vs 6,027"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,098 / 6,027"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,098 / 5,899"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+9.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -32 · 기 +15 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.09% (환산) vs 평균 1.53% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 6,700"}, {"label": "거래량 급증", "ok": false, "detail": "45,067 (환산) vs 평균 758,257"}, {"label": "회전율 급증", "ok": false, "detail": "0.09% (환산) vs 평균 1.53%"}, {"label": "RSI", "ok": true, "detail": "53"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 6,148 (현재 6,060)"}]}
{"ts": "2026-09-28T09:20:48.364329+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 164000, "qty": 10, "cost": 1640246, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 115572, "target": 210643, "signals": [{"label": "손절선", "ok": true, "detail": "115,572 (현재 163,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 210,643"}, {"label": "추세(10일선)", "ok": true, "detail": "153,880"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +222 · 기 -12"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 162,889"}]}
{"ts": "2026-10-01T09:00:53.929412+09:00", "side": "SELL", "code": "316140", "name": "우리금융지주", "price": 32800, "qty": 329, "proceeds": 10770157, "entry_price": 32319, "pnl": 135762, "pnl_pct": 1.49, "hold_from": "2026-08-24T13:12:20.462989+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "34,483 (현재 33,850)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "31,995"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -303 · 기 -158"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-01T09:02:45.598935+09:00", "side": "BUY", "code": "051900", "name": "LG생활건강", "price": 271500, "qty": 14, "cost": 3801570, "path": "pullback", "reason": "눌림목 지정가 277,258 도달", "stop": 242017, "target": 315725, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "270,000 vs 256,700"}, {"label": "정배열(5>10)", "ok": true, "detail": "267,300 / 256,700"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "267,300 / 248,933"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -51 · 기 +19 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.15% (환산) vs 평균 0.53% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 314,500"}, {"label": "거래량 급증", "ok": false, "detail": "22,107 (환산) vs 평균 78,860"}, {"label": "회전율 급증", "ok": false, "detail": "0.15% (환산) vs 평균 0.53%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 277,258 (현재 270,000)"}]}
{"ts": "2026-10-02T09:01:23.796646+09:00", "side": "BUY", "code": "372170", "name": "윤성에프앤씨", "price": 21400, "qty": 103, "cost": 2204531, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 16123, "target": 25941, "signals": [{"label": "손절선", "ok": true, "detail": "16,123 (현재 21,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 25,941"}, {"label": "추세(10일선)", "ok": true, "detail": "19,228"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +2 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 20,989"}]}
{"ts": "2026-10-08T09:04:49.324670+09:00", "side": "SELL", "code": "079650", "name": "서산", "price": 5300, "qty": 466, "proceeds": 2464984, "entry_price": 7082, "pnl": -835881, "pnl_pct": -25.17, "hold_from": "2026-09-04T12:36:28.825230+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,326 (현재 5,290)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,716"}, {"label": "추세(10일선)", "ok": true, "detail": "4,910"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -30 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-08T09:50:45.349013+09:00", "side": "BUY", "code": "056360", "name": "코위버", "price": 6360, "qty": 731, "cost": 4649857, "path": "pullback", "reason": "눌림목 지정가 6,523 도달", "stop": 5770, "target": 7245, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,360 vs 6,318"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,492 / 6,318"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,492 / 5,507"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+13.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +22 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.61% (환산) vs 평균 2.14% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 7,140"}, {"label": "거래량 급증", "ok": false, "detail": "157,353 (환산) vs 평균 209,822"}, {"label": "회전율 급증", "ok": false, "detail": "1.61% (환산) vs 평균 2.14%"}, {"label": "RSI", "ok": true, "detail": "57"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 6,523 (현재 6,360)"}]}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944261+09:00",
"updated_at": "2026-06-15T15:25:02.281173+09:00"
}
@@ -1,35 +1,213 @@
{
"cash": 93565534.975,
"cash": 27884752.730150037,
"initial": 100000000,
"positions": {
"035420": {
"code": "035420",
"name": "NAVER",
"252990": {
"code": "252990",
"name": "샘씨엔에스",
"sources": [
"interest"
"auto"
],
"qty": 27,
"entry_price": 238277.77777777778,
"entry_at": "2026-06-12T09:00:34.009427+09:00",
"stop": 203394,
"target": 308046,
"peak": 263000,
"qty": 335,
"entry_price": 14067.880597014926,
"entry_at": "2026-09-03T09:02:24.986233+09:00",
"stop": 18268,
"target": 18598,
"peak": 20700,
"trailing_on": true,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 13,982 도달",
"cur_price": 18340,
"tranches": 2,
"tranche_value": 2363869.6412348715,
"last_add_price": 14660
},
"041520": {
"code": "041520",
"name": "이엘씨",
"sources": [
"auto"
],
"qty": 722,
"entry_price": 12086.675900277009,
"entry_at": "2026-09-10T09:00:55.814433+09:00",
"stop": 10997,
"target": 14266,
"peak": 13200,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 233,744 도달",
"cur_price": 248750,
"entry_reason": "눌림목 지정가 12,003 도달",
"cur_price": 12790,
"tranches": 2,
"tranche_value": 3332473.3402132783,
"last_add_price": 244500
"tranche_value": 4373273.034451844,
"last_add_price": 12290
},
"261520": {
"code": "261520",
"name": "이지스",
"sources": [
"watch"
],
"qty": 1325,
"entry_price": 5240.950943396227,
"entry_at": "2026-09-15T10:28:40.300992+09:00",
"stop": 4646,
"target": 6431,
"peak": 6040,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 5,108 도달",
"cur_price": 5860,
"tranches": 2,
"tranche_value": 3476292.7545329626,
"last_add_price": 5390
},
"372170": {
"code": "372170",
"name": "윤성에프앤씨",
"sources": [
"auto"
],
"qty": 268,
"entry_price": 20782.089552238805,
"entry_at": "2026-09-16T13:08:39.885461+09:00",
"stop": 17965,
"target": 26416,
"peak": 25500,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 21,341 도달",
"cur_price": 23800,
"tranches": 2,
"tranche_value": 2789435.574661536,
"last_add_price": 21400
},
"307870": {
"code": "307870",
"name": "비투엔",
"sources": [
"watch"
],
"qty": 5343,
"entry_price": 1220.0830993823695,
"entry_at": "2026-10-01T09:00:54.040849+09:00",
"stop": 1066,
"target": 1528,
"peak": 1308,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 1,241 도달",
"cur_price": 1297,
"tranches": 2,
"tranche_value": 3260787.278232004,
"last_add_price": 1243
},
"048910": {
"code": "048910",
"name": "대원미디어",
"sources": [
"auto"
],
"qty": 1456,
"entry_price": 7078.8873626373625,
"entry_at": "2026-10-01T09:58:43.236173+09:00",
"stop": 7732,
"target": 8151,
"peak": 8390,
"trailing_on": true,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 8260,
"tranches": 2,
"tranche_value": 5159308.130815627,
"last_add_price": 7170
},
"295310": {
"code": "295310",
"name": "에이치브이엠",
"sources": [
"watch"
],
"qty": 58,
"entry_price": 75200,
"entry_at": "2026-10-07T09:06:54.037811+09:00",
"stop": 67951,
"target": 89698,
"peak": 76900,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 74200,
"tranches": 1,
"tranche_value": 4370283.78095412,
"last_add_price": 75200
},
"214450": {
"code": "214450",
"name": "파마리서치",
"sources": [
"auto"
],
"qty": 16,
"entry_price": 301000,
"entry_at": "2026-10-08T09:02:53.413414+09:00",
"stop": 276000,
"target": 351000,
"peak": 306000,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 303,967 도달",
"cur_price": 302000,
"tranches": 1,
"tranche_value": 5056717.232835032,
"last_add_price": 301000
}
},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100666534.975,
"max_drawdown": 0.005230139292375102,
"last_exit": {},
"realized_pnl": -20150909.788250048,
"closed_trades": 66,
"wins": 9,
"peak_equity": 101695667.93550003,
"max_drawdown": 0.24548980547857824,
"last_exit": {
"030000": "2026-09-10",
"035420": "2026-06-23",
"403870": "2026-07-02",
"095610": "2026-09-09",
"093370": "2026-07-20",
"240810": "2026-07-24",
"009150": "2026-07-06",
"034730": "2026-07-08",
"214450": "2026-09-02",
"003490": "2026-07-07",
"319660": "2026-07-23",
"105560": "2026-07-20",
"086790": "2026-07-21",
"003680": "2026-09-09",
"252990": "2026-08-25",
"010950": "2026-09-10",
"090850": "2026-08-05",
"161890": "2026-07-30",
"078930": "2026-09-30",
"138040": "2026-09-08",
"218150": "2026-08-19",
"194370": "2026-08-24",
"068270": "2026-08-26",
"012450": "2026-09-15",
"060250": "2026-09-16",
"004370": "2026-09-17",
"316140": "2026-10-01",
"257720": "2026-10-01",
"300720": "2026-10-07",
"327260": "2026-10-08"
},
"created_at": "2026-06-11T09:00:01.944270+09:00",
"updated_at": "2026-06-15T15:25:02.282444+09:00"
"updated_at": "2026-10-08T15:28:47.144334+09:00"
}
@@ -1,2 +1,185 @@
{"ts": "2026-06-12T09:00:34.009437+09:00", "side": "BUY", "code": "035420", "name": "NAVER", "price": 232500, "qty": 14, "cost": 3255488, "path": "pullback", "reason": "눌림목 지정가 233,744 도달", "stop": 197616, "target": 302268, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "233,500 vs 224,820"}, {"label": "정배열(5>20)", "ok": true, "detail": "258,500 / 224,820"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "258,500 / 215,623"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+8.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -159 · 기 +1,154 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.99% (환산) vs 평균 2.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 304,000"}, {"label": "거래량 급증", "ok": false, "detail": "4,694,733 (환산) vs 평균 3,404,288"}, {"label": "회전율 급증", "ok": false, "detail": "2.99% (환산) vs 평균 2.17%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 233,744 (현재 233,500)"}]}
{"ts": "2026-06-12T10:25:04.174047+09:00", "side": "BUY", "code": "035420", "name": "NAVER", "price": 244500, "qty": 13, "cost": 3178977, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 197616, "target": 302268, "signals": [{"label": "손절선", "ok": true, "detail": "197,616 (현재 244,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 302,268"}, {"label": "추세(20일선)", "ok": true, "detail": "225,370"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -116 · 기 +1,185"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 244,128"}]}
{"ts": "2026-06-17T16:47:28.507097+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 62900, "qty": 47, "cost": 2956743, "path": "pullback", "reason": "눌림목 지정가 64,531 도달", "stop": 52447, "target": 83807, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "62,800 vs 52,858"}, {"label": "정배열(5>20)", "ok": true, "detail": "59,880 / 52,858"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "59,880 / 49,831"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -4,434 · 기 +5,040 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+9%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "11.31% vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "9,309,211 vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": true, "detail": "11.31% vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "58"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 64,531 (현재 62,800)"}]}
{"ts": "2026-06-19T09:02:06.348139+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19160, "qty": 326, "cost": 6247097, "path": "pullback", "reason": "눌림목 지정가 20,044 도달", "stop": 18100, "target": 21279, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "19,150 vs 18,862"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,616 / 18,862"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,616 / 19,361"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-31.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,162 · 기 +2,080 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "37,673 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,044 (현재 19,150)"}]}
{"ts": "2026-06-23T09:00:09.447545+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18820, "qty": 326, "proceeds": 6123356, "entry_price": 19160, "pnl": -123741, "pnl_pct": -1.77, "hold_from": "2026-06-19T09:02:06.348137+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,100 (현재 18,710)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,279"}, {"label": "추세(20일선)", "ok": false, "detail": "18,840"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -318 · 기 -227"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,528"}]}
{"ts": "2026-06-23T15:10:02.424604+09:00", "side": "SELL", "code": "035420", "name": "NAVER", "price": 203000, "qty": 27, "proceeds": 5470312, "entry_price": 238278, "pnl": -964153, "pnl_pct": -14.81, "hold_from": "2026-06-12T09:00:34.009427+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "203,394 (현재 203,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 308,046"}, {"label": "추세(20일선)", "ok": false, "detail": "231,595"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +408 · 기 -163"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-06-24T09:04:06.114453+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 52300, "qty": 47, "proceeds": 2453307, "entry_price": 62900, "pnl": -503437, "pnl_pct": -16.85, "hold_from": "2026-06-17T16:47:28.507089+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "52,447 (현재 52,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 83,807"}, {"label": "추세(20일선)", "ok": false, "detail": "52,332"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +711 · 기 -2,060"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 66,717"}]}
{"ts": "2026-06-24T09:04:06.114580+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 156700, "qty": 17, "cost": 2664300, "path": "pullback", "reason": "눌림목 지정가 165,075 도달", "stop": 129363, "target": 211374, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "156,200 vs 121,650"}, {"label": "정배열(5>20)", "ok": true, "detail": "143,260 / 121,650"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "143,260 / 121,703"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +143 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.83% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "2,860,073 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "5.83% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 165,075 (현재 156,200)"}]}
{"ts": "2026-06-24T09:12:05.349711+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 17270, "qty": 199, "cost": 3437246, "path": "pullback", "reason": "눌림목 지정가 17,366 도달", "stop": 14804, "target": 22203, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "17,280 vs 12,914"}, {"label": "정배열(5>20)", "ok": true, "detail": "16,364 / 12,914"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,364 / 11,500"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +1,740 · 기 -508 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+16%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.34% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": false, "detail": "2,509,340 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": false, "detail": "2.34% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "65"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 17,366 (현재 17,280)"}]}
{"ts": "2026-06-24T09:46:04.781319+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158200, "qty": 17, "cost": 2689803, "path": "pullback", "reason": "눌림목 지정가 158,264 도달", "stop": 130097, "target": 214407, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "158,000 vs 136,315"}, {"label": "정배열(5>20)", "ok": true, "detail": "172,240 / 136,315"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,240 / 99,750"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+56.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +41 · 기 +65 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+5%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.25% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "822,940 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "4.25% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,264 (현재 158,000)"}]}
{"ts": "2026-06-24T11:12:05.898443+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 18260, "qty": 188, "cost": 3433395, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 14804, "target": 22203, "signals": [{"label": "손절선", "ok": true, "detail": "14,804 (현재 18,250)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,203"}, {"label": "추세(20일선)", "ok": true, "detail": "12,962"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +1,742 · 기 -508"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 18,094"}]}
{"ts": "2026-06-25T09:00:23.578154+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 179100, "qty": 15, "cost": 2686903, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 130097, "target": 214407, "signals": [{"label": "손절선", "ok": true, "detail": "130,097 (현재 178,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 214,407"}, {"label": "추세(20일선)", "ok": true, "detail": "137,330"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -56 · 기 +134"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 167,275"}]}
{"ts": "2026-06-25T09:02:04.071867+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 53200, "qty": 49, "cost": 2607191, "path": "pullback", "reason": "눌림목 지정가 63,881 도달", "stop": 43200, "target": 73200, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "53,300 vs 52,382"}, {"label": "정배열(5>20)", "ok": true, "detail": "57,980 / 52,382"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "57,980 / 49,672"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+15.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +406 · 기 -1,461 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+29%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.21% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "1,819,047 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "2.21% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 63,881 (현재 53,300)"}]}
{"ts": "2026-06-26T09:00:05.688310+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 50000, "qty": 49, "proceeds": 2445222, "entry_price": 53200, "pnl": -161969, "pnl_pct": -6.02, "hold_from": "2026-06-25T09:02:04.071865+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "43,200 (현재 51,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 73,200"}, {"label": "추세(20일선)", "ok": false, "detail": "52,268"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -311 · 기 -267"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 57,056"}]}
{"ts": "2026-06-26T10:14:04.059906+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 166900, "qty": 16, "cost": 2670801, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 129363, "target": 211374, "signals": [{"label": "손절선", "ok": true, "detail": "129,363 (현재 166,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 211,374"}, {"label": "추세(20일선)", "ok": true, "detail": "122,175"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -126 · 기 +320"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 165,659"}]}
{"ts": "2026-06-29T11:36:01.264617+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 745000, "qty": 4, "cost": 2980447, "path": "pullback", "reason": "눌림목 지정가 748,914 도달", "stop": 645871, "target": 943257, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "744,000 vs 676,650"}, {"label": "정배열(5>20)", "ok": true, "detail": "766,800 / 676,650"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "766,800 / 530,600"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -194 · 기 +152 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+8%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.35% (환산) vs 평균 0.45% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 883,000"}, {"label": "거래량 급증", "ok": true, "detail": "977,966 (환산) vs 평균 329,863"}, {"label": "회전율 급증", "ok": true, "detail": "1.35% (환산) vs 평균 0.45%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 748,914 (현재 744,000)"}]}
{"ts": "2026-06-29T14:18:01.484808+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 778000, "qty": 4, "cost": 3112467, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 645871, "target": 943257, "signals": [{"label": "손절선", "ok": true, "detail": "645,871 (현재 779,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 943,257"}, {"label": "추세(20일선)", "ok": true, "detail": "678,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -200 · 기 +143"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 778,043"}]}
{"ts": "2026-06-30T09:00:05.639912+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 55300, "qty": 43, "cost": 2378257, "path": "pullback", "reason": "눌림목 지정가 63,981 도달", "stop": 44022, "target": 77857, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "53,700 vs 52,402"}, {"label": "정배열(5>20)", "ok": true, "detail": "58,060 / 52,402"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "58,060 / 49,679"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +222 · 기 -821 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+28%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "827 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 63,981 (현재 53,700)"}]}
{"ts": "2026-07-01T09:06:03.209135+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 357500, "qty": 15, "cost": 5363304, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 325040, "target": 422420, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "357,000 vs 296,975"}, {"label": "정배열(5>20)", "ok": true, "detail": "317,600 / 296,975"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "317,600 / 305,700"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -75 · 기 +221 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+35%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.89% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "196,247 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.89% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "68"}]}
{"ts": "2026-07-01T09:24:02.309730+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 372000, "qty": 14, "cost": 5208781, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 325040, "target": 422420, "signals": [{"label": "손절선", "ok": true, "detail": "325,040 (현재 372,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 422,420"}, {"label": "추세(20일선)", "ok": true, "detail": "297,725"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -75 · 기 +221"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 368,499"}]}
{"ts": "2026-07-01T11:06:02.351611+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 212000, "qty": 32, "proceeds": 6770771, "entry_price": 167997, "pnl": 1394065, "pnl_pct": 26.19, "hold_from": "2026-06-24T09:46:04.781317+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "213,193 (현재 213,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "139,065"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -229 · 기 +300"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T10:18:03.830979+09:00", "side": "BUY", "code": "009150", "name": "삼성전기", "price": 1985000, "qty": 3, "cost": 5955893, "path": "pullback", "reason": "눌림목 지정가 1,994,011 도달", "stop": 1901000, "target": 2153000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "1,982,000 vs 1,967,800"}, {"label": "정배열(5>20)", "ok": true, "detail": "2,038,800 / 1,967,800"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "2,038,800 / 1,295,483"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +337 · 기 +193 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.19% (환산) vs 평균 1.76% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 2,417,000"}, {"label": "거래량 급증", "ok": true, "detail": "3,133,469 (환산) vs 평균 1,312,239"}, {"label": "회전율 급증", "ok": true, "detail": "4.19% (환산) vs 평균 1.76%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 1,994,011 (현재 1,982,000)"}]}
{"ts": "2026-07-02T10:34:04.117399+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18860, "qty": 324, "cost": 6111557, "path": "pullback", "reason": "눌림목 지정가 19,970 도달", "stop": 17760, "target": 21060, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "18,860 vs 18,848"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,558 / 18,848"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,558 / 19,356"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -339 · 기 +110 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+35%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.80% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "924,985 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.80% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "48"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,970 (현재 18,860)"}]}
{"ts": "2026-07-02T11:20:02.053948+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 22550, "qty": 271, "cost": 6111967, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17760, "target": 21060, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 19,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,060"}, {"label": "추세(20일선)", "ok": true, "detail": "18,872"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,250"}]}
{"ts": "2026-07-02T11:22:01.962941+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 19000, "qty": 595, "proceeds": 11282955, "entry_price": 20541, "pnl": -940568, "pnl_pct": -7.5, "hold_from": "2026-07-02T10:34:04.117397+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,371 (현재 19,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,881"}, {"label": "추세(20일선)", "ok": true, "detail": "18,860"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T12:42:02.217684+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 15250, "qty": 387, "proceeds": 5890242, "entry_price": 17751, "pnl": -980399, "pnl_pct": -14.09, "hold_from": "2026-06-24T09:12:05.349710+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "15,280 (현재 15,280)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,692"}, {"label": "추세(20일선)", "ok": true, "detail": "12,814"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,920 · 기 +434"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T14:28:02.056486+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 134800, "qty": 33, "proceeds": 4439726, "entry_price": 161645, "pnl": -895375, "pnl_pct": -16.61, "hold_from": "2026-06-24T09:04:06.114579+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "134,769 (현재 134,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 215,398"}, {"label": "추세(20일선)", "ok": true, "detail": "120,575"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +395 · 기 -1,516"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T15:04:06.183065+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 43900, "qty": 43, "proceeds": 1884019, "entry_price": 55300, "pnl": -494238, "pnl_pct": -20.61, "hold_from": "2026-06-30T09:00:05.639905+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "44,022 (현재 43,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 77,857"}, {"label": "추세(20일선)", "ok": false, "detail": "51,912"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +2,379 · 기 -6,625"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 59,411"}]}
{"ts": "2026-07-03T09:00:08.430585+09:00", "side": "SELL", "code": "009150", "name": "삼성전기", "price": 1895000, "qty": 3, "proceeds": 5673914, "entry_price": 1985000, "pnl": -281979, "pnl_pct": -4.53, "hold_from": "2026-07-02T10:18:03.830976+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "1,901,000 (현재 1,894,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 2,153,000"}, {"label": "추세(20일선)", "ok": false, "detail": "1,963,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +132 · 기 +129"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 2,084,495"}]}
{"ts": "2026-07-03T09:02:04.917379+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 133400, "qty": 16, "cost": 2134720, "path": "pullback", "reason": "눌림목 지정가 163,875 도달", "stop": 104263, "target": 191674, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "133,300 vs 120,505"}, {"label": "정배열(5>20)", "ok": true, "detail": "138,680 / 120,505"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "138,680 / 121,322"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+18.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +674 · 기 -2,170 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.16% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": false, "detail": "571,607 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": false, "detail": "1.16% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 163,875 (현재 133,300)"}]}
{"ts": "2026-07-03T09:02:04.917552+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 14660, "qty": 173, "cost": 2536560, "path": "pullback", "reason": "눌림목 지정가 17,179 도달", "stop": 11914, "target": 20152, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "14,680 vs 12,784"}, {"label": "정배열(5>20)", "ok": true, "detail": "15,844 / 12,784"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,844 / 11,456"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,591 · 기 +253 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+36%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.92% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": false, "detail": "991,593 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": false, "detail": "0.92% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 17,179 (현재 14,680)"}]}
{"ts": "2026-07-03T09:10:03.668540+09:00", "side": "SELL", "code": "034730", "name": "SK", "price": 656000, "qty": 8, "proceeds": 5237766, "entry_price": 761500, "pnl": -855147, "pnl_pct": -13.85, "hold_from": "2026-06-29T11:36:01.264615+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "662,371 (현재 657,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 959,757"}, {"label": "추세(20일선)", "ok": false, "detail": "672,300"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -51 · 기 +3"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T09:50:01.665084+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 328500, "qty": 29, "proceeds": 9507923, "entry_price": 364500, "pnl": -1064162, "pnl_pct": -9.88, "hold_from": "2026-07-01T09:06:03.209133+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "331,504 (현재 331,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 430,491"}, {"label": "추세(20일선)", "ok": true, "detail": "295,675"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -127 · 기 +294"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T10:24:02.770189+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19040, "qty": 308, "cost": 5865200, "path": "pullback", "reason": "눌림목 지정가 20,004 도달", "stop": 17921, "target": 21277, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "19,030 vs 18,856"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,592 / 18,856"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,592 / 19,359"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -35 · 기 +168 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.54% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "619,193 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.54% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,004 (현재 19,030)"}]}
{"ts": "2026-07-06T09:00:08.148622+09:00", "side": "BUY", "code": "009150", "name": "삼성전기", "price": 1993000, "qty": 2, "cost": 3986598, "path": "pullback", "reason": "눌림목 지정가 1,995,439 도달", "stop": 1901000, "target": 2177000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "1,975,000 vs 1,967,450"}, {"label": "정배열(5>20)", "ok": true, "detail": "2,037,400 / 1,967,450"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "2,037,400 / 1,295,367"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+9.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +151 · 기 +57 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+9%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.82% (환산) vs 평균 1.76% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 2,417,000"}, {"label": "거래량 급증", "ok": false, "detail": "615,080 (환산) vs 평균 1,312,239"}, {"label": "회전율 급증", "ok": false, "detail": "0.82% (환산) vs 평균 1.76%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 1,995,439 (현재 1,975,000)"}]}
{"ts": "2026-07-06T09:00:08.151514+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 690000, "qty": 5, "cost": 3450518, "path": "pullback", "reason": "눌림목 지정가 747,129 도달", "stop": 604000, "target": 862000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "695,000 vs 674,200"}, {"label": "정배열(5>20)", "ok": true, "detail": "757,000 / 674,200"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "757,000 / 529,783"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -81 · 기 +57 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+21%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.17% (환산) vs 평균 0.45% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 883,000"}, {"label": "거래량 급증", "ok": false, "detail": "124,240 (환산) vs 평균 329,863"}, {"label": "회전율 급증", "ok": false, "detail": "0.17% (환산) vs 평균 0.45%"}, {"label": "RSI", "ok": true, "detail": "54"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 747,129 (현재 695,000)"}]}
{"ts": "2026-07-06T09:34:02.056068+09:00", "side": "BUY", "code": "003490", "name": "대한항공", "price": 29100, "qty": 200, "cost": 5820873, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 27052, "target": 33195, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "29,100 vs 25,960"}, {"label": "정배열(5>20)", "ok": true, "detail": "26,160 / 25,960"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "26,160 / 25,220"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-3.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -142 · 기 +2,052 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.9/5"}, {"label": "상승여력", "ok": true, "detail": "+19%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.31% (환산) vs 평균 0.58% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 28,400"}, {"label": "거래량 급증", "ok": true, "detail": "4,788,480 (환산) vs 평균 2,146,231"}, {"label": "회전율 급증", "ok": true, "detail": "1.31% (환산) vs 평균 0.58%"}, {"label": "RSI", "ok": true, "detail": "65"}]}
{"ts": "2026-07-06T10:06:01.980443+09:00", "side": "SELL", "code": "009150", "name": "삼성전기", "price": 1900000, "qty": 2, "proceeds": 3792590, "entry_price": 1993000, "pnl": -194008, "pnl_pct": -4.67, "hold_from": "2026-07-06T09:00:08.148616+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "1,901,000 (현재 1,901,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 2,177,000"}, {"label": "추세(20일선)", "ok": false, "detail": "1,963,750"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +135 · 기 +46"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 2,089,780"}]}
{"ts": "2026-07-07T09:12:07.264150+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 155900, "qty": 16, "cost": 2494774, "path": "pullback", "reason": "눌림목 지정가 158,314 도달", "stop": 127872, "target": 211957, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "155,800 vs 136,205"}, {"label": "정배열(5>20)", "ok": true, "detail": "171,800 / 136,205"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,800 / 99,713"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +80 · 기 -197 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+14%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.33% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "645,567 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "3.33% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,314 (현재 155,800)"}]}
{"ts": "2026-07-07T12:02:08.178098+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 12230, "qty": 173, "proceeds": 2111664, "entry_price": 14660, "pnl": -424896, "pnl_pct": -16.58, "hold_from": "2026-07-03T09:02:04.917551+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "11,914 (현재 12,230)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,152"}, {"label": "추세(20일선)", "ok": false, "detail": "12,661"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -1,558 · 기 -4"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 15,662"}]}
{"ts": "2026-07-07T14:08:24.985266+09:00", "side": "SELL", "code": "003490", "name": "대한항공", "price": 26600, "qty": 200, "proceeds": 5309626, "entry_price": 29100, "pnl": -511247, "pnl_pct": -8.59, "hold_from": "2026-07-06T09:34:02.056065+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "27,052 (현재 26,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 33,195"}, {"label": "추세(20일선)", "ok": true, "detail": "25,870"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -532 · 기 +3,040"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 29,724"}]}
{"ts": "2026-07-07T14:08:27.625222+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 183300, "qty": 11, "cost": 2016602, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 104263, "target": 191674, "signals": [{"label": "손절선", "ok": true, "detail": "104,263 (현재 183,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 191,674"}, {"label": "추세(20일선)", "ok": true, "detail": "120,210"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +263 · 기 -1,543"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 141,825"}]}
{"ts": "2026-07-07T14:08:27.626449+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 177000, "qty": 14, "cost": 2478372, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 127872, "target": 211957, "signals": [{"label": "손절선", "ok": true, "detail": "127,872 (현재 177,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 211,957"}, {"label": "추세(20일선)", "ok": true, "detail": "134,320"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +152 · 기 -281"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 165,142"}]}
{"ts": "2026-07-07T14:08:30.307609+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 20750, "qty": 282, "cost": 5852378, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17921, "target": 21277, "signals": [{"label": "손절선", "ok": true, "detail": "17,921 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,277"}, {"label": "추세(20일선)", "ok": true, "detail": "18,884"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 +113"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,358"}]}
{"ts": "2026-07-07T14:08:30.308497+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 815000, "qty": 4, "cost": 3260489, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 604000, "target": 862000, "signals": [{"label": "손절선", "ok": true, "detail": "604,000 (현재 815,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 862,000"}, {"label": "추세(20일선)", "ok": true, "detail": "673,250"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -44 · 기 +3"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 721,777"}]}
{"ts": "2026-07-08T09:00:17.141650+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 108300, "qty": 27, "proceeds": 2918398, "entry_price": 153730, "pnl": -1232925, "pnl_pct": -29.55, "hold_from": "2026-07-03T09:02:04.917377+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "128,455 (현재 112,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 204,279"}, {"label": "추세(20일선)", "ok": false, "detail": "119,475"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +243 · 기 -857"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:00:17.142747+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 137900, "qty": 30, "proceeds": 4128933, "entry_price": 165747, "pnl": -844213, "pnl_pct": -16.8, "hold_from": "2026-07-07T09:12:07.264149+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "138,020 (현재 137,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 221,200"}, {"label": "추세(20일선)", "ok": true, "detail": "135,310"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +458 · 기 -572"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:00:17.143299+09:00", "side": "SELL", "code": "034730", "name": "SK", "price": 602000, "qty": 9, "proceeds": 5407435, "entry_price": 745556, "pnl": -1303572, "pnl_pct": -19.25, "hold_from": "2026-07-06T09:00:08.151507+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "650,225 (현재 602,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 936,217"}, {"label": "추세(20일선)", "ok": false, "detail": "669,550"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +49 · 기 -19"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:12:05.961489+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18900, "qty": 590, "proceeds": 11129256, "entry_price": 19857, "pnl": -588322, "pnl_pct": -4.82, "hold_from": "2026-07-03T10:24:02.770188+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "18,903 (현재 18,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,766"}, {"label": "추세(20일선)", "ok": true, "detail": "18,850"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +193 · 기 +193"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:48:01.868099+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 161100, "qty": 34, "cost": 5478222, "path": "pullback", "reason": "눌림목 지정가 168,676 도달", "stop": 158000, "target": 167300, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "161,600 vs 149,870"}, {"label": "정배열(5>20)", "ok": true, "detail": "169,960 / 149,870"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "169,960 / 113,252"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+77.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +198 · 기 -309 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+11%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.83% (환산) vs 평균 2.68% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 196,500"}, {"label": "거래량 급증", "ok": false, "detail": "1,110,233 (환산) vs 평균 777,376"}, {"label": "회전율 급증", "ok": false, "detail": "3.83% (환산) vs 평균 2.68%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 168,676 (현재 161,600)"}]}
{"ts": "2026-07-08T10:26:01.962983+09:00", "side": "SELL", "code": "319660", "name": "피에스케이", "price": 157600, "qty": 34, "proceeds": 5347951, "entry_price": 161100, "pnl": -130270, "pnl_pct": -2.17, "hold_from": "2026-07-08T09:48:01.868097+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "158,000 (현재 158,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 167,300"}, {"label": "추세(20일선)", "ok": true, "detail": "149,690"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +198 · 기 -312"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 170,862"}]}
{"ts": "2026-07-09T09:02:04.457277+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 144400, "qty": 14, "cost": 2021903, "path": "pullback", "reason": "눌림목 지정가 156,479 도달", "stop": 113618, "target": 205964, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "144,400 vs 135,635"}, {"label": "정배열(5>20)", "ok": true, "detail": "169,520 / 135,635"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "169,520 / 99,523"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+42.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +503 · 기 -501 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+23%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.94% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "569,187 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "2.94% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 156,479 (현재 144,400)"}]}
{"ts": "2026-07-09T09:02:04.457421+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19110, "qty": 290, "cost": 5542731, "path": "pullback", "reason": "눌림목 지정가 20,021 도달", "stop": 18017, "target": 21296, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "19,070 vs 18,858"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,600 / 18,858"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,600 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+0.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -10 · 기 +164 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.08% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "93,540 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.08% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,021 (현재 19,070)"}]}
{"ts": "2026-07-09T09:02:04.457549+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 158300, "qty": 35, "cost": 5541331, "path": "pullback", "reason": "눌림목 지정가 168,455 도달", "stop": 158000, "target": 158900, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "158,200 vs 149,700"}, {"label": "정배열(5>20)", "ok": true, "detail": "169,280 / 149,700"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "169,280 / 113,195"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+81.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +296 · 기 -306 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.72% (환산) vs 평균 2.68% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 196,500"}, {"label": "거래량 급증", "ok": false, "detail": "499,233 (환산) vs 평균 777,376"}, {"label": "회전율 급증", "ok": false, "detail": "1.72% (환산) vs 평균 2.68%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 168,455 (현재 158,200)"}]}
{"ts": "2026-07-09T09:12:02.503058+09:00", "side": "SELL", "code": "319660", "name": "피에스케이", "price": 156900, "qty": 35, "proceeds": 5480792, "entry_price": 158300, "pnl": -60540, "pnl_pct": -0.88, "hold_from": "2026-07-09T09:02:04.457547+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "158,000 (현재 156,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "149,635"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +296 · 기 -306"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-09T09:36:03.452342+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 132000, "qty": 39, "cost": 5148772, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 120761, "target": 154477, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "131,900 vs 108,920"}, {"label": "정배열(5>20)", "ok": true, "detail": "115,560 / 108,920"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,560 / 113,228"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+22.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -184 · 기 +964 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.91% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "1,026,867 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.91% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-10T09:00:10.854533+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158000, "qty": 13, "cost": 2054308, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 113618, "target": 205964, "signals": [{"label": "손절선", "ok": true, "detail": "113,618 (현재 157,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 205,964"}, {"label": "추세(20일선)", "ok": true, "detail": "136,265"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +452 · 기 -360"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 154,000"}]}
{"ts": "2026-07-10T09:00:10.855604+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 164000, "qty": 33, "cost": 5412812, "path": "pullback", "reason": "눌림목 지정가 169,105 도달", "stop": 158000, "target": 176000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "162,200 vs 149,900"}, {"label": "정배열(5>20)", "ok": true, "detail": "170,080 / 149,900"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "170,080 / 113,262"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+85.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +225 · 기 -178 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+10%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.58% (환산) vs 평균 2.68% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 196,500"}, {"label": "거래량 급증", "ok": false, "detail": "746,107 (환산) vs 평균 777,376"}, {"label": "회전율 급증", "ok": false, "detail": "2.58% (환산) vs 평균 2.68%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 169,105 (현재 162,200)"}]}
{"ts": "2026-07-10T09:28:02.614093+09:00", "side": "BUY", "code": "105560", "name": "KB금융", "price": 185900, "qty": 29, "cost": 5391909, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 171311, "target": 215077, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "185,600 vs 159,890"}, {"label": "정배열(5>20)", "ok": true, "detail": "159,900 / 159,890"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "159,900 / 157,368"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+26.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,310 · 기 +1,527 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+16%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.06% (환산) vs 평균 0.46% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 182,700"}, {"label": "거래량 급증", "ok": true, "detail": "3,715,113 (환산) vs 평균 1,640,325"}, {"label": "회전율 급증", "ok": true, "detail": "1.06% (환산) vs 평균 0.46%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-10T09:46:11.469025+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 12830, "qty": 179, "cost": 2296914, "path": "pullback", "reason": "눌림목 지정가 16,984 도달", "stop": 10360, "target": 17770, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "12,820 vs 12,690"}, {"label": "정배열(5>20)", "ok": true, "detail": "15,472 / 12,690"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,472 / 11,425"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+1.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +228 · 기 +229 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+56%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "9.75% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "10,460,473 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "9.75% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,984 (현재 12,820)"}]}
{"ts": "2026-07-10T10:14:04.432989+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 173600, "qty": 32, "cost": 5556033, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 158000, "target": 176000, "signals": [{"label": "손절선", "ok": true, "detail": "158,000 (현재 173,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 176,000"}, {"label": "추세(20일선)", "ok": true, "detail": "150,470"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +205 · 기 -155"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 173,548"}]}
{"ts": "2026-07-10T11:16:02.469865+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 13850, "qty": 166, "cost": 2299445, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10360, "target": 17770, "signals": [{"label": "손절선", "ok": true, "detail": "10,360 (현재 13,850)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,770"}, {"label": "추세(20일선)", "ok": true, "detail": "12,742"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +210 · 기 +239"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,843"}]}
{"ts": "2026-07-10T12:14:02.666464+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 126200, "qty": 15, "cost": 1893284, "path": "pullback", "reason": "눌림목 지정가 162,268 도달", "stop": 96000, "target": 186600, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "126,200 vs 120,150"}, {"label": "정배열(5>20)", "ok": true, "detail": "137,260 / 120,150"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "137,260 / 121,203"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +483 · 기 -241 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "7.53% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,695,762 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "7.53% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 162,268 (현재 126,200)"}]}
{"ts": "2026-07-13T09:00:09.888217+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 136900, "qty": 38, "cost": 5202980, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 120761, "target": 154477, "signals": [{"label": "손절선", "ok": true, "detail": "120,761 (현재 136,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,477"}, {"label": "추세(20일선)", "ok": true, "detail": "109,140"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -64 · 기 +919"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,864"}]}
{"ts": "2026-07-13T09:12:04.294393+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 341500, "qty": 15, "cost": 5123268, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 310647, "target": 403206, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "341,000 vs 296,175"}, {"label": "정배열(5>20)", "ok": true, "detail": "314,400 / 296,175"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "314,400 / 305,433"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -63 · 기 +144 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.90% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "197,687 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.90% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-07-13T09:26:03.405300+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 139100, "qty": 13, "cost": 1808571, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 96000, "target": 186600, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 139,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 186,600"}, {"label": "추세(20일선)", "ok": true, "detail": "120,795"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -213 · 기 +772"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 136,162"}]}
{"ts": "2026-07-13T09:36:02.268298+09:00", "side": "BUY", "code": "105560", "name": "KB금융", "price": 191000, "qty": 29, "cost": 5539831, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 171311, "target": 215077, "signals": [{"label": "손절선", "ok": true, "detail": "171,311 (현재 191,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 215,077"}, {"label": "추세(20일선)", "ok": true, "detail": "160,160"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -867 · 기 +1,393"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 190,952"}]}
{"ts": "2026-07-13T12:44:04.349246+09:00", "side": "SELL", "code": "319660", "name": "피에스케이", "price": 187900, "qty": 65, "proceeds": 12189684, "entry_price": 168726, "pnl": 1220839, "pnl_pct": 11.36, "hold_from": "2026-07-10T09:00:10.855598+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "189,133 (현재 187,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "151,185"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -254 · 기 +457"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-14T10:20:06.141551+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 310500, "qty": 15, "proceeds": 4648418, "entry_price": 341500, "pnl": -474851, "pnl_pct": -9.08, "hold_from": "2026-07-13T09:12:04.294392+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "310,647 (현재 310,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 403,206"}, {"label": "추세(20일선)", "ok": true, "detail": "294,650"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -50 · 기 +82"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 351,713"}]}
{"ts": "2026-07-16T09:00:08.249132+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 209000, "qty": 27, "proceeds": 5631996, "entry_price": 150948, "pnl": 1555785, "pnl_pct": 38.46, "hold_from": "2026-07-09T09:02:04.457275+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "210,764 (현재 207,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "138,765"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -799 · 기 +891"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-16T09:00:08.249940+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18580, "qty": 290, "proceeds": 5377693, "entry_price": 19110, "pnl": -165038, "pnl_pct": -2.77, "hold_from": "2026-07-09T09:02:04.457419+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,017 (현재 18,560)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,296"}, {"label": "추세(20일선)", "ok": false, "detail": "18,832"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -120 · 기 -150"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,486"}]}
{"ts": "2026-07-16T09:38:02.508846+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 138900, "qty": 40, "cost": 5556833, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 128529, "target": 159642, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "138,800 vs 119,185"}, {"label": "정배열(5>20)", "ok": true, "detail": "124,360 / 119,185"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "124,360 / 118,712"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+40.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +71 · 기 +417 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.76% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": true, "detail": "2,097,713 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": true, "detail": "0.76% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-20T09:04:02.871730+09:00", "side": "SELL", "code": "105560", "name": "KB금융", "price": 173000, "qty": 58, "proceeds": 10014434, "entry_price": 188450, "pnl": -917306, "pnl_pct": -8.2, "hold_from": "2026-07-10T09:28:02.614091+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "173,293 (현재 173,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 218,763"}, {"label": "추세(20일선)", "ok": true, "detail": "159,270"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -332 · 기 +291"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-20T12:28:02.310171+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 10310, "qty": 345, "proceeds": 3550014, "entry_price": 13321, "pnl": -1046345, "pnl_pct": -22.6, "hold_from": "2026-07-10T09:46:11.469023+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,360 (현재 10,310)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 19,242"}, {"label": "추세(20일선)", "ok": false, "detail": "14,755"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -887 · 기 +515"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-21T09:06:03.147127+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 127800, "qty": 40, "proceeds": 5102032, "entry_price": 138900, "pnl": -454802, "pnl_pct": -7.99, "hold_from": "2026-07-16T09:38:02.508844+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "128,529 (현재 127,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 159,642"}, {"label": "추세(20일선)", "ok": true, "detail": "118,640"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +121 · 기 +190"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 142,068"}]}
{"ts": "2026-07-21T10:12:01.040324+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 167000, "qty": 33, "cost": 5511827, "path": "pullback", "reason": "눌림목 지정가 169,640 도달", "stop": 158000, "target": 185000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "167,000 vs 150,140"}, {"label": "정배열(5>20)", "ok": true, "detail": "171,040 / 150,140"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,040 / 113,342"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+92.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -534 · 기 +316 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+7%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.29% (환산) vs 평균 2.68% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 196,500"}, {"label": "거래량 급증", "ok": false, "detail": "951,819 (환산) vs 평균 777,376"}, {"label": "회전율 급증", "ok": false, "detail": "3.29% (환산) vs 평균 2.68%"}, {"label": "RSI", "ok": true, "detail": "58"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 169,640 (현재 167,000)"}]}
{"ts": "2026-07-21T12:06:01.164333+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 344500, "qty": 14, "cost": 4823723, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 313808, "target": 405884, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "344,500 vs 296,350"}, {"label": "정배열(5>20)", "ok": true, "detail": "315,100 / 296,350"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "315,100 / 305,492"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+39.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -7 · 기 +1 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+43%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.38% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "247,158 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "2.38% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "65"}]}
{"ts": "2026-07-22T09:00:06.709714+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 177600, "qty": 31, "cost": 5506426, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 158000, "target": 185000, "signals": [{"label": "손절선", "ok": true, "detail": "158,000 (현재 178,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 185,000"}, {"label": "추세(20일선)", "ok": true, "detail": "150,715"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -377 · 기 +151"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 175,894"}]}
{"ts": "2026-07-22T09:04:01.813458+09:00", "side": "BUY", "code": "161890", "name": "한국콜마", "price": 99200, "qty": 56, "cost": 5556033, "path": "pullback", "reason": "눌림목 지정가 98,848 도달", "stop": 92100, "target": 113400, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "98,800 vs 98,505"}, {"label": "정배열(5>20)", "ok": true, "detail": "107,060 / 98,505"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "107,060 / 91,913"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +29 · 기 -34 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.16% (환산) vs 평균 1.76% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 124,600"}, {"label": "거래량 급증", "ok": false, "detail": "272,433 (환산) vs 평균 415,172"}, {"label": "회전율 급증", "ok": false, "detail": "1.16% (환산) vs 평균 1.76%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 98,848 (현재 98,800)"}]}
{"ts": "2026-07-22T12:12:00.734253+09:00", "side": "SELL", "code": "319660", "name": "피에스케이", "price": 157300, "qty": 64, "proceeds": 10047569, "entry_price": 172134, "pnl": -970684, "pnl_pct": -8.62, "hold_from": "2026-07-21T10:12:01.040322+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "158,000 (현재 157,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 200,403"}, {"label": "추세(20일선)", "ok": true, "detail": "149,655"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -407 · 기 +141"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-22T14:18:05.419864+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14030, "qty": 197, "cost": 2764325, "path": "pullback", "reason": "눌림목 지정가 14,023 도달", "stop": 11815, "target": 18460, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "14,020 vs 12,776"}, {"label": "정배열(5>20)", "ok": true, "detail": "14,552 / 12,776"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,552 / 13,283"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,005 · 기 +1,723 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.97% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "585,754 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.97% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "53"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 14,023 (현재 14,020)"}]}
{"ts": "2026-07-22T15:04:00.990876+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158100, "qty": 15, "cost": 2371856, "path": "pullback", "reason": "눌림목 지정가 158,107 도달", "stop": 129761, "target": 214778, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "158,000 vs 136,315"}, {"label": "정배열(5>20)", "ok": true, "detail": "172,240 / 136,315"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,240 / 99,750"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+49.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -311 · 기 +22 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.02% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "971,360 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": true, "detail": "5.02% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,107 (현재 158,000)"}]}
{"ts": "2026-07-23T09:00:22.456369+09:00", "side": "BUY", "code": "319660", "name": "피에스케이", "price": 160100, "qty": 34, "cost": 5444217, "path": "pullback", "reason": "눌림목 지정가 169,062 도달", "stop": 158000, "target": 164300, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "158,100 vs 149,695"}, {"label": "정배열(5>20)", "ok": true, "detail": "169,260 / 149,695"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "169,260 / 113,193"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+76.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -162 · 기 +37 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.05% (환산) vs 평균 2.68% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 196,500"}, {"label": "거래량 급증", "ok": false, "detail": "303,687 (환산) vs 평균 777,376"}, {"label": "회전율 급증", "ok": false, "detail": "1.05% (환산) vs 평균 2.68%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 169,062 (현재 158,100)"}]}
{"ts": "2026-07-23T09:32:04.406594+09:00", "side": "SELL", "code": "319660", "name": "피에스케이", "price": 157800, "qty": 34, "proceeds": 5354738, "entry_price": 160100, "pnl": -89479, "pnl_pct": -1.44, "hold_from": "2026-07-23T09:00:22.456363+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "158,000 (현재 157,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "149,685"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -162 · 기 +37"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-23T09:44:04.277794+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18850, "qty": 290, "cost": 5467320, "path": "pullback", "reason": "눌림목 지정가 20,001 도달", "stop": 17760, "target": 21030, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "18,850 vs 18,847"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,556 / 18,847"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,556 / 19,356"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+13.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -25 · 기 +6 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.34% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "388,767 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.34% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "48"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,001 (현재 18,850)"}]}
{"ts": "2026-07-23T09:50:01.509013+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 88200, "qty": 54, "cost": 4763514, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 80223, "target": 104155, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "88,200 vs 75,070"}, {"label": "정배열(5>20)", "ok": true, "detail": "83,000 / 75,070"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,000 / 75,058"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+47.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -150 · 기 +299 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.85% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "785,587 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.85% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-23T12:18:01.539249+09:00", "side": "BUY", "code": "161890", "name": "한국콜마", "price": 103100, "qty": 54, "cost": 5568235, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 92100, "target": 113400, "signals": [{"label": "손절선", "ok": true, "detail": "92,100 (현재 103,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 113,400"}, {"label": "추세(20일선)", "ok": true, "detail": "98,720"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +222 · 기 -227"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 103,029"}]}
{"ts": "2026-07-23T12:22:02.589476+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 356000, "qty": 13, "cost": 4628694, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 313808, "target": 405884, "signals": [{"label": "손절선", "ok": true, "detail": "313,808 (현재 356,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 405,884"}, {"label": "추세(20일선)", "ok": true, "detail": "296,925"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +6 · 기 +26"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 355,034"}]}
{"ts": "2026-07-23T12:54:04.491410+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90900, "qty": 53, "cost": 4818423, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 80223, "target": 104155, "signals": [{"label": "손절선", "ok": true, "detail": "80,223 (현재 91,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 104,155"}, {"label": "추세(20일선)", "ok": true, "detail": "75,210"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -169 · 기 +311"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 90,945"}]}
{"ts": "2026-07-24T09:00:05.632693+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 111800, "qty": 28, "proceeds": 3124296, "entry_price": 132189, "pnl": -577559, "pnl_pct": -15.42, "hold_from": "2026-07-10T12:14:02.666463+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "102,302 (현재 113,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 191,963"}, {"label": "추세(20일선)", "ok": false, "detail": "119,505"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -480 · 기 -206"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-24T09:00:05.633650+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18730, "qty": 290, "proceeds": 5421108, "entry_price": 18850, "pnl": -46212, "pnl_pct": -0.64, "hold_from": "2026-07-23T09:44:04.277793+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,760 (현재 18,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,030"}, {"label": "추세(20일선)", "ok": false, "detail": "18,841"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -5 · 기 -5"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,219"}]}
{"ts": "2026-07-24T09:28:04.944506+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14510, "qty": 191, "cost": 2771826, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11815, "target": 18460, "signals": [{"label": "손절선", "ok": true, "detail": "11,815 (현재 15,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,460"}, {"label": "추세(20일선)", "ok": true, "detail": "12,845"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -857 · 기 +1,172"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,791"}]}
{"ts": "2026-07-27T09:02:06.223077+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19100, "qty": 289, "cost": 5520728, "path": "pullback", "reason": "눌림목 지정가 20,032 도달", "stop": 18023, "target": 21254, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "19,070 vs 18,858"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,600 / 18,858"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,600 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+16.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +101 · 기 -3 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.13% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "146,760 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.13% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,032 (현재 19,070)"}]}
{"ts": "2026-07-27T09:02:06.223286+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7470, "qty": 533, "cost": 3982107, "path": "pullback", "reason": "눌림목 지정가 7,910 도달", "stop": 6645, "target": 9119, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "7,470 vs 4,876"}, {"label": "정배열(5>20)", "ok": true, "detail": "6,452 / 4,876"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,452 / 4,889"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+82.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +49 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.79% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "49,327 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "0.79% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "74"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,910 (현재 7,470)"}]}
{"ts": "2026-07-27T09:08:02.082537+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7910, "qty": 503, "cost": 3979327, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 6645, "target": 9119, "signals": [{"label": "손절선", "ok": true, "detail": "6,645 (현재 7,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,119"}, {"label": "추세(20일선)", "ok": true, "detail": "4,897"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +49 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,747"}]}
{"ts": "2026-07-27T13:40:01.090664+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6780, "qty": 1036, "proceeds": 7010383, "entry_price": 7684, "pnl": -951051, "pnl_pct": -11.76, "hold_from": "2026-07-27T09:02:06.223284+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "6,854 (현재 6,770)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,344"}, {"label": "추세(20일선)", "ok": true, "detail": "4,841"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +50 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-27T14:20:01.056082+09:00", "side": "BUY", "code": "218150", "name": "미래생명자원", "price": 2130, "qty": 1218, "cost": 2594729, "path": "pullback", "reason": "눌림목 지정가 2,135 도달", "stop": 1775, "target": 2840, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "2,130 vs 2,030"}, {"label": "정배열(5>20)", "ok": true, "detail": "2,415 / 2,030"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "2,415 / 2,265"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+41.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -118 · 기 +4 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "6.22% (환산) vs 평균 15.95% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 3,180"}, {"label": "거래량 급증", "ok": false, "detail": "1,270,804 (환산) vs 평균 3,257,264"}, {"label": "회전율 급증", "ok": false, "detail": "6.22% (환산) vs 평균 15.95%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 2,135 (현재 2,130)"}]}
{"ts": "2026-07-28T09:00:15.827850+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 126200, "qty": 15, "proceeds": 1889309, "entry_price": 158100, "pnl": -482547, "pnl_pct": -20.18, "hold_from": "2026-07-22T15:04:00.990875+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "129,761 (현재 128,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 214,778"}, {"label": "추세(20일선)", "ok": false, "detail": "134,815"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +269 · 기 -554"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 168,554"}]}
{"ts": "2026-07-28T09:00:15.829236+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6780, "qty": 495, "cost": 3356603, "path": "pullback", "reason": "눌림목 지정가 7,884 도달", "stop": 5916, "target": 8509, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "7,050 vs 4,855"}, {"label": "정배열(5>20)", "ok": true, "detail": "6,368 / 4,855"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,368 / 4,882"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+73.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +86 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.01% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "813 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "0.01% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "69"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,884 (현재 7,050)"}]}
{"ts": "2026-07-28T09:02:04.745721+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 11920, "qty": 388, "proceeds": 4615941, "entry_price": 14266, "pnl": -920209, "pnl_pct": -16.45, "hold_from": "2026-07-22T14:18:05.419863+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "11,984 (현재 11,960)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,831"}, {"label": "추세(20일선)", "ok": false, "detail": "12,673"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +84 · 기 -61"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-28T09:10:01.738088+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7100, "qty": 473, "cost": 3358804, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5916, "target": 8509, "signals": [{"label": "손절선", "ok": true, "detail": "5,916 (현재 7,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,509"}, {"label": "추세(20일선)", "ok": true, "detail": "4,857"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +86 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,079"}]}
{"ts": "2026-07-29T09:02:02.221428+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 117900, "qty": 44, "cost": 5188378, "path": "pullback", "reason": "눌림목 지정가 118,115 도달", "stop": 111200, "target": 131300, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "117,800 vs 113,445"}, {"label": "정배열(5>20)", "ok": true, "detail": "117,020 / 113,445"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "117,020 / 109,982"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+45.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +39 · 기 +307 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+26%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": false, "detail": "44,660 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "57"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 118,115 (현재 117,800)"}]}
{"ts": "2026-07-29T09:16:04.460893+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 120500, "qty": 43, "cost": 5182277, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111200, "target": 131300, "signals": [{"label": "손절선", "ok": true, "detail": "111,200 (현재 120,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 131,300"}, {"label": "추세(20일선)", "ok": true, "detail": "113,580"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +39 · 기 +307"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 120,492"}]}
{"ts": "2026-07-29T09:24:08.691208+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 292, "cost": 5534230, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 18023, "target": 21254, "signals": [{"label": "손절선", "ok": true, "detail": "18,023 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,254"}, {"label": "추세(20일선)", "ok": true, "detail": "18,884"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +99 · 기 +138"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,418"}]}
{"ts": "2026-07-29T09:24:08.691404+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 128600, "qty": 77, "proceeds": 9882891, "entry_price": 134418, "pnl": -468862, "pnl_pct": -4.33, "hold_from": "2026-07-09T09:36:03.452341+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "122,826 (현재 116,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 157,603"}, {"label": "추세(20일선)", "ok": true, "detail": "107,870"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +245"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:24:09.102125+09:00", "side": "BUY", "code": "218150", "name": "미래생명자원", "price": 1951, "qty": 1330, "cost": 2595219, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 1775, "target": 2840, "signals": [{"label": "손절선", "ok": true, "detail": "1,775 (현재 2,450)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 2,840"}, {"label": "추세(20일선)", "ok": true, "detail": "2,005"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +40 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 2,287"}]}
{"ts": "2026-07-29T09:28:01.378937+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6390, "qty": 819, "cost": 5234195, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5998, "target": 7174, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "6,380 vs 5,358"}, {"label": "정배열(5>20)", "ok": true, "detail": "5,742 / 5,358"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,742 / 5,622"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +73 · 기 -3 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.62% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "134,413 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.62% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "73"}]}
{"ts": "2026-07-29T10:42:05.407176+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5980, "qty": 819, "proceeds": 4888070, "entry_price": 6390, "pnl": -346125, "pnl_pct": -6.42, "hold_from": "2026-07-29T09:28:01.378936+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,998 (현재 5,980)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,174"}, {"label": "추세(20일선)", "ok": true, "detail": "5,338"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +77 · 기 -3"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,521"}]}
{"ts": "2026-07-29T11:18:04.419939+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6000, "qty": 968, "proceeds": 5796674, "entry_price": 6936, "pnl": -918733, "pnl_pct": -13.5, "hold_from": "2026-07-28T09:00:15.829228+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "6,040 (현재 6,020)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,729"}, {"label": "추세(20일선)", "ok": true, "detail": "4,803"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +185 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-30T09:04:06.866646+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6130, "qty": 411, "cost": 2519808, "path": "pullback", "reason": "눌림목 지정가 7,792 도달", "stop": 5127, "target": 8135, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "6,120 vs 4,808"}, {"label": "정배열(5>20)", "ok": true, "detail": "6,182 / 4,808"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,182 / 4,866"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+65.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +234 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.31% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "267,687 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "4.31% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,792 (현재 6,120)"}]}
{"ts": "2026-07-30T09:24:06.917510+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 131800, "qty": 37, "cost": 4877331, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 120690, "target": 154020, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "132,000 vs 108,925"}, {"label": "정배열(5>20)", "ok": true, "detail": "115,580 / 108,925"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,580 / 113,230"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+51.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -6 · 기 +178 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.66% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "738,813 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.66% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-30T11:58:07.822065+09:00", "side": "SELL", "code": "161890", "name": "한국콜마", "price": 91700, "qty": 110, "proceeds": 10067330, "entry_price": 101115, "pnl": -1056938, "pnl_pct": -9.31, "hold_from": "2026-07-22T09:04:01.813457+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "92,100 (현재 91,800)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 119,144"}, {"label": "추세(20일선)", "ok": false, "detail": "98,155"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -7 · 기 +40"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-31T09:04:07.266662+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 69100, "qty": 107, "proceeds": 7379282, "entry_price": 89537, "pnl": -2202655, "pnl_pct": -22.83, "hold_from": "2026-07-23T09:50:01.509012+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "81,303 (현재 79,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 106,006"}, {"label": "추세(20일선)", "ok": true, "detail": "74,655"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -179 · 기 +325"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-31T09:06:06.413012+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6550, "qty": 384, "cost": 2515577, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5127, "target": 8135, "signals": [{"label": "손절선", "ok": true, "detail": "5,127 (현재 6,640)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,135"}, {"label": "추세(20일선)", "ok": true, "detail": "4,834"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +355 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,453"}]}
{"ts": "2026-08-03T09:34:07.943956+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 120300, "qty": 37, "proceeds": 4442420, "entry_price": 131800, "pnl": -434911, "pnl_pct": -8.73, "hold_from": "2026-07-30T09:24:06.917508+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "120,690 (현재 120,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,020"}, {"label": "추세(20일선)", "ok": true, "detail": "108,345"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -35 · 기 +48"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,311"}]}
{"ts": "2026-08-03T09:44:09.371424+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6120, "qty": 804, "cost": 4921218, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5752, "target": 6857, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "6,120 vs 5,345"}, {"label": "정배열(5>20)", "ok": true, "detail": "5,690 / 5,345"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,690 / 5,618"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+31.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +80 · 기 +5 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.61% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "132,260 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.61% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-08-03T12:00:11.776427+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 12910, "qty": 333, "cost": 4299675, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11713, "target": 15304, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "12,930 vs 11,922"}, {"label": "정배열(5>20)", "ok": true, "detail": "12,138 / 11,922"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "12,138 / 11,924"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +7 · 기 +10 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+74%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.72% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 12,870"}, {"label": "거래량 급증", "ok": true, "detail": "208,994 (환산) vs 평균 99,303"}, {"label": "회전율 급증", "ok": true, "detail": "0.72% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-03T12:02:09.723448+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 13420, "qty": 320, "cost": 4295044, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11713, "target": 15304, "signals": [{"label": "손절선", "ok": true, "detail": "11,713 (현재 13,450)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 15,304"}, {"label": "추세(20일선)", "ok": true, "detail": "11,948"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +7 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,339"}]}
{"ts": "2026-08-04T09:04:08.850906+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 135900, "qty": 12, "cost": 1631045, "path": "pullback", "reason": "눌림목 지정가 156,279 도달", "stop": 104818, "target": 198064, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "135,700 vs 135,200"}, {"label": "정배열(5>20)", "ok": true, "detail": "167,780 / 135,200"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "167,780 / 99,378"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+24.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +23 · 기 +54 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+45%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "6.62% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": true, "detail": "1,281,140 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": true, "detail": "6.62% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 156,279 (현재 135,700)"}]}
{"ts": "2026-08-05T09:00:13.753117+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 147600, "qty": 11, "cost": 1623844, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 104818, "target": 198064, "signals": [{"label": "손절선", "ok": true, "detail": "104,818 (현재 147,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 198,064"}, {"label": "추세(20일선)", "ok": true, "detail": "135,790"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -256 · 기 +200"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 145,836"}]}
{"ts": "2026-08-05T09:02:12.586918+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5700, "qty": 804, "proceeds": 4573864, "entry_price": 6120, "pnl": -347355, "pnl_pct": -6.86, "hold_from": "2026-08-03T09:44:09.371421+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,752 (현재 5,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 6,857"}, {"label": "추세(20일선)", "ok": true, "detail": "5,326"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +14 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,233"}]}
{"ts": "2026-08-05T09:18:09.783204+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13560, "qty": 171, "cost": 2319108, "path": "pullback", "reason": "눌림목 지정가 13,947 도달", "stop": 11230, "target": 18219, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "13,540 vs 12,752"}, {"label": "정배열(5>20)", "ok": true, "detail": "14,456 / 12,752"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,456 / 13,275"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-2.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -72 · 기 +134 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.18% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "708,253 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "1.18% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,947 (현재 13,540)"}]}
{"ts": "2026-08-05T10:00:11.361876+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14270, "qty": 162, "cost": 2312087, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11230, "target": 18219, "signals": [{"label": "손절선", "ok": true, "detail": "11,230 (현재 14,340)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,219"}, {"label": "추세(20일선)", "ok": true, "detail": "12,792"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -72 · 기 +134"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,337"}]}
{"ts": "2026-08-06T09:00:17.919075+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18730, "qty": 581, "proceeds": 10860910, "entry_price": 19025, "pnl": -194048, "pnl_pct": -1.55, "hold_from": "2026-07-27T09:02:06.223075+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,070 (현재 18,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,933"}, {"label": "추세(20일선)", "ok": false, "detail": "18,842"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -203 · 기 -147"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-06T09:34:15.275497+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 192000, "qty": 25, "cost": 4800720, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 179508, "target": 216984, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "191,800 vs 177,680"}, {"label": "정배열(5>20)", "ok": true, "detail": "186,140 / 177,680"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "186,140 / 176,671"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +255 · 기 -319 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+44%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.67% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 191,700"}, {"label": "거래량 급증", "ok": true, "detail": "1,582,767 (환산) vs 평균 738,783"}, {"label": "회전율 급증", "ok": true, "detail": "0.67% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-07T09:12:15.076918+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 196400, "qty": 25, "cost": 4910736, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 179508, "target": 216984, "signals": [{"label": "손절선", "ok": true, "detail": "179,508 (현재 196,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 216,984"}, {"label": "추세(20일선)", "ok": true, "detail": "177,905"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +338 · 기 +73"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 196,264"}]}
{"ts": "2026-08-12T09:30:11.936727+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 400000, "qty": 27, "proceeds": 10778940, "entry_price": 350037, "pnl": 1326522, "pnl_pct": 14.27, "hold_from": "2026-07-21T12:06:01.164331+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "399,647 (현재 399,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "299,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -6 · 기 +72"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-12T09:38:12.223439+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19050, "qty": 268, "cost": 5106166, "path": "pullback", "reason": "눌림목 지정가 20,036 도달", "stop": 17979, "target": 21193, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "19,040 vs 18,856"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,594 / 18,856"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,594 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +113 · 기 +20 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.22% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "250,373 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.22% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,036 (현재 19,040)"}]}
{"ts": "2026-08-13T09:04:16.533363+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18820, "qty": 268, "proceeds": 5033925, "entry_price": 19050, "pnl": -72241, "pnl_pct": -1.21, "hold_from": "2026-08-12T09:38:12.223437+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,979 (현재 18,830)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,193"}, {"label": "추세(20일선)", "ok": false, "detail": "18,846"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -50 · 기 -76"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,414"}]}
{"ts": "2026-08-13T09:06:16.481896+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 403000, "qty": 10, "cost": 4030605, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 365665, "target": 477670, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "404,000 vs 299,325"}, {"label": "정배열(5>20)", "ok": true, "detail": "327,000 / 299,325"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "327,000 / 306,483"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+33.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +27 · 기 +23 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+25%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.93% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "200,187 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.93% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "75"}]}
{"ts": "2026-08-13T14:06:12.772245+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 416500, "qty": 10, "cost": 4165625, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 365665, "target": 477670, "signals": [{"label": "손절선", "ok": true, "detail": "365,665 (현재 416,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 477,670"}, {"label": "추세(20일선)", "ok": true, "detail": "299,925"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +19"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 415,570"}]}
{"ts": "2026-08-18T09:02:22.304396+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 115600, "qty": 87, "proceeds": 10037588, "entry_price": 119185, "pnl": -333067, "pnl_pct": -3.01, "hold_from": "2026-07-29T09:02:02.221426+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "111,408 (현재 116,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 134,739"}, {"label": "추세(20일선)", "ok": true, "detail": "113,375"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +129 · 기 -137"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-19T09:02:19.956688+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 135200, "qty": 23, "proceeds": 3103536, "entry_price": 141496, "pnl": -151352, "pnl_pct": -4.45, "hold_from": "2026-08-04T09:04:08.850905+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "111,689 (현재 135,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 201,110"}, {"label": "추세(20일선)", "ok": false, "detail": "135,165"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -41 · 기 -106"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-19T09:06:21.232233+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5530, "qty": 795, "proceeds": 4387777, "entry_price": 6333, "pnl": -647608, "pnl_pct": -12.68, "hold_from": "2026-07-30T09:04:06.866644+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "5,362 (현재 5,560)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,274"}, {"label": "추세(20일선)", "ok": true, "detail": "4,780"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +101 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-19T09:06:21.871145+09:00", "side": "SELL", "code": "218150", "name": "미래생명자원", "price": 2030, "qty": 2548, "proceeds": 5162354, "entry_price": 2037, "pnl": -27595, "pnl_pct": -0.32, "hold_from": "2026-07-27T14:20:01.056080+09:00", "reason": "시간손절 — 22일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "1,775 (현재 2,025)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 2,720"}, {"label": "추세(20일선)", "ok": false, "detail": "2,079"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +20 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-20T09:02:17.450992+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 33400, "qty": 50, "cost": 1670251, "path": "pullback", "reason": "눌림목 지정가 35,709 도달", "stop": 25688, "target": 48823, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "33,000 vs 26,058"}, {"label": "정배열(5>20)", "ok": true, "detail": "35,100 / 26,058"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "35,100 / 31,286"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -71 · 기 +7 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+106%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.67% (환산) vs 평균 3.74% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 45,300"}, {"label": "거래량 급증", "ok": false, "detail": "60,673 (환산) vs 평균 409,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.67% (환산) vs 평균 3.74%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 35,709 (현재 33,000)"}]}
{"ts": "2026-08-20T09:02:17.451493+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 144900, "qty": 13, "cost": 1883983, "path": "pullback", "reason": "눌림목 지정가 157,486 도달", "stop": 115629, "target": 203443, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "145,500 vs 135,690"}, {"label": "정배열(5>20)", "ok": true, "detail": "169,740 / 135,690"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "169,740 / 99,542"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+11.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +107 · 기 -119 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+48%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.38% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "266,680 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "1.38% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 157,486 (현재 145,500)"}]}
{"ts": "2026-08-20T09:02:17.451858+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 5500, "qty": 379, "cost": 2084813, "path": "pullback", "reason": "눌림목 지정가 7,771 도달", "stop": 4466, "target": 7567, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "5,480 vs 4,776"}, {"label": "정배열(5>20)", "ok": true, "detail": "6,054 / 4,776"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,054 / 4,856"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +53 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.51% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "93,867 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "1.51% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "54"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,771 (현재 5,480)"}]}
{"ts": "2026-08-20T09:02:17.452078+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 115000, "qty": 42, "cost": 4830724, "path": "pullback", "reason": "눌림목 지정가 117,922 도달", "stop": 111200, "target": 122600, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "114,800 vs 113,295"}, {"label": "정배열(5>20)", "ok": true, "detail": "116,420 / 113,295"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "116,420 / 109,932"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+16.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +156 · 기 -217 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.02% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": false, "detail": "34,100 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": false, "detail": "0.02% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 117,922 (현재 114,800)"}]}
{"ts": "2026-08-20T09:16:21.642669+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 35800, "qty": 47, "cost": 1682852, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 25688, "target": 48823, "signals": [{"label": "손절선", "ok": true, "detail": "25,688 (현재 36,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 48,823"}, {"label": "추세(20일선)", "ok": true, "detail": "26,208"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -71 · 기 +7"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 35,971"}]}
{"ts": "2026-08-24T09:00:21.444249+09:00", "side": "SELL", "code": "194370", "name": "제이에스코퍼레이션", "price": 12790, "qty": 653, "proceeds": 8335584, "entry_price": 13160, "pnl": -259135, "pnl_pct": -2.81, "hold_from": "2026-08-03T12:00:11.776417+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "11,874 (현재 12,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 15,732"}, {"label": "추세(20일선)", "ok": true, "detail": "11,913"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -59 · 기 -20"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-24T09:02:22.276759+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 31550, "qty": 153, "cost": 4827874, "path": "pullback", "reason": "눌림목 지정가 31,946 도달", "stop": 29900, "target": 34850, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "31,500 vs 31,388"}, {"label": "정배열(5>20)", "ok": true, "detail": "32,320 / 31,388"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "32,320 / 30,870"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-0.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -988 · 기 +144 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+39%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.08% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 33,850"}, {"label": "거래량 급증", "ok": false, "detail": "551,940 (환산) vs 평균 2,489,243"}, {"label": "회전율 급증", "ok": false, "detail": "0.08% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 31,946 (현재 31,500)"}]}
{"ts": "2026-08-25T09:20:20.388186+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 12360, "qty": 333, "proceeds": 4107854, "entry_price": 13905, "pnl": -523341, "pnl_pct": -11.11, "hold_from": "2026-08-05T09:18:09.783202+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "11,576 (현재 12,370)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,564"}, {"label": "추세(20일선)", "ok": false, "detail": "12,694"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +34 · 기 -30"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-25T09:34:17.879110+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 118200, "qty": 41, "cost": 4846927, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111200, "target": 122600, "signals": [{"label": "손절선", "ok": true, "detail": "111,200 (현재 118,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 122,600"}, {"label": "추세(20일선)", "ok": true, "detail": "113,465"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -39 · 기 +19"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 117,689"}]}
{"ts": "2026-08-25T09:46:20.069247+09:00", "side": "BUY", "code": "316140", "name": "우리금융지주", "price": 32400, "qty": 149, "cost": 4828324, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 29900, "target": 34850, "signals": [{"label": "손절선", "ok": true, "detail": "29,900 (현재 32,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 34,850"}, {"label": "추세(20일선)", "ok": true, "detail": "31,432"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,322 · 기 +436"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 32,372"}]}
{"ts": "2026-08-25T12:16:22.494708+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19190, "qty": 253, "cost": 4855798, "path": "pullback", "reason": "눌림목 지정가 20,031 도달", "stop": 18112, "target": 21346, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "19,180 vs 18,864"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,622 / 18,864"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,622 / 19,362"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+0.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +274 · 기 -97 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.21% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "241,100 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.21% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,031 (현재 19,180)"}]}
{"ts": "2026-08-26T09:36:23.410378+09:00", "side": "SELL", "code": "068270", "name": "셀트리온", "price": 193100, "qty": 50, "proceeds": 9636173, "entry_price": 194200, "pnl": -75284, "pnl_pct": -0.57, "hold_from": "2026-08-06T09:34:15.275495+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "181,408 (현재 193,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 219,784"}, {"label": "추세(20일선)", "ok": true, "detail": "177,750"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -64 · 기 -2"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-26T09:36:23.410657+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 133100, "qty": 33, "cost": 4392959, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 121465, "target": 156370, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "133,200 vs 108,985"}, {"label": "정배열(5>20)", "ok": true, "detail": "115,820 / 108,985"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,820 / 113,250"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+7.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +5 · 기 -29 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+27%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.70% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "789,493 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.70% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-08-27T09:00:37.362598+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 143200, "qty": 31, "cost": 4439866, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 121465, "target": 156370, "signals": [{"label": "손절선", "ok": true, "detail": "121,465 (현재 141,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 156,370"}, {"label": "추세(20일선)", "ok": true, "detail": "109,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +46 · 기 -18"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 137,114"}]}
{"ts": "2026-09-02T09:08:22.019259+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 401500, "qty": 20, "proceeds": 8014342, "entry_price": 409750, "pnl": -181888, "pnl_pct": -2.01, "hold_from": "2026-08-13T09:06:16.481894+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "372,040 (현재 403,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 485,170"}, {"label": "추세(20일선)", "ok": true, "detail": "299,312"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +27 · 기 -26"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-03T09:02:24.986236+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13520, "qty": 174, "cost": 2352833, "path": "pullback", "reason": "눌림목 지정가 13,982 도달", "stop": 11243, "target": 18074, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "13,430 vs 12,746"}, {"label": "정배열(5>20)", "ok": true, "detail": "14,434 / 12,746"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,434 / 13,273"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-9.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -6 · 기 +7 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+86%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.08% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "47,427 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.08% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,982 (현재 13,430)"}]}
{"ts": "2026-09-07T09:02:26.079168+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 157600, "qty": 12, "cost": 1891484, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 115629, "target": 203443, "signals": [{"label": "손절선", "ok": true, "detail": "115,629 (현재 157,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 203,443"}, {"label": "추세(20일선)", "ok": true, "detail": "136,265"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +212 · 기 -125"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 154,446"}]}
{"ts": "2026-09-08T15:20:32.432185+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 130000, "qty": 83, "proceeds": 10768960, "entry_price": 116581, "pnl": 1091308, "pnl_pct": 11.51, "hold_from": "2026-08-20T09:02:17.452076+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "130,576 (현재 130,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "114,070"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -147 · 기 +423"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-09T09:02:32.233013+09:00", "side": "BUY", "code": "012450", "name": "한화에어로스페이스", "price": 1028000, "qty": 4, "cost": 4112617, "path": "pullback", "reason": "눌림목 지정가 1,094,989 도달", "stop": 998000, "target": 1088000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "1,025,000 vs 1,024,150"}, {"label": "정배열(5>20)", "ok": true, "detail": "1,138,600 / 1,024,150"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "1,138,600 / 1,049,650"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+5.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -67 · 기 +27 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+62%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.11% (환산) vs 평균 0.44% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 1,255,000"}, {"label": "거래량 급증", "ok": false, "detail": "58,353 (환산) vs 평균 227,218"}, {"label": "회전율 급증", "ok": false, "detail": "0.11% (환산) vs 평균 0.44%"}, {"label": "RSI", "ok": true, "detail": "48"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 1,094,989 (현재 1,025,000)"}]}
{"ts": "2026-09-09T09:04:29.142548+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 4745, "qty": 379, "proceeds": 1794848, "entry_price": 5500, "pnl": -289964, "pnl_pct": -13.73, "hold_from": "2026-08-20T09:02:17.451855+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "4,466 (현재 4,765)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,567"}, {"label": "추세(20일선)", "ok": true, "detail": "4,740"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -13 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 5,871"}]}
{"ts": "2026-09-09T09:04:29.143163+09:00", "side": "BUY", "code": "004370", "name": "농심", "price": 418500, "qty": 12, "cost": 5022753, "path": "pullback", "reason": "눌림목 지정가 420,748 도달", "stop": 393372, "target": 468756, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "418,000 vs 384,125"}, {"label": "정배열(5>20)", "ok": true, "detail": "406,100 / 384,125"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "406,100 / 370,025"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+9.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +11 · 기 -28 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+35%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.18% (환산) vs 평균 0.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 440,000"}, {"label": "거래량 급증", "ok": false, "detail": "10,720 (환산) vs 평균 31,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.18% (환산) vs 평균 0.51%"}, {"label": "RSI", "ok": true, "detail": "64"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 420,748 (현재 418,000)"}]}
{"ts": "2026-09-09T09:10:28.368498+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 150800, "qty": 25, "proceeds": 3762648, "entry_price": 150996, "pnl": -12818, "pnl_pct": -0.13, "hold_from": "2026-08-20T09:02:17.451490+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "122,357 (현재 150,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 208,274"}, {"label": "추세(20일선)", "ok": true, "detail": "135,935"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +129 · 기 +107"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-09T09:10:28.369785+09:00", "side": "BUY", "code": "060250", "name": "NHN KCP", "price": 14750, "qty": 264, "cost": 3894584, "path": "pullback", "reason": "눌림목 지정가 15,087 도달", "stop": 13230, "target": 17790, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "14,690 vs 14,358"}, {"label": "정배열(5>20)", "ok": true, "detail": "15,356 / 14,358"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,356 / 13,592"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+24.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +52 · 기 +69 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.42% (환산) vs 평균 1.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 16,190"}, {"label": "거래량 급증", "ok": false, "detail": "166,893 (환산) vs 평균 608,910"}, {"label": "회전율 급증", "ok": false, "detail": "0.42% (환산) vs 평균 1.51%"}, {"label": "RSI", "ok": true, "detail": "53"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 15,087 (현재 14,690)"}]}
{"ts": "2026-09-10T09:00:55.811688+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18810, "qty": 253, "proceeds": 4749650, "entry_price": 19190, "pnl": -106148, "pnl_pct": -1.98, "hold_from": "2026-08-25T12:16:22.494706+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,112 (현재 18,810)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,346"}, {"label": "추세(20일선)", "ok": false, "detail": "18,845"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -262 · 기 -49"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,555"}]}
{"ts": "2026-09-10T09:00:55.814439+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11890, "qty": 367, "cost": 4364285, "path": "pullback", "reason": "눌림목 지정가 12,003 도달", "stop": 10784, "target": 14102, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "11,900 vs 10,158"}, {"label": "정배열(5>20)", "ok": true, "detail": "11,810 / 10,158"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "11,810 / 9,431"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+36.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +5 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 0.30% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 13,450"}, {"label": "거래량 급증", "ok": false, "detail": "0 (환산) vs 평균 36,789"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 0.30%"}, {"label": "RSI", "ok": true, "detail": "66"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 12,003 (현재 11,900)"}]}
{"ts": "2026-09-10T10:04:32.005765+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 152500, "qty": 64, "proceeds": 9740968, "entry_price": 137992, "pnl": 908143, "pnl_pct": 10.51, "hold_from": "2026-08-26T09:36:23.410654+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "153,508 (현재 152,800)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "109,965"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +0 · 기 +306"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-10T13:40:32.100659+09:00", "side": "BUY", "code": "012450", "name": "한화에어로스페이스", "price": 1070000, "qty": 4, "cost": 4280642, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 998000, "target": 1088000, "signals": [{"label": "손절선", "ok": true, "detail": "998,000 (현재 1,069,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 1,088,000"}, {"label": "추세(20일선)", "ok": true, "detail": "1,026,350"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -44 · 기 +13"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 1,068,505"}]}
{"ts": "2026-09-10T14:02:37.660602+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 117800, "qty": 33, "cost": 3887983, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 105837, "target": 141726, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "117,700 vs 76,545"}, {"label": "정배열(5>20)", "ok": true, "detail": "88,900 / 76,545"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "88,900 / 75,550"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+67.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +196 · 기 -113 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.95% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "884,443 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.95% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "82"}]}
{"ts": "2026-09-11T09:04:34.983232+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14660, "qty": 161, "cost": 2360614, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11243, "target": 18074, "signals": [{"label": "손절선", "ok": true, "detail": "11,243 (현재 14,510)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,074"}, {"label": "추세(20일선)", "ok": true, "detail": "12,800"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -103 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,275"}]}
{"ts": "2026-09-15T10:28:40.298899+09:00", "side": "SELL", "code": "012450", "name": "한화에어로스페이스", "price": 1063000, "qty": 8, "proceeds": 8487417, "entry_price": 1049000, "pnl": 94158, "pnl_pct": 1.33, "hold_from": "2026-09-09T09:02:32.233010+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "1,064,063 (현재 1,064,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": false, "detail": "1,089,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +88 · 기 -20"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-15T10:28:40.300996+09:00", "side": "BUY", "code": "261520", "name": "이지스", "price": 5100, "qty": 681, "cost": 3473621, "path": "pullback", "reason": "눌림목 지정가 5,108 도달", "stop": 4497, "target": 6305, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "5,100 vs 4,478"}, {"label": "정배열(5>20)", "ok": true, "detail": "5,199 / 4,478"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,199 / 4,257"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -27 · 기 +72 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.68% (환산) vs 평균 1.40% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 5,510"}, {"label": "거래량 급증", "ok": false, "detail": "70,632 (환산) vs 평균 143,937"}, {"label": "회전율 급증", "ok": false, "detail": "0.68% (환산) vs 평균 1.40%"}, {"label": "RSI", "ok": true, "detail": "62"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 5,108 (현재 5,100)"}]}
{"ts": "2026-09-16T09:02:38.444209+09:00", "side": "SELL", "code": "060250", "name": "NHN KCP", "price": 12830, "qty": 264, "proceeds": 3380515, "entry_price": 14750, "pnl": -514069, "pnl_pct": -13.02, "hold_from": "2026-09-09T09:10:28.369781+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "13,230 (현재 12,850)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,790"}, {"label": "추세(20일선)", "ok": false, "detail": "14,266"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +14 · 기 +18"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 15,348"}]}
{"ts": "2026-09-16T13:08:39.885464+09:00", "side": "BUY", "code": "372170", "name": "윤성에프앤씨", "price": 20200, "qty": 138, "cost": 2788018, "path": "pullback", "reason": "눌림목 지정가 21,341 도달", "stop": 17261, "target": 26078, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "20,150 vs 16,762"}, {"label": "정배열(5>20)", "ok": true, "detail": "19,666 / 16,762"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,666 / 19,448"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+39.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +0 · 기 +1 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.18% (환산) vs 평균 0.56% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 23,400"}, {"label": "거래량 급증", "ok": false, "detail": "14,282 (환산) vs 평균 44,688"}, {"label": "회전율 급증", "ok": false, "detail": "0.18% (환산) vs 평균 0.56%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 21,341 (현재 20,150)"}]}
{"ts": "2026-09-17T09:04:38.313675+09:00", "side": "SELL", "code": "004370", "name": "농심", "price": 392500, "qty": 12, "proceeds": 4700816, "entry_price": 418500, "pnl": -321938, "pnl_pct": -6.21, "hold_from": "2026-09-09T09:04:29.143160+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "393,372 (현재 393,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 468,756"}, {"label": "추세(20일선)", "ok": true, "detail": "382,875"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +9 · 기 +1"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 427,680"}]}
{"ts": "2026-09-17T09:04:38.748549+09:00", "side": "BUY", "code": "300720", "name": "한일시멘트", "price": 16170, "qty": 314, "cost": 5078142, "path": "pullback", "reason": "눌림목 지정가 16,386 도달", "stop": 15324, "target": 17862, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "16,080 vs 15,208"}, {"label": "정배열(5>20)", "ok": true, "detail": "16,298 / 15,208"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,298 / 14,435"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+16.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +7 · 기 +8 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+27%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.02% (환산) vs 평균 0.12% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 17,140"}, {"label": "거래량 급증", "ok": false, "detail": "11,173 (환산) vs 평균 89,609"}, {"label": "회전율 급증", "ok": false, "detail": "0.02% (환산) vs 평균 0.12%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,386 (현재 16,080)"}]}
{"ts": "2026-09-18T10:36:42.187283+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 12290, "qty": 355, "cost": 4363604, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10784, "target": 14102, "signals": [{"label": "손절선", "ok": true, "detail": "10,784 (현재 12,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 14,102"}, {"label": "추세(20일선)", "ok": true, "detail": "10,178"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +17 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 12,253"}]}
{"ts": "2026-09-29T11:06:44.794791+09:00", "side": "BUY", "code": "261520", "name": "이지스", "price": 5390, "qty": 644, "cost": 3471681, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 4497, "target": 6305, "signals": [{"label": "손절선", "ok": true, "detail": "4,497 (현재 5,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 6,305"}, {"label": "추세(20일선)", "ok": true, "detail": "4,490"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -18 · 기 +5"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 5,298"}]}
{"ts": "2026-09-30T14:04:45.036517+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 106100, "qty": 33, "proceeds": 3494472, "entry_price": 117800, "pnl": -393511, "pnl_pct": -9.93, "hold_from": "2026-09-10T14:02:37.660601+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "105,837 (현재 106,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 141,726"}, {"label": "추세(20일선)", "ok": true, "detail": "75,975"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +293 · 기 -347"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 121,202"}]}
{"ts": "2026-09-30T14:04:45.461296+09:00", "side": "BUY", "code": "257720", "name": "실리콘투", "price": 38400, "qty": 84, "cost": 3226084, "path": "pullback", "reason": "눌림목 지정가 39,510 도달", "stop": 33500, "target": 48200, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "38,350 vs 37,605"}, {"label": "정배열(5>20)", "ok": true, "detail": "41,460 / 37,605"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "41,460 / 36,080"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+8.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +56 · 기 -522 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+56%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.25% (환산) vs 평균 1.56% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 48,250"}, {"label": "거래량 급증", "ok": false, "detail": "822,398 (환산) vs 평균 1,023,767"}, {"label": "회전율 급증", "ok": false, "detail": "1.25% (환산) vs 평균 1.56%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 39,510 (현재 38,350)"}]}
{"ts": "2026-10-01T09:00:54.036317+09:00", "side": "SELL", "code": "316140", "name": "우리금융지주", "price": 32800, "qty": 302, "proceeds": 9886284, "entry_price": 31969, "pnl": 230086, "pnl_pct": 2.6, "hold_from": "2026-08-24T09:02:22.276742+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "34,483 (현재 33,850)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "31,505"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -303 · 기 -158"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-01T09:00:54.040856+09:00", "side": "BUY", "code": "307870", "name": "비투엔", "price": 1198, "qty": 2721, "cost": 3260247, "path": "pullback", "reason": "눌림목 지정가 1,241 도달", "stop": 1047, "target": 1501, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "1,201 vs 1,140"}, {"label": "정배열(5>20)", "ok": true, "detail": "1,313 / 1,140"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "1,313 / 1,000"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+34.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -78 · 기 +4 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 1.90% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 1,390"}, {"label": "거래량 급증", "ok": false, "detail": "0 (환산) vs 평균 1,410,399"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 1.90%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 1,241 (현재 1,201)"}]}
{"ts": "2026-10-01T09:58:42.889067+09:00", "side": "SELL", "code": "257720", "name": "실리콘투", "price": 37550, "qty": 84, "proceeds": 3148049, "entry_price": 38400, "pnl": -78035, "pnl_pct": -2.21, "hold_from": "2026-09-30T14:04:45.461284+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "33,500 (현재 37,550)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 48,200"}, {"label": "추세(20일선)", "ok": false, "detail": "37,565"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -231 · 기 -208"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 40,093"}]}
{"ts": "2026-10-01T09:58:43.236189+09:00", "side": "BUY", "code": "048910", "name": "대원미디어", "price": 6990, "qty": 737, "cost": 5152403, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 6454, "target": 8062, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "7,000 vs 4,806"}, {"label": "정배열(5>20)", "ok": true, "detail": "5,552 / 4,806"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,552 / 4,914"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+49.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -49 · 기 +12 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.76% (환산) vs 평균 0.30% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,630"}, {"label": "거래량 급증", "ok": true, "detail": "95,240 (환산) vs 평균 38,349"}, {"label": "회전율 급증", "ok": true, "detail": "0.76% (환산) vs 평균 0.30%"}, {"label": "RSI", "ok": true, "detail": "86"}]}
{"ts": "2026-10-01T10:16:47.426944+09:00", "side": "BUY", "code": "048910", "name": "대원미디어", "price": 7170, "qty": 719, "cost": 5156003, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 6454, "target": 8062, "signals": [{"label": "손절선", "ok": true, "detail": "6,454 (현재 7,180)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,062"}, {"label": "추세(20일선)", "ok": true, "detail": "4,814"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -49 · 기 +12"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,169"}]}
{"ts": "2026-10-01T12:24:42.427319+09:00", "side": "BUY", "code": "307870", "name": "비투엔", "price": 1243, "qty": 2622, "cost": 3259635, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 1047, "target": 1501, "signals": [{"label": "손절선", "ok": true, "detail": "1,047 (현재 1,250)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 1,501"}, {"label": "추세(20일선)", "ok": true, "detail": "1,143"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -68 · 기 +4"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 1,249"}]}
{"ts": "2026-10-02T09:01:23.801920+09:00", "side": "BUY", "code": "372170", "name": "윤성에프앤씨", "price": 21400, "qty": 130, "cost": 2782417, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17261, "target": 26078, "signals": [{"label": "손절선", "ok": true, "detail": "17,261 (현재 21,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 26,078"}, {"label": "추세(20일선)", "ok": true, "detail": "16,840"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +2 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 21,139"}]}
{"ts": "2026-10-07T09:06:53.601882+09:00", "side": "SELL", "code": "300720", "name": "한일시멘트", "price": 15940, "qty": 314, "proceeds": 4995400, "entry_price": 16170, "pnl": -82742, "pnl_pct": -1.42, "hold_from": "2026-09-17T09:04:38.748535+09:00", "reason": "시간손절 — 20일 보유·진전 없음(자본 회전)", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "15,324 (현재 16,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,862"}, {"label": "추세(20일선)", "ok": true, "detail": "15,204"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -16 · 기 +15"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 16,456"}]}
{"ts": "2026-10-07T09:06:54.037826+09:00", "side": "BUY", "code": "295310", "name": "에이치브이엠", "price": 75200, "qty": 58, "cost": 4362254, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 67951, "target": 89698, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "74,500 vs 51,322"}, {"label": "정배열(5>20)", "ok": true, "detail": "62,140 / 51,322"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "62,140 / 47,968"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+45.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -68 · 기 +241 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+12%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.24% (환산) vs 평균 0.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 61,900"}, {"label": "거래량 급증", "ok": true, "detail": "278,607 (환산) vs 평균 120,414"}, {"label": "회전율 급증", "ok": true, "detail": "2.24% (환산) vs 평균 0.97%"}, {"label": "RSI", "ok": true, "detail": "77"}]}
{"ts": "2026-10-08T09:02:53.411594+09:00", "side": "SELL", "code": "327260", "name": "RF머트리얼즈", "price": 52400, "qty": 97, "proceeds": 5072889, "entry_price": 34563, "pnl": 1719786, "pnl_pct": 51.61, "hold_from": "2026-08-20T09:02:17.450989+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "53,034 (현재 52,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(20일선)", "ok": true, "detail": "44,220"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -85 · 기 +19"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-10-08T09:02:53.413418+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 301000, "qty": 16, "cost": 4816722, "path": "pullback", "reason": "눌림목 지정가 303,967 도달", "stop": 276000, "target": 351000, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "303,500 vs 294,300"}, {"label": "정배열(5>20)", "ok": true, "detail": "306,900 / 294,300"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "306,900 / 304,808"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-5.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +88 · 기 -120 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+73%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.37% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": false, "detail": "38,207 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": false, "detail": "0.37% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 303,967 (현재 303,500)"}]}
@@ -1,35 +0,0 @@
{
"cash": 92805420.975,
"initial": 100000000,
"positions": {
"035420": {
"code": "035420",
"name": "NAVER",
"sources": [
"interest"
],
"qty": 31,
"entry_price": 232048.38709677418,
"entry_at": "2026-06-11T10:25:02.231934+09:00",
"stop": 197164,
"target": 301817,
"peak": 263000,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 232,280 도달",
"cur_price": 248750,
"tranches": 2,
"tranche_value": 3741721.8543046354,
"last_add_price": 238500
}
},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100958420.975,
"max_drawdown": 0.0059876134567287895,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944280+09:00",
"updated_at": "2026-06-15T15:25:02.283725+09:00"
}
@@ -1,2 +0,0 @@
{"ts": "2026-06-11T10:25:02.231935+09:00", "side": "BUY", "code": "035420", "name": "NAVER", "price": 226000, "qty": 16, "cost": 3616542, "path": "pullback", "reason": "눌림목 지정가 232,280 도달", "stop": 195800, "target": 286400, "signals": [{"label": "추세(20일선 위)", "ok": true, "detail": "226,000 vs 224,445"}, {"label": "정배열(5>20)", "ok": true, "detail": "257,000 / 224,445"}, {"label": "추세 기울기(20일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "257,000 / 215,498"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -85 · 기 +1,187 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+45%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": false, "detail": "약세 — 돌파만 허용"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "6.51% (환산) vs 평균 2.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 304,000"}, {"label": "거래량 급증", "ok": true, "detail": "10,205,151 (환산) vs 평균 3,404,288"}, {"label": "회전율 급증", "ok": true, "detail": "6.51% (환산) vs 평균 2.17%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 232,280 (현재 226,000)"}]}
{"ts": "2026-06-12T09:20:02.965943+09:00", "side": "BUY", "code": "035420", "name": "NAVER", "price": 238500, "qty": 15, "cost": 3578037, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 195800, "target": 286400, "signals": [{"label": "손절선", "ok": true, "detail": "195,800 (현재 238,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 286,400"}, {"label": "추세(20일선)", "ok": true, "detail": "225,070"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -159 · 기 +1,154"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 237,628"}]}
@@ -1,13 +1,204 @@
{
"cash": 100000000,
"cash": 23737717.020249996,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944290+09:00",
"updated_at": "2026-06-15T15:25:02.284988+09:00"
"positions": {
"086790": {
"code": "086790",
"name": "하나금융지주",
"sources": [
"auto"
],
"qty": 83,
"entry_price": 123226.50602409638,
"entry_at": "2026-07-30T09:04:07.455785+09:00",
"stop": 113273,
"target": 143133,
"peak": 141800,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 122,665 도달",
"cur_price": 125200,
"tranches": 2,
"tranche_value": 5180051.180047499,
"last_add_price": 125200
},
"194370": {
"code": "194370",
"name": "제이에스코퍼레이션",
"sources": [
"auto"
],
"qty": 677,
"entry_price": 13160.10339734121,
"entry_at": "2026-08-03T12:00:11.789224+09:00",
"stop": 11874,
"target": 15732,
"peak": 14150,
"trailing_on": false,
"scaled_out": false,
"entry_path": "breakout",
"entry_reason": "거래량·회전율 동반 전고점 돌파",
"cur_price": 12200,
"tranches": 2,
"tranche_value": 4464385.527789056,
"last_add_price": 13420
},
"252990": {
"code": "252990",
"name": "샘씨엔에스",
"sources": [
"auto"
],
"qty": 357,
"entry_price": 14066.050420168067,
"entry_at": "2026-09-03T09:02:24.991070+09:00",
"stop": 18268,
"target": 18596,
"peak": 20700,
"trailing_on": true,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 13,982 도달",
"cur_price": 18340,
"tranches": 2,
"tranche_value": 2517324.6404987266,
"last_add_price": 14660
},
"478560": {
"code": "478560",
"name": "블랙야크아이앤씨",
"sources": [
"watch"
],
"qty": 2595,
"entry_price": 3193.8670520231212,
"entry_at": "2026-09-04T11:14:26.856101+09:00",
"stop": 2875,
"target": 3831,
"peak": 3500,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 3,140 도달",
"cur_price": 3345,
"tranches": 2,
"tranche_value": 4146153.6468727933,
"last_add_price": 3255
},
"372170": {
"code": "372170",
"name": "윤성에프앤씨",
"sources": [
"auto"
],
"qty": 272,
"entry_price": 20782.352941176472,
"entry_at": "2026-09-16T13:08:39.890765+09:00",
"stop": 17966,
"target": 26416,
"peak": 25500,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 21,341 도달",
"cur_price": 23800,
"tranches": 2,
"tranche_value": 2842559.6395269656,
"last_add_price": 21400
},
"041520": {
"code": "041520",
"name": "이엘씨",
"sources": [
"auto"
],
"qty": 688,
"entry_price": 11935.31976744186,
"entry_at": "2026-09-16T13:08:40.269206+09:00",
"stop": 10788,
"target": 14231,
"peak": 13200,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 11,955 도달",
"cur_price": 12790,
"tranches": 2,
"tranche_value": 4111186.507374883,
"last_add_price": 12170
},
"025860": {
"code": "025860",
"name": "남해화학",
"sources": [
"watch"
],
"qty": 848,
"entry_price": 6130,
"entry_at": "2026-09-18T09:14:43.475556+09:00",
"stop": 5730,
"target": 6930,
"peak": 6250,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 6,155 도달",
"cur_price": 5940,
"tranches": 1,
"tranche_value": 5205107.041859375,
"last_add_price": 6130
},
"300720": {
"code": "300720",
"name": "한일시멘트",
"sources": [
"watch"
],
"qty": 323,
"entry_price": 16110,
"entry_at": "2026-09-30T11:40:45.860919+09:00",
"stop": 15271,
"target": 17789,
"peak": 16290,
"trailing_on": false,
"scaled_out": false,
"entry_path": "pullback",
"entry_reason": "눌림목 지정가 16,390 도달",
"cur_price": 16110,
"tranches": 1,
"tranche_value": 5207239.454359375,
"last_add_price": 16110
}
},
"realized_pnl": -19540851.041000046,
"closed_trades": 50,
"wins": 5,
"peak_equity": 101690998.13850003,
"max_drawdown": 0.2049465569225206,
"last_exit": {
"086790": "2026-07-29",
"030000": "2026-09-04",
"403870": "2026-07-02",
"095610": "2026-09-16",
"093370": "2026-07-20",
"240810": "2026-07-24",
"034730": "2026-07-03",
"214450": "2026-09-08",
"055550": "2026-07-20",
"024060": "2026-07-27",
"003680": "2026-08-20",
"252990": "2026-09-01",
"010950": "2026-08-03",
"090850": "2026-08-06",
"078930": "2026-09-03",
"068270": "2026-09-04",
"327260": "2026-09-07",
"138040": "2026-09-08",
"000720": "2026-09-16",
"004370": "2026-09-18",
"079650": "2026-09-30"
},
"created_at": "2026-06-11T09:00:01.944290+09:00",
"updated_at": "2026-10-08T15:28:47.147135+09:00"
}
@@ -0,0 +1,143 @@
{"ts": "2026-06-17T11:15:02.310687+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 130900, "qty": 47, "cost": 6153223, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 121254, "target": 150191, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "129,300 vs 118,950"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,400 / 118,950"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,400 / 118,307"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-13.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +371 · 기 +158 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+22%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.12% (환산) vs 평균 0.38% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 126,500"}, {"label": "거래량 급증", "ok": true, "detail": "3,094,534 (환산) vs 평균 1,031,725"}, {"label": "회전율 급증", "ok": true, "detail": "1.12% (환산) vs 평균 0.38%"}, {"label": "RSI", "ok": true, "detail": "60"}]}
{"ts": "2026-06-17T16:47:28.529957+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 62900, "qty": 47, "cost": 2956743, "path": "pullback", "reason": "눌림목 지정가 64,531 도달", "stop": 52447, "target": 83807, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "62,800 vs 53,510"}, {"label": "정배열(5>10)", "ok": true, "detail": "59,880 / 53,510"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "59,880 / 49,831"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -4,434 · 기 +5,040 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+9%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "11.31% vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "9,309,211 vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": true, "detail": "11.31% vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "58"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 64,531 (현재 62,800)"}]}
{"ts": "2026-06-19T09:02:06.351442+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19160, "qty": 325, "cost": 6227934, "path": "pullback", "reason": "눌림목 지정가 20,044 도달", "stop": 18100, "target": 21279, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,150 vs 18,963"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,616 / 18,963"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,616 / 19,361"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-31.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,162 · 기 +2,080 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "37,673 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,044 (현재 19,150)"}]}
{"ts": "2026-06-19T13:32:02.797519+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 121100, "qty": 47, "proceeds": 5680601, "entry_price": 130900, "pnl": -472622, "pnl_pct": -7.49, "hold_from": "2026-06-17T11:15:02.310686+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "121,254 (현재 121,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 150,191"}, {"label": "추세(10일선)", "ok": true, "detail": "119,670"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +531 · 기 -129"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 134,178"}]}
{"ts": "2026-06-23T09:00:09.548616+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18820, "qty": 325, "proceeds": 6104573, "entry_price": 19160, "pnl": -123361, "pnl_pct": -1.77, "hold_from": "2026-06-19T09:02:06.351441+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,100 (현재 18,710)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,279"}, {"label": "추세(10일선)", "ok": false, "detail": "18,919"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -318 · 기 -227"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,528"}]}
{"ts": "2026-06-23T09:04:35.878315+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 121000, "qty": 49, "cost": 5929889, "path": "pullback", "reason": "눌림목 지정가 122,636 도달", "stop": 111004, "target": 140992, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "121,200 vs 119,680"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,840 / 119,680"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,840 / 118,418"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-14.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +310 · 기 -361 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+30%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.12% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": false, "detail": "324,680 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": false, "detail": "0.12% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 122,636 (현재 121,200)"}]}
{"ts": "2026-06-24T09:04:06.117082+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 52300, "qty": 47, "proceeds": 2453307, "entry_price": 62900, "pnl": -503437, "pnl_pct": -16.85, "hold_from": "2026-06-17T16:47:28.529950+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "52,447 (현재 52,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 83,807"}, {"label": "추세(10일선)", "ok": false, "detail": "52,460"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +711 · 기 -2,060"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 66,717"}]}
{"ts": "2026-06-24T09:04:06.117214+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 156700, "qty": 18, "cost": 2821023, "path": "pullback", "reason": "눌림목 지정가 165,075 도달", "stop": 129363, "target": 211374, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "156,200 vs 128,170"}, {"label": "정배열(5>10)", "ok": true, "detail": "143,260 / 128,170"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "143,260 / 121,703"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +143 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+6%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.83% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "2,860,073 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "5.83% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 165,075 (현재 156,200)"}]}
{"ts": "2026-06-24T09:12:05.351845+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 17270, "qty": 199, "cost": 3437246, "path": "pullback", "reason": "눌림목 지정가 17,366 도달", "stop": 14804, "target": 22203, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "17,280 vs 13,750"}, {"label": "정배열(5>10)", "ok": true, "detail": "16,364 / 13,750"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,364 / 11,500"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+21.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +1,740 · 기 -508 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+16%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.34% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": false, "detail": "2,509,340 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": false, "detail": "2.34% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "65"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 17,366 (현재 17,280)"}]}
{"ts": "2026-06-24T09:46:04.783231+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158200, "qty": 17, "cost": 2689803, "path": "pullback", "reason": "눌림목 지정가 158,264 도달", "stop": 130097, "target": 214407, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "158,000 vs 153,380"}, {"label": "정배열(5>10)", "ok": true, "detail": "172,240 / 153,380"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,240 / 99,750"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+56.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +41 · 기 +65 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+5%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.25% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "822,940 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "4.25% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,264 (현재 158,000)"}]}
{"ts": "2026-06-24T11:12:05.900560+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 18260, "qty": 188, "cost": 3433395, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 14804, "target": 22203, "signals": [{"label": "손절선", "ok": true, "detail": "14,804 (현재 18,250)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,203"}, {"label": "추세(10일선)", "ok": true, "detail": "13,847"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +1,742 · 기 -508"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 18,094"}]}
{"ts": "2026-06-25T09:00:23.676126+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 179100, "qty": 15, "cost": 2686903, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 130097, "target": 214407, "signals": [{"label": "손절선", "ok": true, "detail": "130,097 (현재 178,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 214,407"}, {"label": "추세(10일선)", "ok": true, "detail": "155,410"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -56 · 기 +134"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 167,275"}]}
{"ts": "2026-06-25T09:02:04.073952+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 53200, "qty": 49, "cost": 2607191, "path": "pullback", "reason": "눌림목 지정가 63,881 도달", "stop": 43200, "target": 73200, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,300 vs 52,560"}, {"label": "정배열(5>10)", "ok": true, "detail": "57,980 / 52,560"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "57,980 / 49,672"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+15.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +406 · 기 -1,461 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+29%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.21% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "1,819,047 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "2.21% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 63,881 (현재 53,300)"}]}
{"ts": "2026-06-26T09:00:05.786723+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 50000, "qty": 49, "proceeds": 2445222, "entry_price": 53200, "pnl": -161969, "pnl_pct": -6.02, "hold_from": "2026-06-25T09:02:04.073951+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "43,200 (현재 51,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 73,200"}, {"label": "추세(10일선)", "ok": false, "detail": "52,330"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -311 · 기 -267"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 57,056"}]}
{"ts": "2026-06-26T10:06:05.265619+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 110600, "qty": 49, "proceeds": 5408832, "entry_price": 121000, "pnl": -521057, "pnl_pct": -8.6, "hold_from": "2026-06-23T09:04:35.878312+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "111,004 (현재 110,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 140,992"}, {"label": "추세(10일선)", "ok": false, "detail": "118,630"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +34 · 기 -271"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 124,653"}]}
{"ts": "2026-06-26T10:14:04.062152+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 166900, "qty": 16, "cost": 2670801, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 129363, "target": 211374, "signals": [{"label": "손절선", "ok": true, "detail": "129,363 (현재 166,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 211,374"}, {"label": "추세(10일선)", "ok": true, "detail": "129,220"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -126 · 기 +320"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 165,659"}]}
{"ts": "2026-06-29T11:36:01.266385+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 745000, "qty": 4, "cost": 2980447, "path": "pullback", "reason": "눌림목 지정가 748,914 도달", "stop": 645871, "target": 943257, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "744,000 vs 735,600"}, {"label": "정배열(5>10)", "ok": true, "detail": "766,800 / 735,600"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "766,800 / 530,600"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -194 · 기 +152 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+8%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.35% (환산) vs 평균 0.45% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 883,000"}, {"label": "거래량 급증", "ok": true, "detail": "977,966 (환산) vs 평균 329,863"}, {"label": "회전율 급증", "ok": true, "detail": "1.35% (환산) vs 평균 0.45%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 748,914 (현재 744,000)"}]}
{"ts": "2026-06-29T14:18:01.486781+09:00", "side": "BUY", "code": "034730", "name": "SK", "price": 778000, "qty": 4, "cost": 3112467, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 645871, "target": 943257, "signals": [{"label": "손절선", "ok": true, "detail": "645,871 (현재 779,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 943,257"}, {"label": "추세(10일선)", "ok": true, "detail": "739,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -200 · 기 +143"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 778,043"}]}
{"ts": "2026-06-30T09:00:05.752645+09:00", "side": "BUY", "code": "403870", "name": "HPSP", "price": 55300, "qty": 43, "cost": 2378257, "path": "pullback", "reason": "눌림목 지정가 63,981 도달", "stop": 44022, "target": 77857, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "53,700 vs 52,600"}, {"label": "정배열(5>10)", "ok": true, "detail": "58,060 / 52,600"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "58,060 / 49,679"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +222 · 기 -821 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+28%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 5.97% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 71,500"}, {"label": "거래량 급증", "ok": false, "detail": "827 (환산) vs 평균 4,912,598"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 5.97%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 63,981 (현재 53,700)"}]}
{"ts": "2026-07-01T09:06:03.211367+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 357500, "qty": 15, "cost": 5363304, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 325040, "target": 422420, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "357,000 vs 307,050"}, {"label": "정배열(5>10)", "ok": true, "detail": "317,600 / 307,050"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "317,600 / 305,700"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -75 · 기 +221 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+35%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.89% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "196,247 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.89% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "68"}]}
{"ts": "2026-07-01T09:24:02.311832+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 372000, "qty": 14, "cost": 5208781, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 325040, "target": 422420, "signals": [{"label": "손절선", "ok": true, "detail": "325,040 (현재 372,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 422,420"}, {"label": "추세(10일선)", "ok": true, "detail": "308,550"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -75 · 기 +221"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 368,499"}]}
{"ts": "2026-07-01T11:06:02.353871+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 212000, "qty": 32, "proceeds": 6770771, "entry_price": 167997, "pnl": 1394065, "pnl_pct": 26.19, "hold_from": "2026-06-24T09:46:04.783230+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "213,193 (현재 213,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "158,880"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -229 · 기 +300"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T10:18:03.834928+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 122200, "qty": 45, "cost": 5499825, "path": "pullback", "reason": "눌림목 지정가 122,215 도달", "stop": 111572, "target": 143456, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "122,000 vs 119,760"}, {"label": "정배열(5>10)", "ok": true, "detail": "121,000 / 119,760"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "121,000 / 118,432"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +22 · 기 -44 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+29%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.80% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": true, "detail": "2,199,199 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": true, "detail": "0.80% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 122,215 (현재 122,000)"}]}
{"ts": "2026-07-02T10:40:02.952671+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18960, "qty": 322, "cost": 6106036, "path": "pullback", "reason": "눌림목 지정가 19,970 도달", "stop": 17790, "target": 21300, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,960 vs 18,944"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,578 / 18,944"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,578 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+3.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -339 · 기 +110 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.86% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "986,112 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.86% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 19,970 (현재 18,960)"}]}
{"ts": "2026-07-02T11:20:02.056072+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 22550, "qty": 271, "cost": 6111967, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17790, "target": 21300, "signals": [{"label": "손절선", "ok": true, "detail": "17,790 (현재 19,350)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,300"}, {"label": "추세(10일선)", "ok": true, "detail": "18,983"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,350"}]}
{"ts": "2026-07-02T11:22:01.965010+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 19000, "qty": 593, "proceeds": 11245029, "entry_price": 20601, "pnl": -972973, "pnl_pct": -7.77, "hold_from": "2026-07-02T10:40:02.952669+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "19,431 (현재 19,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,941"}, {"label": "추세(10일선)", "ok": true, "detail": "18,958"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -339 · 기 +110"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T12:42:02.220043+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 15250, "qty": 387, "proceeds": 5890242, "entry_price": 17751, "pnl": -980399, "pnl_pct": -14.09, "hold_from": "2026-06-24T09:12:05.351843+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "15,280 (현재 15,280)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 22,692"}, {"label": "추세(10일선)", "ok": true, "detail": "13,550"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -1,920 · 기 +434"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T14:30:02.062791+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 134100, "qty": 34, "proceeds": 4550509, "entry_price": 161500, "pnl": -941314, "pnl_pct": -16.97, "hold_from": "2026-06-24T09:04:06.117213+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "134,624 (현재 134,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 215,253"}, {"label": "추세(10일선)", "ok": true, "detail": "125,960"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +395 · 기 -1,516"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-02T15:04:06.185101+09:00", "side": "SELL", "code": "403870", "name": "HPSP", "price": 43900, "qty": 43, "proceeds": 1884019, "entry_price": 55300, "pnl": -494238, "pnl_pct": -20.61, "hold_from": "2026-06-30T09:00:05.752638+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "44,022 (현재 43,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 77,857"}, {"label": "추세(10일선)", "ok": false, "detail": "51,620"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +2,379 · 기 -6,625"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 59,411"}]}
{"ts": "2026-07-03T09:02:04.920019+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 133400, "qty": 16, "cost": 2134720, "path": "pullback", "reason": "눌림목 지정가 163,875 도달", "stop": 104263, "target": 191674, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "133,300 vs 125,880"}, {"label": "정배열(5>10)", "ok": true, "detail": "138,680 / 125,880"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "138,680 / 121,322"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+18.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +674 · 기 -2,170 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+31%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.16% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": false, "detail": "571,607 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": false, "detail": "1.16% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 163,875 (현재 133,300)"}]}
{"ts": "2026-07-03T09:02:04.920417+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 14660, "qty": 173, "cost": 2536560, "path": "pullback", "reason": "눌림목 지정가 17,179 도달", "stop": 11914, "target": 20152, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,680 vs 13,490"}, {"label": "정배열(5>10)", "ok": true, "detail": "15,844 / 13,490"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,844 / 11,456"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+12.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,591 · 기 +253 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+36%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.92% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": false, "detail": "991,593 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": false, "detail": "0.92% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 17,179 (현재 14,680)"}]}
{"ts": "2026-07-03T09:10:03.670622+09:00", "side": "SELL", "code": "034730", "name": "SK", "price": 656000, "qty": 8, "proceeds": 5237766, "entry_price": 761500, "pnl": -855147, "pnl_pct": -13.85, "hold_from": "2026-06-29T11:36:01.266384+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "662,371 (현재 657,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 959,757"}, {"label": "추세(10일선)", "ok": false, "detail": "726,900"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -51 · 기 +3"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T09:50:01.667559+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 328500, "qty": 29, "proceeds": 9507923, "entry_price": 364500, "pnl": -1064162, "pnl_pct": -9.88, "hold_from": "2026-07-01T09:06:03.211365+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "331,504 (현재 331,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 430,491"}, {"label": "추세(10일선)", "ok": true, "detail": "304,450"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -127 · 기 +294"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-03T10:16:02.530929+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 126200, "qty": 44, "cost": 5553633, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111572, "target": 143456, "signals": [{"label": "손절선", "ok": true, "detail": "111,572 (현재 126,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 143,456"}, {"label": "추세(10일선)", "ok": true, "detail": "120,180"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -140 · 기 +278"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 125,493"}]}
{"ts": "2026-07-03T10:24:02.772093+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19040, "qty": 309, "cost": 5884243, "path": "pullback", "reason": "눌림목 지정가 20,004 도달", "stop": 17921, "target": 21277, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,030 vs 18,951"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,592 / 18,951"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,592 / 19,359"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -35 · 기 +168 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+34%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.54% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "619,193 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.54% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,004 (현재 19,030)"}]}
{"ts": "2026-07-07T09:12:07.266482+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 155900, "qty": 16, "cost": 2494774, "path": "pullback", "reason": "눌림목 지정가 158,314 도달", "stop": 127872, "target": 211957, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "155,800 vs 153,160"}, {"label": "정배열(5>10)", "ok": true, "detail": "171,800 / 153,160"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,800 / 99,713"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +80 · 기 -197 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+14%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.33% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "645,567 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "3.33% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,314 (현재 155,800)"}]}
{"ts": "2026-07-07T12:02:08.180568+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 12230, "qty": 173, "proceeds": 2111664, "entry_price": 14660, "pnl": -424896, "pnl_pct": -16.58, "hold_from": "2026-07-03T09:02:04.920416+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "11,914 (현재 12,230)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,152"}, {"label": "추세(10일선)", "ok": false, "detail": "13,245"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -1,558 · 기 -4"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 15,662"}]}
{"ts": "2026-07-07T14:08:32.989792+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 183300, "qty": 11, "cost": 2016602, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 104263, "target": 191674, "signals": [{"label": "손절선", "ok": true, "detail": "104,263 (현재 183,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 191,674"}, {"label": "추세(10일선)", "ok": true, "detail": "123,150"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +263 · 기 -1,543"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 141,825"}]}
{"ts": "2026-07-07T14:08:32.992041+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 177000, "qty": 14, "cost": 2478372, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 127872, "target": 211957, "signals": [{"label": "손절선", "ok": true, "detail": "127,872 (현재 177,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 211,957"}, {"label": "추세(10일선)", "ok": true, "detail": "148,710"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +152 · 기 -281"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 165,142"}]}
{"ts": "2026-07-07T14:08:32.992753+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 20750, "qty": 283, "cost": 5873131, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17921, "target": 21277, "signals": [{"label": "손절선", "ok": true, "detail": "17,921 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,277"}, {"label": "추세(10일선)", "ok": true, "detail": "18,901"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 +113"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,358"}]}
{"ts": "2026-07-08T09:00:17.244123+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 108300, "qty": 27, "proceeds": 2918398, "entry_price": 153730, "pnl": -1232925, "pnl_pct": -29.55, "hold_from": "2026-07-03T09:02:04.920018+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "128,455 (현재 112,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 204,279"}, {"label": "추세(10일선)", "ok": false, "detail": "123,820"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +243 · 기 -857"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:00:17.245316+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 137900, "qty": 30, "proceeds": 4128933, "entry_price": 165747, "pnl": -844213, "pnl_pct": -16.8, "hold_from": "2026-07-07T09:12:07.266481+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "138,020 (현재 137,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 221,200"}, {"label": "추세(10일선)", "ok": false, "detail": "151,370"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +458 · 기 -572"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-08T09:12:05.963550+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18900, "qty": 592, "proceeds": 11166982, "entry_price": 19857, "pnl": -590392, "pnl_pct": -4.82, "hold_from": "2026-07-03T10:24:02.772092+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "18,903 (현재 18,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,766"}, {"label": "추세(10일선)", "ok": false, "detail": "18,938"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +193 · 기 +193"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-09T09:02:04.459886+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19110, "qty": 297, "cost": 5676521, "path": "pullback", "reason": "눌림목 지정가 20,021 도달", "stop": 18017, "target": 21296, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,070 vs 18,955"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,600 / 18,955"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,600 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+0.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -10 · 기 +164 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.08% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "93,540 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.08% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "50"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,021 (현재 19,070)"}]}
{"ts": "2026-07-09T09:36:03.454534+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 132000, "qty": 40, "cost": 5280792, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 120761, "target": 154477, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "131,900 vs 108,660"}, {"label": "정배열(5>10)", "ok": true, "detail": "115,560 / 108,660"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,560 / 113,228"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+22.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -184 · 기 +964 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.91% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "1,026,867 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.91% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-10T09:00:10.959018+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158000, "qty": 15, "cost": 2370356, "path": "pullback", "reason": "눌림목 지정가 157,800 도달", "stop": 129200, "target": 215600, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "157,000 vs 153,280"}, {"label": "정배열(5>10)", "ok": true, "detail": "172,040 / 153,280"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,040 / 99,733"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+53.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +452 · 기 -360 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+13%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.95% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "763,807 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "3.95% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 157,800 (현재 157,000)"}]}
{"ts": "2026-07-10T09:58:09.974240+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 168200, "qty": 14, "cost": 2355153, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 129200, "target": 215600, "signals": [{"label": "손절선", "ok": true, "detail": "129,200 (현재 168,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 215,600"}, {"label": "추세(10일선)", "ok": true, "detail": "154,390"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +452 · 기 -360"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 167,600"}]}
{"ts": "2026-07-10T11:06:06.467292+09:00", "side": "BUY", "code": "093370", "name": "후성", "price": 13660, "qty": 151, "cost": 2062969, "path": "pullback", "reason": "눌림목 지정가 16,984 도달", "stop": 10621, "target": 19737, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,630 vs 13,385"}, {"label": "정배열(5>10)", "ok": true, "detail": "15,634 / 13,385"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "15,634 / 11,439"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +210 · 기 +239 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+47%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "14.23% (환산) vs 평균 4.77% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 20,050"}, {"label": "거래량 급증", "ok": true, "detail": "15,267,605 (환산) vs 평균 5,114,125"}, {"label": "회전율 급증", "ok": true, "detail": "14.23% (환산) vs 평균 4.77%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,984 (현재 13,630)"}]}
{"ts": "2026-07-10T12:14:02.668438+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 126200, "qty": 15, "cost": 1893284, "path": "pullback", "reason": "눌림목 지정가 162,268 도달", "stop": 96000, "target": 186600, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "126,200 vs 125,170"}, {"label": "정배열(5>10)", "ok": true, "detail": "137,260 / 125,170"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "137,260 / 121,203"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+17.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +483 · 기 -241 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+38%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "7.53% (환산) vs 평균 2.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 183,300"}, {"label": "거래량 급증", "ok": true, "detail": "3,695,762 (환산) vs 평균 1,234,482"}, {"label": "회전율 급증", "ok": true, "detail": "7.53% (환산) vs 평균 2.51%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 162,268 (현재 126,200)"}]}
{"ts": "2026-07-13T09:00:09.988994+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 136900, "qty": 39, "cost": 5339901, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 120761, "target": 154477, "signals": [{"label": "손절선", "ok": true, "detail": "120,761 (현재 136,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,477"}, {"label": "추세(10일선)", "ok": true, "detail": "109,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -64 · 기 +919"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,864"}]}
{"ts": "2026-07-13T09:12:04.296873+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 341500, "qty": 15, "cost": 5123268, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 310647, "target": 403206, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "341,000 vs 305,450"}, {"label": "정배열(5>10)", "ok": true, "detail": "314,400 / 305,450"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "314,400 / 305,433"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -63 · 기 +144 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.90% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "197,687 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "1.90% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "64"}]}
{"ts": "2026-07-13T09:26:03.407256+09:00", "side": "BUY", "code": "240810", "name": "원익IPS", "price": 139100, "qty": 13, "cost": 1808571, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 96000, "target": 186600, "signals": [{"label": "손절선", "ok": true, "detail": "96,000 (현재 139,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 186,600"}, {"label": "추세(10일선)", "ok": true, "detail": "126,460"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -213 · 기 +772"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 136,162"}]}
{"ts": "2026-07-13T10:04:06.408909+09:00", "side": "BUY", "code": "055550", "name": "신한지주", "price": 113100, "qty": 51, "cost": 5768965, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 104723, "target": 129854, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "113,050 vs 100,265"}, {"label": "정배열(5>10)", "ok": true, "detail": "101,790 / 100,265"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "101,790 / 96,676"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+31.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -368 · 기 +1,005 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.71% (환산) vs 평균 0.35% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 109,800"}, {"label": "거래량 급증", "ok": true, "detail": "3,366,381 (환산) vs 평균 1,653,581"}, {"label": "회전율 급증", "ok": true, "detail": "0.71% (환산) vs 평균 0.35%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-14T10:20:06.143905+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 310500, "qty": 15, "proceeds": 4648418, "entry_price": 341500, "pnl": -474851, "pnl_pct": -9.08, "hold_from": "2026-07-13T09:12:04.296871+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "310,647 (현재 310,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 403,206"}, {"label": "추세(10일선)", "ok": true, "detail": "302,400"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -50 · 기 +82"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 351,713"}]}
{"ts": "2026-07-16T09:00:08.347687+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 209000, "qty": 29, "proceeds": 6049181, "entry_price": 162924, "pnl": 1323672, "pnl_pct": 28.28, "hold_from": "2026-07-10T09:00:10.959008+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "210,764 (현재 207,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "158,280"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -799 · 기 +891"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-16T09:00:08.349387+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18580, "qty": 297, "proceeds": 5507499, "entry_price": 19110, "pnl": -169022, "pnl_pct": -2.77, "hold_from": "2026-07-09T09:02:04.459884+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,017 (현재 18,560)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,296"}, {"label": "추세(10일선)", "ok": false, "detail": "18,904"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -120 · 기 -150"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,486"}]}
{"ts": "2026-07-20T09:02:02.272710+09:00", "side": "SELL", "code": "055550", "name": "신한지주", "price": 104000, "qty": 51, "proceeds": 5293657, "entry_price": 113100, "pnl": -475308, "pnl_pct": -8.05, "hold_from": "2026-07-13T10:04:06.408908+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "104,723 (현재 104,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 129,854"}, {"label": "추세(10일선)", "ok": true, "detail": "99,380"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -111 · 기 +265"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 115,714"}]}
{"ts": "2026-07-20T10:50:03.214952+09:00", "side": "SELL", "code": "093370", "name": "후성", "price": 10600, "qty": 151, "proceeds": 1597479, "entry_price": 13660, "pnl": -465491, "pnl_pct": -22.4, "hold_from": "2026-07-10T11:06:06.467291+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "10,621 (현재 10,610)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 19,737"}, {"label": "추세(10일선)", "ok": false, "detail": "12,043"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -893 · 기 +516"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,615"}]}
{"ts": "2026-07-21T12:06:01.166188+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 344500, "qty": 14, "cost": 4823723, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 313808, "target": 405884, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "344,500 vs 305,800"}, {"label": "정배열(5>10)", "ok": true, "detail": "315,100 / 305,800"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "315,100 / 305,492"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+39.7%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -7 · 기 +1 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+43%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.38% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "247,158 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "2.38% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "65"}]}
{"ts": "2026-07-22T14:18:05.421715+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14030, "qty": 206, "cost": 2890614, "path": "pullback", "reason": "눌림목 지정가 14,023 도달", "stop": 11815, "target": 18460, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,020 vs 12,975"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,552 / 12,975"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,552 / 13,283"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+10.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -1,005 · 기 +1,723 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.97% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "585,754 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.97% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "53"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 14,023 (현재 14,020)"}]}
{"ts": "2026-07-22T15:04:00.992935+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 158100, "qty": 16, "cost": 2529979, "path": "pullback", "reason": "눌림목 지정가 158,107 도달", "stop": 129761, "target": 214778, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "158,000 vs 153,380"}, {"label": "정배열(5>10)", "ok": true, "detail": "172,240 / 153,380"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "172,240 / 99,750"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+49.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -311 · 기 +22 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+24%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.02% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "971,360 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": true, "detail": "5.02% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,107 (현재 158,000)"}]}
{"ts": "2026-07-23T09:50:01.510936+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 88200, "qty": 57, "cost": 5028154, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 80223, "target": 104155, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "88,200 vs 80,900"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,000 / 80,900"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,000 / 75,058"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+47.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -150 · 기 +299 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+17%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.85% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "785,587 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.85% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-07-23T10:24:01.500387+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 152100, "qty": 16, "proceeds": 2428854, "entry_price": 158100, "pnl": -101125, "pnl_pct": -3.8, "hold_from": "2026-07-22T15:04:00.992934+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "129,761 (현재 152,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 214,778"}, {"label": "추세(10일선)", "ok": false, "detail": "152,780"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -101 · 기 -107"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 167,575"}]}
{"ts": "2026-07-23T10:30:02.480604+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 14140, "qty": 251, "cost": 3549672, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 12315, "target": 17791, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "14,180 vs 12,429"}, {"label": "정배열(5>10)", "ok": true, "detail": "13,154 / 12,429"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "13,154 / 12,687"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+55.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +109 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "84.26% (환산) vs 평균 31.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 13,990"}, {"label": "거래량 급증", "ok": true, "detail": "12,639,979 (환산) vs 평균 4,720,532"}, {"label": "회전율 급증", "ok": true, "detail": "84.26% (환산) vs 평균 31.47%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-07-23T12:22:02.591407+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 356000, "qty": 14, "cost": 4984748, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 313808, "target": 405884, "signals": [{"label": "손절선", "ok": true, "detail": "313,808 (현재 356,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 405,884"}, {"label": "추세(10일선)", "ok": true, "detail": "306,950"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +6 · 기 +26"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 355,034"}]}
{"ts": "2026-07-23T12:54:04.493271+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90900, "qty": 55, "cost": 5000250, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 80223, "target": 104155, "signals": [{"label": "손절선", "ok": true, "detail": "80,223 (현재 91,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 104,155"}, {"label": "추세(10일선)", "ok": true, "detail": "81,180"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -169 · 기 +311"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 90,945"}]}
{"ts": "2026-07-24T09:00:05.732126+09:00", "side": "SELL", "code": "240810", "name": "원익IPS", "price": 111800, "qty": 28, "proceeds": 3124296, "entry_price": 132189, "pnl": -577559, "pnl_pct": -15.42, "hold_from": "2026-07-10T12:14:02.668437+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "102,302 (현재 113,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 191,963"}, {"label": "추세(10일선)", "ok": false, "detail": "123,880"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -480 · 기 -206"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-24T09:00:05.734614+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 155500, "qty": 16, "cost": 2488373, "path": "pullback", "reason": "눌림목 지정가 158,093 도달", "stop": 127139, "target": 212221, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "155,000 vs 153,080"}, {"label": "정배열(5>10)", "ok": true, "detail": "171,640 / 153,080"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,640 / 99,700"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+44.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +200 · 기 -454 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+27%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "3.77% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "730,327 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": false, "detail": "3.77% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 158,093 (현재 155,000)"}]}
{"ts": "2026-07-24T09:02:04.039961+09:00", "side": "BUY", "code": "024060", "name": "흥구석유", "price": 15060, "qty": 236, "cost": 3554693, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 12315, "target": 17791, "signals": [{"label": "손절선", "ok": true, "detail": "12,315 (현재 15,020)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,791"}, {"label": "추세(10일선)", "ok": true, "detail": "12,513"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +208 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,766"}]}
{"ts": "2026-07-24T09:28:04.946271+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14510, "qty": 200, "cost": 2902435, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11815, "target": 18460, "signals": [{"label": "손절선", "ok": true, "detail": "11,815 (현재 15,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,460"}, {"label": "추세(10일선)", "ok": true, "detail": "13,113"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -857 · 기 +1,172"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,791"}]}
{"ts": "2026-07-24T10:02:04.395802+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 306, "cost": 5799570, "path": "pullback", "reason": "눌림목 지정가 20,012 도달", "stop": 17843, "target": 21164, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "18,950 vs 18,943"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,576 / 18,943"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,576 / 19,358"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+19.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +2 · 기 -5 (천주)"}, {"label": "투자의견", "ok": true, "detail": "4.0/5"}, {"label": "상승여력", "ok": true, "detail": "+33%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.39% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "454,069 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.39% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,012 (현재 18,950)"}]}
{"ts": "2026-07-27T09:04:05.309208+09:00", "side": "SELL", "code": "024060", "name": "흥구석유", "price": 12200, "qty": 487, "proceeds": 5929814, "entry_price": 14586, "pnl": -1174551, "pnl_pct": -16.36, "hold_from": "2026-07-23T10:30:02.480603+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "12,708 (현재 12,250)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,342"}, {"label": "추세(10일선)", "ok": true, "detail": "12,236"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -85 · 기 +2"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-27T09:06:01.452072+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7720, "qty": 547, "cost": 4223473, "path": "pullback", "reason": "눌림목 지정가 7,907 도달", "stop": 6890, "target": 9380, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "7,720 vs 5,416"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,502 / 5,416"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,502 / 4,893"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+88.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +49 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.17% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "258,673 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "4.17% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": false, "detail": "78"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,907 (현재 7,720)"}]}
{"ts": "2026-07-27T12:50:05.820784+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 8080, "qty": 523, "cost": 4226474, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 6890, "target": 9380, "signals": [{"label": "손절선", "ok": true, "detail": "6,890 (현재 8,070)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,380"}, {"label": "추세(10일선)", "ok": true, "detail": "5,452"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +50 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,997"}]}
{"ts": "2026-07-27T13:34:01.435246+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 7050, "qty": 1070, "proceeds": 7528790, "entry_price": 7896, "pnl": -921157, "pnl_pct": -10.71, "hold_from": "2026-07-27T09:06:01.452056+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "7,066 (현재 7,050)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,556"}, {"label": "추세(10일선)", "ok": true, "detail": "5,350"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +50 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-28T09:00:15.935347+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6780, "qty": 510, "cost": 3458319, "path": "pullback", "reason": "눌림목 지정가 7,884 도달", "stop": 5916, "target": 8509, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "7,050 vs 5,350"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,368 / 5,350"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,368 / 4,882"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+73.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +86 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.01% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "813 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "0.01% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "69"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,884 (현재 7,050)"}]}
{"ts": "2026-07-28T09:02:04.747643+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 11920, "qty": 406, "proceeds": 4830083, "entry_price": 14266, "pnl": -962966, "pnl_pct": -16.45, "hold_from": "2026-07-22T14:18:05.421714+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "11,984 (현재 11,960)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,831"}, {"label": "추세(10일선)", "ok": false, "detail": "12,769"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +84 · 기 -61"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-28T09:04:02.736929+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 123200, "qty": 16, "proceeds": 1967356, "entry_price": 155500, "pnl": -521017, "pnl_pct": -20.77, "hold_from": "2026-07-24T09:00:05.734608+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "127,139 (현재 123,600)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 212,221"}, {"label": "추세(10일선)", "ok": false, "detail": "149,940"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +269 · 기 -554"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 166,050"}]}
{"ts": "2026-07-28T09:10:01.739939+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 7100, "qty": 487, "cost": 3458219, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5916, "target": 8509, "signals": [{"label": "손절선", "ok": true, "detail": "5,916 (현재 7,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,509"}, {"label": "추세(10일선)", "ok": true, "detail": "5,354"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +86 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,079"}]}
{"ts": "2026-07-29T09:02:02.224639+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 117900, "qty": 45, "cost": 5306296, "path": "pullback", "reason": "눌림목 지정가 118,115 도달", "stop": 111200, "target": 131300, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "117,800 vs 116,390"}, {"label": "정배열(5>10)", "ok": true, "detail": "117,020 / 116,390"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "117,020 / 109,982"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+45.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +39 · 기 +307 (천주)"}, {"label": "투자의견", "ok": true, "detail": "3.8/5"}, {"label": "상승여력", "ok": true, "detail": "+26%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.03% (환산) vs 평균 0.17% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 123,300"}, {"label": "거래량 급증", "ok": false, "detail": "44,660 (환산) vs 평균 289,522"}, {"label": "회전율 급증", "ok": false, "detail": "0.03% (환산) vs 평균 0.17%"}, {"label": "RSI", "ok": true, "detail": "57"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 118,115 (현재 117,800)"}]}
{"ts": "2026-07-29T09:16:04.462967+09:00", "side": "BUY", "code": "138040", "name": "메리츠금융지주", "price": 120500, "qty": 44, "cost": 5302795, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111200, "target": 131300, "signals": [{"label": "손절선", "ok": true, "detail": "111,200 (현재 120,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 131,300"}, {"label": "추세(10일선)", "ok": true, "detail": "116,660"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +39 · 기 +307"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 120,492"}]}
{"ts": "2026-07-29T09:24:09.111290+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 18950, "qty": 306, "cost": 5799570, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17843, "target": 21164, "signals": [{"label": "손절선", "ok": true, "detail": "17,843 (현재 20,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,164"}, {"label": "추세(10일선)", "ok": true, "detail": "18,901"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +99 · 기 +138"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,268"}]}
{"ts": "2026-07-29T09:24:09.112024+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 128600, "qty": 79, "proceeds": 10139589, "entry_price": 134419, "pnl": -481104, "pnl_pct": -4.33, "hold_from": "2026-07-09T09:36:03.454533+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "122,827 (현재 116,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 157,603"}, {"label": "추세(10일선)", "ok": true, "detail": "105,620"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +245"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T09:28:01.380944+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6390, "qty": 843, "cost": 5387578, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5998, "target": 7174, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,380 vs 5,591"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,742 / 5,591"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,742 / 5,622"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+43.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +73 · 기 -3 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.62% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "134,413 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.62% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "73"}]}
{"ts": "2026-07-29T10:42:05.409278+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5980, "qty": 843, "proceeds": 5031310, "entry_price": 6390, "pnl": -356268, "pnl_pct": -6.42, "hold_from": "2026-07-29T09:28:01.380943+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,998 (현재 5,980)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,174"}, {"label": "추세(10일선)", "ok": true, "detail": "5,551"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +77 · 기 -3"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,521"}]}
{"ts": "2026-07-29T11:18:04.421952+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 6000, "qty": 997, "proceeds": 5970335, "entry_price": 6936, "pnl": -946202, "pnl_pct": -13.5, "hold_from": "2026-07-28T09:00:15.935337+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "6,040 (현재 6,020)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,729"}, {"label": "추세(10일선)", "ok": true, "detail": "5,246"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +185 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-29T12:04:01.685681+09:00", "side": "SELL", "code": "086790", "name": "하나금융지주", "price": 119200, "qty": 89, "proceeds": 10588113, "entry_price": 124178, "pnl": -465345, "pnl_pct": -4.01, "hold_from": "2026-07-02T10:18:03.834926+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "114,299 (현재 119,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 143,934"}, {"label": "추세(10일선)", "ok": false, "detail": "119,490"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -11 · 기 -51"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-30T09:04:07.455793+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 121300, "qty": 42, "cost": 5095364, "path": "pullback", "reason": "눌림목 지정가 122,665 도달", "stop": 111347, "target": 141206, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "121,200 vs 119,680"}, {"label": "정배열(5>10)", "ok": true, "detail": "120,840 / 119,680"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "120,840 / 118,418"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+34.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -214 · 기 +182 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+36%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.18% (환산) vs 평균 0.37% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 130,700"}, {"label": "거래량 급증", "ok": false, "detail": "494,880 (환산) vs 평균 1,026,357"}, {"label": "회전율 급증", "ok": false, "detail": "0.18% (환산) vs 평균 0.37%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 122,665 (현재 121,200)"}]}
{"ts": "2026-07-30T09:04:07.457329+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6130, "qty": 423, "cost": 2593379, "path": "pullback", "reason": "눌림목 지정가 7,792 도달", "stop": 5127, "target": 8135, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,120 vs 5,256"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,182 / 5,256"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,182 / 4,866"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+65.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +234 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "4.31% (환산) vs 평균 5.69% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 8,460"}, {"label": "거래량 급증", "ok": false, "detail": "267,687 (환산) vs 평균 353,452"}, {"label": "회전율 급증", "ok": false, "detail": "4.31% (환산) vs 평균 5.69%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 7,792 (현재 6,120)"}]}
{"ts": "2026-07-30T09:16:11.909415+09:00", "side": "BUY", "code": "086790", "name": "하나금융지주", "price": 125200, "qty": 41, "cost": 5133970, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 111347, "target": 141206, "signals": [{"label": "손절선", "ok": true, "detail": "111,347 (현재 125,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 141,206"}, {"label": "추세(10일선)", "ok": true, "detail": "120,100"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -214 · 기 +182"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 124,618"}]}
{"ts": "2026-07-30T09:24:07.511684+09:00", "side": "BUY", "code": "010950", "name": "S-Oil", "price": 131800, "qty": 38, "cost": 5009151, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 120690, "target": 154020, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "132,000 vs 108,670"}, {"label": "정배열(5>10)", "ok": true, "detail": "115,580 / 108,670"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "115,580 / 113,230"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+51.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -6 · 기 +178 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+20%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.66% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 116,900"}, {"label": "거래량 급증", "ok": true, "detail": "738,813 (환산) vs 평균 359,705"}, {"label": "회전율 급증", "ok": true, "detail": "0.66% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-07-30T09:26:07.458683+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 6300, "qty": 855, "cost": 5387308, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5917, "target": 7067, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,280 vs 5,581"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,722 / 5,581"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,722 / 5,620"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+46.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +87 · 기 +6 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.51% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "109,080 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.51% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "71"}]}
{"ts": "2026-07-31T09:04:07.865082+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 69100, "qty": 112, "proceeds": 7724109, "entry_price": 89526, "pnl": -2304295, "pnl_pct": -22.82, "hold_from": "2026-07-23T09:50:01.510934+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "81,291 (현재 79,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 105,995"}, {"label": "추세(10일선)", "ok": false, "detail": "80,070"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -179 · 기 +325"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-07-31T09:06:06.999372+09:00", "side": "BUY", "code": "003680", "name": "한성기업", "price": 6550, "qty": 396, "cost": 2594189, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5127, "target": 8135, "signals": [{"label": "손절선", "ok": true, "detail": "5,127 (현재 6,640)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,135"}, {"label": "추세(10일선)", "ok": true, "detail": "5,308"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +355 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,453"}]}
{"ts": "2026-08-03T09:34:08.538141+09:00", "side": "SELL", "code": "010950", "name": "S-Oil", "price": 120300, "qty": 38, "proceeds": 4562486, "entry_price": 131800, "pnl": -446665, "pnl_pct": -8.73, "hold_from": "2026-07-30T09:24:07.511679+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "120,690 (현재 120,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 154,020"}, {"label": "추세(10일선)", "ok": true, "detail": "107,510"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -35 · 기 +48"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 135,311"}]}
{"ts": "2026-08-03T12:00:11.789230+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 12910, "qty": 345, "cost": 4454618, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 11713, "target": 15304, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "12,930 vs 12,003"}, {"label": "정배열(5>10)", "ok": true, "detail": "12,138 / 12,003"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "12,138 / 11,924"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+27.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +7 · 기 +10 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+74%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.72% (환산) vs 평균 0.34% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 12,870"}, {"label": "거래량 급증", "ok": true, "detail": "208,994 (환산) vs 평균 99,303"}, {"label": "회전율 급증", "ok": true, "detail": "0.72% (환산) vs 평균 0.34%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-03T12:02:09.735696+09:00", "side": "BUY", "code": "194370", "name": "제이에스코퍼레이션", "price": 13420, "qty": 332, "cost": 4456108, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11713, "target": 15304, "signals": [{"label": "손절선", "ok": true, "detail": "11,713 (현재 13,450)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 15,304"}, {"label": "추세(10일선)", "ok": true, "detail": "12,055"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +7 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 13,339"}]}
{"ts": "2026-08-04T09:50:09.564920+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 90400, "qty": 50, "cost": 4520678, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 82166, "target": 106869, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "90,300 vs 81,110"}, {"label": "정배열(5>10)", "ok": true, "detail": "83,420 / 81,110"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "83,420 / 75,093"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+56.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +68 · 기 +117 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+14%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.87% (환산) vs 평균 0.42% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 87,600"}, {"label": "거래량 급증", "ok": true, "detail": "805,693 (환산) vs 평균 392,182"}, {"label": "회전율 급증", "ok": true, "detail": "0.87% (환산) vs 평균 0.42%"}, {"label": "RSI", "ok": true, "detail": "69"}]}
{"ts": "2026-08-04T10:32:12.762882+09:00", "side": "BUY", "code": "078930", "name": "GS", "price": 93500, "qty": 48, "cost": 4488673, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 82166, "target": 106869, "signals": [{"label": "손절선", "ok": true, "detail": "82,166 (현재 93,400)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 106,869"}, {"label": "추세(10일선)", "ok": true, "detail": "81,420"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +57 · 기 +117"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 93,241"}]}
{"ts": "2026-08-04T13:26:12.235766+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5910, "qty": 855, "proceeds": 5043197, "entry_price": 6300, "pnl": -344111, "pnl_pct": -6.19, "hold_from": "2026-07-30T09:26:07.458674+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,917 (현재 5,910)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 7,067"}, {"label": "추세(10일선)", "ok": true, "detail": "5,544"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +36 · 기 +9"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 6,419"}]}
{"ts": "2026-08-05T09:02:13.217011+09:00", "side": "BUY", "code": "090850", "name": "현대이지웰", "price": 5710, "qty": 911, "cost": 5202590, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5372, "target": 6387, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "5,730 vs 5,526"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,612 / 5,526"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,612 / 5,611"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+11.9%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +14 · 기 -2 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.46% (환산) vs 평균 0.22% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 5,700"}, {"label": "거래량 급증", "ok": true, "detail": "99,627 (환산) vs 평균 49,512"}, {"label": "회전율 급증", "ok": true, "detail": "0.46% (환산) vs 평균 0.22%"}, {"label": "RSI", "ok": true, "detail": "61"}]}
{"ts": "2026-08-06T09:00:18.887672+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18730, "qty": 612, "proceeds": 11440408, "entry_price": 18950, "pnl": -158732, "pnl_pct": -1.16, "hold_from": "2026-07-24T10:02:04.395801+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,996 (현재 18,750)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 20,858"}, {"label": "추세(10일선)", "ok": false, "detail": "18,923"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -203 · 기 -147"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-06T09:00:18.926770+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13300, "qty": 181, "cost": 2407661, "path": "pullback", "reason": "눌림목 지정가 13,975 도달", "stop": 11012, "target": 17875, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,300 vs 12,903"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,408 / 12,903"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,408 / 13,271"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-5.1%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -352 · 기 +499 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.00% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "7 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.00% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,975 (현재 13,300)"}]}
{"ts": "2026-08-06T09:06:09.886971+09:00", "side": "SELL", "code": "090850", "name": "현대이지웰", "price": 5470, "qty": 911, "proceeds": 4973453, "entry_price": 5710, "pnl": -229137, "pnl_pct": -4.2, "hold_from": "2026-08-05T09:02:13.217002+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "5,372 (현재 5,490)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 6,387"}, {"label": "추세(10일선)", "ok": false, "detail": "5,502"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -30 · 기 -23"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 5,823"}]}
{"ts": "2026-08-06T09:34:15.869494+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 192000, "qty": 27, "cost": 5184778, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 179508, "target": 216984, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "191,800 vs 180,670"}, {"label": "정배열(5>10)", "ok": true, "detail": "186,140 / 180,670"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "186,140 / 176,671"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+20.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +255 · 기 -319 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+44%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.67% (환산) vs 평균 0.32% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 191,700"}, {"label": "거래량 급증", "ok": true, "detail": "1,582,767 (환산) vs 평균 738,783"}, {"label": "회전율 급증", "ok": true, "detail": "0.67% (환산) vs 평균 0.32%"}, {"label": "RSI", "ok": true, "detail": "63"}]}
{"ts": "2026-08-07T09:02:12.155244+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14290, "qty": 169, "cost": 2415372, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11012, "target": 17875, "signals": [{"label": "손절선", "ok": true, "detail": "11,012 (현재 14,240)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 17,875"}, {"label": "추세(10일선)", "ok": true, "detail": "12,997"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -216 · 기 +313"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,046"}]}
{"ts": "2026-08-07T09:12:15.674263+09:00", "side": "BUY", "code": "068270", "name": "셀트리온", "price": 196400, "qty": 26, "cost": 5107166, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 179508, "target": 216984, "signals": [{"label": "손절선", "ok": true, "detail": "179,508 (현재 196,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 216,984"}, {"label": "추세(10일선)", "ok": true, "detail": "181,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +338 · 기 +73"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 196,264"}]}
{"ts": "2026-08-12T09:30:12.531219+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 400000, "qty": 28, "proceeds": 11178160, "entry_price": 350250, "pnl": 1369689, "pnl_pct": 14.2, "hold_from": "2026-07-21T12:06:01.166187+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "399,647 (현재 399,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "311,300"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -6 · 기 +72"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-12T09:38:12.813434+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19050, "qty": 282, "cost": 5372906, "path": "pullback", "reason": "눌림목 지정가 20,036 도달", "stop": 17979, "target": 21193, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,040 vs 18,952"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,594 / 18,952"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,594 / 19,360"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+2.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +113 · 기 +20 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+32%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.22% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "250,373 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.22% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "49"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,036 (현재 19,040)"}]}
{"ts": "2026-08-13T09:00:31.755564+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18880, "qty": 282, "proceeds": 5313778, "entry_price": 19050, "pnl": -59128, "pnl_pct": -0.89, "hold_from": "2026-08-12T09:38:12.813428+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "17,979 (현재 18,930)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,193"}, {"label": "추세(10일선)", "ok": false, "detail": "18,941"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -50 · 기 -76"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,411"}]}
{"ts": "2026-08-13T09:08:17.368056+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 402500, "qty": 11, "cost": 4428164, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 365165, "target": 477170, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "402,000 vs 311,550"}, {"label": "정배열(5>10)", "ok": true, "detail": "326,600 / 311,550"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "326,600 / 306,450"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+32.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +27 · 기 +23 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+26%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "2.20% (환산) vs 평균 0.93% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 334,500"}, {"label": "거래량 급증", "ok": true, "detail": "228,347 (환산) vs 평균 96,436"}, {"label": "회전율 급증", "ok": true, "detail": "2.20% (환산) vs 평균 0.93%"}, {"label": "RSI", "ok": true, "detail": "75"}]}
{"ts": "2026-08-13T14:02:17.124172+09:00", "side": "BUY", "code": "214450", "name": "파마리서치", "price": 415500, "qty": 11, "cost": 4571186, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 365165, "target": 477170, "signals": [{"label": "손절선", "ok": true, "detail": "365,165 (현재 415,500)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 477,170"}, {"label": "추세(10일선)", "ok": true, "detail": "312,900"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +43 · 기 +19"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 415,052"}]}
{"ts": "2026-08-20T10:00:18.515203+09:00", "side": "SELL", "code": "003680", "name": "한성기업", "price": 5360, "qty": 819, "proceeds": 4381280, "entry_price": 6333, "pnl": -806288, "pnl_pct": -15.36, "hold_from": "2026-07-30T09:04:07.457322+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,363 (현재 5,360)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 8,274"}, {"label": "추세(10일선)", "ok": true, "detail": "5,180"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +53 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-08-20T10:34:23.489981+09:00", "side": "BUY", "code": "327260", "name": "RF머트리얼즈", "price": 35100, "qty": 54, "cost": 1895684, "path": "pullback", "reason": "눌림목 지정가 35,709 도달", "stop": 27388, "target": 50523, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "35,050 vs 28,827"}, {"label": "정배열(5>10)", "ok": true, "detail": "35,510 / 28,827"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "35,510 / 31,320"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+29.3%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -51 · 기 +8 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+94%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "11.17% (환산) vs 평균 3.74% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 45,300"}, {"label": "거래량 급증", "ok": true, "detail": "1,004,500 (환산) vs 평균 409,191"}, {"label": "회전율 급증", "ok": true, "detail": "11.17% (환산) vs 평균 3.74%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 35,709 (현재 35,050)"}]}
{"ts": "2026-09-01T09:02:25.883275+09:00", "side": "SELL", "code": "252990", "name": "샘씨엔에스", "price": 12660, "qty": 350, "proceeds": 4422360, "entry_price": 13778, "pnl": -400674, "pnl_pct": -8.11, "hold_from": "2026-08-06T09:00:18.926753+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "11,541 (현재 12,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,253"}, {"label": "추세(10일선)", "ok": false, "detail": "12,846"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -53 · 기 -347"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-01T09:10:27.217699+09:00", "side": "BUY", "code": "030000", "name": "제일기획", "price": 19310, "qty": 281, "cost": 5426924, "path": "pullback", "reason": "눌림목 지정가 20,043 도달", "stop": 18249, "target": 21431, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "19,310 vs 18,979"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,648 / 18,979"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,648 / 19,364"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-9.8%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -11 · 기 +18 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+30%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.16% (환산) vs 평균 0.62% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 21,150"}, {"label": "거래량 급증", "ok": false, "detail": "182,700 (환산) vs 평균 711,709"}, {"label": "회전율 급증", "ok": false, "detail": "0.16% (환산) vs 평균 0.62%"}, {"label": "RSI", "ok": true, "detail": "52"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 20,043 (현재 19,310)"}]}
{"ts": "2026-09-03T09:00:33.152755+09:00", "side": "SELL", "code": "078930", "name": "GS", "price": 119500, "qty": 98, "proceeds": 11688164, "entry_price": 91918, "pnl": 2678812, "pnl_pct": 30.01, "hold_from": "2026-08-04T09:50:09.564914+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "119,430 (현재 119,100)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "83,990"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +156 · 기 -35"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-03T09:02:24.991073+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 13520, "qty": 186, "cost": 2515097, "path": "pullback", "reason": "눌림목 지정가 13,982 도달", "stop": 11243, "target": 18074, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "13,430 vs 12,916"}, {"label": "정배열(5>10)", "ok": true, "detail": "14,434 / 12,916"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "14,434 / 13,273"}, {"label": "상대강도(시장대비)", "ok": false, "detail": "-9.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -6 · 기 +7 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+86%"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.08% (환산) vs 평균 1.47% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 15,870"}, {"label": "거래량 급증", "ok": false, "detail": "47,427 (환산) vs 평균 882,736"}, {"label": "회전율 급증", "ok": false, "detail": "0.08% (환산) vs 평균 1.47%"}, {"label": "RSI", "ok": true, "detail": "51"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 13,982 (현재 13,430)"}]}
{"ts": "2026-09-04T11:14:26.502015+09:00", "side": "SELL", "code": "030000", "name": "제일기획", "price": 18930, "qty": 281, "proceeds": 5308957, "entry_price": 19310, "pnl": -117967, "pnl_pct": -1.97, "hold_from": "2026-09-01T09:10:27.217697+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "18,249 (현재 18,940)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 21,431"}, {"label": "추세(10일선)", "ok": false, "detail": "18,942"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -2 · 기 -39"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 19,673"}]}
{"ts": "2026-09-04T11:14:26.856117+09:00", "side": "BUY", "code": "478560", "name": "블랙야크아이앤씨", "price": 3135, "qty": 1322, "cost": 4145092, "path": "pullback", "reason": "눌림목 지정가 3,140 도달", "stop": 2812, "target": 3781, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "3,135 vs 3,004"}, {"label": "정배열(5>10)", "ok": true, "detail": "3,264 / 3,004"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "3,264 / 2,715"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+18.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +4 · 기 -12 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.24% (환산) vs 평균 0.96% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 3,495"}, {"label": "거래량 급증", "ok": false, "detail": "63,650 (환산) vs 평균 247,795"}, {"label": "회전율 급증", "ok": false, "detail": "0.24% (환산) vs 평균 0.96%"}, {"label": "RSI", "ok": true, "detail": "60"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 3,140 (현재 3,135)"}]}
{"ts": "2026-09-04T12:34:27.150209+09:00", "side": "SELL", "code": "068270", "name": "셀트리온", "price": 187800, "qty": 53, "proceeds": 9933991, "entry_price": 194158, "pnl": -357953, "pnl_pct": -3.27, "hold_from": "2026-08-06T09:34:15.869489+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "181,367 (현재 187,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 219,742"}, {"label": "추세(10일선)", "ok": false, "detail": "189,720"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -128 · 기 -47"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-04T12:36:28.830728+09:00", "side": "BUY", "code": "079650", "name": "서산", "price": 6800, "qty": 314, "cost": 2135520, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 5443, "target": 9513, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,780 vs 5,060"}, {"label": "정배열(5>10)", "ok": true, "detail": "5,202 / 5,060"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "5,202 / 4,364"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+58.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +193 · 기 +5 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "47.40% (환산) vs 평균 23.23% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 6,760"}, {"label": "거래량 급증", "ok": true, "detail": "9,478,267 (환산) vs 평균 4,645,030"}, {"label": "회전율 급증", "ok": true, "detail": "47.40% (환산) vs 평균 23.23%"}, {"label": "RSI", "ok": true, "detail": "67"}]}
{"ts": "2026-09-07T11:00:32.675549+09:00", "side": "BUY", "code": "478560", "name": "블랙야크아이앤씨", "price": 3255, "qty": 1273, "cost": 4144237, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 2812, "target": 3781, "signals": [{"label": "손절선", "ok": true, "detail": "2,812 (현재 3,260)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 3,781"}, {"label": "추세(10일선)", "ok": true, "detail": "3,016"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +6 · 기 -11"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 3,241"}]}
{"ts": "2026-09-07T15:06:35.053408+09:00", "side": "SELL", "code": "327260", "name": "RF머트리얼즈", "price": 33650, "qty": 54, "proceeds": 1813557, "entry_price": 35100, "pnl": -82128, "pnl_pct": -4.13, "hold_from": "2026-08-20T10:34:23.489978+09:00", "reason": "추세·수급 이탈", "partial": false, "signals": [{"label": "손절선", "ok": true, "detail": "27,388 (현재 33,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 50,523"}, {"label": "추세(10일선)", "ok": false, "detail": "35,015"}, {"label": "수급(외/기)", "ok": false, "detail": "외 -281 · 기 -15"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 37,418"}]}
{"ts": "2026-09-07T15:06:35.464019+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 153300, "qty": 14, "cost": 2146522, "path": "pullback", "reason": "눌림목 지정가 157,907 도달", "stop": 124661, "target": 210578, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "153,100 vs 152,890"}, {"label": "정배열(5>10)", "ok": true, "detail": "171,260 / 152,890"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "171,260 / 99,668"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+26.6%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +153 · 기 -70 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+41%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "5.10% (환산) vs 평균 3.06% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 202,500"}, {"label": "거래량 급증", "ok": false, "detail": "987,306 (환산) vs 평균 591,925"}, {"label": "회전율 급증", "ok": true, "detail": "5.10% (환산) vs 평균 3.06%"}, {"label": "RSI", "ok": true, "detail": "59"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 157,907 (현재 153,100)"}]}
{"ts": "2026-09-08T09:18:27.729650+09:00", "side": "SELL", "code": "214450", "name": "파마리서치", "price": 370000, "qty": 22, "proceeds": 8124127, "entry_price": 409000, "pnl": -875223, "pnl_pct": -9.54, "hold_from": "2026-08-13T09:08:17.368050+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "371,344 (현재 370,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 484,313"}, {"label": "추세(10일선)", "ok": true, "detail": "308,350"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +65 · 기 -32"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-08T09:18:27.730352+09:00", "side": "BUY", "code": "004370", "name": "농심", "price": 414000, "qty": 12, "cost": 4968745, "path": "pullback", "reason": "눌림목 지정가 420,641 도달", "stop": 388711, "target": 464578, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "413,500 vs 397,100"}, {"label": "정배열(5>10)", "ok": true, "detail": "405,200 / 397,100"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "405,200 / 369,950"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+6.2%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +14 · 기 -27 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+37%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.50% (환산) vs 평균 0.51% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 440,000"}, {"label": "거래량 급증", "ok": false, "detail": "30,353 (환산) vs 평균 31,191"}, {"label": "회전율 급증", "ok": false, "detail": "0.50% (환산) vs 평균 0.51%"}, {"label": "RSI", "ok": true, "detail": "62"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 420,641 (현재 413,500)"}]}
{"ts": "2026-09-08T09:26:31.516062+09:00", "side": "BUY", "code": "095610", "name": "테스", "price": 163200, "qty": 13, "cost": 2121918, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 124661, "target": 210578, "signals": [{"label": "손절선", "ok": true, "detail": "124,661 (현재 162,900)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 210,578"}, {"label": "추세(10일선)", "ok": true, "detail": "153,870"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +196 · 기 +54"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 162,804"}]}
{"ts": "2026-09-08T12:32:31.611863+09:00", "side": "BUY", "code": "079650", "name": "서산", "price": 7390, "qty": 288, "cost": 2128639, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 5443, "target": 9513, "signals": [{"label": "손절선", "ok": true, "detail": "5,443 (현재 7,380)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,513"}, {"label": "추세(10일선)", "ok": true, "detail": "5,120"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +103 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 7,239"}]}
{"ts": "2026-09-08T15:20:32.435154+09:00", "side": "SELL", "code": "138040", "name": "메리츠금융지주", "price": 130000, "qty": 89, "proceeds": 11547438, "entry_price": 119185, "pnl": 938347, "pnl_pct": 9.07, "hold_from": "2026-07-29T09:02:02.224637+09:00", "reason": "트레일링 익절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "130,576 (현재 130,300)"}, {"label": "목표/트레일링", "ok": null, "detail": "트레일링 ON"}, {"label": "추세(10일선)", "ok": true, "detail": "117,640"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -147 · 기 +423"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-09T09:02:32.237897+09:00", "side": "BUY", "code": "000720", "name": "현대건설", "price": 135800, "qty": 30, "cost": 4074611, "path": "breakout", "reason": "거래량·회전율 동반 전고점 돌파", "stop": 121854, "target": 163692, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "135,800 vs 115,200"}, {"label": "정배열(5>10)", "ok": true, "detail": "116,420 / 115,200"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "116,420 / 113,853"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+34.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 -280 · 기 +899 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+22%"}, {"label": "어닝 서프라이즈", "ok": true, "detail": "+ ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "1.32% (환산) vs 평균 0.58% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": true, "detail": "고점 122,600"}, {"label": "거래량 급증", "ok": true, "detail": "1,470,387 (환산) vs 평균 648,608"}, {"label": "회전율 급증", "ok": true, "detail": "1.32% (환산) vs 평균 0.58%"}, {"label": "RSI", "ok": true, "detail": "66"}]}
{"ts": "2026-09-09T09:06:32.117834+09:00", "side": "BUY", "code": "000720", "name": "현대건설", "price": 141500, "qty": 28, "cost": 3962594, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 121854, "target": 163692, "signals": [{"label": "손절선", "ok": true, "detail": "121,854 (현재 141,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 163,692"}, {"label": "추세(10일선)", "ok": true, "detail": "115,740"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -280 · 기 +899"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 140,627"}]}
{"ts": "2026-09-11T09:04:34.987337+09:00", "side": "BUY", "code": "252990", "name": "샘씨엔에스", "price": 14660, "qty": 171, "cost": 2507236, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 11243, "target": 18074, "signals": [{"label": "손절선", "ok": true, "detail": "11,243 (현재 14,510)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 18,074"}, {"label": "추세(10일선)", "ok": true, "detail": "13,024"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -103 · 기 +10"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 14,275"}]}
{"ts": "2026-09-16T09:06:37.347501+09:00", "side": "SELL", "code": "095610", "name": "테스", "price": 128800, "qty": 27, "proceeds": 3470819, "entry_price": 158067, "pnl": -797621, "pnl_pct": -18.52, "hold_from": "2026-09-07T15:06:35.464006+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "129,556 (현재 129,200)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 215,088"}, {"label": "추세(10일선)", "ok": false, "detail": "150,500"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -853 · 기 +920"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-16T10:54:38.080667+09:00", "side": "SELL", "code": "000720", "name": "현대건설", "price": 124000, "qty": 58, "proceeds": 7177976, "entry_price": 138552, "pnl": -859230, "pnl_pct": -10.5, "hold_from": "2026-09-09T09:02:32.237894+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "124,070 (현재 124,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 167,515"}, {"label": "추세(10일선)", "ok": true, "detail": "114,020"}, {"label": "수급(외/기)", "ok": true, "detail": "외 -193 · 기 +18"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-16T13:08:39.890768+09:00", "side": "BUY", "code": "372170", "name": "윤성에프앤씨", "price": 20200, "qty": 140, "cost": 2828424, "path": "pullback", "reason": "눌림목 지정가 21,341 도달", "stop": 17261, "target": 26078, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "20,150 vs 19,073"}, {"label": "정배열(5>10)", "ok": true, "detail": "19,666 / 19,073"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "19,666 / 19,448"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+39.5%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +0 · 기 +1 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.18% (환산) vs 평균 0.56% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 23,400"}, {"label": "거래량 급증", "ok": false, "detail": "14,282 (환산) vs 평균 44,688"}, {"label": "회전율 급증", "ok": false, "detail": "0.18% (환산) vs 평균 0.56%"}, {"label": "RSI", "ok": true, "detail": "56"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 21,341 (현재 20,150)"}]}
{"ts": "2026-09-16T13:08:40.269221+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 11710, "qty": 351, "cost": 4110827, "path": "pullback", "reason": "눌림목 지정가 11,955 도달", "stop": 10532, "target": 14065, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "11,710 vs 11,339"}, {"label": "정배열(5>10)", "ok": true, "detail": "11,772 / 11,339"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "11,772 / 9,428"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+33.0%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +14 · 기 +0 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.05% (환산) vs 평균 0.30% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 13,450"}, {"label": "거래량 급증", "ok": false, "detail": "6,274 (환산) vs 평균 36,789"}, {"label": "회전율 급증", "ok": false, "detail": "0.05% (환산) vs 평균 0.30%"}, {"label": "RSI", "ok": true, "detail": "64"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 11,955 (현재 11,710)"}]}
{"ts": "2026-09-17T13:50:40.416232+09:00", "side": "BUY", "code": "041520", "name": "이엘씨", "price": 12170, "qty": 337, "cost": 4101905, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 10532, "target": 14065, "signals": [{"label": "손절선", "ok": true, "detail": "10,532 (현재 12,170)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 14,065"}, {"label": "추세(10일선)", "ok": true, "detail": "11,385"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +16 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 12,093"}]}
{"ts": "2026-09-18T09:14:43.474929+09:00", "side": "SELL", "code": "004370", "name": "농심", "price": 387500, "qty": 12, "proceeds": 4640932, "entry_price": 414000, "pnl": -327813, "pnl_pct": -6.4, "hold_from": "2026-09-08T09:18:27.730350+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "388,711 (현재 388,000)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 464,578"}, {"label": "추세(10일선)", "ok": false, "detail": "394,550"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +11 · 기 -2"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 423,340"}]}
{"ts": "2026-09-18T09:14:43.475558+09:00", "side": "BUY", "code": "025860", "name": "남해화학", "price": 6130, "qty": 848, "cost": 5199020, "path": "pullback", "reason": "눌림목 지정가 6,155 도달", "stop": 5730, "target": 6930, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "6,130 vs 6,034"}, {"label": "정배열(5>10)", "ok": true, "detail": "6,112 / 6,034"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "6,112 / 5,900"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+13.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +4 · 기 +62 (천주)"}, {"label": "애널", "ok": null, "detail": "데이터 없음(통과)"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.13% (환산) vs 평균 1.53% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 6,700"}, {"label": "거래량 급증", "ok": false, "detail": "62,267 (환산) vs 평균 758,257"}, {"label": "회전율 급증", "ok": false, "detail": "0.13% (환산) vs 평균 1.53%"}, {"label": "RSI", "ok": true, "detail": "55"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 6,155 (현재 6,130)"}]}
{"ts": "2026-09-30T11:40:45.422955+09:00", "side": "SELL", "code": "079650", "name": "서산", "price": 5760, "qty": 602, "proceeds": 3460758, "entry_price": 7082, "pnl": -803401, "pnl_pct": -18.67, "hold_from": "2026-09-04T12:36:28.830726+09:00", "reason": "손절", "partial": false, "signals": [{"label": "손절선", "ok": false, "detail": "5,765 (현재 5,730)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 9,716"}, {"label": "추세(10일선)", "ok": true, "detail": "4,954"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +67 · 기 +0"}, {"label": "분할(2/2)", "ok": null, "detail": "추가 종료"}]}
{"ts": "2026-09-30T11:40:45.860932+09:00", "side": "BUY", "code": "300720", "name": "한일시멘트", "price": 16110, "qty": 323, "cost": 5204311, "path": "pullback", "reason": "눌림목 지정가 16,390 도달", "stop": 15271, "target": 17789, "signals": [{"label": "추세(10일선 위)", "ok": true, "detail": "16,100 vs 15,864"}, {"label": "정배열(5>10)", "ok": true, "detail": "16,302 / 15,864"}, {"label": "추세 기울기(10일선↑)", "ok": true, "detail": "우상향"}, {"label": "중기 정배열(5>60)", "ok": true, "detail": "16,302 / 14,436"}, {"label": "상대강도(시장대비)", "ok": true, "detail": "+15.4%p"}, {"label": "수급(5일 외/기)", "ok": true, "detail": "외 +18 · 기 -4 (천주)"}, {"label": "상승여력", "ok": true, "detail": "+27%"}, {"label": "목표가 리비전", "ok": true, "detail": "상향 ➕"}, {"label": "시장 강세", "ok": true, "detail": "강세"}, {"label": "과열 아님(회전율)", "ok": true, "detail": "0.05% (환산) vs 평균 0.12% (보류 3배↑)"}, {"label": "돌파(전고점)", "ok": false, "detail": "고점 17,140"}, {"label": "거래량 급증", "ok": false, "detail": "39,139 (환산) vs 평균 89,609"}, {"label": "회전율 급증", "ok": false, "detail": "0.05% (환산) vs 평균 0.12%"}, {"label": "RSI", "ok": true, "detail": "61"}, {"label": "눌림목 도달", "ok": true, "detail": "지정가 16,390 (현재 16,100)"}]}
{"ts": "2026-10-02T09:01:23.806528+09:00", "side": "BUY", "code": "372170", "name": "윤성에프앤씨", "price": 21400, "qty": 132, "cost": 2825224, "path": "add", "reason": "추격매수 — 2/2차 (추세 지속)", "stop": 17261, "target": 26078, "signals": [{"label": "손절선", "ok": true, "detail": "17,261 (현재 21,700)"}, {"label": "목표/트레일링", "ok": null, "detail": "목표 26,078"}, {"label": "추세(10일선)", "ok": true, "detail": "19,228"}, {"label": "수급(외/기)", "ok": true, "detail": "외 +2 · 기 +0"}, {"label": "분할(1/2)", "ok": null, "detail": "추가 트리거 21,139"}]}
@@ -1,13 +0,0 @@
{
"cash": 100000000,
"initial": 100000000,
"positions": {},
"realized_pnl": 0.0,
"closed_trades": 0,
"wins": 0,
"peak_equity": 100000000,
"max_drawdown": 0.0,
"last_exit": {},
"created_at": "2026-06-11T09:00:01.944300+09:00",
"updated_at": "2026-06-15T15:25:02.286210+09:00"
}

Some files were not shown because too many files have changed in this diff Show More