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; }