auto: 일일 백업 2026-06-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,24 @@ def load_flow(code: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
# 프로세스 메모이즈 — 같은 sweep(한 프로세스) 안에서 종목당 sqlite를 1회만 읽는다.
|
||||
# backtest.run 은 sweep CLI 프로세스에서만 호출되므로(엔진/웹은 apply_params만 씀) stale 위험 없음.
|
||||
_HIST_MEMO: dict = {}
|
||||
_FLOW_MEMO: dict = {}
|
||||
|
||||
|
||||
def _hist_cached(code: str) -> list[dict]:
|
||||
if code not in _HIST_MEMO:
|
||||
_HIST_MEMO[code] = load_history(code)
|
||||
return _HIST_MEMO[code]
|
||||
|
||||
|
||||
def _flow_cached(code: str):
|
||||
if code not in _FLOW_MEMO:
|
||||
_FLOW_MEMO[code] = load_flow(code)
|
||||
return _FLOW_MEMO[code]
|
||||
|
||||
|
||||
def _flow_net_upto(flow: dict | None, dates_seen: list[str]) -> dict:
|
||||
"""최근 FLOW_DAYS 일 외국인·기관 누적. flow 없으면 중립(둘 다 +1 → 수급 통과)."""
|
||||
if flow is None:
|
||||
@@ -82,12 +100,12 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
|
||||
|
||||
hist = {}
|
||||
for code in codes:
|
||||
h = [c for c in load_history(code) if date_from <= c['date'] <= date_to]
|
||||
h = [c for c in _hist_cached(code) if date_from <= c['date'] <= date_to]
|
||||
if len(h) > config.SMA_LONG + config.ATR_PERIOD + 5:
|
||||
hist[code] = h
|
||||
if not hist:
|
||||
return {'error': 'no_data', 'trades': 0}
|
||||
flows = {code: load_flow(code) for code in hist}
|
||||
flows = {code: _flow_cached(code) for code in hist}
|
||||
|
||||
# code별 date→index, 전체 거래일 축
|
||||
idx_map = {code: {c['date']: i for i, c in enumerate(h)} for code, h in hist.items()}
|
||||
@@ -99,6 +117,7 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
|
||||
wins = closed = 0
|
||||
gross_win = gross_loss = 0.0
|
||||
peak_eq = capital
|
||||
exited: dict[str, str] = {} # code → 전량청산일 (당일 재진입 금지)
|
||||
max_dd = 0.0
|
||||
comm, tax = config.COMMISSION_RATE, config.SELL_TAX_RATE
|
||||
|
||||
@@ -108,9 +127,82 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
|
||||
def _qty_for(fill, value):
|
||||
return int(value // (fill * (1 + comm)))
|
||||
|
||||
slip = getattr(config, 'BT_SLIPPAGE', 0.002)
|
||||
pending: list[dict] = [] # 전일 종가 신호 → 오늘 시가 체결 주문 (look-ahead 제거)
|
||||
|
||||
for day in all_dates:
|
||||
day_prices = {}
|
||||
# ---- 보유 판단 (당일 종가 기준): 손절/익절(전량·분할) → 추격매수 ----
|
||||
|
||||
# ---- 0. 전일 큐잉 주문을 오늘 시가에 체결 (+슬리피지: 매수 비싸게/매도 싸게) ----
|
||||
for od in pending:
|
||||
code = od['code']
|
||||
i = idx_map.get(code, {}).get(day)
|
||||
if i is None:
|
||||
continue # 오늘 거래 없으면 주문 소멸 — 신호 지속 시 다음 종가에 재큐잉됨
|
||||
open_px = hist[code][i].get('open') or hist[code][i]['close']
|
||||
kind = od['kind']
|
||||
if kind in ('sell', 'scale_out'):
|
||||
pos = positions.get(code)
|
||||
if not pos:
|
||||
continue
|
||||
fill = open_px * (1 - slip)
|
||||
total = pos['qty']
|
||||
qty = total if od.get('frac', 1.0) >= 1.0 else max(1, int(total * od['frac']))
|
||||
qty = min(qty, total)
|
||||
proceeds = qty * fill * (1 - comm - tax)
|
||||
cost = qty * pos['entry_price'] * (1 + comm)
|
||||
pnl = proceeds - cost
|
||||
cash += proceeds
|
||||
realized += pnl
|
||||
if qty >= total:
|
||||
positions.pop(code)
|
||||
exited[code] = day # 당일 재진입 금지 (엔진과 동일 규칙)
|
||||
closed += 1
|
||||
if pnl > 0:
|
||||
wins += 1
|
||||
else:
|
||||
pos['qty'] = total - qty
|
||||
if pnl > 0:
|
||||
gross_win += pnl
|
||||
else:
|
||||
gross_loss += -pnl
|
||||
elif kind == 'add':
|
||||
pos = positions.get(code)
|
||||
if not pos:
|
||||
continue
|
||||
fill = open_px * (1 + slip)
|
||||
tranche_val = pos.get('tranche_value') or (pos['entry_price'] * pos['qty'])
|
||||
qty = _qty_for(fill, tranche_val)
|
||||
cost = qty * fill * (1 + comm)
|
||||
if qty >= 1 and cost <= cash:
|
||||
new_qty = pos['qty'] + qty
|
||||
pos['entry_price'] = (pos['entry_price'] * pos['qty'] + fill * qty) / new_qty
|
||||
pos['qty'] = new_qty
|
||||
pos['tranches'] = pos.get('tranches', 1) + 1
|
||||
pos['last_add_price'] = fill
|
||||
cash -= cost
|
||||
nstop, ntarget = signals.compute_stop_target(pos['entry_price'], od['atr'], od['recent_low'])
|
||||
pos['stop'] = max(pos['stop'], nstop)
|
||||
pos['target'] = ntarget
|
||||
elif kind == 'buy':
|
||||
if code in positions or len(positions) >= config.MAX_POSITIONS or exited.get(code) == day:
|
||||
continue
|
||||
fill = open_px * (1 + slip)
|
||||
stop, target = signals.compute_stop_target(fill, od['atr'], od['recent_low'])
|
||||
eq = equity(day_prices)
|
||||
tranche_val = signals.target_value(eq, fill, stop) / max(1, config.ENTRY_TRANCHES)
|
||||
qty = _qty_for(fill, tranche_val)
|
||||
cost = qty * fill * (1 + comm)
|
||||
if qty < 1 or cost > cash:
|
||||
continue
|
||||
cash -= cost
|
||||
positions[code] = {'code': code, 'name': code, 'qty': qty, 'entry_price': fill,
|
||||
'stop': stop, 'target': target, 'peak': fill, 'trailing_on': False,
|
||||
'scaled_out': False, 'tranches': 1, 'tranche_value': tranche_val,
|
||||
'last_add_price': fill, 'entry_date': day}
|
||||
pending = []
|
||||
|
||||
# ---- 1. 보유 판단 (당일 종가 기준) → 내일 시가 주문 큐잉 ----
|
||||
for code in list(positions.keys()):
|
||||
h = hist.get(code)
|
||||
i = idx_map[code].get(day)
|
||||
@@ -129,46 +221,17 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
|
||||
pos.update(dec['position_update'])
|
||||
act = dec['action']
|
||||
if act in ('sell', 'scale_out'):
|
||||
fill = ind['price']
|
||||
total = pos['qty']
|
||||
qty = total if dec.get('sell_frac', 1.0) >= 1.0 else max(1, int(total * dec['sell_frac']))
|
||||
qty = min(qty, total)
|
||||
proceeds = qty * fill * (1 - comm - tax)
|
||||
cost = qty * pos['entry_price'] * (1 + comm)
|
||||
pnl = proceeds - cost
|
||||
cash += proceeds
|
||||
realized += pnl
|
||||
if qty >= total:
|
||||
positions.pop(code)
|
||||
closed += 1
|
||||
if pnl > 0:
|
||||
wins += 1
|
||||
else:
|
||||
pos['qty'] = total - qty
|
||||
if pnl > 0:
|
||||
gross_win += pnl
|
||||
else:
|
||||
gross_loss += -pnl
|
||||
pending.append({'kind': act, 'code': code, 'frac': dec.get('sell_frac', 1.0)})
|
||||
elif act == 'add':
|
||||
fill = ind['price']
|
||||
tranche_val = pos.get('tranche_value') or (pos['entry_price'] * pos['qty'])
|
||||
qty = _qty_for(fill, tranche_val)
|
||||
cost = qty * fill * (1 + comm)
|
||||
if qty >= 1 and cost <= cash:
|
||||
new_qty = pos['qty'] + qty
|
||||
pos['entry_price'] = (pos['entry_price'] * pos['qty'] + fill * qty) / new_qty
|
||||
pos['qty'] = new_qty
|
||||
pos['tranches'] = pos.get('tranches', 1) + 1
|
||||
pos['last_add_price'] = fill
|
||||
cash -= cost
|
||||
nstop, ntarget = signals.compute_stop_target(pos['entry_price'], ind['atr'], ind['recent_low'])
|
||||
pos['stop'] = max(pos['stop'], nstop)
|
||||
pos['target'] = ntarget
|
||||
pending.append({'kind': 'add', 'code': code,
|
||||
'atr': ind['atr'], 'recent_low': ind['recent_low']})
|
||||
|
||||
# ---- 신규 매수 판단 (1차 트랜치) ----
|
||||
# ---- 2. 신규 매수 판단 (당일 종가) → 내일 시가 주문 큐잉 ----
|
||||
for code, h in hist.items():
|
||||
if code in positions or len(positions) >= config.MAX_POSITIONS:
|
||||
continue
|
||||
if exited.get(code) == day: # 당일 청산 종목 재매수 금지
|
||||
continue
|
||||
i = idx_map[code].get(day)
|
||||
if i is None:
|
||||
continue
|
||||
@@ -183,19 +246,8 @@ def run(overrides: dict | None, codes: list[str], date_from: str = '', date_to:
|
||||
flow = _flow_net_upto(flows.get(code), dates_seen)
|
||||
dec = signals.evaluate_candidate(code, code, [], ind, flow, None, True)
|
||||
if dec['action'] == 'buy':
|
||||
fill = dec['buy_price']
|
||||
stop, target = signals.compute_stop_target(fill, ind['atr'], ind['recent_low'])
|
||||
eq = equity(day_prices)
|
||||
tranche_val = signals.target_value(eq, fill, stop) / max(1, config.ENTRY_TRANCHES)
|
||||
qty = _qty_for(fill, tranche_val)
|
||||
cost = qty * fill * (1 + comm)
|
||||
if qty < 1 or cost > cash:
|
||||
continue
|
||||
cash -= cost
|
||||
positions[code] = {'code': code, 'name': code, 'qty': qty, 'entry_price': fill,
|
||||
'stop': stop, 'target': target, 'peak': fill, 'trailing_on': False,
|
||||
'scaled_out': False, 'tranches': 1, 'tranche_value': tranche_val,
|
||||
'last_add_price': fill, 'entry_date': day}
|
||||
pending.append({'kind': 'buy', 'code': code,
|
||||
'atr': ind['atr'], 'recent_low': ind['recent_low']})
|
||||
|
||||
eq = equity(day_prices)
|
||||
if eq > peak_eq:
|
||||
|
||||
Reference in New Issue
Block a user