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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-30 02:00:02 +09:00
parent 17ad101eb1
commit 1bbb61520c
154 changed files with 2564 additions and 662 deletions
@@ -67,6 +67,95 @@ def _save_all(rows: list[dict]) -> None:
tmp.replace(JOURNAL)
def _advance(qty: int, avg: float, buy_qty: int, sell_qty: int, buy_avg: float) -> tuple[int, float]:
"""running 보유수량·평단 1스텝 진행.
매도는 **전 보유분 우선 소진**(평단 유지) → 잔여 당일 매수분을 가중평균 블렌딩.
이 순서가 '전일 보유 평단' 정의의 핵심 — 당일 매수가 매도 원가에 섞이지 않고,
매도로 원물이 다 빠지면 남는 건 당일 매수분 평단이 된다.
"""
consumed = min(sell_qty, qty)
qty -= consumed
rem_buy = buy_qty - (sell_qty - consumed) # 당일매수분 중 매도로 안 빠진 잔량
if rem_buy > 0:
avg = (qty * avg + rem_buy * buy_avg) / (qty + rem_buy)
qty += rem_buy
qty = max(qty, 0)
if qty == 0:
avg = 0.0 # 전량 청산 → 평단 리셋 (이후 순수 데이트레이딩을 보유분으로 오인 방지)
return qty, avg
def _apply_rebase(row: dict, sell_avg: float, sell_qty: int, pre_avg: float) -> None:
"""row의 pl_amt/prft_rt를 '전일 보유 평단(pre_avg)' 기준으로 덮어쓴다 (in-place).
그로스 = (매도평단 - 전일평단) × 매도수량, pl_amt = 그로스 - 수수료·세금 (ka10170과 동일 net).
"""
gross = (sell_avg - pre_avg) * sell_qty
fee = int(row.get('cmsn_tax') or 0)
row['pl_amt'] = round(gross) - fee
cost = pre_avg * sell_qty
row['prft_rt'] = round(row['pl_amt'] / cost * 100, 2) if cost else 0.0
row['_pl_rebased'] = True
row['prev_avg'] = round(pre_avg)
def rebase_pl(rows: list[dict]) -> list[dict]:
"""매도 실현손익을 '전일 보유 평단' 기준으로 재계산해 **새 리스트** 반환 (파일 미변경, 표시 전용).
ka10170은 당일 매수+매도가 같이 있으면 당일 매수평단↔매도평단 round-trip으로 pl_amt를 매겨
어제까지 보유한 원물의 손익을 가린다 (2026-06-29 0193W0: 6/26 보유 62@24,550 +
당일 62@21,875 매수 후 62@21,885 매도 → ka10170 +220원, 실제 원물 손익 약 -165,630원).
(account, code)별로 날짜순 재생(_advance)하며, **당일 매수가 동반된 매도 행**만
당일 매수 전 보유 평단을 원가로 잡아 덮어쓴다. 순수 매도(당일 매수 없음)는 키움 값이
정확하므로 건드리지 않는다.
⚠️ 입력 rows는 각 (account, code)의 **전체 이력**을 포함해야 재생이 정확하다.
"""
out = [dict(r) for r in rows]
by_key: dict[tuple, list[dict]] = {}
for r in out:
by_key.setdefault((r.get('account'), r.get('code')), []).append(r)
for grp in by_key.values():
grp.sort(key=lambda r: (r.get('date', ''), r.get('collected_at', '')))
qty, avg = 0, 0.0
for r in grp:
bq = int(r.get('buy_qty') or 0)
sq = int(r.get('sell_qty') or 0)
ba = float(r.get('buy_avg') or 0)
sa = float(r.get('sell_avg') or 0)
pre_avg = avg # 당일 매수 반영 전 = 전일 보유 평단
if sq > 0 and bq > 0 and pre_avg > 0:
_apply_rebase(r, sa, sq, pre_avg)
qty, avg = _advance(qty, avg, bq, sq, ba)
return out
def prev_avg_map(before_iso: str) -> dict:
"""파일 이력 중 before_iso **미만**(전일까지)을 (account, code)별 재생해 시작 평단 맵 반환.
라이브 journal(당일분)의 round-trip 오염 행을 재계산할 때 원가(전일 보유 평단) 소스.
{(account, code): avg}. 보유 0이면 0.
"""
rows = [r for r in _load_all() if r.get('date', '') < before_iso]
by_key: dict[tuple, list[dict]] = {}
for r in rows:
by_key.setdefault((r.get('account'), r.get('code')), []).append(r)
out: dict[tuple, float] = {}
for key, grp in by_key.items():
grp.sort(key=lambda r: (r.get('date', ''), r.get('collected_at', '')))
qty, avg = 0, 0.0
for r in grp:
qty, avg = _advance(
qty, avg,
int(r.get('buy_qty') or 0), int(r.get('sell_qty') or 0),
float(r.get('buy_avg') or 0),
)
out[key] = avg
return out
def record_trades(all_trades: dict, *, base_dt: str | None = None, quiet: bool = False, tag: str = 'collect') -> dict:
"""이미 받은 ka10170 응답({label: [trades]})을 jsonl에 적재 (idempotent).
`(iso_date, account in all_trades)` 단위 재적재 — 다른 계좌는 영향 X.