refactor: GT·시뮬·운영 3축 정리 및 hybrid 실거래 정합
Phase C/dry-run·미사용 모듈·재생성 HTML을 제거하고, 운영 체결을 sim_causal_hybrid와 동일한 hybrid 로직으로 통합한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
"""
|
||||
3단계: monitor_rules 발화 시 빗썸 실주문 (가드·로그).
|
||||
|
||||
dry-run·live 체결 배분: 시뮬 sim_causal_hybrid 와 동일 (hybrid_sim_execution).
|
||||
체결 배분: 시뮬 sim_causal_hybrid 와 동일
|
||||
- fire_outcomes 부트스트랩 + hybrid_sim_execution.plan_live_hit
|
||||
- enhanced=False, hybrid DD tier, EV/WF 매수 필터
|
||||
LIVE_TRADING_ENABLED=1 필수.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -9,16 +12,15 @@ from __future__ import annotations
|
||||
import json
|
||||
import time
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from config import (
|
||||
CHART_LOOKBACK_DAYS,
|
||||
COIN_NAME,
|
||||
GT_INITIAL_CASH_KRW,
|
||||
LIVE_COOLDOWN_MIN,
|
||||
LIVE_DAILY_KRW_MAX,
|
||||
LIVE_DAILY_LOSS_LIMIT_KRW,
|
||||
LIVE_HYBRID_BOOTSTRAP_FIRES,
|
||||
LIVE_MAX_TRADES_PER_DAY,
|
||||
LIVE_ORDER_KRW,
|
||||
LIVE_TRADING_ENABLED,
|
||||
@@ -27,64 +29,48 @@ from config import (
|
||||
TRADING_FEE_RATE,
|
||||
)
|
||||
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||
from deepcoin.ground_truth.ground_truth import load_ground_truth
|
||||
from deepcoin.matching.live_eval import evaluate_live_rules
|
||||
from deepcoin.matching.live_sizing import LivePositionState, live_sizing_enabled
|
||||
from deepcoin.matching.load_rules import load_monitor_rules
|
||||
from deepcoin.matching.position_sizing import (
|
||||
load_ev_wf_approved_rule_ids,
|
||||
top_leg_ids_by_forward_return,
|
||||
)
|
||||
from deepcoin.ops.alert_message import build_rule_alert_message
|
||||
from deepcoin.ops.hybrid_sim_execution import (
|
||||
SimTradeResult,
|
||||
build_live_signal_history,
|
||||
hit_key,
|
||||
plan_live_hit,
|
||||
replay_paper_portfolio,
|
||||
sort_hits_sim_order,
|
||||
)
|
||||
from deepcoin.ops.monitor import Monitor
|
||||
from deepcoin.ops.paper_portfolio import PaperPortfolio
|
||||
from deepcoin.paths import (
|
||||
LIVE_SIGNAL_HISTORY_JSON,
|
||||
LIVE_TRADES_LOG,
|
||||
PAPER_FIRES_LOG,
|
||||
resolve_ground_truth_file,
|
||||
)
|
||||
|
||||
|
||||
class LiveTrader(Monitor):
|
||||
"""
|
||||
규칙 발화 시 실거래 실행. LIVE_TRADING_ENABLED=0 이면 모의(sim hybrid)만.
|
||||
규칙 발화 시 빗썸 실주문. 배분은 시뮬 sim_causal_hybrid 와 동일 엔진.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Monitor 초기화, 일별 카운터 비움."""
|
||||
"""Monitor 초기화, hybrid 이력·일별 카운터."""
|
||||
if not LIVE_TRADING_ENABLED:
|
||||
raise RuntimeError(
|
||||
"LIVE_TRADING_ENABLED=0 — 실거래만 지원합니다. "
|
||||
".env 에 LIVE_TRADING_ENABLED=1 설정 후 재기동하세요."
|
||||
)
|
||||
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
|
||||
self._gt_trades: list[dict] = []
|
||||
self._large_legs: set[int] = set()
|
||||
self._approved_rules: set[str] = set()
|
||||
self._position_state = LivePositionState.load()
|
||||
self._paper = PaperPortfolio.load() if not LIVE_TRADING_ENABLED else None
|
||||
self._live_signal_history: list[dict[str, Any]] = []
|
||||
self._ohlc_df = None
|
||||
self._load_sizing_context()
|
||||
if self._paper_mode and self._paper.signal_history:
|
||||
self._resync_paper_from_sim()
|
||||
if LIVE_TRADING_ENABLED:
|
||||
self._live_signal_history = self._load_live_signal_history()
|
||||
self._persisted_ops_signals: list[dict[str, Any]] = self._load_persisted_signals()
|
||||
self._live_signal_history = self._init_signal_history()
|
||||
self._load_ohlc_df()
|
||||
|
||||
@property
|
||||
def _paper_mode(self) -> bool:
|
||||
"""dry-run: 모의 계좌·시뮬 hybrid 체결."""
|
||||
return not LIVE_TRADING_ENABLED and self._paper is not None
|
||||
|
||||
def _load_live_signal_history(self) -> list[dict[str, Any]]:
|
||||
"""live 시뮬 정합용 발화 이력."""
|
||||
def _load_persisted_signals(self) -> list[dict[str, Any]]:
|
||||
"""live_signal_history.json."""
|
||||
if not LIVE_SIGNAL_HISTORY_JSON.is_file():
|
||||
return []
|
||||
try:
|
||||
@@ -93,12 +79,28 @@ class LiveTrader(Monitor):
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
|
||||
def _save_live_signal_history(self) -> None:
|
||||
"""live 발화 이력 저장."""
|
||||
def _init_signal_history(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
시뮬과 동일 hybrid 입력: fire_outcomes 부트스트랩 + 운영 저장분.
|
||||
|
||||
Returns:
|
||||
병합된 발화 이력.
|
||||
"""
|
||||
merged = build_live_signal_history(self._persisted_ops_signals)
|
||||
n_boot = max(len(merged) - len(self._persisted_ops_signals), 0)
|
||||
print(
|
||||
f"[06] hybrid 이력: total={len(merged)} "
|
||||
f"(bootstrap={'on' if LIVE_HYBRID_BOOTSTRAP_FIRES else 'off'}, "
|
||||
f"from_fires~{n_boot}, ops_persisted={len(self._persisted_ops_signals)})"
|
||||
)
|
||||
return merged
|
||||
|
||||
def _save_persisted_ops_signals(self) -> None:
|
||||
"""운영 체결분만 저장 (fire_outcomes 부트스트랩은 재로드)."""
|
||||
LIVE_SIGNAL_HISTORY_JSON.parent.mkdir(parents=True, exist_ok=True)
|
||||
LIVE_SIGNAL_HISTORY_JSON.write_text(
|
||||
json.dumps(
|
||||
{"signals": self._live_signal_history[-2000:]},
|
||||
{"signals": self._persisted_ops_signals[-2000:]},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
@@ -106,7 +108,7 @@ class LiveTrader(Monitor):
|
||||
)
|
||||
|
||||
def _live_signal_seen(self, hit: dict[str, Any]) -> bool:
|
||||
"""live 이력에 동일 봉 발화가 있는지."""
|
||||
"""이력에 동일 봉 발화가 있는지."""
|
||||
dt, rid, side = hit_key(hit)
|
||||
return any(
|
||||
str(s["dt"]) == dt and str(s["rule_id"]) == rid and str(s["side"]) == side
|
||||
@@ -114,35 +116,18 @@ class LiveTrader(Monitor):
|
||||
)
|
||||
|
||||
def _append_live_signal(self, hit: dict[str, Any]) -> None:
|
||||
"""live 발화 이력 추가."""
|
||||
"""체결 성공 발화를 전체 이력·운영 저장분에 추가."""
|
||||
if self._live_signal_seen(hit):
|
||||
return
|
||||
self._live_signal_history.append(
|
||||
{
|
||||
"dt": str(hit["dt"]),
|
||||
"rule_id": str(hit["rule_id"]),
|
||||
"side": str(hit["side"]),
|
||||
"close": float(hit["close"]),
|
||||
}
|
||||
)
|
||||
|
||||
def _balances_for_trading(self) -> dict[str, dict[str, float]] | None:
|
||||
"""
|
||||
dry-run: paper_portfolio만. live: 빗썸 API.
|
||||
"""
|
||||
if self._paper_mode:
|
||||
return self._paper.balances_dict()
|
||||
try:
|
||||
return self.load_balances_dict()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _load_sizing_context(self) -> None:
|
||||
"""GT leg·EV/WF 통과 규칙 캐시."""
|
||||
gt = load_ground_truth(resolve_ground_truth_file()) or {}
|
||||
self._gt_trades = gt.get("trades") or []
|
||||
self._large_legs = top_leg_ids_by_forward_return(self._gt_trades)
|
||||
self._approved_rules = load_ev_wf_approved_rule_ids()
|
||||
row = {
|
||||
"dt": str(hit["dt"]),
|
||||
"rule_id": str(hit["rule_id"]),
|
||||
"side": str(hit["side"]),
|
||||
"close": float(hit["close"]),
|
||||
}
|
||||
self._live_signal_history.append(row)
|
||||
if not any(hit_key(s) == hit_key(row) for s in self._persisted_ops_signals):
|
||||
self._persisted_ops_signals.append(row)
|
||||
|
||||
def _reset_day_if_needed(self) -> None:
|
||||
"""날짜 변경 시 일별 한도 카운터 초기화."""
|
||||
@@ -159,42 +144,21 @@ class LiveTrader(Monitor):
|
||||
with LIVE_TRADES_LOG.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
|
||||
def _append_paper_fire(
|
||||
self,
|
||||
hit: dict[str, Any],
|
||||
planned_krw: float,
|
||||
would_trade: bool,
|
||||
skip_reason: str = "",
|
||||
order_log: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Phase C paper_fires.jsonl."""
|
||||
PAPER_FIRES_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
row = {
|
||||
"ts": datetime.now().isoformat(timespec="seconds"),
|
||||
"signal_dt": hit.get("dt"),
|
||||
"rule_id": hit.get("rule_id"),
|
||||
"side": hit.get("side"),
|
||||
"close": float(hit.get("close") or 0),
|
||||
"planned_krw": round(float(planned_krw), 0),
|
||||
"would_trade": bool(would_trade),
|
||||
"skip_reason": skip_reason or "",
|
||||
"live_enabled": bool(LIVE_TRADING_ENABLED),
|
||||
"order_message": (order_log or {}).get("message", ""),
|
||||
"sizing": "sim_causal_hybrid",
|
||||
}
|
||||
with PAPER_FIRES_LOG.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
||||
|
||||
def _can_trade(self, rule_id: str, planned_krw: float | None = None) -> tuple[bool, str]:
|
||||
"""
|
||||
쿨다운(1봉=3분) + live 일한도. dry-run은 일한도만 생략.
|
||||
쿨다운(1봉=3분) + 일한도·손실한도·거래횟수.
|
||||
|
||||
Args:
|
||||
rule_id: 규칙 ID.
|
||||
planned_krw: 예정 매수 원화.
|
||||
|
||||
Returns:
|
||||
(허용 여부, 거절 사유).
|
||||
"""
|
||||
self._reset_day_if_needed()
|
||||
last = self._rule_last_unix.get(rule_id, 0.0)
|
||||
if time.time() - last < LIVE_COOLDOWN_MIN * 60:
|
||||
return False, f"규칙 쿨다운({LIVE_COOLDOWN_MIN}분)"
|
||||
if self._paper_mode:
|
||||
return True, ""
|
||||
if self._day_trades >= LIVE_MAX_TRADES_PER_DAY:
|
||||
return False, "일 최대 거래 수 초과"
|
||||
need = float(planned_krw if planned_krw is not None else LIVE_ORDER_KRW)
|
||||
@@ -212,42 +176,81 @@ class LiveTrader(Monitor):
|
||||
except Exception:
|
||||
self._ohlc_df = None
|
||||
|
||||
def _resync_paper_from_sim(self) -> None:
|
||||
"""기존 paper 잔고를 sim_causal_hybrid replay 로 맞춤."""
|
||||
def _sim_plan(self, hit: dict[str, Any]) -> SimTradeResult:
|
||||
"""시뮬 hybrid 배분 1건 (누적 이력·인과, 현금·보유 제약)."""
|
||||
if self._ohlc_df is None:
|
||||
self._load_ohlc_df()
|
||||
if self._ohlc_df is None or getattr(self._ohlc_df, "empty", True):
|
||||
return
|
||||
replayed, _ = replay_paper_portfolio(
|
||||
self._paper.signal_history,
|
||||
self._ohlc_df,
|
||||
approved_buy_rules=self._approved_rules,
|
||||
)
|
||||
self._paper.cash_krw = replayed.cash_krw
|
||||
self._paper.qty = replayed.qty
|
||||
self._paper.qty_by_leg = dict(replayed.qty_by_leg)
|
||||
self._paper.current_leg_id = replayed.current_leg_id
|
||||
self._paper.save()
|
||||
|
||||
def _sim_plan(self, hit: dict[str, Any]) -> Any:
|
||||
"""시뮬 hybrid 배분 1건."""
|
||||
if self._ohlc_df is None:
|
||||
self._load_ohlc_df()
|
||||
if self._paper_mode:
|
||||
hist = list(self._paper.signal_history)
|
||||
else:
|
||||
hist = list(self._live_signal_history)
|
||||
return plan_live_hit(
|
||||
hist,
|
||||
list(self._live_signal_history),
|
||||
hit,
|
||||
self._ohlc_df,
|
||||
approved_buy_rules=self._approved_rules,
|
||||
approved_buy_rules=None,
|
||||
)
|
||||
|
||||
def _execute_live_order(self, hit: dict[str, Any], plan: Any) -> dict[str, Any]:
|
||||
"""실거래: 시뮬 planned 금액·수량으로 API 주문."""
|
||||
@staticmethod
|
||||
def _cap_plan_to_exchange(plan: SimTradeResult, hit: dict[str, Any], balances: dict) -> SimTradeResult:
|
||||
"""
|
||||
시뮬 planned 금액을 거래소 가용 잔고 이내로 제한.
|
||||
|
||||
Args:
|
||||
plan: hybrid 시뮬 배분 결과.
|
||||
hit: 발화.
|
||||
balances: load_balances_dict() 결과.
|
||||
|
||||
Returns:
|
||||
조정된 SimTradeResult.
|
||||
"""
|
||||
sym = balances.get(SYMBOL, {})
|
||||
price = float(hit["close"])
|
||||
side = hit["side"]
|
||||
|
||||
if not plan.ok or plan.amount_krw <= 0:
|
||||
return plan
|
||||
|
||||
if side == "buy":
|
||||
krw = float(sym.get("krw") or 0)
|
||||
max_buy = max(krw / (1.0 + TRADING_FEE_RATE) - 1.0, 0.0)
|
||||
capped = min(float(plan.amount_krw), max_buy)
|
||||
if capped <= 0:
|
||||
return SimTradeResult(
|
||||
plan.hit, 0.0, 0.0, False, "거래소 현금 부족(시뮬 대비)"
|
||||
)
|
||||
if capped < plan.amount_krw - 1.0:
|
||||
return SimTradeResult(
|
||||
plan.hit,
|
||||
round(capped, 0),
|
||||
0.0,
|
||||
True,
|
||||
f"sim_buy capped ₩{capped:,.0f} (plan ₩{plan.amount_krw:,.0f})",
|
||||
leg_id=plan.leg_id,
|
||||
)
|
||||
return plan
|
||||
|
||||
held = float(sym.get("balance") or 0)
|
||||
if held <= 0:
|
||||
return SimTradeResult(plan.hit, 0.0, 0.0, False, "거래소 보유 없음")
|
||||
sell_qty = min(float(plan.sell_qty), held)
|
||||
if sell_qty <= 0:
|
||||
return SimTradeResult(plan.hit, 0.0, 0.0, False, "매도 수량 0")
|
||||
gross = round(sell_qty * price, 0)
|
||||
if sell_qty < plan.sell_qty - 1e-8:
|
||||
return SimTradeResult(
|
||||
plan.hit,
|
||||
gross,
|
||||
sell_qty,
|
||||
True,
|
||||
f"sim_sell capped qty={sell_qty:.4f}",
|
||||
leg_id=plan.leg_id,
|
||||
)
|
||||
return plan
|
||||
|
||||
def _execute_live_order(
|
||||
self, hit: dict[str, Any], plan: SimTradeResult, balances: dict
|
||||
) -> dict[str, Any]:
|
||||
"""실거래: 시뮬 plan(잔고 cap)으로 API 주문."""
|
||||
side = hit["side"]
|
||||
price = float(hit["close"])
|
||||
plan = self._cap_plan_to_exchange(plan, hit, balances)
|
||||
record: dict[str, Any] = {
|
||||
"ts": datetime.now().isoformat(timespec="seconds"),
|
||||
"rule_id": hit["rule_id"],
|
||||
@@ -269,29 +272,15 @@ class LiveTrader(Monitor):
|
||||
record["ok"] = bool(ok)
|
||||
record["message"] = "buyCoinMarket" if ok else "buy failed"
|
||||
elif side == "sell":
|
||||
bal = self.load_balances_dict().get(SYMBOL, {})
|
||||
held = float(bal.get("balance") or 0)
|
||||
if held <= 0:
|
||||
record["message"] = "보유 없음"
|
||||
else:
|
||||
sell_qty = min(float(plan.sell_qty), held)
|
||||
if sell_qty <= 0:
|
||||
record["message"] = "매도 수량 0"
|
||||
else:
|
||||
gross = sell_qty * price
|
||||
record["amount_krw"] = round(gross, 0)
|
||||
ok = self.sellCoinMarket(SYMBOL, int(price), sell_qty)
|
||||
record["ok"] = bool(ok)
|
||||
record["sell_qty"] = sell_qty
|
||||
record["message"] = (
|
||||
f"sell qty={sell_qty:.4f}" if ok else "sell failed"
|
||||
)
|
||||
if record["ok"] and live_sizing_enabled():
|
||||
fee = gross * TRADING_FEE_RATE
|
||||
self._position_state.record_sell(
|
||||
gross, fee, full_close=(sell_qty >= held * 0.999)
|
||||
)
|
||||
self._position_state.save()
|
||||
sell_qty = float(plan.sell_qty)
|
||||
gross = sell_qty * price
|
||||
record["amount_krw"] = round(gross, 0)
|
||||
ok = self.sellCoinMarket(SYMBOL, int(price), sell_qty)
|
||||
record["ok"] = bool(ok)
|
||||
record["sell_qty"] = sell_qty
|
||||
record["message"] = (
|
||||
f"sell qty={sell_qty:.4f}" if ok else "sell failed"
|
||||
)
|
||||
else:
|
||||
record["message"] = f"unknown side {side}"
|
||||
except Exception as exc:
|
||||
@@ -302,85 +291,22 @@ class LiveTrader(Monitor):
|
||||
self._day_spent_krw += spent
|
||||
self._day_trades += 1
|
||||
self._rule_last_unix[hit["rule_id"]] = time.time()
|
||||
if live_sizing_enabled() and side == "buy":
|
||||
fee = spent * TRADING_FEE_RATE
|
||||
self._position_state.record_buy(hit["dt"], price, spent, fee)
|
||||
self._position_state.save()
|
||||
self._append_live_signal(hit)
|
||||
self._save_live_signal_history()
|
||||
self._save_persisted_ops_signals()
|
||||
return record
|
||||
|
||||
def _process_paper_batch(self, new_hits: list[dict[str, Any]]) -> None:
|
||||
"""
|
||||
dry-run: 신규 발화를 이력에 넣고 시뮬 전체 재생 후 알림.
|
||||
"""
|
||||
if not new_hits:
|
||||
return
|
||||
if self._ohlc_df is None:
|
||||
self._load_ohlc_df()
|
||||
for hit in new_hits:
|
||||
self._paper.append_signal(hit)
|
||||
|
||||
replayed, results = replay_paper_portfolio(
|
||||
self._paper.signal_history,
|
||||
self._ohlc_df,
|
||||
approved_buy_rules=self._approved_rules,
|
||||
)
|
||||
self._paper.cash_krw = replayed.cash_krw
|
||||
self._paper.qty = replayed.qty
|
||||
self._paper.qty_by_leg = dict(replayed.qty_by_leg)
|
||||
self._paper.current_leg_id = replayed.current_leg_id
|
||||
|
||||
for hit in new_hits:
|
||||
key = hit_key(hit)
|
||||
res = results.get(key)
|
||||
if res is None:
|
||||
self._paper.mark_processed(hit["rule_id"], hit["dt"])
|
||||
continue
|
||||
log = {
|
||||
"ok": res.ok,
|
||||
"message": res.message,
|
||||
"amount_krw": res.amount_krw,
|
||||
"sell_qty": res.sell_qty,
|
||||
}
|
||||
self._append_paper_fire(
|
||||
hit, res.amount_krw, res.ok, "" if res.ok else res.message, log
|
||||
)
|
||||
self._paper.mark_processed(hit["rule_id"], hit["dt"])
|
||||
print(f" [{hit['side']}] {hit['rule_id']} @ {hit['dt']}")
|
||||
print(f" order: {res.message} ok={res.ok}")
|
||||
if not res.ok:
|
||||
continue
|
||||
self._rule_last_unix[hit["rule_id"]] = time.time()
|
||||
post_balances = self._paper.balances_dict()
|
||||
msg = build_rule_alert_message(
|
||||
hit,
|
||||
post_balances,
|
||||
trade_krw=res.amount_krw,
|
||||
trade_qty=res.sell_qty if hit["side"] == "sell" else None,
|
||||
)
|
||||
sym = post_balances.get(SYMBOL, {})
|
||||
msg += (
|
||||
f"\n[모의잔고·체결후] 현금 {_fmt_paper_krw(sym.get('krw', 0))} · "
|
||||
f"보유 {float(sym.get('balance', 0)):.4f} {SYMBOL}"
|
||||
)
|
||||
msg += f"\n[체결] {res.message}"
|
||||
self._send_coin_msg(msg)
|
||||
self._paper.save()
|
||||
|
||||
def run_once(self) -> None:
|
||||
"""1회: 규칙 평가 → 시뮬 hybrid 체결 → 텔레그램."""
|
||||
"""1회: 규칙 평가 → hybrid 배분 → 빗썸 주문 → 텔레그램."""
|
||||
from deepcoin.data.ops_sync import ensure_ops_candles
|
||||
|
||||
ensure_ops_candles()
|
||||
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)} · sim=hybrid · bar={MATCH_PRIMARY_INTERVAL}m"
|
||||
f"{COIN_NAME} LIVE rules={len(rules)} · sim=hybrid · bar={MATCH_PRIMARY_INTERVAL}m"
|
||||
)
|
||||
if not rules:
|
||||
print(" monitor_rules 없음")
|
||||
print(" monitor_rules 없음 — scripts/04_match_rules.py 실행")
|
||||
return
|
||||
|
||||
fired = evaluate_live_rules(rules, force_refresh=True)
|
||||
@@ -388,28 +314,14 @@ class LiveTrader(Monitor):
|
||||
print(" 발화 없음")
|
||||
return
|
||||
|
||||
if self._paper_mode:
|
||||
print(
|
||||
f" [paper] 현금 ₩{self._paper.cash_krw:,.0f} · "
|
||||
f"보유 {self._paper.qty:.4f} {SYMBOL} "
|
||||
f"(초기 ₩{GT_INITIAL_CASH_KRW:,.0f})"
|
||||
)
|
||||
try:
|
||||
balances = self.load_balances_dict()
|
||||
except Exception:
|
||||
balances = {}
|
||||
|
||||
new_paper_hits: list[dict[str, Any]] = []
|
||||
for hit in sort_hits_sim_order(fired):
|
||||
rid = hit["rule_id"]
|
||||
if self._paper_mode and self._paper.already_processed(rid, hit["dt"]):
|
||||
print(f" [{hit['side']}] {rid} @ {hit['dt']} (이미 처리)")
|
||||
continue
|
||||
if LIVE_TRADING_ENABLED and self._live_signal_seen(hit):
|
||||
continue
|
||||
|
||||
if hit["side"] == "buy" and rid not in self._approved_rules:
|
||||
print(f" [{hit['side']}] {rid} @ {hit['dt']}")
|
||||
print(" skip: EV/WF 미통과 규칙")
|
||||
if self._paper_mode:
|
||||
self._append_paper_fire(hit, 0.0, False, "EV/WF 미통과 규칙")
|
||||
self._paper.mark_processed(rid, hit["dt"])
|
||||
if self._live_signal_seen(hit):
|
||||
continue
|
||||
|
||||
plan_preview = self._sim_plan(hit)
|
||||
@@ -417,24 +329,23 @@ class LiveTrader(Monitor):
|
||||
if not ok:
|
||||
print(f" [{hit['side']}] {rid} @ {hit['dt']}")
|
||||
print(f" skip: {reason}")
|
||||
if self._paper_mode:
|
||||
self._append_paper_fire(
|
||||
hit, plan_preview.amount_krw, False, reason
|
||||
)
|
||||
self._paper.mark_processed(rid, hit["dt"])
|
||||
continue
|
||||
|
||||
if self._paper_mode:
|
||||
new_paper_hits.append(hit)
|
||||
if not plan_preview.ok:
|
||||
print(f" [{hit['side']}] {rid} @ {hit['dt']}")
|
||||
print(f" skip: {plan_preview.message}")
|
||||
continue
|
||||
|
||||
print(f" [{hit['side']}] {rid} @ {hit['dt']}")
|
||||
log = self._execute_live_order(hit, plan_preview)
|
||||
log = self._execute_live_order(hit, plan_preview, balances)
|
||||
self._append_log(log)
|
||||
print(f" order: {log['message']} ok={log['ok']}")
|
||||
if not log["ok"]:
|
||||
continue
|
||||
balances = self._balances_for_trading()
|
||||
try:
|
||||
balances = self.load_balances_dict()
|
||||
except Exception:
|
||||
balances = None
|
||||
msg = build_rule_alert_message(
|
||||
hit,
|
||||
balances,
|
||||
@@ -444,23 +355,15 @@ class LiveTrader(Monitor):
|
||||
if balances:
|
||||
sym = balances.get(SYMBOL, {})
|
||||
msg += (
|
||||
f"\n[잔고] 현금 {_fmt_paper_krw(sym.get('krw', 0))} · "
|
||||
f"\n[잔고] 현금 ₩{float(sym.get('krw', 0)):,.0f} · "
|
||||
f"보유 {float(sym.get('balance', 0)):.4f} {SYMBOL}"
|
||||
)
|
||||
msg += f"\n[체결] {log['message']}"
|
||||
self._send_coin_msg(msg)
|
||||
|
||||
if self._paper_mode and new_paper_hits:
|
||||
self._process_paper_batch(new_paper_hits)
|
||||
|
||||
def run_loop(self, sleep_sec: int) -> None:
|
||||
"""상시 루프."""
|
||||
print(f"[06] 실거래 루프 시작 · sleep={sleep_sec}s")
|
||||
while True:
|
||||
self.run_once()
|
||||
time.sleep(sleep_sec)
|
||||
|
||||
|
||||
def _fmt_paper_krw(value: float) -> str:
|
||||
"""원화 표시."""
|
||||
return f"₩{float(value):,.0f}"
|
||||
|
||||
Reference in New Issue
Block a user