From dd1ad7d96401a605eeef4b43702beaf0a33cef70 Mon Sep 17 00:00:00 2001 From: hyowons Date: Fri, 3 Jul 2026 02:00:02 +0900 Subject: [PATCH] =?UTF-8?q?auto:=20=EC=9D=BC=EC=9D=BC=20=EB=B0=B1=EC=97=85?= =?UTF-8?q?=202026-07-03=2002:00?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- agents/stock/workspace/scripts/behive_web.py | 344 +++++++++++------- .../stock/workspace/scripts/realtime_hub.py | 1 + .../stock/workspace/scripts/stock_analysis.py | 94 ++++- .../state/market_indicators_history.jsonl | 2 + .../workspace/state/sim/equity_history.json | 2 +- .../workspace/state/sim/index_history.json | 2 +- .../state/sim/variants/v1/portfolio.json | 39 +- .../state/sim/variants/v1/trades.jsonl | 4 + .../state/sim/variants/v101/portfolio.json | 39 +- .../state/sim/variants/v101/trades.jsonl | 4 + .../state/sim/variants/v103/portfolio.json | 43 ++- .../state/sim/variants/v103/trades.jsonl | 4 + .../state/sim/variants/v105/portfolio.json | 106 ++---- .../state/sim/variants/v105/trades.jsonl | 7 + .../state/sim/variants/v107/portfolio.json | 106 ++---- .../state/sim/variants/v107/trades.jsonl | 7 + .../state/sim/variants/v147/portfolio.json | 48 +-- .../state/sim/variants/v147/trades.jsonl | 2 + .../state/sim/variants/v149/portfolio.json | 67 ++-- .../state/sim/variants/v149/trades.jsonl | 3 + .../state/sim/variants/v151/portfolio.json | 48 +-- .../state/sim/variants/v151/trades.jsonl | 2 + .../state/sim/variants/v155/portfolio.json | 142 +++----- .../state/sim/variants/v155/trades.jsonl | 9 + .../state/sim/variants/v157/portfolio.json | 137 +++---- .../state/sim/variants/v157/trades.jsonl | 9 + .../state/sim/variants/v159/portfolio.json | 137 +++---- .../state/sim/variants/v159/trades.jsonl | 9 + .../state/sim/variants/v161/portfolio.json | 55 ++- .../state/sim/variants/v161/trades.jsonl | 5 + .../state/sim/variants/v165/portfolio.json | 55 ++- .../state/sim/variants/v165/trades.jsonl | 5 + .../state/sim/variants/v167/portfolio.json | 55 ++- .../state/sim/variants/v167/trades.jsonl | 5 + .../state/sim/variants/v189/portfolio.json | 137 +++---- .../state/sim/variants/v189/trades.jsonl | 9 + .../state/sim/variants/v195/portfolio.json | 55 ++- .../state/sim/variants/v195/trades.jsonl | 2 + .../state/sim/variants/v197/portfolio.json | 48 +-- .../state/sim/variants/v197/trades.jsonl | 2 + .../state/sim/variants/v199/portfolio.json | 92 +++-- .../state/sim/variants/v199/trades.jsonl | 7 + .../state/sim/variants/v201/portfolio.json | 55 ++- .../state/sim/variants/v201/trades.jsonl | 2 + .../state/sim/variants/v203/portfolio.json | 137 +++---- .../state/sim/variants/v203/trades.jsonl | 9 + .../state/sim/variants/v205/portfolio.json | 67 ++-- .../state/sim/variants/v205/trades.jsonl | 3 + .../state/sim/variants/v207/portfolio.json | 55 ++- .../state/sim/variants/v207/trades.jsonl | 5 + .../state/sim/variants/v209/portfolio.json | 55 ++- .../state/sim/variants/v209/trades.jsonl | 5 + .../state/sim/variants/v211/portfolio.json | 59 ++- .../state/sim/variants/v211/trades.jsonl | 5 + .../state/sim/variants/v213/portfolio.json | 142 +++----- .../state/sim/variants/v213/trades.jsonl | 9 + .../state/sim/variants/v215/portfolio.json | 128 ++----- .../state/sim/variants/v215/trades.jsonl | 8 + .../state/sim/variants/v217/portfolio.json | 72 +++- .../state/sim/variants/v217/trades.jsonl | 3 + .../state/sim/variants/v218/portfolio.json | 72 +++- .../state/sim/variants/v218/trades.jsonl | 3 + .../state/sim/variants/v223/portfolio.json | 72 +++- .../state/sim/variants/v223/trades.jsonl | 3 + .../state/sim/variants/v224/portfolio.json | 72 +++- .../state/sim/variants/v224/trades.jsonl | 3 + .../state/sim/variants/v229/portfolio.json | 72 +++- .../state/sim/variants/v229/trades.jsonl | 3 + .../state/sim/variants/v230/portfolio.json | 72 +++- .../state/sim/variants/v230/trades.jsonl | 3 + .../state/sim/variants/v235/portfolio.json | 72 +++- .../state/sim/variants/v235/trades.jsonl | 3 + .../state/sim/variants/v236/portfolio.json | 72 +++- .../state/sim/variants/v236/trades.jsonl | 3 + .../state/sim/variants/v265/portfolio.json | 72 +++- .../state/sim/variants/v265/trades.jsonl | 3 + .../state/sim/variants/v266/portfolio.json | 72 +++- .../state/sim/variants/v266/trades.jsonl | 3 + .../state/sim/variants/v271/portfolio.json | 72 +++- .../state/sim/variants/v271/trades.jsonl | 3 + .../state/sim/variants/v272/portfolio.json | 72 +++- .../state/sim/variants/v272/trades.jsonl | 3 + .../state/sim/variants/v277/portfolio.json | 72 +++- .../state/sim/variants/v277/trades.jsonl | 3 + .../state/sim/variants/v278/portfolio.json | 72 +++- .../state/sim/variants/v278/trades.jsonl | 3 + .../state/sim/variants/v283/portfolio.json | 72 +++- .../state/sim/variants/v283/trades.jsonl | 3 + .../state/sim/variants/v284/portfolio.json | 72 +++- .../state/sim/variants/v284/trades.jsonl | 3 + .../state/sim/variants/v296/portfolio.json | 112 +++--- .../state/sim/variants/v296/trades.jsonl | 4 + .../state/sim/variants/v298/portfolio.json | 117 +++--- .../state/sim/variants/v298/trades.jsonl | 7 + .../state/sim/variants/v299/portfolio.json | 102 +++--- .../state/sim/variants/v299/trades.jsonl | 7 + .../state/sim/variants/v300/portfolio.json | 132 +++---- .../state/sim/variants/v300/trades.jsonl | 8 + .../state/sim/variants/v301/portfolio.json | 39 +- .../state/sim/variants/v301/trades.jsonl | 4 + .../state/sim/variants/v302/portfolio.json | 60 ++- .../state/sim/variants/v302/trades.jsonl | 5 + .../state/sim/variants/v303/portfolio.json | 72 +++- .../state/sim/variants/v303/trades.jsonl | 3 + .../state/sim/variants/v304/portfolio.json | 92 +++-- .../state/sim/variants/v304/trades.jsonl | 11 + .../state/sim/variants/v305/portfolio.json | 69 ++-- .../state/sim/variants/v305/trades.jsonl | 10 + .../state/sim/variants/v306/portfolio.json | 43 ++- .../state/sim/variants/v306/trades.jsonl | 1 + .../state/sim/variants/v307/portfolio.json | 74 +++- .../state/sim/variants/v307/trades.jsonl | 3 + .../state/sim/variants/v308/portfolio.json | 68 ++-- .../state/sim/variants/v308/trades.jsonl | 5 + .../state/sim/variants/v309/portfolio.json | 130 +++---- .../state/sim/variants/v309/trades.jsonl | 7 + .../state/sim/variants/v310/portfolio.json | 53 +-- .../state/sim/variants/v310/trades.jsonl | 4 + .../state/sim/variants/v311/portfolio.json | 123 +++---- .../state/sim/variants/v311/trades.jsonl | 9 + .../state/sim/variants/v312/portfolio.json | 105 +++--- .../state/sim/variants/v312/trades.jsonl | 5 + .../state/sim/variants/v313/portfolio.json | 72 +++- .../state/sim/variants/v313/trades.jsonl | 3 + .../state/sim/variants/v314/portfolio.json | 61 +++- .../state/sim/variants/v314/trades.jsonl | 5 + .../state/sim/variants/v315/portfolio.json | 42 ++- .../state/sim/variants/v315/trades.jsonl | 4 + .../state/sim/variants/v316/portfolio.json | 16 +- .../state/sim/variants/v317/portfolio.json | 84 ++--- .../state/sim/variants/v317/trades.jsonl | 3 + .../state/sim/variants/v318/portfolio.json | 102 +++--- .../state/sim/variants/v318/trades.jsonl | 4 + .../state/sim/variants/v319/portfolio.json | 14 +- .../state/sim/variants/v320/portfolio.json | 16 +- .../state/sim/variants/v321/portfolio.json | 18 +- .../state/sim/variants/v4/portfolio.json | 45 ++- .../state/sim/variants/v4/trades.jsonl | 4 + .../state/sim/variants/v40/portfolio.json | 69 ++-- .../state/sim/variants/v40/trades.jsonl | 2 + .../state/sim/variants/v41/portfolio.json | 43 ++- .../state/sim/variants/v41/trades.jsonl | 4 + .../state/sim/variants/v47/portfolio.json | 106 ++---- .../state/sim/variants/v47/trades.jsonl | 7 + .../state/sim/variants/v49/portfolio.json | 101 ++--- .../state/sim/variants/v49/trades.jsonl | 7 + .../state/sim/variants/v51/portfolio.json | 101 ++--- .../state/sim/variants/v51/trades.jsonl | 7 + .../state/sim/variants/v57/portfolio.json | 39 +- .../state/sim/variants/v57/trades.jsonl | 4 + .../state/sim/variants/v59/portfolio.json | 39 +- .../state/sim/variants/v59/trades.jsonl | 4 + .../state/sim/variants/v81/portfolio.json | 101 ++--- .../state/sim/variants/v81/trades.jsonl | 7 + .../state/sim/variants/v87/portfolio.json | 45 ++- .../state/sim/variants/v87/trades.jsonl | 4 + .../state/sim/variants/v89/portfolio.json | 45 ++- .../state/sim/variants/v89/trades.jsonl | 4 + .../state/sim/variants/v91/portfolio.json | 60 ++- .../state/sim/variants/v91/trades.jsonl | 5 + .../state/sim/variants/v93/portfolio.json | 45 ++- .../state/sim/variants/v93/trades.jsonl | 4 + .../state/sim/variants/v95/portfolio.json | 101 ++--- .../state/sim/variants/v95/trades.jsonl | 7 + .../state/sim/variants/v97/portfolio.json | 43 ++- .../state/sim/variants/v97/trades.jsonl | 4 + .../state/sim/variants/v99/portfolio.json | 39 +- .../state/sim/variants/v99/trades.jsonl | 4 + .../stock/workspace/state/sim/watchlist.json | 5 - .../stock/workspace/state/trade_journal.jsonl | 4 + 170 files changed, 4295 insertions(+), 2635 deletions(-) diff --git a/agents/stock/workspace/scripts/behive_web.py b/agents/stock/workspace/scripts/behive_web.py index 0243652..4fd056c 100644 --- a/agents/stock/workspace/scripts/behive_web.py +++ b/agents/stock/workspace/scripts/behive_web.py @@ -709,6 +709,8 @@ def _fetch_quotes_batch(entries: list[dict]) -> list[dict]: out['open'] = snap.get('open') out['high'] = snap.get('high') out['low'] = snap.get('low') + out['volume'] = snap.get('volume') + out['_nxt_volume'] = snap.get('_nxt_volume', 0) out['_krx_price'] = snap.get('_krx_price', 0) out['_nxt_price'] = snap.get('_nxt_price', 0) continue @@ -748,6 +750,7 @@ def _fetch_quotes_batch(entries: list[dict]) -> list[dict]: out['open'] = q.get('open') or None out['high'] = q.get('high') or None out['low'] = q.get('low') or None + out['volume'] = q.get('volume') out['_krx_price'] = price out['_nxt_price'] = 0 miss_codes, miss_indices = rest_codes, rest_indices @@ -773,6 +776,8 @@ def _fetch_quotes_batch(entries: list[dict]) -> list[dict]: for idx, code in zip(nxt_probe_indices, nxt_probe_codes): nxt_q = nxt_batch.get(code) if nxt_batch else None cards[idx]['_nxt_price'] = (nxt_q or {}).get('price', 0) if nxt_q else 0 + # NXT 거래량 — RVOL은 KRX 기준(일봉 평균과 동일)이라 KRX=AL−NXT 계산에 사용. + cards[idx]['_nxt_volume'] = (nxt_q or {}).get('volume', 0) if nxt_q else 0 for idx, code in zip(miss_indices, miss_codes): q = batch.get(code) @@ -788,6 +793,7 @@ def _fetch_quotes_batch(entries: list[dict]) -> list[dict]: out['open'] = q.get('open') or None out['high'] = q.get('high') or None out['low'] = q.get('low') or None + out['volume'] = q.get('volume') else: out['quote_error'] = '가격 0' # 마크 판정: NXT batch 가격 vs 기본 batch 가격 비교. @@ -802,6 +808,8 @@ def _fetch_quotes_batch(entries: list[dict]) -> list[dict]: 'open': out['open'], 'high': out['high'], 'low': out['low'], + 'volume': out.get('volume'), + '_nxt_volume': out.get('_nxt_volume', 0), '_krx_price': price, '_nxt_price': nxt_price, } @@ -843,6 +851,7 @@ def _fetch_quote(entry: dict) -> dict: out['open'] = snap.get('open') out['high'] = snap.get('high') out['low'] = snap.get('low') + out['volume'] = snap.get('volume') return out try: @@ -855,6 +864,7 @@ def _fetch_quote(entry: dict) -> dict: out['price'] = price out['day_change'] = q.get('change') out['day_change_pct'] = q.get('change_pct') + out['volume'] = q.get('volume') except Exception as e: out['quote_error'] = str(e)[:80] @@ -866,6 +876,7 @@ def _fetch_quote(entry: dict) -> dict: 'open': out['open'], 'high': out['high'], 'low': out['low'], + 'volume': out.get('volume'), } with _quote_cache_lock: _quote_cache[target_token] = (snap, time.time() + QUOTE_CACHE_TTL) @@ -2029,6 +2040,37 @@ def _buy_dd_html(buy_field, alerted: set[str]) -> str: return ''.join(parts) +def _vol_surge_badge(code: str, today_volume) -> str: + """오늘 거래량이 직전 20거래일 평균 대비 1.5배 이상이면 🔥 배지(배수 표기). + + - 20일 평균: dcc._select_latest(code, 21) 순수 SQLite 읽기(렌더 중 키움 호출 X). + - 장중 환산: sa.session_vol_frac로 정규장 오늘 누적 거래량을 하루치로 투영. + - 데이터 부족(차트 미조회로 캐시 없음 등)·임계 미만이면 '' 반환. + """ + if not code or not today_volume: + return '' + try: + import daily_candles_cache as dcc + import stock_analysis as sa + today = datetime.now(KST).strftime('%Y%m%d') + vols = [b['volume'] for b in dcc._select_latest(code, 21) + if str(b.get('date') or '') != today and (b.get('volume') or 0) > 0] + if len(vols) < 5: + return '' + avg20 = sum(vols[:20]) / min(len(vols), 20) + if avg20 <= 0: + return '' + frac = sa.session_vol_frac([{'date': today}]) + proj = (today_volume / frac) if frac else today_volume + rvol = proj / avg20 + except Exception: + return '' + if rvol < 1.5: + return '' + level = 'red' if rvol >= 3 else 'warm' + return f'🔥{rvol:.1f}' + + def _render_row(c: dict, source: str = 'watchlist') -> str: stock = html.escape(c.get('stock', '')) code = html.escape(c.get('code', '') or '') @@ -2274,7 +2316,7 @@ def _render_row(c: dict, source: str = 'watchlist') -> str: return f'''
-
{dot_html}{stock}{tag_chip}{status_cell}{trade_mark}{alert_badge}
+
{dot_html}{stock}{tag_chip}{status_cell}{trade_mark}{alert_badge}{_vol_surge_badge(code, (max(0, (c.get('volume') or 0) - (c.get('_nxt_volume') or 0)) or None))}
{price_cell}
@@ -2982,7 +3024,7 @@ def _render_holding_row(r: dict, total_value: int, show_day_change: bool = False
{_buy_rank_badge(buy_amount)}{stock}{acct_label}
-
📦 {qty:,}{_pending_badges_html(r)}{weight:.2f}%{star}
+
📦 {qty:,}{_pending_badges_html(r)}{_vol_surge_badge(code, (r.get('ohlc_krx') or {}).get('volume'))}{weight:.2f}%{star}
{summary_journal_html}
@@ -4333,11 +4375,6 @@ header.top { header.top .titles { display: flex; flex-direction: column; gap: 2px; min-width: 0; } header.top h1 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: #f0f0f0; animation: refresh-flash 0.8s ease-out; } header.top .meta { font-size: 11px; color: #8b8f9a; font-variant-numeric: tabular-nums; animation: refresh-flash-meta 0.8s ease-out; } -/* 실시간(WebSocket) 연결 상태 점 — 24px 탭영역 안에 10px 컬러 점. 초록=연결 / 빨강=끊김 / 회색=확인중 */ -.ws-dot { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex: none; padding: 0; margin: 0; background: transparent; border: 0; cursor: pointer; } -.ws-dot::before { content: ''; width: 10px; height: 10px; border-radius: 50%; background: #5a5f6c; transition: background 0.3s, box-shadow 0.3s; } -.ws-dot.on::before { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.7); } -.ws-dot.off::before { background: #ff4d5e; box-shadow: 0 0 6px rgba(255,77,94,0.65); } /* 페이지 로드(첫 진입·새로고침) 시 1회 발동. 탭 클릭(hashchange)은 reload 아니므로 발동 안 함. */ @keyframes refresh-flash { 0%, 20% { color: #ff4d5e; } @@ -4347,54 +4384,28 @@ header.top .meta { font-size: 11px; color: #8b8f9a; font-variant-numeric: tabula 0%, 20% { color: #ff4d5e; } 100% { color: #8b8f9a; } } -.refresh { - flex: none; display: inline-flex; align-items: center; justify-content: center; - width: 36px; height: 36px; border-radius: 8px; - background: #1c2230; border: 1px solid #2a3142; color: #e6e6e6; - font-size: 17px; text-decoration: none; user-select: none; - transition: background 0.15s, transform 0.1s; -} -.refresh:hover { background: #232a3b; } -.refresh:active { transform: scale(0.95); background: #2a3245; } -.refresh.spinning { background: rgba(255,77,94,0.15); border-color: #ff4d5e; color: #ff4d5e; } - .top-actions { display: flex; align-items: center; gap: 8px; flex: none; } -.auto-toggle { - flex: none; display: inline-flex; align-items: center; gap: 6px; - padding: 0 10px; height: 36px; border-radius: 8px; - background: #1c2230; border: 1px solid #2a3142; color: #8b8f9a; - font: 600 12px/1 inherit; - font-variant-numeric: tabular-nums; - cursor: pointer; user-select: none; - transition: background 0.15s, color 0.15s, border-color 0.15s; +/* 자동 갱신 상태 배지 — 클릭 불가 표시 전용. 장중=초록+남은초, 장마감=흐림. */ +.auto-badge { + flex: none; display: inline-flex; align-items: center; justify-content: center; + user-select: none; } -.auto-toggle:hover { background: #232a3b; } -.auto-toggle:active { transform: scale(0.95); } -.auto-toggle .auto-dot { - width: 7px; height: 7px; border-radius: 50%; - background: #5a5f6c; - transition: background 0.15s, box-shadow 0.15s; +/* 상태 점 — 장중 초록 / 장마감 어두운 회색. */ +.auto-badge::before { + content: ''; width: 7px; height: 7px; border-radius: 50%; + background: #3a3f4a; transition: background 0.2s, box-shadow 0.2s; } -.auto-toggle[aria-pressed="true"] { - background: rgba(255,77,94,0.15); border-color: #ff4d5e; color: #ff4d5e; -} -.auto-toggle[aria-pressed="true"] .auto-dot { - background: #ff4d5e; box-shadow: 0 0 6px rgba(255,77,94,0.7); - animation: auto-pulse 1.5s ease-in-out infinite; -} -.auto-toggle.closed, -.auto-toggle.closed:hover { - background: #15171f; border-color: #1f2330; color: #5a5f6c; - cursor: not-allowed; -} -.auto-toggle.closed:active { transform: none; } -.auto-toggle.closed .auto-dot { - background: #3a3f4a; box-shadow: none; animation: none; -} -@keyframes auto-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.45; } +.auto-badge.live::before { background: #22c55e; box-shadow: 0 0 5px rgba(34,197,94,0.6); } +.top-trade-btn { + flex: none; display: inline-flex; align-items: center; justify-content: center; + height: 36px; padding: 0 12px; border-radius: 8px; + background: #2a1418; color: #ff8a95; border: 1px solid rgba(255,138,149,0.45); + font-size: 13px; font-weight: 600; line-height: 1; font-family: inherit; + cursor: pointer; user-select: none; white-space: nowrap; + transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s; } +.top-trade-btn:hover { background: rgba(255,138,149,0.16); border-color: #ff8a95; color: #fff; } +.top-trade-btn:active { transform: scale(0.95); } .section-label { padding: 22px 18px 8px; margin-top: 10px; @@ -4598,6 +4609,8 @@ dl.pending-detail .pending-line-meta { margin-top: 2px; } .detail .block.chart-svg .chart-tabs button.active { background: rgba(251,191,36,0.12); color: #fbbf24; border-color: #fbbf24; } .detail .block.chart-svg .chart-panel { display: none; } .detail .block.chart-svg .chart-panel.active { display: block; } +.detail .block.chart-svg .chart-cap { margin-top: 6px; padding: 6px 4px 0; font-size: 8px; line-height: 1.5; color: #7a8493; } +.detail .block.chart-svg .chart-cap b { color: #9aa3b0; font-weight: 600; } .detail .block.chart-svg .chart-subtabs { display: flex; gap: 4px; margin-bottom: 6px; flex-wrap: wrap; } .detail .block.chart-svg .chart-subtabs button { padding: 3px 9px; background: transparent; color: #7a8493; border: 1px solid #2a2f3a; border-radius: 5px; cursor: pointer; font-size: 11px; font-weight: 600; font-family: inherit; } .detail .block.chart-svg .chart-subtabs button:hover { color: #cfd5df; border-color: #3f4654; } @@ -4625,6 +4638,9 @@ dl.pending-detail .pending-line-meta { margin-top: 2px; } .actions .btn-alerts-reset:hover { background: rgba(201,204,211,0.06); border-color: #6aa9ff; color: #6aa9ff; } .alert-mark { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; color: #ffd66e; background: rgba(255,214,110,0.10); border: 1px solid rgba(255,214,110,0.32); vertical-align: middle; } .badge.alerted { color: #ffd66e; background: rgba(255,214,110,0.10); border: 1px solid rgba(255,214,110,0.32); padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; } +.badge.vol-hot { padding: 1px 5px; border-radius: 8px; font-size: 9px; font-weight: 700; font-variant-numeric: tabular-nums; } +.badge.vol-hot.vol-warm { color: #f5a623; background: rgba(245,166,35,0.14); border: 1px solid rgba(245,166,35,0.45); } +.badge.vol-hot.vol-red { color: #ef4444; background: rgba(239,68,68,0.16); border: 1px solid rgba(239,68,68,0.5); } .alert-notice { margin: 0 0 10px; padding: 8px 10px; border-radius: 6px; font-size: 12px; color: #ffd66e; background: rgba(255,214,110,0.08); border-left: 3px solid #ffd66e; } .buy-raw { color: #c9ccd3; margin-bottom: 4px; } .buy-level { color: #e6e6e6; line-height: 1.5; } @@ -5396,18 +5412,24 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; } .order-side-toggle button.info-tab { color: #8db4ff; max-width: 100px; } .order-side-toggle button.info-tab:hover { background: #1f2840; color: #c9e0ff; } /* 좌우 2-컬럼 강제 (호가창 좁게, 인풋 영역 넓게). flex로 grid 안 남은 공간 채워 stretch */ -.order-body { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.25fr); gap: 10px; align-items: stretch; flex: 1; min-height: 0; } +.order-body { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.25fr); grid-template-rows: minmax(0, 1fr); gap: 10px; align-items: stretch; flex: 1; min-height: 0; } /* 호가창만 폰트 ~70% 압축, 별도 스크롤. height 100%로 인풋 영역과 동일 stretch */ .orderbook { display: flex; flex-direction: column; justify-content: center; border: 1px solid #1f2330; border-radius: 6px; overflow-y: auto; overflow-x: hidden; height: 100%; min-height: 0; scrollbar-width: thin; } .orderbook::-webkit-scrollbar { width: 4px; } .orderbook::-webkit-scrollbar-thumb { background: #2a2e3a; border-radius: 2px; } -.orderbook .ob-row { display: grid; grid-template-columns: 1fr 1fr; padding: 1px 5px; font-size: 7.5px; line-height: 1.35; font-variant-numeric: tabular-nums; cursor: pointer; border-bottom: 1px solid #15171e; transition: box-shadow 0.08s; } +.orderbook .ob-row { display: grid; grid-template-columns: 1fr 1fr; padding: 1px 5px; font-size: 7.5px; line-height: 1.35; font-variant-numeric: tabular-nums; cursor: pointer; border-bottom: 1px solid #15171e; transition: box-shadow 0.08s; color: #565c68; } .orderbook .ob-row:last-of-type { border-bottom: 0; } .orderbook .ob-row.ask { background: linear-gradient(to left, rgba(201,48,62,0.16), transparent 60%); color: #ff8a95; } .orderbook .ob-row.bid { background: linear-gradient(to left, rgba(42,95,179,0.16), transparent 60%); color: #8db4ff; } .orderbook .ob-row:hover { background: #1c1f29; } +.orderbook .ob-row.ob-current { background-color: rgba(230,230,230,0.13); color: #fff; font-weight: 700; box-shadow: inset 3px 0 0 #e6e6e6; } +.orderbook .ob-row.ob-synthetic { background: rgba(230,230,230,0.10); } +.orderbook .ob-row.ob-synthetic .qty { opacity: 0.85; color: #d0d0d0; font-size: 6.5px; } +.orderbook .ob-row.ob-cap { background: #12141b; } +.orderbook .ob-cap-mark { display: inline-block; font-size: 6.5px; font-weight: 800; padding: 0 2px; border-radius: 2px; margin-right: 1px; vertical-align: middle; } +.orderbook .ob-cap-mark.up { color: #ff8a95; background: rgba(201,48,62,0.28); } +.orderbook .ob-cap-mark.down { color: #8db4ff; background: rgba(42,95,179,0.28); } .orderbook .ob-row.selected { box-shadow: inset 0 0 0 1.5px #ffcc33; color: #fff !important; font-weight: 700; background-color: rgba(255,204,51,0.18) !important; } -.orderbook .ob-divider { padding: 2px 5px; font-size: 8.5px; font-weight: 700; color: #f0f0f0; background: #161922; text-align: center; border-top: 1px solid #1f2330; border-bottom: 1px solid #1f2330; font-variant-numeric: tabular-nums; } .orderbook .ob-row .price { text-align: left; } .orderbook .ob-row .qty { text-align: right; opacity: 0.8; font-size: 7px; } .price-tick-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin-top: 4px; } @@ -5456,9 +5478,6 @@ input[data-order-qty].qty-over { color: #ff8a95; } .order-actions .btn-confirm[data-side="SELL"] { background: #2a5fb3; } .order-actions button:active { transform: translateY(1px); } .order-actions button:disabled { opacity: 0.5; cursor: not-allowed; } -.sub-tab-order-btn { margin-left: auto; background: #2a1418; color: #ff8a95; border: 1px solid rgba(255,138,149,0.45); border-radius: 6px; padding: 6px 14px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; } -.sub-tab-order-btn:hover { background: rgba(255,138,149,0.16); border-color: #ff8a95; color: #fff; } -.sub-tab-order-btn:active { transform: translateY(1px); } .pin-box { max-width: 400px; } .pin-box > .hidden { display: none; } .open-orders-box { max-width: 560px; } @@ -6241,7 +6260,6 @@ def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances: '' '' '' - '' '
' ) @@ -6853,13 +6871,8 @@ def render_html() -> str: '}' 'window.__behive_set_meta_active=setMetaActive;' 'function setMarket(act){' - 'var btn=document.getElementById("auto-toggle");if(!btn)return;' 'var prev=window.__behive_market_active;' 'window.__behive_market_active=!!act;' - 'if(act){btn.classList.remove("closed");btn.removeAttribute("disabled");' - 'btn.title="10초마다 자동 갱신 (자산정보·관리자·가희·관심종목 탭에서 동작, 페이지를 보고 있을 때만)";}' - 'else{btn.classList.add("closed");btn.setAttribute("disabled","");' - 'btn.title="휴장 또는 장 마감 (NXT 평일 08:00~20:00 운영) — 자동 갱신 비활성";}' 'if(prev!==!!act&&window.__behive_auto_arm)window.__behive_auto_arm();' '}' 'window.__behive_set_market=setMarket;' @@ -7932,27 +7945,24 @@ def render_html() -> str: '})();' ) - # 자동 갱신 토글 — localStorage 영속, 계좌 탭 보일 때만 __behive_load 호출. - # 사이클: off(0) → 10s(1) → 3s(2) → off. 기존 "1" 값은 10s 모드와 호환. + # 자동 갱신 — 토글 없이 장 열림 중 항상 3s 주기(백그라운드). 계좌/관심 탭 보일 때만 __behive_load. + # 상단 배지는 상태 점 하나만(클릭 불가): 🟢 장중 자동갱신 / ⚪ 장마감. 카운트다운 없음. + # 장 닫힘 중엔 갱신 멈추고 저속 폴링으로 시장 열림만 감지. # location.reload() 제거 → 깜빡임·탭 클릭 묻힘 없음. fetch 완료 후에 재무장(arm)해 호출 중첩 방지. market_active_js = 'true' if market_active else 'false' auto_reload_script = ( '' ) @@ -8537,7 +8540,7 @@ def render_html() -> str: '\'·\'+meta+\'
\'+' '\'-\'+' '\'
\'+' - '\'평균가 \'+fmtInt(r.buy_avg)+\'원·\'+' + '\'매수가 \'+fmtInt(r.buy_avg)+\'원·\'+' '\'수량 \'+fmtInt(r.buy_qty)+\'주·\'+' '\'금액 \'+fmtInt(r.buy_amt)+\'원
\'+' '\'\';' @@ -8546,16 +8549,23 @@ def render_html() -> str: 'var pl=r.pl_amt||0;' 'var plCls=pl>0?"pl-pos":(pl<0?"pl-neg":"muted");' 'var sgn=pl>0?"+":"";' - 'var reNote=r._pl_rebased?\'
원가 \'+fmtInt(r.prev_avg)+\'원(전일 보유 평단) 기준 · 당일 매수분 제외
\':"";' + 'var costAvg=r._pl_rebased?r.prev_avg:Math.round(r.sell_avg-(pl+(r.cmsn_tax||0))/r.sell_qty);' + 'var costPct=costAvg>0?(r.sell_avg-costAvg)/costAvg*100:0;' + 'var costCol=costPct>0?"#ff4d5e":(costPct<0?"#2c7be5":"#6c7180");' + 'var costSgn=costPct>0?"+":"";' + 'var reNote=r._pl_rebased?\'
원가 \'+fmtInt(r.prev_avg)+\'원(전일 보유 평단) 기준 · 당일 매수분 제외
\':"";' 'out+=\'
  • \'+stockLine+' '\'
    \'+' '\'매도\'+' '\'·\'+meta+\'
    \'+' '\'\'+sgn+fmtInt(pl)+\'원
    \'+' '\'
    \'+' - '\'평균가 \'+fmtInt(r.sell_avg)+\'원·\'+' + '\'매도가 \'+fmtInt(r.sell_avg)+\'원·\'+' '\'수량 \'+fmtInt(r.sell_qty)+\'주·\'+' - '\'금액 \'+fmtInt(r.sell_amt)+\'원
    \'+reNote+' + '\'금액 \'+fmtInt(r.sell_amt)+\'원\'+' + '\'
    \'+' + '\'평단가 \'+fmtInt(costAvg)+\'원·\'+' + '\'\'+costSgn+costPct.toFixed(2)+\'%
    \'+reNote+' '\'
  • \';' '}' 'return out;' @@ -9037,34 +9047,74 @@ function renderBook(book){ if(book.change>0) che.classList.add('up'); else if(book.change<0) che.classList.add('down'); } var ob = $('[data-orderbook]'); if(!ob) return; - var html = ''; - var asks = (book.asks||[]).slice().reverse(); - for(var i=0;i'+fmt(a.price)+''+fmt(a.qty)+''; - } - html += '
    '+fmt(book.price)+'원
    '; + var prevScroll = ob.scrollTop; // 가격 변동에도 스크롤 고정 + var cur = book.price || 0; + var asks = (book.asks||[]).slice().reverse(); // 높은 매도호가 위 → 최우선(낮은) 아래 var bids = book.bids||[]; - for(var j=0;j'+fmt(b.price)+''+fmt(b.qty)+''; + var inBook = false; + for(var t=0;t 0) html += capRowHtml(book.upper_limit, 'up'); // 맨 위 상한가 캡 + if(cur > 0 && !inBook){ + // 현재가가 20호가 어디에도 없음 → 현재가 중심 사다리로 재구성(잔량은 겹치는 호가에 표시). + html += buildCenteredRows(asks, bids, cur); + } else { + // 평소: 매도10 + 매수10 (=20). 현재가는 일치 호가에 하이라이트. + for(var i2=0;i2'+fmt(a.price)+''+fmt(a.qty)+''; } + for(var j2=0;j2'+fmt(b.price)+''+fmt(b.qty)+''; } } + if(book.lower_limit > 0) html += capRowHtml(book.lower_limit, 'down'); // 맨 아래 하한가 캡 ob.innerHTML = html; + syncMarketPriceInput(book); + highlightSelectedTick(); updateMarketPhaseDisplay(); decorateAccounts(); + if(!state.bookCentered){ + var anchor = ob.querySelector('.ob-current') || ob.querySelector('.ob-row.bid'); + if(anchor){ ob.scrollTop = Math.max(0, anchor.offsetTop + anchor.offsetHeight/2 - ob.clientHeight/2); state.bookCentered = true; } + } else { ob.scrollTop = prevScroll; } +} +function syncMarketPriceInput(book){ var otSel = $('[data-order-type]'); if(otSel && otSel.value === 'MARKET' && book.price){ var inp = $('[data-order-price-input]'); if(inp && parseInt(inp.value||'0',10) !== book.price) inp.value = book.price; } - highlightSelectedTick(); - updateMarketPhaseDisplay(); - decorateAccounts(); // 현재가 갱신 → 매수 예수금 충분 여부 색 갱신 - if(!state.bookCentered){ - var div = ob.querySelector('.ob-divider'); - if(div){ - ob.scrollTop = Math.max(0, div.offsetTop + div.offsetHeight/2 - ob.clientHeight/2); - state.bookCentered = true; - } +} +// 한국 주식 호가단위 (2023 KRX 개정, KOSPI/KOSDAQ 통일) +function tickSize(p){ + if(p < 2000) return 1; + if(p < 5000) return 5; + if(p < 20000) return 10; + if(p < 50000) return 50; + if(p < 200000) return 100; + if(p < 500000) return 500; + return 1000; +} +// 현재가가 호가에 없을 때: 현재가 중심 ±10틱 사다리(20행). 잔량은 겹치는 호가에만, 현재가 하이라이트. +function buildCenteredRows(asks, bids, cur){ + var qmap = {}; + for(var i=0;i1?q-1:q); if(q<1) break; dn.push(q); } // 현재가 아래 10틱 + var rows = up.reverse().concat([cur]).concat(dn); // 높은가격 위 → 낮은가격 아래 + var html = ''; + for(var r=0;r'+fmt(pr)+''+qty+''; } + return html; +} +function capRowHtml(price, dir){ + var mk = dir==='up' ? ' ' : ' '; + var lab = dir==='up' ? '상한' : '하한'; + return '
    '+mk+fmt(price)+''+lab+'
    '; } function pollOnce(){ if(document.hidden || !state.isOpen || !state.code) return; @@ -10058,9 +10108,8 @@ window.openPinModal = openPinModal; 갱신 {now}
    - - - + +