552 lines
20 KiB
Python
552 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
measure_ob_exit_early_fire.py
|
|
=============================
|
|
초등 설명
|
|
이 숫자는 \"수익이 몇 %\"가 아닙니다.
|
|
\"매수 100건 중, 진입 후 N봉 안에 호가컷 신호가 몇 건이나 뜨는가\" 비율입니다.
|
|
|
|
줄어든다고 한 것 = 수익이 줄어든다는 말이 아님.
|
|
→ \"진입 직후 호가 때문에 잘릴 뻔한 건수(가짜 청산 위험)\" 가 줄어든다는 뜻.
|
|
|
|
비교 3종
|
|
A) L1 + 순간 OR : 호가를 맨 위, 가드 없음 (위험 설계)
|
|
B) L1 + 가드 : 호가를 맨 위지만 이익/최소보유/OR_MA 가드
|
|
C) L3 + 가드 : 래칫·어깨가 그 시각까지 안 잘랐을 때만 호가컷
|
|
(제안: 래칫→어깨→호가→손절)
|
|
|
|
사용
|
|
cd ~/kis_bot
|
|
.venv/bin/python3 scripts/measure_ob_exit_early_fire.py
|
|
.venv/bin/python3 scripts/measure_ob_exit_early_fire.py --date 2026-07-31
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def _parse_dt(v: Any) -> datetime:
|
|
if isinstance(v, datetime):
|
|
return v
|
|
return datetime.strptime(str(v).strip()[:19], "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
def _snap_to_dt(snap_time: str) -> Optional[datetime]:
|
|
s = str(snap_time or "").strip()
|
|
if len(s) < 14:
|
|
return None
|
|
try:
|
|
return datetime.strptime(s[:14], "%Y%m%d%H%M%S")
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _candle_to_dt(candle_time: str) -> Optional[datetime]:
|
|
s = str(candle_time or "").strip()
|
|
if len(s) < 12:
|
|
return None
|
|
try:
|
|
return datetime.strptime(s[:12], "%Y%m%d%H%M")
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _or_ratio(bid: float, ask: float) -> Optional[float]:
|
|
if ask <= 0:
|
|
return None
|
|
return float(bid) / float(ask)
|
|
|
|
|
|
def _mid_px(best_bid: float, best_ask: float) -> Optional[float]:
|
|
if best_bid > 0 and best_ask > 0:
|
|
return (best_bid + best_ask) / 2.0
|
|
if best_ask > 0:
|
|
return float(best_ask)
|
|
if best_bid > 0:
|
|
return float(best_bid)
|
|
return None
|
|
|
|
|
|
def _parse_ratchet_tiers(raw: str) -> List[Tuple[float, float]]:
|
|
"""'10:2.6,13:2.2' → [(0.10, 0.026), ...] (퍼센트 문자열 → 비율)"""
|
|
tiers: List[Tuple[float, float]] = []
|
|
for chunk in str(raw or "").split(","):
|
|
chunk = chunk.strip()
|
|
if not chunk or ":" not in chunk:
|
|
continue
|
|
g, c = chunk.split(":", 1)
|
|
try:
|
|
gain = abs(float(g)) / 100.0
|
|
cut = abs(float(c)) / 100.0
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if gain > 0 and cut > 0:
|
|
tiers.append((gain, cut))
|
|
tiers.sort(key=lambda x: x[0])
|
|
return tiers
|
|
|
|
|
|
def _rolling_or_ma(history: Sequence[Optional[float]], window: int) -> Optional[float]:
|
|
vals = [x for x in history if x is not None]
|
|
if len(vals) < window:
|
|
return None
|
|
chunk = vals[-window:]
|
|
return sum(chunk) / float(window)
|
|
|
|
|
|
@dataclass
|
|
class Snap:
|
|
t: datetime
|
|
mid: Optional[float]
|
|
or_ratio: Optional[float]
|
|
best_bid: float = 0.0
|
|
best_ask: float = 0.0
|
|
|
|
|
|
@dataclass
|
|
class Bar:
|
|
t: datetime
|
|
high: float
|
|
low: float
|
|
close: float
|
|
|
|
|
|
@dataclass
|
|
class RowOut:
|
|
code: str
|
|
buy_dt: datetime
|
|
buy_price: float
|
|
n_snaps: int
|
|
a_l1_naive: bool
|
|
b_l1_guard: bool
|
|
c_l3_guard: bool
|
|
price_exit_before_ob: str # '', ratchet, shoulder
|
|
name: str = ""
|
|
sell_price: float = 0.0
|
|
qty: int = 0
|
|
actual_pnl: float = 0.0
|
|
actual_profit_rate: float = 0.0
|
|
actual_sell_reason: str = ""
|
|
c_exit_dt: Optional[datetime] = None
|
|
c_exit_px: float = 0.0
|
|
entry_ob_pass: bool = True
|
|
entry_reject_reason: str = ""
|
|
|
|
|
|
def _load_buys(db, strategy: str, day: str) -> List[Dict[str, Any]]:
|
|
return list(
|
|
db.conn.execute(
|
|
"""
|
|
SELECT id, code, name, buy_date, buy_price, sell_price, qty, profit_rate, realized_pnl, sell_date, sell_reason
|
|
FROM trade_history
|
|
WHERE strategy=%s AND DATE(buy_date)=%s
|
|
ORDER BY buy_date, id
|
|
""",
|
|
(strategy, day),
|
|
).fetchall()
|
|
)
|
|
|
|
|
|
def _load_snaps(db, table: str, code: str, t0: datetime, t1: datetime) -> List[Snap]:
|
|
rows = db.conn.execute(
|
|
f"""
|
|
SELECT snap_time, total_bid_qty, total_ask_qty, best_bid, best_ask
|
|
FROM {table}
|
|
WHERE code=%s AND snap_time >= %s AND snap_time < %s
|
|
ORDER BY snap_time ASC, id ASC
|
|
""",
|
|
(code, t0.strftime("%Y%m%d%H%M%S"), t1.strftime("%Y%m%d%H%M%S")),
|
|
).fetchall()
|
|
out: List[Snap] = []
|
|
for r in rows:
|
|
dt = _snap_to_dt(r["snap_time"])
|
|
if not dt:
|
|
continue
|
|
bid = float(r["total_bid_qty"] or 0)
|
|
ask = float(r["total_ask_qty"] or 0)
|
|
out.append(
|
|
Snap(
|
|
t=dt,
|
|
mid=_mid_px(float(r["best_bid"] or 0), float(r["best_ask"] or 0)),
|
|
or_ratio=_or_ratio(bid, ask),
|
|
best_bid=float(r["best_bid"] or 0),
|
|
best_ask=float(r["best_ask"] or 0),
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
def _load_bars(db, code: str, t0: datetime, t1: datetime) -> List[Bar]:
|
|
"""1분봉. candle_time=YYYYMMDDHHMM"""
|
|
# LIKE 는 pymysql % 충돌 → %s 바인딩
|
|
prefix = t0.strftime("%Y%m%d")
|
|
rows = db.conn.execute(
|
|
"""
|
|
SELECT candle_time, high, low, close
|
|
FROM ws_candles
|
|
WHERE code=%s AND timeframe=1 AND candle_time LIKE %s
|
|
ORDER BY candle_time ASC
|
|
""",
|
|
(code, prefix + "%"),
|
|
).fetchall()
|
|
out: List[Bar] = []
|
|
for r in rows:
|
|
dt = _candle_to_dt(r["candle_time"])
|
|
if dt is None or dt < t0.replace(second=0, microsecond=0) or dt >= t1:
|
|
continue
|
|
out.append(
|
|
Bar(
|
|
t=dt,
|
|
high=float(r["high"] or 0),
|
|
low=float(r["low"] or 0),
|
|
close=float(r["close"] or 0),
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
def _price_exit_until(
|
|
bars: List[Bar],
|
|
buy_dt: datetime,
|
|
entry: float,
|
|
until: datetime,
|
|
ratchet_tiers: List[Tuple[float, float]],
|
|
shoulder_min_high: float,
|
|
shoulder_cut: float,
|
|
) -> str:
|
|
"""until 시각까지 래칫/어깨가 먼저 걸리면 'ratchet'|'shoulder', 아니면 ''."""
|
|
max_px = entry
|
|
for b in bars:
|
|
if b.t > until:
|
|
break
|
|
if b.t < buy_dt.replace(second=0, microsecond=0):
|
|
continue
|
|
if b.high > max_px:
|
|
max_px = b.high
|
|
# 래칫
|
|
if ratchet_tiers and entry > 0:
|
|
peak_gain = (max_px - entry) / entry
|
|
cut_ratio = 0.0
|
|
for gain, cut in ratchet_tiers:
|
|
if peak_gain >= gain:
|
|
cut_ratio = cut
|
|
if cut_ratio > 0 and b.low <= max_px * (1.0 - cut_ratio):
|
|
return "ratchet"
|
|
# 어깨
|
|
if entry > 0 and max_px >= entry * (1.0 + shoulder_min_high) and shoulder_cut > 0:
|
|
if b.low <= max_px * (1.0 - shoulder_cut):
|
|
return "shoulder"
|
|
return ""
|
|
|
|
|
|
def evaluate_one(
|
|
snaps: List[Snap],
|
|
bars: List[Bar],
|
|
buy_dt: datetime,
|
|
buy_price: float,
|
|
*,
|
|
n_bars: int,
|
|
bar_minutes: int,
|
|
min_hold_bars: int,
|
|
ob_ratio_min: float,
|
|
min_profit_pct: float,
|
|
ma_window: int,
|
|
ratchet_tiers: List[Tuple[float, float]],
|
|
shoulder_min_high: float,
|
|
shoulder_cut: float,
|
|
) -> Tuple[bool, bool, bool, str, int, Optional[datetime], float, bool, str]:
|
|
window_end = buy_dt + timedelta(minutes=n_bars * bar_minutes)
|
|
min_hold_end = buy_dt + timedelta(minutes=min_hold_bars * bar_minutes)
|
|
profit_line = buy_price * (1.0 + min_profit_pct)
|
|
|
|
in_window = [s for s in snaps if buy_dt <= s.t < window_end]
|
|
n_snaps = len(in_window)
|
|
if n_snaps <= 0:
|
|
return False, False, False, "", 0, None, 0.0, True, ""
|
|
|
|
# 진입 호가필터 (스프레드 상한 0.45%, 매수/매도 잔량비 하한 0.85 기본 적용)
|
|
entry_ob_pass = True
|
|
entry_reject_reason = ""
|
|
close_snaps = [s for s in snaps if abs((s.t - buy_dt).total_seconds()) <= 60.0]
|
|
if close_snaps:
|
|
s0 = min(close_snaps, key=lambda s: abs((s.t - buy_dt).total_seconds()))
|
|
if s0.mid and s0.mid > 0 and s0.best_ask > s0.best_bid > 0:
|
|
spread_pct = (s0.best_ask - s0.best_bid) / s0.mid * 100.0
|
|
if spread_pct > 0.45:
|
|
entry_ob_pass = False
|
|
entry_reject_reason = f"스프레드({spread_pct:.2f}%)"
|
|
if entry_ob_pass and s0.or_ratio is not None and s0.or_ratio < 0.85:
|
|
entry_ob_pass = False
|
|
entry_reject_reason = f"잔량비({s0.or_ratio:.2f}<0.85)"
|
|
|
|
hist_or: List[Optional[float]] = [s.or_ratio for s in snaps if s.t < buy_dt]
|
|
|
|
a_fire = b_fire = c_fire = False
|
|
price_tag = ""
|
|
c_exit_dt = None
|
|
c_exit_px = 0.0
|
|
|
|
for s in in_window:
|
|
hist_or.append(s.or_ratio)
|
|
if s.or_ratio is None:
|
|
continue
|
|
|
|
# A) L1 순간 OR — 가드 없음
|
|
if (not a_fire) and s.or_ratio < ob_ratio_min:
|
|
a_fire = True
|
|
|
|
# 가드 공통
|
|
guard_ok = (
|
|
s.t >= min_hold_end
|
|
and s.mid is not None
|
|
and s.mid >= profit_line
|
|
)
|
|
or_ma = _rolling_or_ma(hist_or, ma_window)
|
|
guard_ob = guard_ok and or_ma is not None and or_ma < ob_ratio_min
|
|
|
|
# B) L1 + 가드 (호가 맨 위라 가격레이어 무시)
|
|
if (not b_fire) and guard_ob:
|
|
b_fire = True
|
|
|
|
# C) L3 + 가드: 그 시각까지 래칫/어깨 미발동일 때만
|
|
if (not c_fire) and guard_ob:
|
|
pe = _price_exit_until(
|
|
bars,
|
|
buy_dt,
|
|
buy_price,
|
|
s.t,
|
|
ratchet_tiers,
|
|
shoulder_min_high,
|
|
shoulder_cut,
|
|
)
|
|
if pe:
|
|
if not price_tag:
|
|
price_tag = pe
|
|
else:
|
|
c_fire = True
|
|
c_exit_dt = s.t
|
|
c_exit_px = s.best_bid if s.best_bid > 0 else (s.mid or 0.0)
|
|
|
|
return a_fire, b_fire, c_fire, price_tag, n_snaps, c_exit_dt, c_exit_px, entry_ob_pass, entry_reject_reason
|
|
|
|
|
|
def _pct(n: int, d: int) -> str:
|
|
if d <= 0:
|
|
return "n/a"
|
|
return f"{100.0 * n / d:.1f}%"
|
|
|
|
|
|
def _load_momentum_exit_params(db) -> Tuple[List[Tuple[float, float]], float, float, str]:
|
|
"""config_momentum 실매값. 어깨는 DB가 비율(0.05)로 들어 있는 현황 반영."""
|
|
row = db.conn.execute(
|
|
"SELECT MOMENTUM_RATCHET_TIERS, MOMENTUM_SHOULDER_MIN_HIGH_PCT, MOMENTUM_SHOULDER_CUT_PCT "
|
|
"FROM config_momentum ORDER BY id DESC LIMIT 1"
|
|
).fetchone()
|
|
d = dict(row) if row else {}
|
|
tiers_s = str(d.get("MOMENTUM_RATCHET_TIERS") or "10:2.6,13:2.2")
|
|
smh = float(d.get("MOMENTUM_SHOULDER_MIN_HIGH_PCT") or 0.05)
|
|
sc = float(d.get("MOMENTUM_SHOULDER_CUT_PCT") or 0.0055)
|
|
# 실수: 5 같이 들어오면 퍼센트로 보고 /100
|
|
if smh > 1.0:
|
|
smh = smh / 100.0
|
|
if sc > 1.0:
|
|
sc = sc / 100.0
|
|
return _parse_ratchet_tiers(tiers_s), smh, sc, tiers_s
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--date", default="2026-07-27")
|
|
ap.add_argument("--strategy", default="MOMENTUM")
|
|
ap.add_argument("--n-bars", type=int, default=5)
|
|
ap.add_argument("--bar-minutes", type=int, default=1)
|
|
ap.add_argument("--min-hold-bars", type=int, default=3)
|
|
ap.add_argument("--ob-ratio-min", type=float, default=0.4)
|
|
ap.add_argument("--min-profit-pct", type=float, default=0.005)
|
|
ap.add_argument("--ma-window", type=int, default=5)
|
|
ap.add_argument("--ob-table", default="ws_orderbook", choices=("ws_orderbook", "ls_ws_orderbook"))
|
|
ap.add_argument("--verbose", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
from database import TradeDB
|
|
|
|
db = TradeDB()
|
|
cols = [r["Field"] for r in db.conn.execute(f"SHOW COLUMNS FROM {args.ob_table}").fetchall()]
|
|
need = {"code", "snap_time", "total_bid_qty", "total_ask_qty", "best_bid", "best_ask"}
|
|
if need - set(cols):
|
|
print("컬럼 부족", need - set(cols))
|
|
return 1
|
|
|
|
tiers, smh, sc, tiers_s = _load_momentum_exit_params(db)
|
|
buys = _load_buys(db, args.strategy, args.date)
|
|
if not buys:
|
|
print("매수 0건")
|
|
return 0
|
|
|
|
lookback = timedelta(minutes=max(args.ma_window, 5) * args.bar_minutes)
|
|
horizon = timedelta(minutes=args.n_bars * args.bar_minutes)
|
|
|
|
rows: List[RowOut] = []
|
|
for b in buys:
|
|
buy_dt = _parse_dt(b["buy_date"])
|
|
buy_px = float(b["buy_price"] or 0)
|
|
code = str(b["code"])
|
|
snaps = _load_snaps(db, args.ob_table, code, buy_dt - lookback, buy_dt + horizon)
|
|
bars = _load_bars(db, code, buy_dt, buy_dt + horizon + timedelta(minutes=1))
|
|
a, bb, c, ptag, n_snaps, c_dt, c_px, ep_pass, ep_reason = evaluate_one(
|
|
snaps,
|
|
bars,
|
|
buy_dt,
|
|
buy_px,
|
|
n_bars=args.n_bars,
|
|
bar_minutes=args.bar_minutes,
|
|
min_hold_bars=args.min_hold_bars,
|
|
ob_ratio_min=args.ob_ratio_min,
|
|
min_profit_pct=args.min_profit_pct,
|
|
ma_window=args.ma_window,
|
|
ratchet_tiers=tiers,
|
|
shoulder_min_high=smh,
|
|
shoulder_cut=sc,
|
|
)
|
|
rows.append(
|
|
RowOut(
|
|
code=code,
|
|
buy_dt=buy_dt,
|
|
buy_price=buy_px,
|
|
n_snaps=n_snaps,
|
|
a_l1_naive=a,
|
|
b_l1_guard=bb,
|
|
c_l3_guard=c,
|
|
price_exit_before_ob=ptag,
|
|
name=str(b.get("name") or ""),
|
|
sell_price=float(b.get("sell_price") or 0),
|
|
qty=int(b.get("qty") or 0),
|
|
actual_pnl=float(b.get("realized_pnl") or 0),
|
|
actual_profit_rate=float(b.get("profit_rate") or 0),
|
|
actual_sell_reason=str(b.get("sell_reason") or ""),
|
|
c_exit_dt=c_dt,
|
|
c_exit_px=c_px,
|
|
entry_ob_pass=ep_pass,
|
|
entry_reject_reason=ep_reason,
|
|
)
|
|
)
|
|
|
|
meas = [r for r in rows if r.n_snaps > 0]
|
|
na = sum(1 for r in meas if r.a_l1_naive)
|
|
nb = sum(1 for r in meas if r.b_l1_guard)
|
|
nc = sum(1 for r in meas if r.c_l3_guard)
|
|
n_price = sum(1 for r in meas if r.price_exit_before_ob)
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print("이 퍼센트는 수익이 아닙니다")
|
|
print(" = (진입 후 N봉 안에 '호가컷 신호'가 뜬 매수 건수) / (호가 있는 매수 건수)")
|
|
print(" '확 줄어든다' = 그 가짜·조기 호가청산 신호가 줄어든다 (수익%% 아님)")
|
|
print("=" * 70)
|
|
print(f"날짜 {args.date} 전략 {args.strategy} 관찰=진입후 {args.n_bars}분")
|
|
print(f"호가테이블 {args.ob_table}")
|
|
print(
|
|
f"가드: 최소보유 {args.min_hold_bars}분 + 이익≥{args.min_profit_pct*100:.2f}% "
|
|
f"+ OR이동평균{args.ma_window}개 < {args.ob_ratio_min}"
|
|
)
|
|
print(f"L3용 가격청산(실매 config): ratchet={tiers_s!r} shoulder_min={smh} cut={sc}")
|
|
print("-" * 70)
|
|
print(f"매수 {len(rows)}건 중 호가데이터 있는 모수 {len(meas)}건")
|
|
print()
|
|
print(f" A) 호가 1순위 + 가드없음(순간OR) : {na}/{len(meas)} = {_pct(na, len(meas))}")
|
|
print(f" → 맨 위에 올리면, 5봉 안에 호가신호로 잘릴 뻔한 비율")
|
|
print()
|
|
print(f" B) 호가 1순위 + 가드있음 : {nb}/{len(meas)} = {_pct(nb, len(meas))}")
|
|
print(f" → 그래도 1순위. 가드만 켠 것 (순서 변경 아님)")
|
|
print()
|
|
print(f" C) 호가 3순위 + 가드있음 : {nc}/{len(meas)} = {_pct(nc, len(meas))}")
|
|
print(f" → 래칫·어깨가 먼저 안 잘랐을 때만 호가컷 카운트")
|
|
if n_price:
|
|
print(f" (같은 창에서 래칫/어깨가 먼저 보인 건수: {n_price})")
|
|
print("-" * 70)
|
|
print("한줄 해석")
|
|
print(f" A→B : 가드 효과로 조기호가신호 {_pct(na, len(meas))} → {_pct(nb, len(meas))}")
|
|
print(f" B→C : 3순위로 내리면 {_pct(nb, len(meas))} → {_pct(nc, len(meas))}")
|
|
if nb == nc:
|
|
print(" ※ B≈C 이면: 이 N봉 안에 래칫/어깨가 거의 안 걸림 → 순서보다 가드가 핵심")
|
|
print("=" * 70)
|
|
|
|
print("\n건별 [A순간 / B가드1순위 / C가드3순위 / 진입필터 & 사후손익]")
|
|
for r in rows:
|
|
if r.n_snaps <= 0:
|
|
print(f" {r.buy_dt.strftime('%H:%M:%S')} {r.code} snaps=0 (호가없음) | 실현손익 {r.actual_pnl:,.0f}원 ({r.actual_profit_rate:+.2f}%)")
|
|
continue
|
|
ef_str = "OK" if r.entry_ob_pass else f"탈락:{r.entry_reject_reason}"
|
|
ob_exit_str = f" → 호가컷({r.c_exit_px:,.0f}원, {r.c_exit_dt.strftime('%H:%M:%S')})" if (r.c_l3_guard and r.c_exit_dt) else ""
|
|
print(
|
|
f" {r.buy_dt.strftime('%H:%M:%S')} {r.code} ({r.name[:6]:<6}) snaps={r.n_snaps:3d} "
|
|
f"A={'Y' if r.a_l1_naive else '.'} "
|
|
f"B={'Y' if r.b_l1_guard else '.'} "
|
|
f"C={'Y' if r.c_l3_guard else '.'}"
|
|
+ (f" (가격먼저:{r.price_exit_before_ob})" if r.price_exit_before_ob else "")
|
|
+ f" | [진입] {ef_str:<12} | [실제] {r.actual_pnl:>10,.0f}원 ({r.actual_profit_rate:>+6.2f}%){ob_exit_str}"
|
|
)
|
|
|
|
# ── 추가: 실매매 vs 호가 가드(C) / 호가필터 적용 시 손익·승률 사후 비교 ──
|
|
print()
|
|
print("=" * 72)
|
|
print("💰 [사후 실현손익 및 승률 비교] 호가필터 & 호가청산(C) 적용 시 예상 효과")
|
|
print("=" * 72)
|
|
print(f" {'구분':<18} {'거래수':>5} {'승률':>8} {'총 실현손익':>15} {'차액(vs원본)':>13} {'평균수익률':>10}")
|
|
print("-" * 72)
|
|
|
|
def _calc_metrics(tr_list: List[RowOut], use_ob_exit: bool) -> Tuple[int, float, float, float]:
|
|
if not tr_list:
|
|
return 0, 0.0, 0.0, 0.0
|
|
tot_pnl = 0.0
|
|
tot_rate = 0.0
|
|
win_cnt = 0
|
|
for tr in tr_list:
|
|
if use_ob_exit and tr.c_l3_guard and tr.c_exit_px > 0 and tr.buy_price > 0 and tr.qty > 0:
|
|
# 0.23% 세금/수수료 기본 반영
|
|
ratio = (tr.c_exit_px / tr.buy_price) - 1.0 - 0.0023
|
|
rate = ratio * 100.0
|
|
pnl = float(round(ratio * tr.buy_price * tr.qty))
|
|
else:
|
|
rate = tr.actual_profit_rate
|
|
pnl = tr.actual_pnl
|
|
tot_pnl += pnl
|
|
tot_rate += rate
|
|
if pnl > 0:
|
|
win_cnt += 1
|
|
cnt = len(tr_list)
|
|
return cnt, (win_cnt / cnt * 100.0), tot_pnl, (tot_rate / cnt)
|
|
|
|
# [0] 원본 실매매
|
|
c0, w0, p0, r0 = _calc_metrics(rows, use_ob_exit=False)
|
|
print(f" [원본] 실제 매매 {c0:4d}건 {w0:7.1f}% {p0:14,.0f}원 {'-':>13} {r0:9.2f}%")
|
|
|
|
# [1] ① 청산 호가매도 ON
|
|
c1, w1, p1, r1 = _calc_metrics(rows, use_ob_exit=True)
|
|
print(f" [①] 청산 호가매도(C) ON {c1:4d}건 {w1:7.1f}% {p1:14,.0f}원 {p1-p0:+12,.0f}원 {r1:9.2f}%")
|
|
|
|
# [2] ② 진입 호가필터 ON
|
|
rows_filt = [r for r in rows if r.entry_ob_pass]
|
|
c2, w2, p2, r2 = _calc_metrics(rows_filt, use_ob_exit=False)
|
|
print(f" [②] 진입 호가필터 ON {c2:4d}건 {w2:7.1f}% {p2:14,.0f}원 {p2-p0:+12,.0f}원 {r2:9.2f}%")
|
|
|
|
# [3] ③ 진입+청산 동시 ON
|
|
c3, w3, p3, r3 = _calc_metrics(rows_filt, use_ob_exit=True)
|
|
print(f" [③] 진입+청산 동시 ON {c3:4d}건 {w3:7.1f}% {p3:14,.0f}원 {p3-p0:+12,.0f}원 {r3:9.2f}%")
|
|
print("=" * 72)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|