auto: 일일 백업 2026-07-16 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3057,6 +3057,12 @@ def _render_holding_row(r: dict, total_value: int, show_day_change: bool = False
|
||||
pl_pairs: list[str] = [
|
||||
f'<dt>평가손익</dt><dd class="num"><span class="{pcls} rt-profit-detail">{sign}{profit:,}원 ({sign}{profit_rate:.2f}%)</span></dd>',
|
||||
]
|
||||
# 수익률 마이너스(손실)일 때만 — 평단가 회복(본전)에 필요한 상승률.
|
||||
if profit_rate < 0 and price > 0 and avg > price:
|
||||
breakeven_pct = (avg - price) / price * 100
|
||||
pl_pairs.append(
|
||||
f'<dt>평단가대비</dt><dd class="num"><span class="muted small">(+{breakeven_pct:.2f}%)</span></dd>'
|
||||
)
|
||||
if day_change:
|
||||
dcls = _profit_class(day_change)
|
||||
dsign = '+' if day_change >= 0 else ''
|
||||
@@ -3182,8 +3188,10 @@ def _render_holding_row(r: dict, total_value: int, show_day_change: bool = False
|
||||
f'<dt>매수후 고점</dt><dd class="num">{peak_high:,}원 '
|
||||
f'<span class="{ddcls}">({dd_pct:.2f}%)</span></dd>'
|
||||
)
|
||||
recover_pct = (peak_high - price) / price * 100
|
||||
pl_pairs.append(
|
||||
f'<dt>고점대비</dt><dd class="num"><span class="muted small">{dd_won:,}원</span></dd>'
|
||||
f'<dt>고점대비</dt><dd class="num"><span class="muted small">{dd_won:,}원</span> '
|
||||
f'<span class="muted small">(+{recover_pct:.2f}%)</span></dd>'
|
||||
)
|
||||
pl_pairs.append('<div class="pl-divider"></div>')
|
||||
pl_dl_inner = ''.join(pl_pairs)
|
||||
@@ -5706,6 +5714,9 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; }
|
||||
.order-info .pl-down { color: #8db4ff; white-space: nowrap; }
|
||||
.order-info b.qty-over { color: #ff8a95; }
|
||||
input[data-order-qty].qty-over { color: #ff8a95; }
|
||||
.order-adr-banner { padding: 8px 12px; margin-bottom: 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600; line-height: 1.4; }
|
||||
.order-adr-banner.adr-over { background: #2a1418; border: 1px solid #5a1f28; color: #ff8a95; }
|
||||
.order-adr-banner.adr-under { background: #101b2e; border: 1px solid #1f3a5a; color: #8ab6ff; }
|
||||
.order-modal-msg { padding: 12px; margin-bottom: 12px; border-radius: 8px; font-size: 13px; line-height: 1.6; white-space: pre-wrap; font-variant-numeric: tabular-nums; }
|
||||
.order-modal-msg.error { background: #2a1418; border: 1px solid #5a1f28; color: #ff8a95; }
|
||||
.order-modal-msg.success { background: #142a18; border: 1px solid #1f5a28; color: #8aff95; }
|
||||
@@ -5869,6 +5880,30 @@ def _adr_class(adr: float | None) -> tuple[str, str]:
|
||||
return 'flat', ''
|
||||
|
||||
|
||||
def _market_adr_signal(market: str) -> dict | None:
|
||||
"""거래 모달용 — 시장(KOSPI/KOSDAQ) 20일 정통 ADR + 과매수/과매도 판정.
|
||||
_adr_class와 동일 임계(≥120 과매수·≤80 과매도). 중립·20일 미만 누적이면 None(배너 미표시).
|
||||
반환 {'value': float, 'zone': '과매수'|'과매도', 'label': '코스피'|'코스닥'}."""
|
||||
if market not in ('KOSPI', 'KOSDAQ'):
|
||||
return None
|
||||
try:
|
||||
live_rows = _fetch_market_indicators() or []
|
||||
today = next((r for r in live_rows if r.get('symbol') == market), None)
|
||||
ma, count, _ = _compute_adr_ma_latest(market, today)
|
||||
if ma is None or count < ADR_MA_WINDOW:
|
||||
return None
|
||||
if ma >= 120:
|
||||
zone = '과매수'
|
||||
elif ma <= 80:
|
||||
zone = '과매도'
|
||||
else:
|
||||
return None
|
||||
return {'value': round(ma, 1), 'zone': zone,
|
||||
'label': _ADR_MARKET_LABELS.get(market, market)}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _build_adr_series_with_live(history_rows: list[dict], live_pt: dict | None) -> list[dict]:
|
||||
"""history(오래된 순) + 오늘 라이브 1점을 합쳐 [{date,adr,rise,fall,is_live}, ...] 반환.
|
||||
rise/fall(상승·하락 종목수)은 정통 합계식 ADR 산출용. 라이브는 history 마지막 date 와 다를 때만 append."""
|
||||
@@ -6772,6 +6807,7 @@ def _render_calc_modal() -> str:
|
||||
'<div class="calc-result" data-calc-result>'
|
||||
'<div class="calc-line"><span class="calc-lbl">등락률</span><span class="calc-val" data-calc-pct>—</span></div>'
|
||||
'<div class="calc-line"><span class="calc-lbl">차이</span><span class="calc-val" data-calc-diff>—</span></div>'
|
||||
'<div class="calc-line"><span class="calc-lbl">기준가 회복 필요</span><span class="calc-val" data-calc-recover>—</span></div>'
|
||||
'<div class="calc-hint" data-calc-hint>기준가·현재가를 입력하세요.</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
@@ -6997,6 +7033,7 @@ def _render_order_modal() -> str:
|
||||
'<span class="change" data-order-change>—</span>'
|
||||
'<span class="order-market-phase" data-market-phase>—</span>'
|
||||
'</div>'
|
||||
'<div class="order-adr-banner hidden" data-order-adr></div>'
|
||||
'<div class="order-modal-msg hidden" data-order-msg></div>'
|
||||
# 1단계: 입력
|
||||
'<div data-order-step="1">'
|
||||
@@ -8602,11 +8639,13 @@ def render_html() -> str:
|
||||
'var cur=num(document.querySelector("[data-calc-cur]"));'
|
||||
'var pctEl=document.querySelector("[data-calc-pct]");'
|
||||
'var diffEl=document.querySelector("[data-calc-diff]");'
|
||||
'var recEl=document.querySelector("[data-calc-recover]");'
|
||||
'var hintEl=document.querySelector("[data-calc-hint]");'
|
||||
'if(!pctEl||!diffEl)return;'
|
||||
'if(isNaN(base)||isNaN(cur)||base===0){'
|
||||
'pctEl.textContent="—";pctEl.style.color=NEU;'
|
||||
'diffEl.textContent="—";diffEl.style.color=NEU;'
|
||||
'if(recEl){recEl.textContent="—";recEl.style.color=NEU;}'
|
||||
'if(hintEl)hintEl.textContent=(!isNaN(base)&&base===0)?"기준가는 0이 될 수 없어요.":"기준가·현재가를 입력하세요.";'
|
||||
'return;'
|
||||
'}'
|
||||
@@ -8617,6 +8656,13 @@ def render_html() -> str:
|
||||
'var dsign=diff>0?"+":(diff<0?"−":"");'
|
||||
'pctEl.textContent=psign+Math.abs(pct).toFixed(2)+"%";pctEl.style.color=col;'
|
||||
'diffEl.textContent=dsign+Math.abs(diff).toLocaleString()+"원";diffEl.style.color=col;'
|
||||
'if(recEl){'
|
||||
'if(cur===0){recEl.textContent="—";recEl.style.color=NEU;}'
|
||||
'else{var rec=(base-cur)/cur*100;'
|
||||
'if(rec>0.005){recEl.textContent="+"+rec.toFixed(2)+"%";}'
|
||||
'else{recEl.textContent="달성";}'
|
||||
'recEl.style.color=NEU;}'
|
||||
'}'
|
||||
'if(hintEl)hintEl.textContent="";'
|
||||
'}'
|
||||
'document.addEventListener("input",function(e){'
|
||||
@@ -9910,6 +9956,18 @@ function renderBook(book){
|
||||
che.classList.remove('up','down');
|
||||
if(book.change>0) che.classList.add('up'); else if(book.change<0) che.classList.add('down');
|
||||
}
|
||||
var adrEl = $('[data-order-adr]');
|
||||
if(adrEl){
|
||||
var a = book.adr;
|
||||
if(a && a.zone){
|
||||
var isOver = a.zone === '과매수';
|
||||
adrEl.className = 'order-adr-banner ' + (isOver ? 'adr-over' : 'adr-under');
|
||||
adrEl.textContent = (isOver?'🔴 ':'🔵 ') + a.label + ' ' + a.zone + ' 구간입니다 (20일 ADR ' + a.value + ')';
|
||||
} else {
|
||||
adrEl.className = 'order-adr-banner hidden';
|
||||
adrEl.textContent = '';
|
||||
}
|
||||
}
|
||||
var ob = $('[data-orderbook]'); if(!ob) return;
|
||||
var prevScroll = ob.scrollTop; // 가격 변동에도 스크롤 고정
|
||||
var cur = book.price || 0;
|
||||
@@ -11480,6 +11538,12 @@ class Handler(BaseHTTPRequestHandler):
|
||||
book['upper_limit'] = 0
|
||||
book['lower_limit'] = 0
|
||||
book['nxt_enable'] = nxt_enable # 위에서 판정한 값 재사용
|
||||
# 종목이 속한 시장(KOSPI/KOSDAQ)의 20일 ADR 과매수/과매도 — 극단일 때만 실림.
|
||||
_mkt = (meta or {}).get('market') or ''
|
||||
book['market'] = _mkt
|
||||
_adr_sig = _market_adr_signal(_mkt)
|
||||
if _adr_sig:
|
||||
book['adr'] = _adr_sig
|
||||
book.pop('raw', None)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
Reference in New Issue
Block a user