Exchange Keys — /api/exchange-keys
거래소/브로커 API 키를 사용자별로 등록/조회/삭제하고, 등록 전 dry-run으로 검증하는 라우트입니다.
공통
- 베이스 경로:
/api/exchange-keys - 인증: 모든 라우트 JWT 필요
- 암호화: API key/secret은
AUTH_SECRET_KEY+ENCRYPTION_SALT기반 Fernet으로 암호화 후 저장 - 멀티유저 격리:
WHERE user_id = current_user.id
Phase 3 제약
현재 paper_mode=true 키만 등록 가능합니다. paper_mode=false 시도는 422 에러. P4-01 이후 라이브 키 허용 (admin role + 확인 모달 필요).
POST /api/exchange-keys
키 등록.
요청 (ExchangeKeyCreate)
| 필드 | 타입 | 필수 | 기본 | 설명 |
|---|---|---|---|---|
asset_class | crypto|us_equity|kr_equity | N | crypto | |
exchange | string (≤32) | Y | — | binance/alpaca/kis 등 |
api_key | string | Y | — | 평문 (서버에서 암호화) |
api_secret | string | Y | — | 평문 (서버에서 암호화) |
base_url | string (≤255) | N | — | Alpaca paper/live 분기 |
account_no | string (≤32) | KIS만 Y | — | KIS 계좌번호 |
account_product_code | string (≤8) | KIS만 Y | — | KIS 상품코드 (예: 01) |
paper_mode | bool | N | true | 현재 false 시 422 |
label | string | N | default | 키 별칭 |
permissions | string | N | trade_only | 권한 메모 |
응답 201 (ExchangeKeyResponse)
{
"id": 17,
"asset_class": "us_equity",
"exchange": "alpaca",
"label": "default",
"permissions": "trade_only",
"is_active": true,
"paper_mode": true,
"api_key_masked": "PKAB...x3f2",
"account_no_masked": null,
"account_product_code": null,
"created_at": "2026-04-26T10:00:00Z"
}
api_key_masked는 앞 4 + ... + 뒤 4 형식. 전체 키는 응답에 절대 포함되지 않음.
에러
- 422 —
paper_mode=false, KIS 필수 필드 누락
cURL
curl -X POST http://localhost:8000/api/exchange-keys \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"asset_class": "us_equity",
"exchange": "alpaca",
"api_key": "PKABCD...",
"api_secret": "secret123...",
"paper_mode": true,
"label": "alpaca-paper"
}'
KIS:
curl -X POST http://localhost:8000/api/exchange-keys \
-H "Authorization: Bearer $JWT" \
-d '{
"asset_class": "kr_equity",
"exchange": "kis",
"api_key": "...",
"api_secret": "...",
"account_no": "12345678",
"account_product_code": "01",
"paper_mode": true
}'
GET /api/exchange-keys
호출자의 키 목록 (마스킹된 채로).
Query 파라미터
| 파라미터 | 기본 | 설명 |
|---|---|---|
asset_class | — | 필터 |
응답 200
[
{...ExchangeKeyResponse...}
]
cURL
curl -H "Authorization: Bearer $JWT" \
http://localhost:8000/api/exchange-keys
DELETE /api/exchange-keys/{id}
본인 키 삭제 (cascade — 연결된 BotManager broker 인스턴스도 release).
응답 204
본문 없음.
에러
- 404 — 다른 user의 키 또는 미존재
cURL
curl -X DELETE -H "Authorization: Bearer $JWT" \
http://localhost:8000/api/exchange-keys/17
POST /api/exchange-keys/test
키를 등록하지 않고 dry-run으로 검증합니다. 임시 broker 인스턴스를 만들어 get_account() 호출 후 즉시 폐기.
요청 (ExchangeKeyTestRequest)
ExchangeKeyCreate와 동일하되 label/permissions 없음.
응답 200 (ExchangeKeyTestResponse)
성공 시:
{
"ok": true,
"asset_class": "us_equity",
"paper_mode": true,
"equity": 100000.0,
"buying_power": 200000.0,
"currency": "USD",
"error": null
}
실패 시:
{
"ok": false,
"asset_class": "us_equity",
"paper_mode": true,
"equity": null,
"buying_power": null,
"currency": null,
"error": "401 Unauthorized: invalid api key"
}
에러
- 422 — 페이로드 invalid
- 응답 자체는 200이지만
ok=false+error필드 사용
cURL
curl -X POST http://localhost:8000/api/exchange-keys/test \
-H "Authorization: Bearer $JWT" \
-d '{
"asset_class": "us_equity",
"exchange": "alpaca",
"api_key": "PKABCD...",
"api_secret": "secret123...",
"paper_mode": true
}'
멀티유저 격리
- 모든 라우트가
WHERE user_id = current_user.id강제 - 다른 user 키 조회/삭제 시 일관되게 404
- BotRegistry는 user별로 broker 인스턴스를 별도 관리 → 키가 누출돼도 다른 user의 broker로 매핑 불가
비고
AUTH_SECRET_KEY또는ENCRYPTION_SALT변경 시 기존 키는 복호화 불가 (mask는****표시). 안전을 위해 시크릿 로테이션 후 재등록 필요- 키 권한은 거래소 측에서 trade-only로 제한 권장 (출금 권한은 절대 부여하지 않음)
- 정상적인 사용에서 응답에 plaintext 키가 포함되는 경우는 없음