feat(spot): 3단계 운영 파이프라인 — composite_v3 + MTF paper/live

MTF 필터 백테스트, paper/live 체결, 빗썸 Private API 연동 및 운영 스크립트·설계 문서를 추가해 2단계 전략을 실거래 단계에 연결한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
xavis
2026-06-12 18:27:34 +09:00
parent 2d515dd669
commit 58802bdc5f
19 changed files with 1485 additions and 10 deletions

View File

@@ -0,0 +1,60 @@
"""빗썸 Private API JWT 인증."""
from __future__ import annotations
import hashlib
import json
import uuid
from typing import Any
from urllib.parse import urlencode
import jwt
def build_jwt_token(
access_key: str,
secret_key: str,
*,
params: dict[str, Any] | None = None,
) -> str:
"""빗썸 v2.1 JWT Bearer 토큰을 생성한다.
Args:
access_key: API Access Key.
secret_key: API Secret Key.
params: POST/DELETE body 또는 query 파라미터. 있으면 query_hash 포함.
Returns:
JWT 문자열 (Bearer 접두사 없음).
"""
import time
payload: dict[str, Any] = {
"access_key": access_key,
"nonce": str(uuid.uuid4()),
"timestamp": round(time.time() * 1000),
}
if params:
query = urlencode(params, doseq=True).encode()
payload["query_hash"] = hashlib.sha512(query).hexdigest()
payload["query_hash_alg"] = "SHA512"
return jwt.encode(payload, secret_key, algorithm="HS256")
def auth_headers(
access_key: str,
secret_key: str,
*,
params: dict[str, Any] | None = None,
) -> dict[str, str]:
"""Authorization 헤더 dict를 반환한다."""
token = build_jwt_token(access_key, secret_key, params=params)
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
def dumps_params(params: dict[str, Any]) -> str:
"""주문 body JSON 직렬화 (키 순서 유지)."""
return json.dumps(params, separators=(",", ":"))

View File

@@ -0,0 +1,143 @@
"""빗썸 Private REST API — 잔고·주문."""
from __future__ import annotations
import logging
import time
from typing import Any
import requests
from deepcoin.api.bithumb_auth import auth_headers, dumps_params
logger = logging.getLogger(__name__)
class BithumbPrivateClient:
"""빗썸 v2.1 Private API 클라이언트."""
def __init__(
self,
access_key: str,
secret_key: str,
base_url: str = "https://api.bithumb.com",
sleep_sec: float = 0.35,
retries: int = 3,
) -> None:
"""클라이언트를 초기화한다.
Args:
access_key: API Access Key.
secret_key: API Secret Key.
base_url: API 베이스 URL.
sleep_sec: 연속 요청 간 대기(초).
retries: 실패 시 재시도 횟수.
"""
if not access_key or not secret_key:
raise ValueError("BITHUMB_ACCESS_KEY / BITHUMB_SECRET_KEY 가 필요합니다.")
self.access_key = access_key
self.secret_key = secret_key
self.base_url = base_url.rstrip("/")
self.sleep_sec = sleep_sec
self.retries = retries
self._session = requests.Session()
def _request(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""인증 요청을 수행한다."""
url = f"{self.base_url}{path}"
body = dumps_params(params) if params else None
headers = auth_headers(
self.access_key,
self.secret_key,
params=params,
)
last_error: Exception | None = None
for attempt in range(1, self.retries + 1):
try:
response = self._session.request(
method,
url,
data=body,
headers=headers,
timeout=30,
)
if response.status_code == 429:
wait = self.sleep_sec * attempt * 3
logger.warning("Rate limit 429 — %ss 대기", wait)
time.sleep(wait)
continue
response.raise_for_status()
payload = response.json()
time.sleep(self.sleep_sec)
if not isinstance(payload, (dict, list)):
raise ValueError(f"Unexpected response: {type(payload)}")
return payload if isinstance(payload, dict) else {"data": payload}
except Exception as exc:
last_error = exc
wait = self.sleep_sec * attempt * 2
logger.warning("Private API failed (%s/%s): %s", attempt, self.retries, exc)
time.sleep(wait)
if last_error is not None:
raise last_error
return {}
def get_accounts(self) -> list[dict[str, Any]]:
"""전체 계좌(잔고)를 조회한다."""
payload = self._request("GET", "/v1/accounts")
if isinstance(payload, list):
return payload
return payload.get("data", payload) if isinstance(payload.get("data"), list) else []
def get_balance(self, currency: str) -> tuple[float, float]:
"""통화별 잔고를 반환한다.
Returns:
(available, locked) 수량 또는 원화.
"""
currency = currency.upper()
for row in self.get_accounts():
if str(row.get("currency", "")).upper() == currency:
return float(row.get("balance", 0)), float(row.get("locked", 0))
return 0.0, 0.0
def market_buy_krw(self, market: str, krw_amount: float) -> dict[str, Any]:
"""시장가 매수 (원화 금액).
Args:
market: 예) KRW-BTC.
krw_amount: 매수 원화 금액.
Returns:
주문 응답 dict.
"""
params = {
"market": market,
"side": "bid",
"price": str(int(krw_amount)),
"ord_type": "price",
}
return self._request("POST", "/v1/orders", params=params)
def market_sell_volume(self, market: str, volume: float) -> dict[str, Any]:
"""시장가 매도 (코인 수량).
Args:
market: 예) KRW-BTC.
volume: 매도 수량.
Returns:
주문 응답 dict.
"""
params = {
"market": market,
"side": "ask",
"volume": f"{volume:.8f}".rstrip("0").rstrip("."),
"ord_type": "market",
}
return self._request("POST", "/v1/orders", params=params)