auto: 일일 백업 2026-06-18 02:00

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-18 02:00:02 +09:00
parent b3696949f5
commit 70e2251995
444 changed files with 9612 additions and 997 deletions
+31 -3
View File
@@ -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 = 화면·상태점용).
# 실제 매매는 전부 변이들이 한다 (메인 계좌 동결).
+53 -16
View File
@@ -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}