17 KiB
17 KiB
API Contracts
- Related REQ:
REQ-001,REQ-002,REQ-003,REQ-004
공통 원칙
- 응답은 JSON 기반으로 반환한다.
- 인증이 필요한 엔드포인트는
Authorization: Bearer <token>을 사용한다. - 권한 계층은
read<trade-write<admin이다. admin이 아닌 요청은 포트폴리오 리소스 접근 시owner_user_id소유권 검사를 추가로 통과해야 한다.- 에러 응답은 HTTP 상태 코드와 함께 원인 메시지를 제공한다.
- Base URL 예시는
http://localhost:8000을 기준으로 한다. - JSON 예시는 대표 형태이며 일부 선택 필드를 생략할 수 있다.
Contract Surface
공개 문서 엔드포인트
GET /docsGET /redocGET /openapi.json
헬스
GET /health(인증 없음)- 정상 응답 예시:
{"status": "ok"}
인증
POST /auth/loginGET /auth/tokensPOST /auth/tokensDELETE /auth/tokens/{token_id}POST /auth/botsGET /auth/botsPOST /auth/bots/{bot_id}/tokensGET /auth/bots/{bot_id}/tokensDELETE /auth/bots/{bot_id}/tokens/{token_id}DELETE /auth/bots/{bot_id}
포트폴리오
GET /portfoliosGET /portfolios/{portfolio_id}POST /portfoliosPATCH /portfolios/{portfolio_id}POST /portfolios/{portfolio_id}/buyPOST /portfolios/{portfolio_id}/sellPOST /portfolios/{portfolio_id}/investPOST /portfolios/{portfolio_id}/transferPOST /portfolios/{portfolio_id}/resetDELETE /portfolios/{portfolio_id}
포트폴리오 댓글 (REQ-001)
POST /portfolios/{portfolio_id}/commentsGET /portfolios/{portfolio_id}/commentsPATCH /portfolios/{portfolio_id}/comments/{comment_id}DELETE /portfolios/{portfolio_id}/comments/{comment_id}
전략셋
GET /strategy-setsPOST /strategy-setsGET /strategy-sets/{strategy_set_id}PATCH /strategy-sets/{strategy_set_id}DELETE /strategy-sets/{strategy_set_id}
사용자
GET /usersGET /users/{user_id}POST /usersPATCH /users/{user_id}DELETE /users/{user_id}
오류 처리 기준
| 시나리오 | HTTP |
|---|---|
| 인증 헤더 누락/형식 오류 | 401 |
| 유효하지 않은 토큰 | 401 |
| 권한 부족 | 403 |
| 유효성 검증 실패 | 422 |
| 리소스 없음 | 404 |
| 내부 예외 | 500 |
상세 엔드포인트 명세
헬스
GET /health
- 인증 없음
- 응답
200:
{"status": "ok"}
포트폴리오
GET /portfolios
- Auth:
read - Ownership:
admin은 전체 조회, 그 외는 본인 소유(owner_user_id == auth_context.user_id)만 조회 - Query:
broker: string | nullstatus_filter: string | null
- 응답
200:PortfolioListItem[]
예시:
curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled" \
-H "Authorization: Bearer <READ_TOKEN>"
[
{
"portfolio_id": "krw-btc",
"broker": "upbit",
"status": "enabled",
"name": "Bitcoin",
"symbol": "KRW-BTC",
"cash_balance": 1000000.0,
"asset_quantity": 0.1,
"price": 100000000.0,
"profit_buffer": 0.0,
"active_strategy_id": "stv-001",
"avabile_strategies": ["stv-001"],
"created_at": "2026-04-03T00:00:00Z",
"updated_at": "2026-04-03T00:00:00Z",
"last_trade_at": "2026-04-03T00:00:00Z",
"total_value": 11000000.0
}
]
GET /portfolios/{portfolio_id}
- Auth:
read - Ownership:
admin이 아니고 소유주 불일치 시403 - Query:
recent_trades: int = 5
- 응답
200:PortfolioDetailResponse
curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \
-H "Authorization: Bearer <READ_TOKEN>"
{
"portfolio": {
"portfolio_id": "krw-btc",
"broker": "upbit",
"status": "enabled",
"name": "Bitcoin",
"symbol": "KRW-BTC",
"owner_user_id": "user-1",
"cash_balance": 1000000.0,
"asset_quantity": 0.1,
"price": 100000000.0,
"profit_buffer": 0.0,
"active_strategy_id": "stv-001",
"avabile_strategies": ["stv-001"],
"created_at": "2026-04-03T00:00:00Z",
"updated_at": "2026-04-03T00:00:00Z",
"last_trade_at": "2026-04-03T00:00:00Z"
},
"recent_entries": [
{
"portfolio_id": "krw-btc",
"recorded_at": "2026-04-03T00:00:00Z",
"action": "buy",
"price": 100000000.0,
"quantity": 0.1,
"fee_amount": 0.0,
"tax_amount": 0.0,
"cash_balance_after": 1000000.0,
"asset_quantity_after": 0.1,
"total_value_after": 11000000.0,
"profit_buffer_after": 0.0,
"strategy_set_id": "stv-001",
"note": ""
}
],
"strategy_set": {
"id": "stv-001",
"name": "target_value",
"config": "{}",
"nick": "target_value:99914b932bd3",
"created_at": "2026-04-03T00:00:00Z"
},
"live_quantity": null
}
POST /portfolios
- Auth:
trade-write - Ownership: 요청 body로 owner를 받지 않고 인증 컨텍스트 사용자로
owner_user_id를 강제 설정 - Body:
CreatePortfolioRequest - 응답
201:PortfolioDetailResponse
curl -X POST "http://localhost:8000/portfolios" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"portfolio_id": "krw-btc",
"broker": "upbit",
"status": "enabled",
"name": "Bitcoin",
"symbol": "KRW-BTC",
"strategy_name": "target_value",
"strategy_params": {},
"active_strategy_id": null,
"avabile_strategies": null,
"cash_balance": 1000000,
"asset_quantity": 0,
"asset_price": 0,
"profit_buffer": 0
}'
PATCH /portfolios/{portfolio_id}
- Auth:
trade-write - Body:
UpdatePortfolioRequest - 응답
200:PortfolioDetailResponse
curl -X PATCH "http://localhost:8000/portfolios/krw-btc" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"broker": "upbit",
"name": "Bitcoin",
"symbol": "KRW-BTC"
}'
거래/자금 이동 엔드포인트
POST /portfolios/{portfolio_id}/buy:BuyRequest->PortfolioDetailResponsePOST /portfolios/{portfolio_id}/sell:SellRequest->PortfolioDetailResponsePOST /portfolios/{portfolio_id}/invest:InvestRequest->PortfolioDetailResponsePOST /portfolios/{portfolio_id}/transfer:TransferRequest->PortfolioDetailResponsePOST /portfolios/{portfolio_id}/reset:ResetPortfolioRequest->PortfolioDetailResponseDELETE /portfolios/{portfolio_id}: admin only, querykeep_ledger: bool = false, 응답204
대표 요청:
curl -X POST "http://localhost:8000/portfolios/krw-btc/buy" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}'
curl -X POST "http://localhost:8000/portfolios/krw-btc/sell" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}'
curl -X POST "http://localhost:8000/portfolios/krw-btc/invest" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"movement_type": "deposit", "amount": 50000, "note": ""}'
curl -X POST "http://localhost:8000/portfolios/krw-btc/transfer" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"direction": "to-buffer", "amount": 10000, "note": ""}'
curl -X POST "http://localhost:8000/portfolios/krw-btc/reset" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"infer_symbol": "KRW-BTC"}'
curl -X DELETE "http://localhost:8000/portfolios/krw-btc?keep_ledger=false" \
-H "Authorization: Bearer <ADMIN_TOKEN>"
포트폴리오 댓글 (REQ-001)
POST /portfolios/{portfolio_id}/comments- Auth:
trade-write - Body:
CommentCreateRequest - Response
201:CommentResponse
- Auth:
GET /portfolios/{portfolio_id}/comments- Auth:
read - Response
200:CommentResponse[]
- Auth:
PATCH /portfolios/{portfolio_id}/comments/{comment_id}- Auth:
trade-write - Body:
CommentUpdateRequest - Response
200:CommentResponse
- Auth:
DELETE /portfolios/{portfolio_id}/comments/{comment_id}- Auth:
trade-write - Response
204
- Auth:
예시:
curl -X POST "http://localhost:8000/portfolios/krw-btc/comments" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"content": "Bought more BTC today.\nLooking good."}'
curl -X GET "http://localhost:8000/portfolios/krw-btc/comments" \
-H "Authorization: Bearer <READ_TOKEN>"
curl -X PATCH "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"content": "Updated note: BTC rally continues."}'
curl -X DELETE "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>"
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"portfolio_id": "krw-btc",
"content": "Bought more BTC today.\nLooking good.",
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
}
전략셋
GET /strategy-sets
- Auth:
read - Response
200:StrategySetResponse[]
POST /strategy-sets
- Auth:
trade-write - Body:
CreateStrategySetRequest - Response
201:StrategySetResponse - 기존
(name, config)쌍이 있으면 기존 strategy set 반환
PATCH /strategy-sets/{strategy_set_id}
- Auth:
trade-write - Body:
UpdateStrategySetRequest - Response
200:StrategySetResponse
DELETE /strategy-sets/{strategy_set_id}
- Auth:
trade-write - Response
204 - in use 상태면
409
대표 요청:
curl -X POST "http://localhost:8000/strategy-sets" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name": "target_value", "config": "{}"}'
curl -X PATCH "http://localhost:8000/strategy-sets/stv-001" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"config": "{\"rebalance\": true}"}'
curl -X DELETE "http://localhost:8000/strategy-sets/stv-001" \
-H "Authorization: Bearer <TRADE_WRITE_TOKEN>"
인증
POST /auth/login
- Auth 없음
- Body:
LoginRequest - Response
200:TokenResponse - 잘못된 username/password면
401
curl -X POST "http://localhost:8000/auth/login" \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "bootstrap123"
}'
{
"access_token": "plain-token-value",
"token_type": "bearer",
"expires_at": "2026-04-03T12:00:00Z",
"permission": "admin",
"user": {
"id": "user-1",
"username": "admin",
"email": "admin@example.com",
"permission": "admin",
"is_active": true,
"created_at": "2026-04-03T00:00:00Z",
"updated_at": "2026-04-03T00:00:00Z",
"last_login_at": "2026-04-03T00:00:00Z"
}
}
GET /auth/tokens
- Auth: DB-issued
read이상 토큰 - Response
200:AuthTokenResponse[]
POST /auth/tokens
- Auth: DB-issued
read이상 토큰 - Body:
CreateClientTokenRequest - Response
201:IssuedClientTokenResponse - 요청 permission이 현재 permission을 넘으면
403
DELETE /auth/tokens/{token_id}
- Auth: DB-issued
read이상 토큰 - Response
204 - 미존재 시
404
대표 요청:
curl -X POST "http://localhost:8000/auth/tokens" \
-H "Authorization: Bearer <SESSION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name": "cli", "permission": "trade-write", "expires_in_days": 30}'
봇 주체와 봇 토큰
POST /auth/bots: admin only,CreateBotPrincipalRequest->BotPrincipalResponseGET /auth/bots: admin only,is_active필터 지원GET /auth/bots/{bot_id}: admin onlyDELETE /auth/bots/{bot_id}: admin only, bot 비활성화 및 연결 토큰 revoke, 응답200POST /auth/bots/{bot_id}/tokens: admin only,CreateBotTokenRequest->IssuedBotTokenResponseGET /auth/bots/{bot_id}/tokens: admin only,AuthTokenResponse[]DELETE /auth/bots/{bot_id}/tokens/{token_id}: admin only,204
대표 요청:
curl -X POST "http://localhost:8000/auth/bots" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name": "rebalance-bot", "permission": "trade-write", "description": "nightly jobs"}'
curl -X POST "http://localhost:8000/auth/bots/bot-1/tokens" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"name": "nightly", "permission": "trade-write", "expires_in_days": 90}'
사용자
모든 /users 엔드포인트는 admin only이다.
GET /users:UserResponse[]GET /users/{user_id}:UserResponse, 미존재 시404POST /users:CreateUserRequest->UserResponse, duplicate username/email이면409PATCH /users/{user_id}:UpdateUserRequest->UserResponse, duplicate면409DELETE /users/{user_id}:204
bootstrap 규칙:
- DB persistence가 활성이고
PORTFOLIOHUB_BOOTSTRAP_USERNAME,PORTFOLIOHUB_BOOTSTRAP_PASSWORD가 설정되면,users테이블이 비어 있을 때만 startup에서 초기 admin user를 생성한다.
대표 요청:
curl -X GET "http://localhost:8000/users" \
-H "Authorization: Bearer <ADMIN_TOKEN>"
curl -X POST "http://localhost:8000/users" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"email": "alice@example.com",
"password": "password123",
"permission": "read",
"is_active": true
}'
요청/응답 모델 요약
Request Models
CreatePortfolioRequest
portfolio_id: strbroker: str = "kis"status: str = "enabled"name: str | nullsymbol: strstrategy_name: str = "target_value"strategy_params: object = {}active_strategy_id: str | nullavabile_strategies: string[] | nullcash_balance: float >= 0asset_quantity: float >= 0asset_price: float >= 0profit_buffer: float >= 0
UpdatePortfolioRequest
broker: str | nullstatus: str | nullname: str | nullsymbol: str | nullstrategy_name: str | nullstrategy_params: object | nullactive_strategy_id: str | nullavabile_strategies: string[] | nullasset_price: float >= 0 | null
BuyRequest / SellRequest
quantity: float > 0
InvestRequest
movement_type: "deposit" | "profit" | "withdraw"amount: float > 0note: str = ""
TransferRequest
direction: "to-buffer" | "from-buffer"amount: float > 0note: str = ""
ResetPortfolioRequest
infer_symbol: str | null
CommentCreateRequest / CommentUpdateRequest
content: str
LoginRequest
username: strpassword: str
CreateClientTokenRequest
name: strpermission: "read" | "trade-write" | "admin" | nullexpires_in_days: int | null
CreateBotPrincipalRequest
name: strpermission: "read" | "trade-write" | "admin"description: str | null
CreateBotTokenRequest
name: strpermission: "read" | "trade-write" | "admin" | nullexpires_in_days: int | null
CreateUserRequest
username: stremail: str | nullpassword: strpermission: "read" | "trade-write" | "admin"is_active: bool
UpdateUserRequest
username: str | nullemail: str | nullpassword: str | nullpermission: "read" | "trade-write" | "admin" | nullis_active: bool | null
CreateStrategySetRequest
name: strconfig: str
UpdateStrategySetRequest
name: str | nullconfig: str | null
Response Models
PortfolioListItem
- portfolio summary fields + computed
total_value
PortfolioSummary
portfolio_id: strbroker: strstatus: strname: strsymbol: strcash_balance: floatasset_quantity: floatprice: floatprofit_buffer: floatactive_strategy_id: str | nullavabile_strategies: string[] | nullcreated_at: datetime | nullupdated_at: datetime | nulllast_trade_at: datetime | null
PortfolioDetailResponse
portfolio: PortfolioSummaryrecent_entries: LedgerEntryResponse[]strategy_set: StrategySetResponse | nulllive_quantity: float | null
StrategySetResponse
id: strname: strconfig: strnick: strcreated_at: datetime | null
CommentResponse
id: strportfolio_id: strcontent: strcreated_at: datetime | nullupdated_at: datetime | null
BotPrincipalResponse
id: strname: strpermission: "read" | "trade-write" | "admin"description: str | nullis_active: boolcreated_at: datetime | nullupdated_at: datetime | nulldeactivated_at: datetime | null
ErrorResponse
detail: str
공통 오류 예시
Missing token
{"detail": "Not authenticated"}
Invalid token
{"detail": "Invalid token"}
Insufficient permission
{"detail": "Insufficient permission"}
문서 동기화 규칙
app/api/또는app/schemas/변경 시 본 문서를 같은 변경 단위에서 갱신한다.REQ-002태그 검색 API가 구현되면 요청/응답 파라미터와 필터 의미를 본 문서에 추가한다.
Open Implementation Points
- 에러 응답 body를 공통 포맷(
error_code,message,details)으로 강제할지 결정한다. - idempotency가 필요한 쓰기 API(예: 재시도 가능 매매 요청)의 키 정책을 확정한다.