""" 3단계: monitor_rules 발화 시 빗썸 실주문 (가드·로그). """ from __future__ import annotations import json import time from datetime import date, datetime from pathlib import Path from typing import Any from config import ( COIN_NAME, LIVE_COOLDOWN_MIN, LIVE_DAILY_KRW_MAX, LIVE_DAILY_LOSS_LIMIT_KRW, LIVE_MAX_TRADES_PER_DAY, LIVE_ORDER_KRW, LIVE_TRADING_ENABLED, SYMBOL, ) from deepcoin.matching.live_eval import evaluate_live_rules from deepcoin.matching.load_rules import load_monitor_rules from deepcoin.ops.alert_message import build_rule_alert_message from deepcoin.ops.monitor import Monitor from deepcoin.paths import LIVE_TRADES_LOG class LiveTrader(Monitor): """ 규칙 발화 시 실거래 실행. LIVE_TRADING_ENABLED=0 이면 주문 없음(드라이런 로그만). """ def __init__(self) -> None: """Monitor 초기화, 일별 카운터 비움.""" super().__init__(cooldown_file=None) self._rule_last_unix: dict[str, float] = {} self._day: str = "" self._day_spent_krw: float = 0.0 self._day_trades: int = 0 self._day_pnl_krw: float = 0.0 def _reset_day_if_needed(self) -> None: """날짜 변경 시 일별 한도 카운터 초기화.""" today = date.today().isoformat() if today != self._day: self._day = today self._day_spent_krw = 0.0 self._day_trades = 0 self._day_pnl_krw = 0.0 def _append_log(self, record: dict[str, Any]) -> None: """ live_trades.jsonl에 한 줄 append. Args: record: 로그 dict. """ LIVE_TRADES_LOG.parent.mkdir(parents=True, exist_ok=True) with LIVE_TRADES_LOG.open("a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") def _can_trade(self, rule_id: str) -> tuple[bool, str]: """ 일·쿨다운·손실 한도 검사. Args: rule_id: 규칙 ID. Returns: (허용 여부, 사유). """ self._reset_day_if_needed() if self._day_trades >= LIVE_MAX_TRADES_PER_DAY: return False, "일 최대 거래 수 초과" if self._day_spent_krw + LIVE_ORDER_KRW > LIVE_DAILY_KRW_MAX: return False, "일 주문 한도 초과" if self._day_pnl_krw <= -abs(LIVE_DAILY_LOSS_LIMIT_KRW): return False, "일 손실 한도 초과" last = self._rule_last_unix.get(rule_id, 0.0) if time.time() - last < LIVE_COOLDOWN_MIN * 60: return False, f"규칙 쿨다운({LIVE_COOLDOWN_MIN}분)" return True, "" def _execute_order(self, hit: dict[str, Any]) -> dict[str, Any]: """ 매수·매도 주문 실행 또는 드라이런. Args: hit: evaluate_live_rules 항목. Returns: 로그용 결과 dict. """ side = hit["side"] price = float(hit["close"]) amount_krw = float(LIVE_ORDER_KRW) record: dict[str, Any] = { "ts": datetime.now().isoformat(timespec="seconds"), "rule_id": hit["rule_id"], "side": side, "signal_dt": hit["dt"], "price": price, "amount_krw": amount_krw, "live_enabled": LIVE_TRADING_ENABLED, "ok": False, "message": "", } if not LIVE_TRADING_ENABLED: record["message"] = "dry_run (LIVE_TRADING_ENABLED=0)" record["ok"] = True return record try: if side == "buy": ok = self.buyCoinMarket(SYMBOL, int(amount_krw), count=None) record["ok"] = bool(ok) record["message"] = "buyCoinMarket" if ok else "buy failed" elif side == "sell": bal = self.load_balances_dict().get(SYMBOL, {}) qty = float(bal.get("balance") or 0) if qty <= 0: record["message"] = "보유 없음" else: ok = self.sellCoinMarket(SYMBOL, int(price), qty) record["ok"] = bool(ok) record["message"] = f"sell qty={qty}" if ok else "sell failed" else: record["message"] = f"unknown side {side}" except Exception as exc: record["message"] = str(exc) if record["ok"]: self._day_spent_krw += amount_krw self._day_trades += 1 self._rule_last_unix[hit["rule_id"]] = time.time() return record def run_once(self) -> None: """1회: 규칙 평가 → (허용 시) 주문 → 텔레그램.""" rules = load_monitor_rules() print( f"[06] {datetime.now():%Y-%m-%d %H:%M:%S} " f"{COIN_NAME} live={'ON' if LIVE_TRADING_ENABLED else 'OFF'} " f"rules={len(rules)}" ) if not rules: print(" monitor_rules 없음") return fired = evaluate_live_rules(rules) balances = None try: balances = self.load_balances_dict() except Exception: pass if not fired: print(" 발화 없음") return for hit in fired: rid = hit["rule_id"] ok, reason = self._can_trade(rid) print(f" [{hit['side']}] {rid} @ {hit['dt']}") if not ok: print(f" skip: {reason}") continue log = self._execute_order(hit) self._append_log(log) print(f" order: {log['message']} ok={log['ok']}") msg = build_rule_alert_message(hit, balances) if log["ok"]: msg += f"\n[체결] {log['message']}" else: msg += f"\n[실패] {log['message']}" self._send_coin_msg(msg) def run_loop(self, sleep_sec: int) -> None: """ 상시 루프. Args: sleep_sec: 대기 초. """ print(f"[06] 실거래 루프 시작 · sleep={sleep_sec}s") while True: self.run_once() time.sleep(sleep_sec)