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:
191
deepcoin/ops/live_trader.py
Normal file
191
deepcoin/ops/live_trader.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user