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:
@@ -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분봉")
|
||||
|
||||
Reference in New Issue
Block a user