GT MTF 프로필·캘리브레이션과 04 매칭/시뮬/실거래 파이프라인을 추가한다.

3분~일봉 GT 타점 분석(03c), leg 체결 순서 수정, 총자산 90% 검증 루프,
walk-forward Go/No-Go 시뮬, monitor·live_trader 및 reference 문서를 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 11:27:50 +09:00
parent b52d61b777
commit 2cb67c42b3
47 changed files with 5956 additions and 209 deletions

View File

@@ -658,7 +658,7 @@ def build_split_buy_peak_sell_trades(
prev_sell_ts = peak.ts
# 마지막 매도 이후 ~ 기간 : 미청산 구간 분할 매수만
# 마지막 매도 이후 ~ 기간 : 분할 매수 후 동일 leg에서 기간말 청산(포트폴리오 정합)
if sell_peaks:
last_peak = sell_peaks[-1]
troughs = _collect_buy_troughs(
@@ -667,25 +667,56 @@ def build_split_buy_peak_sell_trades(
leg_id = len(sell_peaks)
if troughs:
weights = _normalize_weights([1.0 / max(t.price, 1e-9) for t in troughs])
leg_buys: list[TradePoint] = []
for t, w in zip(troughs, weights):
row = _row_at_ts(df, t.ts)
bb_pos, rsi, disp = _bb_context(row)
price = float(row["Low"]) if "Low" in row else t.price
trades.append(
leg_buys.append(
TradePoint(
dt=t.ts.strftime("%Y-%m-%d %H:%M:%S"),
action="buy",
price=round(price, 2),
weight=round(w, 3),
leg_id=leg_id,
memo=f"저점 분할 매수(미청산) · 비중 {w*100:.0f}%",
memo=f"저점 분할 매수 · 비중 {w*100:.0f}% · leg#{leg_id}(기간말)",
bb_pos=bb_pos,
rsi=rsi,
pivot_kind="trough",
)
)
trades.extend(leg_buys)
leg_avg = (
sum(x.price * x.weight for x in leg_buys)
/ max(sum(x.weight for x in leg_buys), 1e-9)
)
end_ts = df.index[-1]
end_row = df.loc[end_ts]
if isinstance(end_row, pd.DataFrame):
end_row = end_row.iloc[-1]
end_price = float(end_row["Close"])
bb_pos, rsi, _ = _bb_context(end_row)
ret = (end_price - leg_avg) / max(leg_avg, 1e-9) * 100.0 if leg_avg > 0 else None
trades.append(
TradePoint(
dt=end_ts.strftime("%Y-%m-%d %H:%M:%S"),
action="sell",
price=round(end_price, 2),
weight=1.0,
leg_id=leg_id,
memo=f"기간말 잔여 청산 · leg#{leg_id}",
bb_pos=bb_pos,
rsi=rsi,
pivot_kind="peak",
forward_return_pct=round(ret, 2) if ret is not None else None,
)
)
for b in leg_buys:
if b.forward_return_pct is None and ret is not None:
b.forward_return_pct = round(
(end_price - b.price) / max(b.price, 1e-9) * 100.0, 2
)
trades.sort(key=lambda t: t.dt)
return trades
@@ -812,14 +843,21 @@ def generate_ground_truth(
t.forward_return_pct or 0.0 for t in trades if t.action == "sell"
)
trades.sort(key=lambda t: t.dt)
trade_dicts = order_trades_leg_block(trades)
last_close = float(df["Close"].iloc[-1])
pnl = simulate_truth_portfolio(
[asdict(t) for t in trades],
trade_dicts,
initial_cash=GT_INITIAL_CASH_KRW,
fee_rate=TRADING_FEE_RATE,
last_price=last_close,
)
pnl_realized = simulate_truth_portfolio(
trade_dicts,
initial_cash=GT_INITIAL_CASH_KRW,
fee_rate=TRADING_FEE_RATE,
last_price=None,
)
_validate_leg_portfolio(trade_dicts, last_close)
return {
"name": "ground_truth_split_buy_peak_sell",
@@ -849,21 +887,118 @@ def generate_ground_truth(
"round_trips": round_trips,
"sum_sell_leg_return_pct": round(total_ret, 2),
**pnl,
"realized_final_asset_krw": pnl_realized.get("final_asset_krw"),
"realized_pnl_krw": pnl_realized.get("pnl_krw"),
"realized_pnl_pct": pnl_realized.get("pnl_pct"),
"unrealized_pnl_krw": round(
float(pnl.get("pnl_krw", 0)) - float(pnl_realized.get("pnl_krw", 0)), 0
),
"execution_order": "leg_block",
},
"note": (
"저점 분할 매수(삼각형 크기=비중), 고점 1~2회 매도. "
"사후 라벨·캘리브레이션용."
"저점 분할 매수(비중=삼각형), 고점 1~2회 매도. "
"체결 순서=leg별 매수→매도(시각순 아님). 기간말 leg는 종가 청산. "
"summary.pnl_pct는 미청산 포함 종가 평가, realized_pnl_pct는 체결만 반영."
),
"trades": [asdict(t) for t in trades],
"trades": trade_dicts,
}
def _truth_simulation_rows(trades: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""TradePoint/dict 리스트를 시간순 dict 행으로 정규화."""
return sorted(
[t if isinstance(t, dict) else asdict(t) for t in trades],
key=lambda x: x["dt"],
)
def _validate_leg_portfolio(
trade_dicts: list[dict[str, Any]],
last_close: float,
) -> None:
"""
leg 블록 체결 후 보유·현금 불변식을 검증합니다.
Args:
trade_dicts: order_trades_leg_block 결과.
last_close: 기간 말 종가.
Raises:
ValueError: leg 매도 후에도 보유가 남는 경우(비정상).
"""
steps = simulate_truth_portfolio_steps(trade_dicts)
if not steps:
return
leg_ids = sorted({int(s["leg_id"]) for s in steps})
for lid in leg_ids:
leg_steps = [s for s in steps if int(s["leg_id"]) == lid]
sells = [s for s in leg_steps if s["action"] == "sell"]
if not sells:
continue
last_sell = sells[-1]
if float(last_sell["holding_qty"]) > 1e-4:
raise ValueError(
f"leg#{lid} 마지막 매도 후 보유 잔존 qty={last_sell['holding_qty']} "
"(leg 블록 체결·매도 비중 합 검토 필요)"
)
final = steps[-1]
if float(final["holding_qty"]) > 1e-2:
raise ValueError(
f"최종 보유 잔존 qty={final['holding_qty']} — 기간말 청산 누락 가능"
)
pnl = simulate_truth_portfolio(trade_dicts, last_price=last_close)
if float(pnl.get("holding_qty", 0)) > 1e-2:
raise ValueError("종가 평가 후에도 미청산 보유가 남음")
def order_trades_leg_block(
trades: list[TradePoint] | list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""
leg별 매수 전량 → 매도 전량 순으로 정렬합니다 (포트폴리오 시뮬·JSON 저장용).
시각순 정렬은 leg가 섞여 매도 미완료·보유 누적 오류를 만듭니다.
Args:
trades: TradePoint 또는 dict 리스트.
Returns:
leg_id, action(buy=0), dt 순 dict 리스트.
"""
rows = [t if isinstance(t, dict) else asdict(t) for t in trades]
def _sort_key(x: dict[str, Any]) -> tuple[int, int, str]:
return (int(x.get("leg_id", 0)), 0 if x.get("action") == "buy" else 1, x["dt"])
return sorted(rows, key=_sort_key)
def order_trades_chronological(
trades: list[TradePoint] | list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""
시각순 dict 리스트 (차트 표시·분석용).
Args:
trades: TradePoint 또는 dict.
Returns:
dt 순 정렬된 dict 리스트.
"""
rows = [t if isinstance(t, dict) else asdict(t) for t in trades]
return sorted(rows, key=lambda x: x["dt"])
def _truth_simulation_rows(
trades: list[dict[str, Any]] | list[TradePoint],
*,
chronological: bool = False,
) -> list[dict[str, Any]]:
"""
포트폴리오 시뮬용 체결 순서로 정규화합니다.
Args:
trades: JSON trades 또는 TradePoint.
chronological: True면 시각순(레거시), False면 leg 블록 순(기본).
Returns:
dict 행 리스트.
"""
if chronological:
return order_trades_chronological(trades)
return order_trades_leg_block(trades)
def simulate_truth_portfolio_steps(
@@ -1024,8 +1159,12 @@ def simulate_truth_portfolio(
if qty < 1e-12:
qty = 0.0
mark_price = float(last_price if last_price is not None else last_trade_price or 0)
holding_value = qty * mark_price
if last_price is None:
mark_price = None
holding_value = 0.0
else:
mark_price = float(last_price)
holding_value = qty * mark_price
final_asset = cash + holding_value
pnl_krw = final_asset - initial_cash
pnl_pct = pnl_krw / initial_cash * 100.0 if initial_cash else 0.0
@@ -1039,7 +1178,7 @@ def simulate_truth_portfolio(
"cash_krw": round(cash, 0),
"holding_qty": round(qty, 6),
"holding_value_krw": round(holding_value, 0),
"mark_price": round(mark_price, 2),
"mark_price": round(mark_price, 2) if last_price is not None else None,
"fee_rate": fee_rate,
}
@@ -1087,16 +1226,18 @@ def print_ground_truth_report(data: dict[str, Any]) -> None:
print(f" 매도 수익 합(참고): {s.get('sum_sell_leg_return_pct')}%")
if s.get("initial_cash_krw"):
print(
f" 시뮬(시작{s['initial_cash_krw']:,.0f}): "
f"최종{s['final_asset_krw']:,.0f} | "
f"수익 ₩{s['pnl_krw']:+,.0f} ({s['pnl_pct']:+.2f}%) | "
f" 포트폴리오: 초기{s['initial_cash_krw']:,.0f} "
f"총보유자산{s['final_asset_krw']:,.0f} | "
f"초기 대비 {s['pnl_pct']:+.2f}% | "
f"수수료 ₩{s['total_fees_krw']:,.0f}"
)
if s.get("holding_qty", 0) > 0:
print(
f" 미청산: {s['holding_qty']}"
f"(평가 ₩{s['holding_value_krw']:,.0f}, 종가 ₩{s['mark_price']:,.0f})"
f"(평가 ₩{s['holding_value_krw']:,.0f}, 종가 ₩{s.get('mark_price', 0):,.0f})"
)
elif s.get("execution_order"):
print(f" 체결 순서: {s['execution_order']} (leg별 매수→매도)")
print(f" 파라미터: {data.get('params')}")
from collections import Counter
@@ -1133,7 +1274,7 @@ def run_from_db(monitor=None, output: Path = DEFAULT_OUTPUT) -> dict[str, Any]:
생성된 dict.
"""
from config import TREND_INTERVAL_1D, TREND_INTERVAL_1H
from monitor import Monitor
from deepcoin.ops.monitor import Monitor
mon = monitor or Monitor(cooldown_file=None)
print(f"정답 생성: 최근 {CHART_LOOKBACK_DAYS}일 3분봉")