diff --git a/agents/stock/workspace/scripts/behive_web.py b/agents/stock/workspace/scripts/behive_web.py index 8a85b6f..5945506 100644 --- a/agents/stock/workspace/scripts/behive_web.py +++ b/agents/stock/workspace/scripts/behive_web.py @@ -596,6 +596,16 @@ def _market_phase_state() -> dict: return {'phase': 'closed', 'label': '장외 시간', 'active': False, 'time': time_str} +def _is_preopen_market_day() -> bool: + """거래일 08:00 전. 장전 가격으로 당일 손익을 만들지 않는다.""" + now = datetime.now(KST) + return ( + now.weekday() < 5 + and now.hour < 8 + and now.strftime('%Y-%m-%d') not in _load_holidays() + ) + + def _load_watchlist() -> list[dict]: if not WATCHLIST.exists(): return [] @@ -975,10 +985,15 @@ def _build_owner_data(positions_by_acc: dict, balances: dict, journal_by_label: net_cash_flow = cash_in - cash_out day_pl_total = (total_net - prev_net - net_cash_flow) if prev_net is not None else None + day_pl_reason = None + if _is_preopen_market_day(): + day_pl_total = None + day_pl_reason = 'preopen' # 휴장일/주말은 시장 변동이 없어야 자연. 단 NXT 가격 보정으로 어제 스냅샷과 차이가 생겨 # day_pl_total이 0이 아닌 값을 갖게 됨 → 사용자 혼란 차단 위해 None으로 강제(미표시). if _market_phase_state().get('phase') in ('holiday', 'weekend'): day_pl_total = None + day_pl_reason = 'closed' owner_data[owner] = { 'labels': lbls, @@ -993,6 +1008,7 @@ def _build_owner_data(positions_by_acc: dict, balances: dict, journal_by_label: 'traded_today': traded_today, 'prev_net': prev_net, 'day_pl_total': day_pl_total, + 'day_pl_reason': day_pl_reason, 'realized_pl_total': realized_pl_total, 'realized_fees_total': realized_fees_total, 'cash_in': cash_in, @@ -1863,14 +1879,19 @@ def _render_row(c: dict, source: str = 'watchlist') -> str: price_cell = '-' diff_cell = f'{html.escape(err) if err else "조회 불가"}' else: - # 가격 출처 마크 — _krx_price/_nxt_price 비교. NXT 가격이 살아있고 KRX와 다르면 NXT. - krx_p = c.get('_krx_price', 0) or 0 + # 거래가능 표시 — 지금 거래시간이면 초록점, 아니면 점 없음. + # 정규장: KRX+NXT 모두 거래 → 초록점 + # NXT 시간대: NXT 거래되는 종목(_nxt_price 살아있음)만 초록점 + # 장마감·휴장·주말: 점 없음 nxt_p = c.get('_nxt_price', 0) or 0 - if nxt_p > 0 and (krx_p == 0 or krx_p != nxt_p): - mark = 'NXT' + _phase = _market_phase_state()['phase'] + if _phase == 'regular': + _tradable = True + elif _phase == 'nxt': + _tradable = nxt_p > 0 else: - mark = 'KRX' - mark_html = f'{mark}' + _tradable = False + mark_html = '' if _tradable else '' # 미리보기 등락은 KRX 전일종가 대비 — ka10095의 change_pct가 이미 KRX 기준이라 그대로 사용. day_pct = c.get('day_change_pct') if c.get('_show_day_change') and isinstance(day_pct, (int, float)): @@ -2131,8 +2152,10 @@ def _render_owner_kpi(owner: str, d: dict, owner_label_text: str, compact: bool day_pl_total = d.get('day_pl_total') if day_pl_total is None: + if d.get('day_pl_reason') == 'preopen': + day_pl_html = '장 시작 전' # 휴장일/주말은 시장 변동이 없는 정상 상태 — "전날 스냅샷 없음"과 구분된 문구. - if _market_phase_state().get('phase') in ('holiday', 'weekend'): + elif _market_phase_state().get('phase') in ('holiday', 'weekend'): day_pl_html = '휴장 — 변동 없음' else: day_pl_html = '전날 스냅샷 없음' @@ -4235,6 +4258,8 @@ details.row + .section-label { margin-top: 14px; padding-top: 24px; border-top-c .ohlc-mini.ohlc-dual .ohlc-cell.ohlc-dim .ohlc-pct { color: #5a5e69 !important; } .ohlc-mini.ohlc-dual .ohlc-cell-empty.ohlc-dim { color: #3a3e48; } /* 미리보기 현재가 왼쪽 출처 마크 — NXT(파랑톤) / KRX(회색톤). 어두운 채도로 보조 정보 강조 X. */ +.px-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px; vertical-align: 1px; } +.px-dot.tradable { background: #2ecc71; box-shadow: 0 0 4px rgba(46,204,113,0.6); } .px-mark { font-size: 8px; font-weight: 600; margin-right: 4px; padding: 1px 3px; border-radius: 2px; background: #14171f; vertical-align: 2px; letter-spacing: 0.3px; } .px-mark.px-nxt { color: #2d5a8a; } .px-mark.px-krx { color: #4a4e58; }