auto: 일일 백업 2026-06-18 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -225,10 +225,33 @@ def _ind_key(code):
|
||||
config.RSI_PERIOD, config.TREND_SLOPE_DAYS)
|
||||
|
||||
|
||||
def _gather(extra_codes=None):
|
||||
LAST_QUOTES_PATH = config.STATE_DIR / 'last_quotes.json'
|
||||
|
||||
|
||||
def _save_last_quotes(quotes: dict) -> None:
|
||||
"""직전 스캔이 모은 시세 — judge_view(관심종목 탭)가 라이브 재호출 없이 같은 값을 재사용."""
|
||||
if not quotes: # 시세 수집 실패(빈 dict)면 직전 정상값 보존 — 덮어쓰지 않음
|
||||
return
|
||||
try:
|
||||
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LAST_QUOTES_PATH.write_text(json.dumps(quotes, ensure_ascii=False))
|
||||
except Exception as e:
|
||||
sys.stderr.write(f'[engine] last_quotes 저장 실패: {e}\n')
|
||||
|
||||
|
||||
def _load_last_quotes() -> dict | None:
|
||||
"""직전 스캔 시세. 없으면 None → 호출부가 라이브 폴백."""
|
||||
try:
|
||||
return json.loads(LAST_QUOTES_PATH.read_text())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _gather(extra_codes=None, quotes=None):
|
||||
"""파라미터 무관한 라이브 데이터 1회 수집 (메인·변이 공유). 종목·시세·시장매핑.
|
||||
|
||||
extra_codes: universe에 없지만 청산 판단을 위해 포함해야 할 보유 코드(수동 삭제된 보유분).
|
||||
quotes: 주어지면 라이브 시세 호출을 건너뛰고 그대로 사용 (judge_view 가 직전 스캔 시세 재사용).
|
||||
"""
|
||||
data.reset_book_memo() # 호가 공유 캐시 리셋 — 이번 스캔의 체결가를 전 선수가 공유
|
||||
_SCAN_CANDLES.clear()
|
||||
@@ -241,7 +264,8 @@ def _gather(extra_codes=None):
|
||||
uni = uni + [{'code': c, 'name': '', 'sources': ['held']}]
|
||||
codes.append(c)
|
||||
seen.add(c)
|
||||
quotes = data.batch_quotes(codes)
|
||||
if not quotes: # None(미지정) 또는 빈 dict(저장본 비었음)면 라이브 수집
|
||||
quotes = data.batch_quotes(codes)
|
||||
cmkt = universe.code_market_map()
|
||||
return uni, quotes, cmkt
|
||||
|
||||
@@ -399,7 +423,9 @@ def judge_view(vid: str) -> dict | None:
|
||||
pf = Portfolio.load(pp, tp)
|
||||
try:
|
||||
backtest.apply_params(params)
|
||||
uni, quotes, cmkt = _gather(set(pf.positions))
|
||||
# 직전 스캔이 저장한 시세 재사용 — 라이브 재호출 없이 보유종목 가격과 같은 시점으로 통일.
|
||||
# 스캔 이력이 아직 없으면(None) _gather 가 라이브 폴백.
|
||||
uni, quotes, cmkt = _gather(set(pf.positions), quotes=_load_last_quotes())
|
||||
return _run_scan(pf, uni, quotes, cmkt, now, execute=False)
|
||||
finally:
|
||||
backtest.apply_params(config.load_params()) # 전역 config 원복 (웹 데몬 잔류 방지)
|
||||
@@ -425,6 +451,7 @@ def scan(force: bool = False) -> dict:
|
||||
return {'skipped': True, 'reason': '장외/휴장', 'at': now.isoformat()}
|
||||
pf = Portfolio.load()
|
||||
uni, quotes, cmkt = _gather(set(pf.positions)) # 보유분은 universe에서 빠져도 청산 위해 포함
|
||||
_save_last_quotes(quotes)
|
||||
snap = _run_scan(pf, uni, quotes, cmkt, now)
|
||||
_attach_benchmark(snap, pf, now)
|
||||
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
@@ -448,6 +475,7 @@ def scan_all(force: bool = False) -> dict:
|
||||
for vpf in vpfs.values():
|
||||
held_union |= set(vpf.positions)
|
||||
uni, quotes, cmkt = _gather(held_union)
|
||||
_save_last_quotes(quotes)
|
||||
|
||||
# 기준 판단 (params.json) — 2026-06-10 기준전략 폐지: 매매 없이 판단만(last_scan = 화면·상태점용).
|
||||
# 실제 매매는 전부 변이들이 한다 (메인 계좌 동결).
|
||||
|
||||
@@ -1945,8 +1945,13 @@ def render_html(view: str | None = None, day_date: str | None = None,
|
||||
|
||||
# 상태 그룹 = 전체 전략(메인+변이) 합산 — 한 계좌라도 보유→보유 / 매수직전→매수 / 대기→대기.
|
||||
# 아무 계좌도 안 보면 기준전략(M) 판정. 카드 체크리스트·사유는 기준전략 시각 유지(합산 정보는 사유 앞에 표시).
|
||||
by_state = {}
|
||||
excluded = [] # 매매 안 하는 종목(ETF/ETN 등) → 하단 제외목록
|
||||
# 출처(origin) 맵 — auto(자동편입) 종목은 별도 섹션으로 분리
|
||||
_wl0 = umod._load_watchlist()
|
||||
origin_map = {c: e.get('origin', '') for c, e in _wl0.items()}
|
||||
|
||||
by_state = {} # 내 종목(워치·관심·보유·수동)
|
||||
by_state_auto = {} # 자동편입(auto) — 하단 별도
|
||||
excluded = [] # 매매 안 하는 종목(ETF/ETN 등) → 하단 제외목록
|
||||
for d in decisions:
|
||||
if umod.is_etf(d.get('name', '')):
|
||||
excluded.append(d)
|
||||
@@ -1969,18 +1974,30 @@ def render_html(view: str | None = None, day_date: str | None = None,
|
||||
elif watch_n.get(code):
|
||||
bits.append(f'{watch_n[code]}개 전략 매수 대기')
|
||||
d = {**d, 'reason': ' · '.join(bits) + f' — {view_label}: {d.get("reason", "")}'}
|
||||
by_state.setdefault(st, []).append(d)
|
||||
# 출처대로만 분류 — auto(자동편입)는 변이가 페이퍼로 보유 중이어도 자동편입 탭에 (보유는 그 안 '보유' 그룹에 표시)
|
||||
tgt = by_state_auto if origin_map.get(code) == 'auto' else by_state
|
||||
tgt.setdefault(st, []).append(d)
|
||||
|
||||
judge_html = ''
|
||||
for st in STATE_ORDER:
|
||||
items = by_state.get(st, [])
|
||||
if not items:
|
||||
continue
|
||||
label, color = STATE_BADGE.get(st, (st, '#888'))
|
||||
judge_html += f'<h3 class="grp"><span class="badge" style="background:{color}">{label}</span> {len(items)}</h3>'
|
||||
for d in items:
|
||||
judge_html += _decision_card(d, watch_n.get(d.get('code'), 0), held_n.get(d.get('code'), 0),
|
||||
_acc_detail(d.get('code')), buy_n.get(d.get('code'), 0))
|
||||
def _state_blocks(bucket):
|
||||
# 상태별 접이식 — 액션 상태(보유·추격·매수·매도·대기)는 펼침, 제외·데이터없음은 접힘(노이즈)
|
||||
_open = {'HOLD', 'ADD', 'BUY', 'SELL', 'WAIT'}
|
||||
out = ''
|
||||
for st in STATE_ORDER:
|
||||
items = bucket.get(st, [])
|
||||
if not items:
|
||||
continue
|
||||
label, color = STATE_BADGE.get(st, (st, '#888'))
|
||||
cards = ''.join(_decision_card(d, watch_n.get(d.get('code'), 0), held_n.get(d.get('code'), 0),
|
||||
_acc_detail(d.get('code')), buy_n.get(d.get('code'), 0))
|
||||
for d in items)
|
||||
op = ' open' if st in _open else ''
|
||||
out += (f'<details class="row state-grp"{op}><summary>'
|
||||
f'<span class="badge" style="background:{color}">{label}</span> {len(items)}</summary>'
|
||||
f'<div class="body">{cards}</div></details>')
|
||||
return out
|
||||
|
||||
mine_n = sum(len(v) for v in by_state.values())
|
||||
judge_html = _state_blocks(by_state)
|
||||
|
||||
# 관찰목록 중 아직 스캔 안 된(방금 추가 등) 종목 → '스캔 대기'로 즉시 표시
|
||||
wl = umod._load_watchlist()
|
||||
@@ -2000,7 +2017,12 @@ def render_html(view: str | None = None, day_date: str | None = None,
|
||||
f'<span class="price muted">다음 스캔 대기</span>{del_btn}</div></div>'
|
||||
)
|
||||
|
||||
# 매매 안 하는 종목(ETF/ETN 등) → 하단 접이식 제외목록
|
||||
# 자동편입(auto) 종목 → 상단 서브탭 '자동편입'으로 분리 (별도 HTML)
|
||||
auto_n = sum(len(v) for v in by_state_auto.values())
|
||||
judge_html_auto = (_state_blocks(by_state_auto) if auto_n else
|
||||
'<div class="empty">자동편입된 종목이 없어요 (매일 장 마감 후 키움 순위정보로 편입)</div>')
|
||||
|
||||
# 매매 안 하는 종목(ETF/ETN 등) → 하단 접이식 제외목록 (내 종목 쪽에 둠)
|
||||
if excluded:
|
||||
judge_html += (
|
||||
f'<details class="row" style="margin-top:16px"><summary>'
|
||||
@@ -2058,6 +2080,12 @@ h1{{font-size:16px;margin:0 0 2px}} .sub{{color:#9ca3af;font-size:11px;margin:0
|
||||
.kpi-sub{{color:#6b7280;font-size:10px;margin-top:2px}}
|
||||
h2{{font-size:15px;margin:22px 0 8px;border-bottom:1px solid #1f2937;padding-bottom:6px}}
|
||||
h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
|
||||
.state-grp{{margin:6px 0;border-top:none}}
|
||||
.state-grp>summary{{padding:8px 2px;font-size:13px;font-weight:600;list-style:none;display:flex;align-items:center;gap:6px}}
|
||||
.state-grp>summary::-webkit-details-marker{{display:none}}
|
||||
.state-grp>summary::before{{content:'▸';color:#6b7280;font-size:11px}}
|
||||
.state-grp[open]>summary::before{{content:'▾'}}
|
||||
.state-grp>.body{{padding:2px 0 4px}}
|
||||
.badge{{color:#fff;border-radius:6px;padding:1px 8px;font-size:12px;font-weight:600}}
|
||||
.row{{background:#141b24;border:1px solid #1f2937;border-radius:10px;margin-bottom:8px;overflow:hidden}}
|
||||
.row>summary{{list-style:none;cursor:pointer;padding:10px 12px;display:flex;flex-wrap:nowrap;align-items:center;gap:8px;font-size:13px;overflow:hidden}}
|
||||
@@ -2156,7 +2184,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
|
||||
#chk-toast .ct-t{{font-size:15px;font-weight:700;color:#fff;margin-bottom:8px}}
|
||||
#chk-toast .ct-d{{font-size:13px;line-height:1.6;color:#cbd5e1;white-space:pre-line}}
|
||||
#chk-toast .ct-x{{margin-top:14px;width:100%;background:#1e3a8a;color:#fff;border:1px solid #2952cc;border-radius:9px;padding:10px;font-size:13px;font-weight:600;cursor:pointer}}
|
||||
input[name="tab"],input[name="subtab"]{{display:none}}
|
||||
input[name="tab"],input[name="subtab"],input[name="wsub"]{{display:none}}
|
||||
.hd{{position:sticky;top:0;z-index:30;background:#0b0f14;padding:6px 0;border-bottom:1px solid #161e29}}
|
||||
#page.dragging .hd{{position:static}}
|
||||
.tabs{{display:flex;gap:6px;margin:0}}
|
||||
@@ -2169,6 +2197,9 @@ input[name="tab"],input[name="subtab"]{{display:none}}
|
||||
#sub-cmp:checked~#p-lab label[for="sub-cmp"],#sub-bt:checked~#p-lab label[for="sub-bt"]{{background:#15406e;color:#fff;border-color:#2952cc}}
|
||||
#p-lab .subpanel{{display:none}}
|
||||
#p-lab .subpanel h2{{margin-top:4px}}
|
||||
#p-watch .subpanel{{display:none}}
|
||||
#wsub-mine:checked~#p-watch #pw-mine,#wsub-auto:checked~#p-watch #pw-auto{{display:block}}
|
||||
#wsub-mine:checked~#p-watch label[for="wsub-mine"],#wsub-auto:checked~#p-watch label[for="wsub-auto"]{{background:#15406e;color:#fff;border-color:#2952cc}}
|
||||
.sortbar{{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}}
|
||||
.sortbar button{{background:#141b24;border:1px solid #1f2937;border-radius:7px;color:#9ca3af;font-size:12px;font-weight:600;padding:7px 10px;cursor:pointer;font-variant-numeric:tabular-nums}}
|
||||
.sortbar button.active{{background:#15406e;color:#fff;border-color:#2952cc}}
|
||||
@@ -2239,6 +2270,8 @@ input[name="tab"],input[name="subtab"]{{display:none}}
|
||||
<input type="radio" name="subtab" id="sub-cmp"{_ck('sub-cmp')}>
|
||||
<input type="radio" name="subtab" id="sub-bt"{_ck('sub-bt')}>
|
||||
<input type="radio" name="subtab" id="sub-day"{_ck('sub-day')}>
|
||||
<input type="radio" name="wsub" id="wsub-mine" checked>
|
||||
<input type="radio" name="wsub" id="wsub-auto">
|
||||
<header class="hd">
|
||||
<h1> 레이 자동매매 시뮬레이션 <span id="health" class="hdot {hcls}" data-detail="{_esc(hdetail)}" role="button" tabindex="0" title="시뮬 상태 보기"></span></h1>
|
||||
<div class=sub>마지막 스캔 {scanned or '—'} · 시장 {mkt_h}</div>
|
||||
@@ -2253,7 +2286,11 @@ input[name="tab"],input[name="subtab"]{{display:none}}
|
||||
<div class="sub">상태 그룹은 전체 전략(메인+변이) 합산 — 한 전략이라도 보유/매수대기면 그 그룹으로 · 배지: 🟢매수준비 · 🟡대기 · 🛒보유 전략 수 (탭하면 어떤 전략인지)</div>
|
||||
<div class="sub">종목명 일부만 입력해도 후보가 떠요 · 후보를 누르면 추가 · 자산(비하이브 워치·관심·보유)은 자동 편입 · 삭제는 카드의 ✕ (수동만) · 보유 중이면 삭제해도 청산까지 추적</div>
|
||||
{view_sel}
|
||||
{judge_html}
|
||||
<nav class="subtabs" id="wsubtabs">
|
||||
<label for="wsub-mine">📌 내 종목 ({mine_n})</label>
|
||||
<label for="wsub-auto">🤖 자동편입 ({auto_n})</label></nav>
|
||||
<div class="subpanel" id="pw-mine">{judge_html}</div>
|
||||
<div class="subpanel" id="pw-auto">{judge_html_auto}</div>
|
||||
</section>
|
||||
<section class="panel" id="p-market">
|
||||
<h2>시장 동향</h2>{market_html}
|
||||
|
||||
Reference in New Issue
Block a user