maxt 0.1.0

One Rust API for Upbit, Bithumb, Binance, and Hyperliquid market data, accounts, and orders.
Documentation
# 공통 API 레퍼런스

[English]common-api.md | [한국어]common-api.ko.md

`Client<A>``Adapter` 구현에 공통 계약을 적용합니다. 거래소 전용 메서드는
`Client::adapter()`가 반환한 `A`에서 호출합니다. 런타임에 거래소를 선택하려면
`Client<Box<dyn Adapter>>`를 사용합니다.

## API 영역

인증 정보는 `Client::new(adapter)` 호출 전에 어댑터에 설정합니다.

| 영역 | 메서드 |
| --- | --- |
| Client | `exchange`, `supports`, `adapter`, `into_adapter` |
| 공개 REST | `markets`, `trades`, `order_book`, `ticker`, `candles`, `funding_rates` |
| 공개 스트림 | `subscribe`, `subscribe_with` |
| 비공개 조회 | `balances`, `open_orders`, `open_orders_on`, `positions`, `positions_on`, `margin_summary`, `funding_payments` |
| 비공개 스트림 | `subscribe_account`, `subscribe_account_with` |
| 비공개 변경 | `place_order`, `cancel_order`, `set_margin` |

공개 REST와 시장 스트림에는 인증 정보가 필요하지 않습니다. 거래소별
`MarketKind`와 기능 지원 범위는 [거래소 지원](providers.ko.md)을 참고하세요.

## 데이터 계약

| 타입·필드 | 계약 |
| --- | --- |
| `Market` | `{ exchange, kind, base, quote }`; `base`, `quote`는 대문자 |
| `Trade` | REST 결과는 최신순; `taker_side`는 taker 주문 방향 |
| `Candle` | `open_time ASC`; `open_time`은 간격 시작 시각 |
| `OrderBook::bids` | `price DESC` |
| `OrderBook::asks` | `price ASC` |
| `Ticker` | 거래소 요약값; 필드 출처와 집계 구간은 거래소별 계약 |
| `Decimal` | 96-bit 계수(coefficient), scale `0..=28`; 가격·수량·비율·금액에 `f64` 미사용 |
| `Option<T>` | 거래소가 값을 주지 않으면 `None`; 추론 또는 0 대입 없음 |
| `Timestamp` | Unix epoch 기준 UTC nanosecond; `Display`는 millisecond RFC 3339; 정확한 값은 `as_nanos()` |

`timestamp`가 로컬 조회 시각으로 정의된 경우, `maxt`가 응답을 읽은 시각이며 거래소
이벤트 시각이 아닙니다.

`parse_decimal_exact()`는 일반 표기 또는 과학적 표기 값을 `Decimal`로 정확히
표현할 수 있을 때만 반환합니다. 반올림하거나 잘라내지 않고 오류를 반환합니다.

## 공개 REST

| 메서드 | 계약 |
| --- | --- |
| `markets(kind)` | 상장된 `MarketInfo`; 유효한 `kind`에 상장이 없으면 `[]` |
| `trades(market, limit)` | 최근 체결; 최신순 |
| `order_book(market, depth)` | 단일 스냅샷(snapshot); `depth != None`이면 `bids.len() <= depth`, `asks.len() <= depth` |
| `ticker(market)` | 거래소 요약값 1건 |
| `candles(request)` | 과거 캔들; `open_time ASC` |
| `funding_rates(request)` | 공개 무기한 선물 funding rate `Page<FundingRate>` |

`limit`, 호가 `depth`, `timestamp` 출처는 거래소별 계약입니다.

## 캔들

### 간격

`supports(Feature::Candles) == true`는 다음 간격을 보장합니다.

`Min1`, `Min3`, `Min5`, `Min15`, `Min30`, `Hour1`, `Hour4`, `Day1`, `Week1`,
`Month1`

나머지 간격은 거래소별 기능입니다. 거래소 매핑이 없으면
`Error::Unsupported`를 반환하며 다른 간격으로 반올림하지 않습니다.

### `CandleRequest`

결과 정렬은 항상 `open_time ASC`입니다.

| 설정 필드 | 선택 |
| --- | --- |
| `from`, `to`, `limit` | `from <= open_time < to`; 앞 `limit` |
| `from`, `to` | `from <= open_time < to` |
| `from`, `limit` | `from <= open_time`; 앞 `limit` |
| `to`, `limit` | `open_time < to`; 뒤 `limit` |
| `limit` |`limit` |
| `from` | `from <= open_time` |
| `to` | `open_time < to`; 거래소 페이지 1개 |
| 없음 | 최신 거래소 페이지 1개 |

| 검증 | 결과 |
| --- | --- |
| `limit == 0` | `Error::InvalidRequest` |
| `from >= to` | `Error::InvalidRequest` |
| 거래소 호출 수 | 요청당 `<= 100` |
| 예상 캔들 수 `> 100 * provider_page_cap` | 네트워크 I/O 전 거절 |

| `Candle::closed` 출처 | 계약 |
| --- | --- |
| REST | `interval_end <= local_read_time` |
| 스트림 | 거래소별 계약; 거래소 레퍼런스 참고 |

## 스트림

### `Subscription`

| 입력 | 계약 |
| --- | --- |
| 구독 집합 | `markets × feeds` |
| 중복 market 또는 feed | 제거; 최초 삽입 순서 유지 |
| `markets.is_empty() || feeds.is_empty()` | `Error::InvalidRequest` |
| 논리 스트림 | 하나 이상의 WebSocket 연결 |

### `StreamConfig`

| 필드 | 기본값 | 계약 |
| --- | ---: | --- |
| `max_reconnect_attempts` | `None` | 유한한 재연결 횟수 제한 없음 |
| `initial_reconnect_delay_ms` | `1_000` | 첫 재연결 대기 시간 |
| `max_reconnect_delay_ms` | `30_000` | 재연결 대기(backoff) 상한 |
| `idle_timeout_ms` | `30_000` | 적용값: `max(idle_timeout_ms, min_idle_timeout)` |
| `buffer_size` | `4_096` | `0 → 1` |
| `overflow` | `Backpressure` | 버퍼가 가득 차면 생산자(producer) 대기 |

| overflow 정책 | 버퍼가 가득 찼을 때 |
| --- | --- |
| `Backpressure` | 소켓 읽기 일시 중지; 의도적인 이벤트 손실 없음 |
| `DropNewest` | 새 데이터와 오류 폐기; 재연결 알림 보존 |

`DropNewest`는 대체 가능한 스냅샷에만 사용합니다. 체결과 확정 캔들 이벤트는
대체할 수 없습니다.

### 상태

| 상태 | 계약 |
| --- | --- |
| `Some(Ok(event))` | 이벤트 |
| `Some(Err(error))` | 비종료 오류 |
| `None` | 스트림 종료 |
| `MarketEvent::Reconnected` | 연결 단절 중 시장 이벤트 유실 |
| `AccountEvent::Reconnected` | 연결 단절 중 계좌 이벤트 유실 |
| 내장 스트림 `Drop` | 스트림 소스(source) 폐기; 내장 연결 작업 전체에 종료 신호 전달 |
| 사용자 스트림 `Drop` | 스트림 소스 폐기; 정리는 생산자 책임 |
| `close().await` | 어댑터의 비동기 정리 완료 후 스트림 소스 폐기 |

재연결 횟수는 내부 연결별로 적용하며 정상 트래픽 이후에도 초기화하지 않습니다.
`AccountEvent::Reconnected` 이후 `balances()`와 `open_orders()`로 상태를 다시
조회합니다.

## 기능 확인

`Client::supports(feature)`는 네트워크 I/O를 수행하지 않습니다.

| 상태 | 계약 |
| --- | --- |
| 작업 매핑됨; 필수 인증 정보 있음 | `supports(feature) == true`; 인자와 거래소 권한은 별도 검증 |
| 비공개 작업 매핑됨; 인증 정보 없음 | `supports(feature) == false`; 호출 결과 `Error::Auth` |
| 구조적으로 미지원 | `supports(feature) == false`; 호출 결과 `Error::Unsupported` |

## 오류와 재시도

| `Error` | 발생 지점 | `is_retryable()` |
| --- | --- | --- |
| `InvalidRequest` | 네트워크 I/O 전 로컬 검증 | `false` |
| `Unsupported` | 작업 매핑 없음 | `false` |
| `Adapter` | 어댑터 또는 외부 디스패처(dispatcher) 계약 위반 | `false` |
| `Auth` | 로컬에서 인증 요청 생성 불가 | `false` |
| `Exchange` | 거래소 오류 응답 | `kind.is_retryable()` |
| `Transport` | DNS, TLS, socket, timeout | `true` |
| `Decode` | 응답 schema 불일치 | `false` |

`maxt`는 REST 호출을 재시도하거나 속도를 제한하지 않습니다. 거래소 한도와
재시도 대기는 애플리케이션에서 적용합니다. `place_order` 또는 `cancel_order` 후
`Error::Transport`가 발생하면 결과를 알 수 없습니다. 재시도 전에 계좌 또는 주문
상태를 조회하세요.

## 비공개 계좌와 주문

| 타입·메서드 | 계약 |
| --- | --- |
| `open_orders*` | 특정 시점의 스냅샷; 거래소의 모든 페이지 순회는 보장하지 않음 |
| `OrderRequest::size` | `Size::Base` 또는 `Size::Quote` |
| 주문 정밀도 | `MarketInfo`에 공통 호가 단위(tick size), 수량 단위(lot size), 최소 명목가치(minimum notional) 없음 |
| `cancel_order` | 체결과 경합 가능; 반환된 `Order`는 거래소 응답이며 최종 체결 상태가 없을 수 있음 |
| `positions*` | `position.quantity == 0` 행 제거 |
| `MarginSummary` | 거래소 미제공 값은 `None` |
| `FundingPayment::amount < 0` | 계좌가 funding 지급 |

주문 값은 `Decimal`로 구성합니다. 지원 주문 형식과 검증 규칙은 거래소별
계약입니다.

### `HistoryRequest`

| 필드·상태 | 계약 |
| --- | --- |
| `from` | `from <= item.timestamp` |
| `to` | `item.timestamp < to` |
| `cursor` | 불투명 재개 지점(opaque resume point); `cursor != None``from` 무시; 같은 어댑터에 변경 없이 전달 |
| `limit` | 페이지 크기 목표값; 최대값 아님 |
| `limit == 0` | 네트워크 I/O 전 `Error::InvalidRequest` |
| 같은 `timestamp` 그룹이 `limit` 경계를 넘음 | 그룹을 다음 페이지로 미루면 `items.len() < limit`; 첫 그룹만으로 `limit`를 넘으면 `items.len() > limit` |
| 다음 요청 | `request.cursor = page.next` |
| 계속 | `page.next.is_some()`; `items.is_empty()`여도 계속 |
| 종료 | `page.next == None` |

### `MarginRequest`

| 상태 | 계약 |
| --- | --- |
| 로컬 검증 | `leverage.is_some() || margin_mode.is_some()` |
| 거래소 검증 | 한 필드 또는 두 필드를 요구할 수 있음 |
| `set_margin()` | 두 변경의 원자성(atomicity) 또는 롤백(rollback) 보장 없음 |

## 거래소 전용 API

`Client::adapter()`가 반환한 `&A`에서 거래소 전용 일괄 조회(batch), 원본
context, alert, ledger 메서드를 호출합니다. 목록은 거래소별 레퍼런스를 참고하세요.

## 외부 어댑터

외부 타입은 `exchange()`와 `supports()`를 구현해 `Adapter`로 사용할 수 있습니다.
지원 메서드만 재정의하며 나머지는 `Error::Unsupported`를 반환합니다. 위 공통
계약을 유지해야 합니다. 새 거래소를 추가하려면 `maxt`에 새 `Exchange` 항목이
필요합니다.

[어댑터 체크리스트](../CONTRIBUTING.ko.md#어댑터-체크리스트)