auto: 일일 백업 2026-07-03 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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'<span class="badge vol-hot vol-{level}">🔥{rvol:.1f}</span>'
|
||||
|
||||
|
||||
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'''<details class="row {cls} mode-{mode}" data-row-key="{row_key}">
|
||||
<summary>
|
||||
<div class="left">
|
||||
<div class="line1">{dot_html}<span class="stock">{stock}</span>{tag_chip}{status_cell}{trade_mark}{alert_badge}</div>
|
||||
<div class="line1">{dot_html}<span class="stock">{stock}</span>{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))}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="price">{price_cell}<span class="caret" aria-hidden="true">▾</span></div>
|
||||
@@ -2982,7 +3024,7 @@ def _render_holding_row(r: dict, total_value: int, show_day_change: bool = False
|
||||
<summary>
|
||||
<div class="left">
|
||||
<div class="line1 line1-held">{_buy_rank_badge(buy_amount)}<span class="{stock_cls}">{stock}</span><span class="code acct-mini">{acct_label}</span></div>
|
||||
<div class="line2"><span class="badge held">📦 {qty:,}</span>{_pending_badges_html(r)}<span class="muted small wt-chip"><span class="rt-weight">{weight:.2f}</span>%</span>{star}</div>
|
||||
<div class="line2"><span class="badge held">📦 {qty:,}</span>{_pending_badges_html(r)}{_vol_surge_badge(code, (r.get('ohlc_krx') or {}).get('volume'))}<span class="muted small wt-chip"><span class="rt-weight">{weight:.2f}</span>%</span>{star}</div>
|
||||
{summary_journal_html}
|
||||
</div>
|
||||
<div class="right">
|
||||
@@ -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:
|
||||
'<button type="button" class="sub-tab active" data-sub-tab="assets" role="tab" aria-selected="true">자산보기</button>'
|
||||
'<button type="button" class="sub-tab" data-sub-tab="chart" role="tab" aria-selected="false">차트보기</button>'
|
||||
'<button type="button" class="sub-tab" data-sub-tab="market" role="tab" aria-selected="false">시장정보</button>'
|
||||
'<button type="button" class="sub-tab-order-btn" data-open-order-modal title="거래 — 종목은 모달에서 선택">💰 거래</button>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
'})();</script>'
|
||||
)
|
||||
|
||||
# 자동 갱신 토글 — 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 = (
|
||||
'<script>(function(){'
|
||||
f'var O=[{auto_refresh_tids_js}];'
|
||||
f'var MARKET={market_active_js};'
|
||||
'var INT=[0,10,3],TICK=1000,KEY="behive.autoreload",t=null,remain=0;'
|
||||
'var SEC=3,TICK=1000,t=null,remain=0;'
|
||||
# 장 닫힘 동안 시장 열림 감지용 저속 폴링. visible 일 때만 동작.
|
||||
'var SLOW_MS=60000,slow=null;'
|
||||
'var btn=document.getElementById("auto-toggle");if(!btn)return;'
|
||||
'var lbl=btn.querySelector("span:last-child");'
|
||||
'function mode(){var v=parseInt(localStorage.getItem(KEY)||"0",10);return (v>=0&&v<INT.length)?v:0;}'
|
||||
'function secs(){return INT[mode()];}'
|
||||
'function idleLbl(){var s=secs();return s>0?("자동 "+s+"s"):"자동";}'
|
||||
'var badge=document.getElementById("auto-badge");'
|
||||
# 배지 점: 장중 초록(.live) / 장마감 어두운 회색. 상태는 marketNow()에만 의존(탭·카운트 무관).
|
||||
'function dot(){if(badge)badge.classList.toggle("live",marketNow());}'
|
||||
'function active(){return document.documentElement.getAttribute("data-tab")||"";}'
|
||||
'function marketNow(){return (typeof window.__behive_market_active!=="undefined")?window.__behive_market_active:MARKET;}'
|
||||
'function ok(){return marketNow()&&mode()>0&&document.visibilityState==="visible"&&O.indexOf(active())>=0;}'
|
||||
'function setLbl(s){if(lbl)lbl.textContent=s;}'
|
||||
'function idleClosedLbl(){return marketNow()?idleLbl():"장마감";}'
|
||||
'function ok(){return marketNow()&&document.visibilityState==="visible"&&O.indexOf(active())>=0;}'
|
||||
'function stop(){if(t){clearInterval(t);t=null;}}'
|
||||
'function stopSlow(){if(slow){clearInterval(slow);slow=null;}}'
|
||||
'function startSlow(){'
|
||||
@@ -7966,10 +7976,11 @@ def render_html() -> str:
|
||||
'function arm(){'
|
||||
'stop();'
|
||||
'if(marketNow())stopSlow();else startSlow();'
|
||||
'if(!ok()){setLbl(idleClosedLbl());return;}'
|
||||
'remain=secs();setLbl("자동 "+remain+"s");'
|
||||
'dot();'
|
||||
'if(!ok())return;'
|
||||
'remain=SEC;'
|
||||
't=setInterval(function(){'
|
||||
'if(!ok()){stop();setLbl(idleClosedLbl());return;}'
|
||||
'if(!ok()){stop();dot();return;}'
|
||||
# 차트 툴팁 떠 있는 동안 자동 갱신 미룸 — swap으로 사라지는 거 방지.
|
||||
'var tt=document.querySelectorAll(".net-chart-tip");'
|
||||
'for(var ti=0;ti<tt.length;ti++){if(tt[ti].style.opacity==="1")return;}'
|
||||
@@ -7985,21 +7996,13 @@ def render_html() -> str:
|
||||
'p.then(arm,arm);'
|
||||
'return;'
|
||||
'}'
|
||||
'setLbl("자동 "+remain+"s");'
|
||||
'},TICK);'
|
||||
'}'
|
||||
'function setUI(m){btn.setAttribute("aria-pressed",m>0?"true":"false");}'
|
||||
'function set(m){localStorage.setItem(KEY,String(m));setUI(m);arm();}'
|
||||
'btn.addEventListener("click",function(){'
|
||||
'if(!marketNow())return;' # 장 닫힘 중엔 토글 무시 (button disabled 안전망)
|
||||
'set((mode()+1)%INT.length);'
|
||||
'});'
|
||||
'window.__behive_auto_arm=arm;'
|
||||
'document.addEventListener("visibilitychange",arm);'
|
||||
'addEventListener("hashchange",arm);'
|
||||
# 첫 진입 시 휴장·주말·장외이면 button에 closed/disabled 즉시 적용 — panels fetch 전에도 시각 일관.
|
||||
'if(window.__behive_set_market)window.__behive_set_market(MARKET);'
|
||||
'setUI(mode());arm();'
|
||||
'arm();'
|
||||
'})();</script>'
|
||||
)
|
||||
|
||||
@@ -8537,7 +8540,7 @@ def render_html() -> str:
|
||||
'\'<span class="ti-sep">·</span>\'+meta+\'</div>\'+'
|
||||
'\'<span class="ti-pl muted">-</span></div>\'+'
|
||||
'\'<div class="ti-row2"><div class="ti-data">\'+'
|
||||
'\'<span class="ti-price"><span class="ti-label">평균가</span> \'+fmtInt(r.buy_avg)+\'원</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-price"><span class="ti-label">매수가</span> \'+fmtInt(r.buy_avg)+\'원</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-qty"><span class="ti-label">수량</span> \'+fmtInt(r.buy_qty)+\'주</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-amt"><span class="ti-label">금액</span> \'+fmtInt(r.buy_amt)+\'원</span></div></div>\'+'
|
||||
'\'</li>\';'
|
||||
@@ -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?\'<div class="muted small" style="margin-top:2px">원가 \'+fmtInt(r.prev_avg)+\'원(전일 보유 평단) 기준 · 당일 매수분 제외</div>\':"";'
|
||||
'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?\'<div class="muted small" style="margin-top:2px;font-size:11px">원가 \'+fmtInt(r.prev_avg)+\'원(전일 보유 평단) 기준 · 당일 매수분 제외</div>\':"";'
|
||||
'out+=\'<li class="trade-item trade-item-sell\'+seedCls+\'">\'+stockLine+'
|
||||
'\'<div class="ti-row1"><div class="ti-meta">\'+'
|
||||
'\'<span class="ti-side side-sell">매도</span>\'+'
|
||||
'\'<span class="ti-sep">·</span>\'+meta+\'</div>\'+'
|
||||
'\'<span class="ti-pl \'+plCls+\'">\'+sgn+fmtInt(pl)+\'원</span></div>\'+'
|
||||
'\'<div class="ti-row2"><div class="ti-data">\'+'
|
||||
'\'<span class="ti-price"><span class="ti-label">평균가</span> \'+fmtInt(r.sell_avg)+\'원</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-price"><span class="ti-label">매도가</span> \'+fmtInt(r.sell_avg)+\'원</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-qty"><span class="ti-label">수량</span> \'+fmtInt(r.sell_qty)+\'주</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-amt"><span class="ti-label">금액</span> \'+fmtInt(r.sell_amt)+\'원</span></div></div>\'+reNote+'
|
||||
'\'<span class="ti-amt"><span class="ti-label">금액</span> \'+fmtInt(r.sell_amt)+\'원</span></div></div>\'+'
|
||||
'\'<div class="ti-row2"><div class="ti-data">\'+'
|
||||
'\'<span class="ti-price"><span class="ti-label">평단가</span> \'+fmtInt(costAvg)+\'원</span><span class="ti-sep">·</span>\'+'
|
||||
'\'<span class="ti-price" style="color:\'+costCol+\'">\'+costSgn+costPct.toFixed(2)+\'%</span></div></div>\'+reNote+'
|
||||
'\'</li>\';'
|
||||
'}'
|
||||
'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<asks.length;i++){
|
||||
var a = asks[i];
|
||||
html += '<div class="ob-row ask" data-ob-price="'+a.price+'"><span class="price">'+fmt(a.price)+'</span><span class="qty">'+fmt(a.qty)+'</span></div>';
|
||||
}
|
||||
html += '<div class="ob-divider">'+fmt(book.price)+'원</div>';
|
||||
var prevScroll = ob.scrollTop; // 가격 변동에도 스크롤 고정
|
||||
var cur = book.price || 0;
|
||||
var asks = (book.asks||[]).slice().reverse(); // 높은 매도호가 위 → 최우선(낮은) 아래
|
||||
var bids = book.bids||[];
|
||||
for(var j=0;j<bids.length;j++){
|
||||
var b = bids[j];
|
||||
html += '<div class="ob-row bid" data-ob-price="'+b.price+'"><span class="price">'+fmt(b.price)+'</span><span class="qty">'+fmt(b.qty)+'</span></div>';
|
||||
var inBook = false;
|
||||
for(var t=0;t<asks.length;t++){ if(asks[t].price===cur) inBook=true; }
|
||||
for(var u=0;u<bids.length;u++){ if(bids[u].price===cur) inBook=true; }
|
||||
var html = '';
|
||||
if(book.upper_limit > 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<asks.length;i2++){ var a=asks[i2];
|
||||
html += '<div class="ob-row ask'+(a.price===cur?' ob-current':'')+'" data-ob-price="'+a.price+'"><span class="price">'+fmt(a.price)+'</span><span class="qty">'+fmt(a.qty)+'</span></div>'; }
|
||||
for(var j2=0;j2<bids.length;j2++){ var b=bids[j2];
|
||||
html += '<div class="ob-row bid'+(b.price===cur?' ob-current':'')+'" data-ob-price="'+b.price+'"><span class="price">'+fmt(b.price)+'</span><span class="qty">'+fmt(b.qty)+'</span></div>'; }
|
||||
}
|
||||
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;i<asks.length;i++) qmap[asks[i].price] = {q:asks[i].qty, side:'ask'};
|
||||
for(var j=0;j<bids.length;j++) qmap[bids[j].price] = {q:bids[j].qty, side:'bid'};
|
||||
var up = [], p = cur;
|
||||
for(var a=0;a<10;a++){ p += tickSize(p); up.push(p); } // 현재가 위 10틱
|
||||
var dn = [], q = cur;
|
||||
for(var b=0;b<10;b++){ q -= tickSize(q>1?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<rows.length;r++){
|
||||
var pr = rows[r], m = qmap[pr];
|
||||
var cls = pr===cur ? 'bid ob-current' : (m ? m.side : '');
|
||||
var qty = m ? fmt(m.q) : '';
|
||||
html += '<div class="ob-row '+cls+'" data-ob-price="'+pr+'"><span class="price">'+fmt(pr)+'</span><span class="qty">'+qty+'</span></div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
function capRowHtml(price, dir){
|
||||
var mk = dir==='up' ? '<b class="ob-cap-mark up">상</b> ' : '<b class="ob-cap-mark down">하</b> ';
|
||||
var lab = dir==='up' ? '상한' : '하한';
|
||||
return '<div class="ob-row ob-cap ob-cap-'+dir+'" data-ob-price="'+price+'"><span class="price">'+mk+fmt(price)+'</span><span class="qty">'+lab+'</span></div>';
|
||||
}
|
||||
function pollOnce(){
|
||||
if(document.hidden || !state.isOpen || !state.code) return;
|
||||
@@ -10058,9 +10108,8 @@ window.openPinModal = openPinModal;
|
||||
<span class="meta">갱신 {now}</span>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<button type="button" id="ws-status" class="ws-dot" title="실시간 연결 상태 (탭하면 표시)" aria-label="실시간 연결 상태"></button>
|
||||
<button id="auto-toggle" class="auto-toggle" type="button" aria-pressed="false" title="자동 갱신 사이클: off → 10s → 3s (자산정보·관리자·가희·관심종목 탭에서 동작, 페이지를 보고 있을 때만)"><span class="auto-dot"></span><span>자동</span></button>
|
||||
<a class="refresh" href="#" onclick="event.preventDefault();window.__behive_manual_refresh&&window.__behive_manual_refresh();" aria-label="새로고침" title="새로고침">↻</a>
|
||||
<span id="auto-badge" class="auto-badge" title="초록 = 장중 3초마다 자동 갱신 / 회색 = 장마감"></span>
|
||||
<button type="button" class="top-trade-btn" data-open-order-modal title="거래 — 종목은 모달에서 선택">거래</button>
|
||||
<div class="opts-wrap">
|
||||
<button type="button" id="opts-toggle" class="opts-btn" aria-haspopup="true" aria-expanded="false" aria-label="옵션" title="옵션">☰</button>
|
||||
<div class="opts-popup" id="opts-popup" hidden role="menu" aria-label="옵션">
|
||||
@@ -10443,7 +10492,6 @@ class Handler(BaseHTTPRequestHandler):
|
||||
import time as _time
|
||||
from orders.pin import PinStore
|
||||
pending = PinStore().peek()
|
||||
unique_codes: set = set()
|
||||
result: dict = {'active': False}
|
||||
if pending is not None and not pending.is_expired() and not pending.consumed:
|
||||
payload = pending.payload or {}
|
||||
@@ -10466,21 +10514,18 @@ class Handler(BaseHTTPRequestHandler):
|
||||
'card_message': card_message,
|
||||
'expiry_seconds': remaining,
|
||||
}
|
||||
if payload.get('symbol'):
|
||||
unique_codes.add(payload['symbol'])
|
||||
# 4계좌 미체결 주문 ka10075 통합
|
||||
open_orders_count = 0
|
||||
try:
|
||||
import kiwoom_client as kc
|
||||
rows = kc.get_open_orders_all()
|
||||
open_orders_count = len(rows)
|
||||
for r in rows:
|
||||
if r.get('code'):
|
||||
unique_codes.add(r['code'])
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
result['open_orders_count'] = open_orders_count
|
||||
result['unique_count'] = len(unique_codes)
|
||||
# 진행중 아이템 수 = 미체결 주문 건수(팝업 행과 동일) + 활성 PIN 카드(있으면 1).
|
||||
# 종목코드 중복제거 X — 같은 종목 2계좌 매도도 2건으로 표시(팝업과 일치).
|
||||
result['unique_count'] = open_orders_count + (1 if result.get('active') else 0)
|
||||
self._send_json(200, result)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@@ -10503,18 +10548,36 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
try:
|
||||
import kiwoom_client as kc
|
||||
book = kc.get_quote_book(code)
|
||||
quote = kc.get_stock_quote(code)
|
||||
# NXT 거래 가능 여부 먼저 판정 (거래소 선택에 사용). 캐시 없거나 모르면 보수적 True.
|
||||
try:
|
||||
meta = kc.lookup_stock_meta(code)
|
||||
nxt_enable = True if (not meta) else bool(meta.get('nxt_enable', True))
|
||||
except Exception:
|
||||
nxt_enable = True
|
||||
# NXT 시간대엔 KRX 호가창이 15:30 종가로 고정 → 현재가(NXT)와 갭 발생.
|
||||
# 그 시간대엔 NXT 호가창·NXT 현재가로 정합시킨다. 단 NXT 미상장 종목은 NXT 호가가
|
||||
# 없으므로 그 시간대에도 KRX 기준으로 표시.
|
||||
_ph = _market_phase_state().get('phase')
|
||||
use_nxt = (_ph == 'nxt') and nxt_enable
|
||||
if use_nxt:
|
||||
book = kc.get_quote_book(code + '_NX')
|
||||
quote = kc.get_stock_quote(code, exchange='NX')
|
||||
else:
|
||||
book = kc.get_quote_book(code)
|
||||
quote = kc.get_stock_quote(code, exchange='KRX')
|
||||
book['price'] = quote.get('price', 0)
|
||||
book['change'] = quote.get('change', 0)
|
||||
book['change_pct'] = quote.get('change_pct', 0.0)
|
||||
book['name'] = quote.get('name', '')
|
||||
# NXT 거래 가능 여부 (ka10099 캐시 nxtEnable) — 캐시 없거나 모르면 보수적 True
|
||||
# 상한가·하한가 (ka10001 upl_pric/lst_pric) — 사다리 호가 생성 범위
|
||||
qraw = quote.get('raw') if isinstance(quote.get('raw'), dict) else {}
|
||||
try:
|
||||
meta = kc.lookup_stock_meta(code)
|
||||
book['nxt_enable'] = True if (not meta) else bool(meta.get('nxt_enable', True))
|
||||
except Exception:
|
||||
book['nxt_enable'] = True
|
||||
book['upper_limit'] = abs(int(str(qraw.get('upl_pric') or '0').strip() or '0'))
|
||||
book['lower_limit'] = abs(int(str(qraw.get('lst_pric') or '0').strip() or '0'))
|
||||
except (ValueError, TypeError):
|
||||
book['upper_limit'] = 0
|
||||
book['lower_limit'] = 0
|
||||
book['nxt_enable'] = nxt_enable # 위에서 판정한 값 재사용
|
||||
book.pop('raw', None)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@@ -10791,6 +10854,14 @@ class Handler(BaseHTTPRequestHandler):
|
||||
tq = qmap.get(code) if isinstance(qmap, dict) else None
|
||||
except Exception:
|
||||
tq = None
|
||||
# 오늘 봉 거래량은 KRX 기준(일봉 히스토리와 동일 기준 → 막대·RVOL·회전율 일관).
|
||||
# 가격 OHLC는 통합(AL) 유지, 거래량만 KRX.
|
||||
krx_vol = 0
|
||||
try:
|
||||
_qk = kc.get_watchlist_quotes([code], exchange='KRX').get(code) or {}
|
||||
krx_vol = _qk.get('volume', 0) or 0
|
||||
except Exception:
|
||||
krx_vol = 0
|
||||
|
||||
live_today_ok = bool(
|
||||
tq and tq.get('price') and tq.get('open')
|
||||
@@ -10804,7 +10875,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
'high': tq['high'],
|
||||
'low': tq['low'],
|
||||
'close': tq['price'],
|
||||
'volume': tq.get('volume', 0),
|
||||
'volume': krx_vol or tq.get('volume', 0),
|
||||
'value': 0,
|
||||
'turnover_rate': None,
|
||||
}]
|
||||
@@ -10820,6 +10891,24 @@ class Handler(BaseHTTPRequestHandler):
|
||||
'sma20': sa.sma(closes, 20),
|
||||
'sma60': sa.sma(closes, 60),
|
||||
}
|
||||
# 마지막 봉이 오늘·정규장 진행형이면 세션경과 비율로 오늘 거래량을 하루치 환산(회전율 '예상')
|
||||
vfrac = None
|
||||
if candles_asc[-1]['date'] == today and _market_phase_state().get('phase') == 'regular':
|
||||
now = datetime.now(KST)
|
||||
hm = now.hour * 60 + now.minute
|
||||
vfrac = min(1.0, max(0.15, (hm - 9 * 60) / 390.0)) # 09:00~15:30 = 390분
|
||||
# 오늘 회전율·RVOL 값 — 차트 아래 표시 (텍스트 라벨은 차트에 안 그림)
|
||||
_turn, _tscaled = sa.turnover_today(candles_asc, vfrac)
|
||||
_rvol, _rscaled = sa.rvol_today(candles_asc, 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 ''
|
||||
)
|
||||
# 분석 페이지와 동일 패턴 — 1M/3M/6M/1Y 4 개 panel + 탭. 250 봉 캐시 슬라이스만 다름.
|
||||
# 분봉 panel 은 빈 placeholder. 탭 클릭 시 클라이언트가 lazy fetch.
|
||||
svg_1y = sa.render_svg_chart(snap, range_key='1Y')
|
||||
@@ -10856,6 +10945,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
f'<div class="chart-panel" data-range="3M">{svg_3m}</div>'
|
||||
f'<div class="chart-panel" data-range="6M">{svg_6m}</div>'
|
||||
f'<div class="chart-panel" data-range="1Y">{svg_1y}</div>'
|
||||
+ _turn_cap
|
||||
)
|
||||
body = html_block.encode('utf-8')
|
||||
body, enc = _maybe_gzip(body, self.headers.get('Accept-Encoding'))
|
||||
|
||||
Reference in New Issue
Block a user