MTF 필터 백테스트, paper/live 체결, 빗썸 Private API 연동 및 운영 스크립트·설계 문서를 추가해 2단계 전략을 실거래 단계에 연결한다. Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
3.3 KiB
Python
128 lines
3.3 KiB
Python
"""매수·매도 체결 로직 (paper / live 공통 사이징)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from deepcoin.ground_truth.order_sizing import max_buy_from_cash
|
|
|
|
|
|
@dataclass
|
|
class TradeResult:
|
|
"""단일 체결 결과."""
|
|
|
|
executed: bool
|
|
side: str
|
|
order_krw: float
|
|
order_coin: float
|
|
fee_krw: float
|
|
price: float
|
|
skip_reason: str = ""
|
|
api_response: dict[str, Any] | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""JSON 직렬화 dict."""
|
|
return {
|
|
"executed": self.executed,
|
|
"side": self.side,
|
|
"order_krw": round(self.order_krw, 0),
|
|
"order_coin": round(self.order_coin, 8),
|
|
"fee_krw": round(self.fee_krw, 2),
|
|
"price": self.price,
|
|
"skip_reason": self.skip_reason,
|
|
"api_response": self.api_response,
|
|
}
|
|
|
|
|
|
def compute_buy_order(
|
|
*,
|
|
cash_krw: float,
|
|
coin_qty: float,
|
|
price: float,
|
|
fee_rate: float,
|
|
min_order_krw: float,
|
|
cluster_size: int = 1,
|
|
) -> TradeResult:
|
|
"""매수 주문 금액·수량을 계산한다."""
|
|
cash = max(float(cash_krw), 0.0)
|
|
equity = cash + float(coin_qty) * price
|
|
cash_cap = max_buy_from_cash(equity, cash)
|
|
per_buy = cash / cluster_size if cluster_size > 0 else cash
|
|
order_krw = min(per_buy, cash, cash_cap)
|
|
|
|
if order_krw < min_order_krw:
|
|
return TradeResult(
|
|
executed=False,
|
|
side="buy",
|
|
order_krw=0.0,
|
|
order_coin=0.0,
|
|
fee_krw=0.0,
|
|
price=price,
|
|
skip_reason="원화 부족 또는 최소 주문 미만",
|
|
)
|
|
|
|
fee = order_krw * fee_rate
|
|
bought = (order_krw - fee) / price
|
|
return TradeResult(
|
|
executed=True,
|
|
side="buy",
|
|
order_krw=order_krw,
|
|
order_coin=bought,
|
|
fee_krw=fee,
|
|
price=price,
|
|
)
|
|
|
|
|
|
def compute_sell_order(
|
|
*,
|
|
coin_qty: float,
|
|
price: float,
|
|
fee_rate: float,
|
|
min_order_krw: float,
|
|
cluster_size: int = 1,
|
|
) -> TradeResult:
|
|
"""매도 주문 수량을 계산한다."""
|
|
qty = max(float(coin_qty), 0.0)
|
|
per_sell = qty / cluster_size if cluster_size > 0 else qty
|
|
order_coin = per_sell
|
|
order_krw = order_coin * price
|
|
|
|
if order_coin <= 0 or order_krw < min_order_krw:
|
|
return TradeResult(
|
|
executed=False,
|
|
side="sell",
|
|
order_krw=0.0,
|
|
order_coin=0.0,
|
|
fee_krw=0.0,
|
|
price=price,
|
|
skip_reason="코인 부족 또는 최소 주문 미만",
|
|
)
|
|
|
|
fee = order_krw * fee_rate
|
|
return TradeResult(
|
|
executed=True,
|
|
side="sell",
|
|
order_krw=order_krw,
|
|
order_coin=order_coin,
|
|
fee_krw=fee,
|
|
price=price,
|
|
)
|
|
|
|
|
|
def apply_trade_to_portfolio(
|
|
portfolio: dict[str, Any],
|
|
trade: TradeResult,
|
|
) -> None:
|
|
"""체결 결과를 포트폴리오에 반영한다."""
|
|
cash = float(portfolio.get("cash_krw", 0))
|
|
coin = float(portfolio.get("coin_qty", 0))
|
|
if not trade.executed:
|
|
return
|
|
if trade.side == "buy":
|
|
portfolio["cash_krw"] = cash - trade.order_krw
|
|
portfolio["coin_qty"] = coin + trade.order_coin
|
|
else:
|
|
portfolio["cash_krw"] = cash + trade.order_krw - trade.fee_krw
|
|
portfolio["coin_qty"] = coin - trade.order_coin
|