refactor docs into technical specs
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
# API Contracts
|
||||
|
||||
- Related REQ: `REQ-001`, `REQ-002`, `REQ-003`
|
||||
|
||||
## 공통 원칙
|
||||
- 응답은 JSON 기반으로 반환한다.
|
||||
- 인증이 필요한 엔드포인트는 `Authorization: Bearer <token>`을 사용한다.
|
||||
- 권한 계층은 `read` < `trade-write` < `admin`이다.
|
||||
- 에러 응답은 HTTP 상태 코드와 함께 원인 메시지를 제공한다.
|
||||
- Base URL 예시는 `http://localhost:8000`을 기준으로 한다.
|
||||
- JSON 예시는 대표 형태이며 일부 선택 필드를 생략할 수 있다.
|
||||
|
||||
## Contract Surface
|
||||
|
||||
### 공개 문서 엔드포인트
|
||||
- `GET /docs`
|
||||
- `GET /redoc`
|
||||
- `GET /openapi.json`
|
||||
|
||||
### 헬스
|
||||
- `GET /health` (인증 없음)
|
||||
- 정상 응답 예시:
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
### 인증
|
||||
- `POST /auth/login`
|
||||
- `GET /auth/tokens`
|
||||
- `POST /auth/tokens`
|
||||
- `DELETE /auth/tokens/{token_id}`
|
||||
- `POST /auth/bots`
|
||||
- `GET /auth/bots`
|
||||
- `POST /auth/bots/{bot_id}/tokens`
|
||||
- `GET /auth/bots/{bot_id}/tokens`
|
||||
- `DELETE /auth/bots/{bot_id}/tokens/{token_id}`
|
||||
- `DELETE /auth/bots/{bot_id}`
|
||||
|
||||
### 포트폴리오
|
||||
- `GET /portfolios`
|
||||
- `GET /portfolios/{portfolio_id}`
|
||||
- `POST /portfolios`
|
||||
- `PATCH /portfolios/{portfolio_id}`
|
||||
- `POST /portfolios/{portfolio_id}/buy`
|
||||
- `POST /portfolios/{portfolio_id}/sell`
|
||||
- `POST /portfolios/{portfolio_id}/invest`
|
||||
- `POST /portfolios/{portfolio_id}/transfer`
|
||||
- `POST /portfolios/{portfolio_id}/reset`
|
||||
- `DELETE /portfolios/{portfolio_id}`
|
||||
|
||||
### 포트폴리오 댓글 (`REQ-001`)
|
||||
- `POST /portfolios/{portfolio_id}/comments`
|
||||
- `GET /portfolios/{portfolio_id}/comments`
|
||||
- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
|
||||
### 전략셋
|
||||
- `GET /strategy-sets`
|
||||
- `POST /strategy-sets`
|
||||
- `GET /strategy-sets/{strategy_set_id}`
|
||||
- `PATCH /strategy-sets/{strategy_set_id}`
|
||||
- `DELETE /strategy-sets/{strategy_set_id}`
|
||||
|
||||
### 사용자
|
||||
- `GET /users`
|
||||
- `GET /users/{user_id}`
|
||||
- `POST /users`
|
||||
- `PATCH /users/{user_id}`
|
||||
- `DELETE /users/{user_id}`
|
||||
|
||||
## 오류 처리 기준
|
||||
|
||||
| 시나리오 | HTTP |
|
||||
| --- | --- |
|
||||
| 인증 헤더 누락/형식 오류 | `401` |
|
||||
| 유효하지 않은 토큰 | `401` |
|
||||
| 권한 부족 | `403` |
|
||||
| 유효성 검증 실패 | `422` |
|
||||
| 리소스 없음 | `404` |
|
||||
| 내부 예외 | `500` |
|
||||
|
||||
## 상세 엔드포인트 명세
|
||||
|
||||
### 헬스
|
||||
|
||||
#### `GET /health`
|
||||
- 인증 없음
|
||||
- 응답 `200`:
|
||||
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
### 포트폴리오
|
||||
|
||||
#### `GET /portfolios`
|
||||
- Auth: `read`
|
||||
- Query:
|
||||
- `broker: string | null`
|
||||
- `status_filter: string | null`
|
||||
- 응답 `200`: `PortfolioListItem[]`
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled" \
|
||||
-H "Authorization: Bearer <READ_TOKEN>"
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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`
|
||||
- Query:
|
||||
- `recent_trades: int = 5`
|
||||
- 응답 `200`: `PortfolioDetailResponse`
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \
|
||||
-H "Authorization: Bearer <READ_TOKEN>"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"portfolio": {
|
||||
"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"
|
||||
},
|
||||
"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`
|
||||
- Body: `CreatePortfolioRequest`
|
||||
- 응답 `201`: `PortfolioDetailResponse`
|
||||
|
||||
```bash
|
||||
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`
|
||||
|
||||
```bash
|
||||
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` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/sell`: `SellRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/invest`: `InvestRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/transfer`: `TransferRequest` -> `PortfolioDetailResponse`
|
||||
- `POST /portfolios/{portfolio_id}/reset`: `ResetPortfolioRequest` -> `PortfolioDetailResponse`
|
||||
- `DELETE /portfolios/{portfolio_id}`: admin only, query `keep_ledger: bool = false`, 응답 `204`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
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`
|
||||
- `GET /portfolios/{portfolio_id}/comments`
|
||||
- Auth: `read`
|
||||
- Response `200`: `CommentResponse[]`
|
||||
- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
- Auth: `trade-write`
|
||||
- Body: `CommentUpdateRequest`
|
||||
- Response `200`: `CommentResponse`
|
||||
- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}`
|
||||
- Auth: `trade-write`
|
||||
- Response `204`
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
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>"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
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`
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "bootstrap123"
|
||||
}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
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` -> `BotPrincipalResponse`
|
||||
- `GET /auth/bots`: admin only, `is_active` 필터 지원
|
||||
- `GET /auth/bots/{bot_id}`: admin only
|
||||
- `DELETE /auth/bots/{bot_id}`: admin only, bot 비활성화 및 연결 토큰 revoke, 응답 `200`
|
||||
- `POST /auth/bots/{bot_id}/tokens`: admin only, `CreateBotTokenRequest` -> `IssuedBotTokenResponse`
|
||||
- `GET /auth/bots/{bot_id}/tokens`: admin only, `AuthTokenResponse[]`
|
||||
- `DELETE /auth/bots/{bot_id}/tokens/{token_id}`: admin only, `204`
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
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`, 미존재 시 `404`
|
||||
- `POST /users`: `CreateUserRequest` -> `UserResponse`, duplicate username/email이면 `409`
|
||||
- `PATCH /users/{user_id}`: `UpdateUserRequest` -> `UserResponse`, duplicate면 `409`
|
||||
- `DELETE /users/{user_id}`: `204`
|
||||
|
||||
bootstrap 규칙:
|
||||
|
||||
- DB persistence가 활성이고 `PORTFOLIOHUB_BOOTSTRAP_USERNAME`, `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`가 설정되면, `users` 테이블이 비어 있을 때만 startup에서 초기 admin user를 생성한다.
|
||||
|
||||
대표 요청:
|
||||
|
||||
```bash
|
||||
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: str`
|
||||
- `broker: str = "kis"`
|
||||
- `status: str = "enabled"`
|
||||
- `name: str | null`
|
||||
- `symbol: str`
|
||||
- `strategy_name: str = "target_value"`
|
||||
- `strategy_params: object = {}`
|
||||
- `active_strategy_id: str | null`
|
||||
- `avabile_strategies: string[] | null`
|
||||
- `cash_balance: float >= 0`
|
||||
- `asset_quantity: float >= 0`
|
||||
- `asset_price: float >= 0`
|
||||
- `profit_buffer: float >= 0`
|
||||
|
||||
`UpdatePortfolioRequest`
|
||||
|
||||
- `broker: str | null`
|
||||
- `status: str | null`
|
||||
- `name: str | null`
|
||||
- `symbol: str | null`
|
||||
- `strategy_name: str | null`
|
||||
- `strategy_params: object | null`
|
||||
- `active_strategy_id: str | null`
|
||||
- `avabile_strategies: string[] | null`
|
||||
- `asset_price: float >= 0 | null`
|
||||
|
||||
`BuyRequest` / `SellRequest`
|
||||
|
||||
- `quantity: float > 0`
|
||||
|
||||
`InvestRequest`
|
||||
|
||||
- `movement_type: "deposit" | "profit" | "withdraw"`
|
||||
- `amount: float > 0`
|
||||
- `note: str = ""`
|
||||
|
||||
`TransferRequest`
|
||||
|
||||
- `direction: "to-buffer" | "from-buffer"`
|
||||
- `amount: float > 0`
|
||||
- `note: str = ""`
|
||||
|
||||
`ResetPortfolioRequest`
|
||||
|
||||
- `infer_symbol: str | null`
|
||||
|
||||
`CommentCreateRequest` / `CommentUpdateRequest`
|
||||
|
||||
- `content: str`
|
||||
|
||||
`LoginRequest`
|
||||
|
||||
- `username: str`
|
||||
- `password: str`
|
||||
|
||||
`CreateClientTokenRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin" | null`
|
||||
- `expires_in_days: int | null`
|
||||
|
||||
`CreateBotPrincipalRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin"`
|
||||
- `description: str | null`
|
||||
|
||||
`CreateBotTokenRequest`
|
||||
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin" | null`
|
||||
- `expires_in_days: int | null`
|
||||
|
||||
`CreateUserRequest`
|
||||
|
||||
- `username: str`
|
||||
- `email: str | null`
|
||||
- `password: str`
|
||||
- `permission: "read" | "trade-write" | "admin"`
|
||||
- `is_active: bool`
|
||||
|
||||
`UpdateUserRequest`
|
||||
|
||||
- `username: str | null`
|
||||
- `email: str | null`
|
||||
- `password: str | null`
|
||||
- `permission: "read" | "trade-write" | "admin" | null`
|
||||
- `is_active: bool | null`
|
||||
|
||||
`CreateStrategySetRequest`
|
||||
|
||||
- `name: str`
|
||||
- `config: str`
|
||||
|
||||
`UpdateStrategySetRequest`
|
||||
|
||||
- `name: str | null`
|
||||
- `config: str | null`
|
||||
|
||||
### Response Models
|
||||
|
||||
`PortfolioListItem`
|
||||
|
||||
- portfolio summary fields + computed `total_value`
|
||||
|
||||
`PortfolioSummary`
|
||||
|
||||
- `portfolio_id: str`
|
||||
- `broker: str`
|
||||
- `status: str`
|
||||
- `name: str`
|
||||
- `symbol: str`
|
||||
- `cash_balance: float`
|
||||
- `asset_quantity: float`
|
||||
- `price: float`
|
||||
- `profit_buffer: float`
|
||||
- `active_strategy_id: str | null`
|
||||
- `avabile_strategies: string[] | null`
|
||||
- `created_at: datetime | null`
|
||||
- `updated_at: datetime | null`
|
||||
- `last_trade_at: datetime | null`
|
||||
|
||||
`PortfolioDetailResponse`
|
||||
|
||||
- `portfolio: PortfolioSummary`
|
||||
- `recent_entries: LedgerEntryResponse[]`
|
||||
- `strategy_set: StrategySetResponse | null`
|
||||
- `live_quantity: float | null`
|
||||
|
||||
`StrategySetResponse`
|
||||
|
||||
- `id: str`
|
||||
- `name: str`
|
||||
- `config: str`
|
||||
- `nick: str`
|
||||
- `created_at: datetime | null`
|
||||
|
||||
`CommentResponse`
|
||||
|
||||
- `id: str`
|
||||
- `portfolio_id: str`
|
||||
- `content: str`
|
||||
- `created_at: datetime | null`
|
||||
- `updated_at: datetime | null`
|
||||
|
||||
`BotPrincipalResponse`
|
||||
|
||||
- `id: str`
|
||||
- `name: str`
|
||||
- `permission: "read" | "trade-write" | "admin"`
|
||||
- `description: str | null`
|
||||
- `is_active: bool`
|
||||
- `created_at: datetime | null`
|
||||
- `updated_at: datetime | null`
|
||||
- `deactivated_at: datetime | null`
|
||||
|
||||
`ErrorResponse`
|
||||
|
||||
- `detail: str`
|
||||
|
||||
## 공통 오류 예시
|
||||
|
||||
### Missing token
|
||||
|
||||
```json
|
||||
{"detail": "Not authenticated"}
|
||||
```
|
||||
|
||||
### Invalid token
|
||||
|
||||
```json
|
||||
{"detail": "Invalid token"}
|
||||
```
|
||||
|
||||
### Insufficient permission
|
||||
|
||||
```json
|
||||
{"detail": "Insufficient permission"}
|
||||
```
|
||||
|
||||
## 문서 동기화 규칙
|
||||
- `app/api/` 또는 `app/schemas/` 변경 시 본 문서를 같은 변경 단위에서 갱신한다.
|
||||
- `REQ-002` 태그 검색 API가 구현되면 요청/응답 파라미터와 필터 의미를 본 문서에 추가한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 에러 응답 body를 공통 포맷(`error_code`, `message`, `details`)으로 강제할지 결정한다.
|
||||
- idempotency가 필요한 쓰기 API(예: 재시도 가능 매매 요청)의 키 정책을 확정한다.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Architecture Overview
|
||||
|
||||
- Related REQ: `REQ-003`
|
||||
|
||||
## 목표
|
||||
`portfolio-hub`는 포트폴리오 상태 관리 기능을 API와 CLI에 동일하게 제공하고, 서비스/도메인/리포지토리 계층 분리를 통해 변경 비용을 낮춘다.
|
||||
|
||||
## 개요와 범위
|
||||
|
||||
런타임/구성요소 기준:
|
||||
|
||||
- Runtime: Python 3.11+
|
||||
- API framework: FastAPI
|
||||
- CLI framework: Typer
|
||||
- DB driver: Psycopg v3
|
||||
- Migration: Alembic
|
||||
|
||||
주요 목표:
|
||||
|
||||
- 포트폴리오 lifecycle 관리(create/update/delete/reset)
|
||||
- 거래 및 잔고 영향 액션 기록(buy/sell/invest/transfer)
|
||||
- API와 CLI에서 동일한 상태/ledger 조회 제공
|
||||
- 공유 service/repository 계층을 통한 동작 일관성 유지
|
||||
|
||||
현재 범위:
|
||||
|
||||
- 인증된 포트폴리오 조회/변경 작업
|
||||
- 관리자 기반 사용자 CRUD
|
||||
- 권한 계층(read, trade-write, admin)
|
||||
- feature flag 기반 PostgreSQL/Timescale 저장소
|
||||
- DB bootstrap 및 migration 운영 명령
|
||||
|
||||
현재 비목표 / 제한:
|
||||
|
||||
- production 용도 메모리 fallback repository 미구현
|
||||
- 외부 broker 주문 실행 미포함
|
||||
- Redis/Loki 관측성 경로는 완전한 runtime wiring 미완료
|
||||
|
||||
## 상위 구성 요소
|
||||
|
||||
| 컴포넌트 | 책임 |
|
||||
| --- | --- |
|
||||
| `app/main.py` | FastAPI 앱 생성, 라우터 등록, 설정 초기화 |
|
||||
| `app/api/` | 인증/헬스/포트폴리오/전략셋/사용자 라우트 |
|
||||
| `app/cli/` | 운영용 명령 인터페이스와 확인 프롬프트 |
|
||||
| `app/services/` | API/CLI 공통 유스케이스 수행 |
|
||||
| `app/domain/` | 포트폴리오 거래 규칙, 밸런스 계산 |
|
||||
| `app/repositories/` | 저장소 추상화 및 Postgres 구현 |
|
||||
| `app/db/`, `migrations/` | DB 초기화 및 스키마 버전 관리 |
|
||||
|
||||
## 엔트리포인트
|
||||
|
||||
HTTP 서비스:
|
||||
|
||||
- App factory: `app.main:create_app`
|
||||
- ASGI app: `app.main:app`
|
||||
- OpenAPI docs: `/docs`
|
||||
- ReDoc: `/redoc`
|
||||
|
||||
CLI:
|
||||
|
||||
- Console script: `portfoliohub`
|
||||
- Entry function: `app.cli.main:main`
|
||||
|
||||
## 기본 런타임 흐름
|
||||
|
||||
### 1. API 기반 거래 처리
|
||||
1. 라우트에서 요청 바디와 권한을 검증한다.
|
||||
2. 서비스 계층에서 도메인 규칙(수량/금액/상태)을 평가한다.
|
||||
3. 리포지토리에서 포트폴리오/원장 데이터를 갱신한다.
|
||||
4. 응답 스키마로 직렬화해 반환한다.
|
||||
|
||||
### 2. CLI 기반 동일 작업 처리
|
||||
1. CLI 명령이 인자를 파싱하고 확인 프롬프트를 처리한다.
|
||||
2. API와 동일한 서비스 메서드를 호출한다.
|
||||
3. 결과를 터미널 친화 포맷으로 출력한다.
|
||||
|
||||
### 3. DB 활성화 경로
|
||||
1. `PORTFOLIOHUB_DB_ENABLED=true` 확인
|
||||
2. DB 연결 정보(`TIMESCALEDB_*`) 로딩
|
||||
3. Postgres 리포지토리 선택
|
||||
4. 필요 시 Alembic 마이그레이션 후 서비스 기동
|
||||
|
||||
## 설계 원칙
|
||||
- **Service Reuse First**: API/CLI는 동일 service 계층을 재사용한다.
|
||||
- **Contract Stability**: 외부 계약은 `app/schemas/`와 기술 문서에서 일관성을 유지한다.
|
||||
- **Persistence Isolation**: 영속성 구현 세부사항은 repository 계층에 격리한다.
|
||||
- **Fail Fast Config**: 필수 설정 누락은 조기 실패로 드러낸다.
|
||||
|
||||
## 테스트와 호환성 기준
|
||||
|
||||
테스트 기준:
|
||||
|
||||
- 테스트 프레임워크는 `pytest`를 사용한다.
|
||||
- 주요 테스트 범위는 API, auth, CLI, domain service를 포함한다.
|
||||
- 재귀 제외 경로는 `temp`, `.git`, `.venv`를 사용한다.
|
||||
|
||||
버전/호환성 기준:
|
||||
|
||||
- 현재 패키지 버전: `0.1.0`
|
||||
- API 버전 노출은 앱 버전 설정(`app_version`)을 따른다.
|
||||
- 최소 Python 버전: `3.11`
|
||||
|
||||
## 향후 확장 포인트
|
||||
|
||||
- Redis 기반 cache/session 통합
|
||||
- Loki logging pipeline 통합
|
||||
- 추가 broker 및 order-execution adapter
|
||||
- 운영/admin 확장 엔드포인트
|
||||
|
||||
## Open Implementation Points
|
||||
- `REQ-002` 태그 검색 구현 시 조회 성능 요구를 만족하는 인덱스/쿼리 전략을 확정한다.
|
||||
- 관측성(메트릭/로그)의 표준 이벤트 스키마를 별도 문서로 확장할지 결정한다.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Auth and Token Lifecycle
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
정적 토큰과 DB 발급 토큰을 병행하는 현재 인증 모델의 동작 기준을 명확히 한다.
|
||||
|
||||
## 권한 모델
|
||||
|
||||
| Permission | 허용 범위 |
|
||||
| --- | --- |
|
||||
| `read` | 조회 계열 API |
|
||||
| `trade-write` | 조회 + 포트폴리오 변경 API |
|
||||
| `admin` | 전체 API + 사용자/봇 관리 |
|
||||
|
||||
## 인증 방식
|
||||
|
||||
- 보호된 API는 `Authorization: Bearer <token>` 헤더를 사용한다.
|
||||
- 인증 소스는 정적 토큰과 DB 저장 토큰을 병행한다.
|
||||
|
||||
권한 계층:
|
||||
|
||||
- `admin`은 전체 엔드포인트 접근 가능
|
||||
- `trade-write`는 `trade-write`와 `read` 범위 접근 가능
|
||||
- `read`는 조회 엔드포인트만 접근 가능
|
||||
|
||||
## 인증 소스
|
||||
- 환경변수 기반 정적 토큰: `PORTFOLIOHUB_*_TOKENS`
|
||||
- DB 저장 토큰: 로그인 세션/클라이언트 토큰/봇 토큰
|
||||
|
||||
## 토큰 라이프사이클
|
||||
1. 사용자 로그인(`POST /auth/login`) 또는 토큰 발급 API 호출
|
||||
2. 토큰 해시 저장 및 prefix 관리
|
||||
3. 요청 처리 시 해시 비교와 만료/폐기 상태 검증
|
||||
4. `DELETE /auth/tokens/{token_id}` 또는 봇 비활성화로 토큰 폐기
|
||||
|
||||
## 실패 응답 기준
|
||||
|
||||
- bearer 헤더 누락 또는 형식 오류: `401`
|
||||
- 알 수 없는 토큰: `401`
|
||||
- 권한 부족: `403`
|
||||
|
||||
## 부트스트랩 사용자 규칙
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`과 `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`가 설정되고 users 테이블이 비어 있으면 초기 admin을 1회 생성한다.
|
||||
- 이미 사용자가 존재하면 추가 생성하지 않는다.
|
||||
|
||||
## Validation Notes
|
||||
- 만료된 토큰은 항상 인증 실패로 처리해야 한다.
|
||||
- 비활성 봇(`is_active=false`)의 토큰은 거부되어야 한다.
|
||||
- 권한 계층 승격 우회가 없는지 API/CLI 양쪽에서 검증한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 정적 토큰과 DB 토큰의 우선순위/병합 정책을 명문화할지 결정한다.
|
||||
- 감사 로그(audit trail)에 토큰 발급/폐기 이벤트를 표준 포맷으로 남길지 확정한다.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Deployment and Operations
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
PortfolioHub를 Linux 환경에서 안정적으로 배포하고 HTTPS 종단, 프로세스 복구, 기본 점검 경로를 표준화한다.
|
||||
|
||||
## 사전 조건
|
||||
|
||||
- public IP를 가진 Linux/Ubuntu 서버
|
||||
- 해당 IP를 가리키는 도메인 DNS `A` record
|
||||
- 방화벽에서 `80`, `443` 포트 개방
|
||||
|
||||
## 권장 배포 토폴로지
|
||||
- App: `uvicorn app.main:app`
|
||||
- Reverse proxy/TLS: Caddy
|
||||
- Process manager: systemd
|
||||
- 선택: Docker Compose + Caddy sidecar
|
||||
|
||||
## Linux Direct Run 기준
|
||||
1. Python 가상환경 생성 후 패키지를 설치한다.
|
||||
2. 환경 변수 또는 `.env`를 구성한다.
|
||||
3. `uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2`를 실행한다.
|
||||
4. `GET /health`를 확인한다.
|
||||
|
||||
## Caddy 설치와 구성
|
||||
|
||||
Ubuntu/Debian 설치 예시:
|
||||
|
||||
```bash
|
||||
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt update
|
||||
sudo apt install caddy
|
||||
```
|
||||
|
||||
설치 검증:
|
||||
|
||||
```bash
|
||||
caddy version
|
||||
```
|
||||
|
||||
기본 Caddyfile:
|
||||
|
||||
```text
|
||||
your-domain.com {
|
||||
reverse_proxy localhost:8000
|
||||
}
|
||||
```
|
||||
|
||||
동작 기준:
|
||||
|
||||
- TLS 인증서 자동 발급/갱신
|
||||
- `80`에서 `443`으로 HTTP redirect
|
||||
- 외부 요청을 내부 `uvicorn` 프로세스(`127.0.0.1:8000`)로 전달
|
||||
|
||||
적용 후 reload:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
## systemd 운영 기준
|
||||
- 서비스 계정 권한 최소화
|
||||
- `Restart=on-failure`
|
||||
- 로그 수집 경로(journalctl 또는 외부 수집기) 확보
|
||||
|
||||
예시 unit 파일:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=PortfolioHub API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
WorkingDirectory=/opt/portfoliohub
|
||||
EnvironmentFile=/opt/portfoliohub/.env
|
||||
ExecStart=/opt/portfoliohub/.venv/bin/uvicorn app.main:app \
|
||||
--host 127.0.0.1 --port 8000 --workers 2
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
등록/기동:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable portfoliohub
|
||||
sudo systemctl start portfoliohub
|
||||
```
|
||||
|
||||
## HTTPS/Caddy 운영 기준
|
||||
- 도메인 DNS가 서버 IP를 가리켜야 한다.
|
||||
- 포트 `80/443`을 개방한다.
|
||||
- Caddyfile에서 앱을 `reverse_proxy`로 연결한다.
|
||||
- 브라우저 기반 frontend가 있으면 `PORTFOLIOHUB_ALLOWED_ORIGINS`에 정확한 HTTPS origin을 지정한다.
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
|
||||
```
|
||||
|
||||
## Docker Compose 운영 기준
|
||||
- 앱 컨테이너는 내부 포트만 expose하고 외부 종단은 Caddy가 담당한다.
|
||||
- `caddy_data`, `caddy_config` 볼륨을 유지해 인증서 상태를 보존한다.
|
||||
- 배포 후 `docker compose ps`, `curl /health`로 기본 상태를 확인한다.
|
||||
|
||||
예시 compose:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
expose:
|
||||
- "8000"
|
||||
environment:
|
||||
- PORTFOLIOHUB_ALLOWED_ORIGINS=https://your-domain.com
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- app
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
```
|
||||
|
||||
compose 네트워크 안에서는 `reverse_proxy app:8000`을 사용한다.
|
||||
|
||||
기동:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 방화벽 설정
|
||||
|
||||
UFW:
|
||||
|
||||
```bash
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
firewalld:
|
||||
|
||||
```bash
|
||||
sudo firewall-cmd --permanent --add-service=http
|
||||
sudo firewall-cmd --permanent --add-service=https
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## 배포 검증
|
||||
|
||||
HTTPS health check:
|
||||
|
||||
```bash
|
||||
curl https://your-domain.com/health
|
||||
```
|
||||
|
||||
운영 상태 확인:
|
||||
|
||||
```bash
|
||||
sudo systemctl status caddy
|
||||
sudo journalctl -u caddy -f
|
||||
```
|
||||
|
||||
## Validation Notes
|
||||
- HTTPS 엔드포인트에서 `/health`, `/docs` 접근이 가능해야 한다.
|
||||
- DB 사용 배포에서는 마이그레이션 적용 후 앱을 기동한다.
|
||||
- CORS 오리진 설정과 실제 프론트엔드 도메인이 일치해야 한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- blue/green 또는 rolling update 같은 무중단 배포 전략 도입 여부를 확정한다.
|
||||
- Loki/metrics 통합 운영 절차를 별도 런북으로 분리할지 결정한다.
|
||||
@@ -0,0 +1,49 @@
|
||||
# portfolio-hub Technical Specification Index
|
||||
|
||||
## 목적
|
||||
- 이 디렉토리는 `portfolio-hub`의 **구현 상세 설계(How)** 를 관리한다.
|
||||
- 승인 기준과 외부 요구사항의 source of truth는 `docs/SRS/`이며, 기술 문서는 해당 요구사항을 구현하기 위한 내부 설계를 정리한다.
|
||||
|
||||
## 문서 사용 원칙
|
||||
1. 외부 계약, acceptance criteria, 상태 전이는 `docs/SRS/`에서 관리한다.
|
||||
2. 내부 모듈 책임, 시퀀스, 데이터 흐름, 예시 payload는 `docs/technical/`에서 관리한다.
|
||||
3. 기술 문서가 SRS와 충돌하면, 별도 owner 결정 전까지 **SRS를 우선**한다.
|
||||
4. 기술 문서를 수정할 때는 관련 REQ ID를 함께 표시해 traceability를 유지한다.
|
||||
|
||||
## 문서 목록
|
||||
|
||||
| 문서 | 목적 | 연관 REQ |
|
||||
| --- | --- | --- |
|
||||
| `project-structure-baseline.md` | 저장소 baseline 디렉토리 구조와 코드/문서 배치 기준 | N/A |
|
||||
| `python-runtime-and-packaging.md` | Python 런타임과 `pyproject.toml` 기반 패키징 기준 | N/A |
|
||||
| `settings-and-env-loading.md` | `.env`/환경변수 로딩, 필수 키, CORS/운영 설정 규칙 | N/A |
|
||||
| `architecture-overview.md` | 시스템 개요, 모듈 책임, 런타임 흐름, 테스트/호환성 기준 | `REQ-003` |
|
||||
| `api-contracts.md` | HTTP API 상세 계약, request/response 모델, 대표 payload 예시 | `REQ-001`, `REQ-002`, `REQ-003` |
|
||||
| `persistence-and-migrations.md` | PostgreSQL/Timescale 연동, 리포지토리 스위치, Alembic 운영, 스키마 baseline | `REQ-001`, `REQ-003` |
|
||||
| `auth-and-token-lifecycle.md` | 로그인/토큰/봇 주체 라이프사이클 및 권한 모델 | N/A |
|
||||
| `management-cli-operations.md` | `portfoliohub` CLI 명령 책임, 종료코드, 운영 플로우 | N/A |
|
||||
| `deployment-and-operations.md` | Linux systemd, Caddy reverse proxy, Docker Compose 운영 절차 | N/A |
|
||||
|
||||
## 연관 REQ 매핑 표
|
||||
|
||||
| REQ ID | Technical Docs |
|
||||
| --- | --- |
|
||||
| `REQ-001` | `api-contracts.md`, `persistence-and-migrations.md` |
|
||||
| `REQ-002` | `api-contracts.md` |
|
||||
| `REQ-003` | `architecture-overview.md`, `api-contracts.md`, `persistence-and-migrations.md` |
|
||||
|
||||
## 권장 작성 순서
|
||||
1. `project-structure-baseline.md`
|
||||
2. `python-runtime-and-packaging.md`
|
||||
3. `settings-and-env-loading.md`
|
||||
4. `architecture-overview.md`
|
||||
5. `api-contracts.md`
|
||||
6. `persistence-and-migrations.md`
|
||||
7. `auth-and-token-lifecycle.md`
|
||||
8. `management-cli-operations.md`
|
||||
9. `deployment-and-operations.md`
|
||||
|
||||
## 추적 규칙
|
||||
- 각 기술 문서는 상단에 관련 REQ를 명시한다.
|
||||
- 구현 착수 전에는 해당 문서의 open issue가 남아 있는지 확인한다.
|
||||
- 운영 중 발견된 차이는 technical 문서에 먼저 반영하고, 외부 계약 변화가 있으면 SRS 업데이트 여부를 별도로 판정한다.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Management CLI Operations
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
`portfoliohub` CLI를 운영 자동화와 수동 점검 모두에 안전하게 사용할 수 있도록 명령 책임과 사용 규칙을 정의한다.
|
||||
|
||||
## 명령 그룹
|
||||
|
||||
| 그룹 | 주요 명령 | 목적 |
|
||||
| --- | --- | --- |
|
||||
| DB 운영 | `init-db`, `migrate-db`, `health-check` | 스키마 적용 및 연결 상태 점검 |
|
||||
| 사용자 관리 | `user list/get/create/update/delete` | 운영 계정 라이프사이클 |
|
||||
| 포트폴리오 조회 | `portfolio list`, `portfolio status` | 상태 확인 |
|
||||
| 포트폴리오 변경 | `portfolio create/update/buy/sell/invest/transfer/reset/delete` | 거래/자금/상태 변경 |
|
||||
|
||||
## 인터페이스 명세
|
||||
|
||||
전역 옵션:
|
||||
|
||||
- `--format [table|json]` (기본값: `table`)
|
||||
|
||||
공통 명령:
|
||||
|
||||
- `portfoliohub init-db [--maintenance-db postgres]`
|
||||
- `portfoliohub migrate-db`
|
||||
- `portfoliohub health-check`
|
||||
|
||||
포트폴리오 하위 명령:
|
||||
|
||||
- `portfoliohub portfolio list [--broker ...] [--status ...]`
|
||||
- `portfoliohub portfolio status <portfolio_id> [--recent 5]`
|
||||
- `portfoliohub portfolio create <portfolio_id> --symbol ... [--name ...] [options] [--force]`
|
||||
- `portfoliohub portfolio update <portfolio_id> [options] [--force]`
|
||||
- `portfoliohub portfolio buy <portfolio_id> --quantity ... --price ... [options] [--force]`
|
||||
- `portfoliohub portfolio sell <portfolio_id> --quantity ... --price ... [options] [--force]`
|
||||
- `portfoliohub portfolio invest <portfolio_id> --type <deposit|profit|withdraw> --amount ... [options] [--force]`
|
||||
- `portfoliohub portfolio transfer <portfolio_id> --direction <to-buffer|from-buffer> --amount ... [options] [--force]`
|
||||
- `portfoliohub portfolio reset <portfolio_id> [--infer-symbol ...] [--force]`
|
||||
- `portfoliohub portfolio delete <portfolio_id> [--keep-ledger] [--force]`
|
||||
|
||||
포트폴리오 댓글 하위 명령:
|
||||
|
||||
- `portfoliohub portfolio comment add <portfolio_id> --content <text>`
|
||||
- `portfoliohub portfolio comment list <portfolio_id>`
|
||||
- `portfoliohub portfolio comment update <portfolio_id> <comment_id> --content <text> [--force]`
|
||||
- `portfoliohub portfolio comment delete <portfolio_id> <comment_id> [--force]`
|
||||
|
||||
전략셋 하위 명령:
|
||||
|
||||
- `portfoliohub strategy list`
|
||||
- `portfoliohub strategy create --name ... --config ...`
|
||||
- `portfoliohub strategy update <strategy_set_id> [--name ...] [--config ...]`
|
||||
- `portfoliohub strategy delete <strategy_set_id> [--force]`
|
||||
|
||||
## 실행 원칙
|
||||
- 쓰기 명령은 기본적으로 확인 프롬프트를 띄운다.
|
||||
- 자동화 스크립트에서는 `--force`를 사용해 비대화식 실행을 보장한다.
|
||||
- DB 타깃 실행 시 `PORTFOLIOHUB_DB_ENABLED=true`와 `TIMESCALEDB_*`를 명시한다.
|
||||
|
||||
## 예시 시나리오
|
||||
1. 신규 환경 준비
|
||||
- `portfoliohub init-db`
|
||||
- `portfoliohub health-check`
|
||||
2. 일상 운영
|
||||
- `portfoliohub portfolio list`
|
||||
- `portfoliohub portfolio status <portfolio_id>`
|
||||
3. 배포 후 검증
|
||||
- `portfoliohub user list`
|
||||
- 주요 API smoke test와 결과 비교
|
||||
|
||||
## 오류/종료코드 가이드
|
||||
- `0`: 정상 종료
|
||||
- `1`: 입력/검증/런타임 오류
|
||||
- 재시도 가능 오류(예: 일시적 DB 연결 실패)는 메시지에 원인을 명확히 표기한다.
|
||||
|
||||
확인 동작:
|
||||
|
||||
- 변경 명령은 기본적으로 확인 프롬프트를 요청한다.
|
||||
- `--force`를 지정하면 확인 프롬프트를 생략한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- JSON 출력 모드(`--json`)를 도입해 자동화 친화성을 높일지 결정한다.
|
||||
- 대량 작업(batch) 명령 추가 시 실패 항목 보고 포맷을 표준화한다.
|
||||
@@ -0,0 +1,279 @@
|
||||
# Persistence and Migrations
|
||||
|
||||
- Related REQ: `REQ-001`, `REQ-003`
|
||||
|
||||
## 목표
|
||||
DB 기반 영속성 경로를 안정적으로 운영하고, 스키마 변경 이력과 문서 동기화를 일관되게 유지한다.
|
||||
|
||||
## 저장소 선택 규칙
|
||||
- `PORTFOLIOHUB_DB_ENABLED=true`이면 Postgres 리포지토리를 사용한다.
|
||||
- DB 비활성 모드는 개발/부트스트랩 목적의 제한적 동작으로 간주한다.
|
||||
|
||||
비활성 동작 기준:
|
||||
|
||||
- `PORTFOLIOHUB_DB_ENABLED=false`이면 미구현 placeholder repository가 `FeatureNotReadyError`를 발생시킨다.
|
||||
|
||||
## 데이터 모델 핵심 영역
|
||||
|
||||
| 테이블 | 역할 |
|
||||
| --- | --- |
|
||||
| `portfolios` | 현재 포트폴리오 상태 |
|
||||
| `portfolio_ledger` | 거래/자금이동 이력 스냅샷 |
|
||||
| `portfolio_comments` | 포트폴리오 코멘트 이력 (`REQ-001`) |
|
||||
| `strategy_sets` | 전략 설정 스냅샷 |
|
||||
| `users` | 운영 사용자 |
|
||||
| `auth_tokens` | 세션/클라이언트/봇 토큰 |
|
||||
| `bot_principals` | 외부 봇 주체 |
|
||||
|
||||
## 스키마 문서 기준
|
||||
|
||||
- Source of truth: Alembic revisions in `migrations/versions`
|
||||
- Current baseline revision: `20260410_0009`
|
||||
- 상세 테이블/컬럼/인덱스 제약은 이 문서가 Alembic revision과 동기화된 기준 문서다.
|
||||
|
||||
## 마이그레이션 운영 흐름
|
||||
1. 신규 스키마 변경을 Alembic revision으로 추가한다.
|
||||
2. `migrations/versions/`에 revision 파일을 저장한다.
|
||||
3. 본 문서에 revision ID와 스키마 변경 내역을 반영한다.
|
||||
4. `portfoliohub migrate-db` 또는 Alembic `upgrade head`로 적용한다.
|
||||
|
||||
## 명령 기준
|
||||
- 초기 DB 준비: `portfoliohub init-db`
|
||||
- 버전 업그레이드: `portfoliohub migrate-db`
|
||||
- 상태 점검: `portfoliohub health-check`
|
||||
|
||||
명령 동작:
|
||||
|
||||
- `init-db`: target DB가 없으면 생성 후 Alembic `head` 적용
|
||||
- `migrate-db`: 기존 target DB에 Alembic `head` 적용
|
||||
|
||||
## 스키마 기준
|
||||
|
||||
### Conventions
|
||||
|
||||
- 데이터 타입 표기는 SQLAlchemy/Alembic 선언명을 따른다.
|
||||
- `NULL`은 nullable을 의미한다.
|
||||
- Default는 DB-level `server_default`를 의미한다.
|
||||
- FK 제약이 없어도 logical relationship column은 문서화한다.
|
||||
|
||||
### Tables
|
||||
|
||||
#### users
|
||||
|
||||
Purpose:
|
||||
|
||||
- 운영 사용자 레코드와 향후 identity mapping 기준 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| username | Text | NO | - | Unique username |
|
||||
| email | Text | YES | - | Unique when present |
|
||||
| permission | Text | NO | `read` | User permission level |
|
||||
| is_active | Boolean | NO | TRUE | Whether login is allowed |
|
||||
| password_hash | Text | YES | - | Scrypt password hash |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
| last_login_at | DateTime(timezone=True) | YES | - | Last successful login timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_users_username` on (`username`)
|
||||
- Unique: `uq_users_email` on (`email`)
|
||||
|
||||
#### auth_tokens
|
||||
|
||||
Purpose:
|
||||
|
||||
- DB 발급 session/client/bot token 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| user_id | Text | NO | - | Logical user reference |
|
||||
| name | Text | NO | - | Human-readable token name |
|
||||
| token_prefix | Text | NO | - | Display prefix |
|
||||
| token_hash | Text | NO | - | Unique SHA-256 token hash |
|
||||
| permission | Text | NO | - | Effective token permission |
|
||||
| token_type | Text | NO | - | `session`, `client`, or `bot` |
|
||||
| bot_principal_id | Text | YES | - | Logical reference to `bot_principals.id` |
|
||||
| expires_at | DateTime(timezone=True) | YES | - | Expiration time |
|
||||
| last_used_at | DateTime(timezone=True) | YES | - | Last successful use |
|
||||
| revoked_at | DateTime(timezone=True) | YES | - | Revocation time |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_auth_tokens_token_hash` on (`token_hash`)
|
||||
|
||||
#### bot_principals
|
||||
|
||||
Purpose:
|
||||
|
||||
- 외부 bot identity와 권한 ceiling 관리
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| name | Text | NO | - | Unique bot name |
|
||||
| permission | Text | NO | - | Bot permission ceiling |
|
||||
| description | Text | YES | - | Optional description |
|
||||
| is_active | Boolean | NO | TRUE | False면 bot token 거부 |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
| deactivated_at | DateTime(timezone=True) | YES | - | Deactivation time |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_bot_principals_name` on (`name`)
|
||||
|
||||
#### strategy_sets
|
||||
|
||||
Purpose:
|
||||
|
||||
- 정규화된 전략 설정 snapshot 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| name | Text | NO | - | Strategy name, unique pair 일부 |
|
||||
| config | Text | NO | `""` | Strategy config payload |
|
||||
| nick | Text | NO | - | Unique nickname |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Unique: `uq_strategy_sets_name_config` on (`name`, `config`)
|
||||
- Unique: `uq_strategy_sets_nick` on (`nick`)
|
||||
|
||||
#### portfolios
|
||||
|
||||
Purpose:
|
||||
|
||||
- 현재 포트폴리오 상태와 strategy linkage 저장
|
||||
|
||||
Current baseline note:
|
||||
|
||||
- 아래 컬럼은 revision `20260403_0008`까지의 마이그레이션 기준이다.
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| portfolio_id | Text | NO | - | Primary Key |
|
||||
| broker | Text | NO | `kis` | Broker code |
|
||||
| status | Text | NO | `enabled` | Portfolio status |
|
||||
| name | Text | NO | - | Asset name |
|
||||
| symbol | Text | NO | - | Trading symbol |
|
||||
| cash_balance | Float | NO | - | Current cash balance |
|
||||
| asset_quantity | Float | NO | - | Current asset quantity |
|
||||
| asset_price | Float | NO | 0 | Current price snapshot |
|
||||
| profit_buffer | Float | NO | 0 | Profit buffer amount |
|
||||
| active_strategy_id | Text | YES | - | Logical reference to `strategy_sets.id` |
|
||||
| avabile_strategies | Text[] | YES | - | Nullable array of `strategy_sets.id` |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
| last_trade_at | DateTime(timezone=True) | YES | - | Last trade time |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `portfolio_id`
|
||||
- Foreign key constraints: none in baseline
|
||||
|
||||
#### portfolio_ledger
|
||||
|
||||
Purpose:
|
||||
|
||||
- 포트폴리오 액션별 append-like ledger snapshot 저장
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` |
|
||||
| recorded_at | DateTime(timezone=True) | NO | - | Event timestamp |
|
||||
| action | Text | NO | - | Action type |
|
||||
| price | Float | NO | - | Trade/valuation unit price |
|
||||
| quantity | Float | NO | - | Quantity delta |
|
||||
| fee_amount | Float | NO | 0 | Fees |
|
||||
| tax_amount | Float | NO | 0 | Taxes |
|
||||
| cash_balance_after | Float | NO | - | Post-action cash |
|
||||
| asset_quantity_after | Float | NO | - | Post-action asset quantity |
|
||||
| total_value_after | Float | NO | - | Post-action total value |
|
||||
| profit_buffer_after | Float | NO | 0 | Post-action profit buffer |
|
||||
| strategy_set_id | Text | NO | `legacy` | Logical strategy set reference |
|
||||
| note | Text | NO | `""` | Operator/system note |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: none in baseline
|
||||
- Foreign key constraints: none in baseline
|
||||
|
||||
#### portfolio_comments
|
||||
|
||||
Purpose:
|
||||
|
||||
- 포트폴리오별 multiline comment history 저장 (`REQ-001`)
|
||||
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| id | Text | NO | - | Primary Key (UUID) |
|
||||
| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` |
|
||||
| content | Text | NO | - | Multiline comment text |
|
||||
| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
|
||||
| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp |
|
||||
|
||||
Constraints:
|
||||
|
||||
- Primary key: `id`
|
||||
- Foreign key constraints: none
|
||||
|
||||
### Indexes
|
||||
|
||||
| Index | Table | Columns/Expressions | Unique | Intent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ix_users_created_at` | `users` | `created_at` | no | recency listing |
|
||||
| `idx_auth_tokens_user_created` | `auth_tokens` | `user_id`, `created_at` | no | current user token listing |
|
||||
| `idx_auth_tokens_hash_active` | `auth_tokens` | `token_hash` | no | bearer token lookup |
|
||||
| `idx_auth_tokens_bot_created` | `auth_tokens` | `bot_principal_id`, `created_at` | no | bot token listing |
|
||||
| `idx_bot_principals_active_created` | `bot_principals` | `is_active`, `created_at` | no | active/inactive bot filtering |
|
||||
| `idx_portfolio_ledger_portfolio_time` | `portfolio_ledger` | `portfolio_id`, `recorded_at DESC` | no | recent ledger query |
|
||||
| `idx_portfolios_broker_status_updated` | `portfolios` | `broker`, `status`, `updated_at DESC` | no | broker/status recency filtering |
|
||||
| `idx_portfolio_comments_portfolio_created` | `portfolio_comments` | `portfolio_id`, `created_at DESC` | no | reverse chronological comments |
|
||||
|
||||
Conditional migration behavior:
|
||||
|
||||
- `timescaledb` extension 존재 시 `create_hypertable('portfolio_ledger', 'recorded_at', if_not_exists => TRUE)` 실행
|
||||
- 이 경우 `portfolio_ledger`는 hypertable로 동작한다.
|
||||
|
||||
## Integrity Notes
|
||||
|
||||
현재 baseline은 DB-level FK constraint를 의도적으로 두지 않는다.
|
||||
|
||||
- logical relation은 application/repository logic이 관리한다.
|
||||
- 향후 FK constraint를 추가하면 migration ID와 함께 본 문서를 갱신한다.
|
||||
- application layer cascade:
|
||||
- 포트폴리오 삭제 시 연관 `portfolio_comments`를 hard delete 한다.
|
||||
|
||||
## Validation Notes
|
||||
- 마이그레이션은 빈 DB와 기존 DB 모두에서 연속 적용 가능해야 한다.
|
||||
- 파괴적 변경(drop/rename)은 백필 또는 호환 단계 계획 없이 직접 반영하지 않는다.
|
||||
- 문서 동기화 게이트(`scripts/check_docs_sync.py`)를 통과해야 한다.
|
||||
|
||||
## Revision Mapping
|
||||
|
||||
| Revision ID | File | Summary |
|
||||
| --- | --- | --- |
|
||||
| 20260403_0001 | migrations/versions/20260403_0001_initial_portfolio_schema.py | Initial schema for strategy sets, portfolios, ledger, indexes, optional hypertable |
|
||||
| 20260403_0002 | migrations/versions/20260403_0002_strategy_set_columns.py | Rename `strategy_name` to `name`, `strategy_config` to `config`, add unique `nick` |
|
||||
| 20260403_0003 | migrations/versions/20260403_0003_portfolio_strategy_refs.py | Remove embedded strategy fields, add `active_strategy_id` and `avabile_strategies` |
|
||||
| 20260403_0004 | migrations/versions/20260403_0004_users_table.py | Add `users` table with unique username/email and created_at index |
|
||||
| 20260403_0005 | migrations/versions/20260403_0005_auth_user_fields_and_tokens.py | Add user auth fields and `auth_tokens` table |
|
||||
| 20260403_0006 | migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py | Add `bot_principals` and bot token linkage |
|
||||
| 20260403_0007 | migrations/versions/20260403_0007_bot_principal_permission_only.py | Remove `owner_user_id` and enforce bot-local permission model |
|
||||
| 20260403_0008 | migrations/versions/20260403_0008_portfolio_name_column.py | Add non-null `portfolios.name` and backfill from `symbol` |
|
||||
| 20260410_0009 | migrations/versions/20260410_0009_portfolio_comments_table.py | Add `portfolio_comments` table for per-portfolio memo history |
|
||||
|
||||
## Open Implementation Points
|
||||
- `portfolio_ledger`의 Timescale hypertable 강제 여부와 보존 정책(retention)을 확정한다.
|
||||
- `REQ-002` 태그 검색 구현 시 인덱스 전략(B-tree/GiN 등)과 마이그레이션 단계를 추가 정의한다.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Project Structure Baseline
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
`portfolio-hub` 구현 및 유지보수 시 디렉토리 책임을 명확히 하여 API/CLI/도메인/저장소 변경의 영향 범위를 예측 가능하게 유지한다.
|
||||
|
||||
## Target Baseline Structure
|
||||
|
||||
| 경로 | 책임 | 구현 메모 |
|
||||
| --- | --- | --- |
|
||||
| `docs/SRS/` | 외부 계약, 승인 기준, REQ 상태 관리 | requirement source of truth |
|
||||
| `docs/technical/` | 구현 상세 설계와 운영 메모 | SRS를 재정의하지 않음 |
|
||||
| `app/api/` | HTTP 라우트, 요청/응답 경계 | 서비스 계층 호출 전담 |
|
||||
| `app/cli/` | Typer CLI 명령과 입출력 | 서비스 계층 재사용 |
|
||||
| `app/services/` | 유스케이스 조합, 권한/검증 흐름 | API/CLI 공통 오케스트레이션 |
|
||||
| `app/domain/` | 포트폴리오 비즈니스 규칙 | 기술 의존성 최소화 |
|
||||
| `app/repositories/` | 영속성 인터페이스와 구현체 | DB feature flag 지원 |
|
||||
| `app/schemas/` | API 계약 스키마(Pydantic) | 요청/응답 안정성 |
|
||||
| `migrations/` | Alembic 마이그레이션 | 스키마 변경 이력 |
|
||||
| `tests/` | API/도메인/CLI/계약 테스트 | 회귀 방지 게이트 |
|
||||
|
||||
## Root-Level Exception Rules
|
||||
- 루트에는 `README.md`, `pyproject.toml`, `alembic.ini`, `.github/` 등 저장소 메타파일만 둔다.
|
||||
- 실행 코드와 테스트 코드는 목적 디렉토리로 수렴시킨다.
|
||||
|
||||
## Documentation Responsibilities
|
||||
- `README.md`: 빠른 시작, 실행 방법, 운영 필수 변수 요약
|
||||
- `docs/SRS/`: 왜(Why), 무엇(What), 수락 기준
|
||||
- `docs/technical/`: 어떻게(How), 모듈 경계, 예시 흐름
|
||||
|
||||
## Validation Notes
|
||||
- 신규 모듈 추가 시 책임이 상기 경로 정의와 충돌하지 않는지 확인한다.
|
||||
- API 계약 변경 시 `app/schemas/`와 `docs/technical/api-contracts.md`를 함께 동기화한다.
|
||||
- 스키마 변경 시 `migrations/versions/`와 문서 업데이트를 같은 변경 단위에 포함한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- Redis/Loki 실제 운영 경로가 코드에 전면 반영되면 `app/core/` 또는 별도 infra 패키지 분리 여부를 확정한다.
|
||||
- 태그 검색 기능(`REQ-002`) 구현 시 서비스/리포지토리 경계의 책임 분리를 추가 명세한다.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Python Runtime and Packaging
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
Python `3.11+` 및 `pyproject.toml` 중심 패키징 기준을 유지하고, 로컬/CI/운영의 실행 환경 편차를 줄인다.
|
||||
|
||||
## Runtime Baseline
|
||||
- 공식 지원 런타임은 Python `3.11` 이상이다.
|
||||
- 로컬 개발은 `venv` 기반 실행을 기본으로 한다.
|
||||
- 개발/테스트 의존성은 editable install(`pip install -e .[dev]`)로 일관화한다.
|
||||
|
||||
## Required Files
|
||||
|
||||
| 파일 | 책임 | 필수 내용 |
|
||||
| --- | --- | --- |
|
||||
| `pyproject.toml` | 프로젝트 메타데이터와 의존성 기준 | `requires-python >=3.11` |
|
||||
| `README.md` | 설치/실행/테스트 진입점 | venv 생성 및 실행 절차 |
|
||||
| `alembic.ini` | DB migration 런타임 설정 | Alembic 동작 기준 |
|
||||
|
||||
## Packaging Rules
|
||||
- 패키징 source of truth는 `pyproject.toml`로 유지한다.
|
||||
- CLI 엔트리포인트 `portfoliohub`는 패키지 설치 후 동일하게 동작해야 한다.
|
||||
- 운영 이미지/환경에서도 개발과 동일한 Python major/minor를 사용한다.
|
||||
|
||||
## Validation Notes
|
||||
- `pip install -e .[dev]` 후 `uvicorn app.main:app --reload`와 `pytest`가 실행 가능해야 한다.
|
||||
- `portfoliohub --help`가 정상 출력되어야 한다.
|
||||
- Alembic 명령(`upgrade head`)이 현재 마이그레이션 체인에서 실패하지 않아야 한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 락파일 기반 재현성(`uv lock`, `poetry.lock` 등) 도입 여부는 팀 운영 정책에 따라 확정한다.
|
||||
- 배포 환경 컨테이너 표준화 시 base image pinning 정책을 문서화한다.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Settings and Env Loading
|
||||
|
||||
- Related REQ: N/A
|
||||
|
||||
## 목표
|
||||
설정값 로딩 경로를 단일화하여 로컬/운영 간 동작 차이를 최소화하고, 보안 민감 값을 안전하게 주입한다.
|
||||
|
||||
## 설정 로딩 우선순위
|
||||
1. 환경 변수
|
||||
2. `.env` 파일
|
||||
3. 코드 기본값
|
||||
|
||||
## 핵심 설정 그룹
|
||||
|
||||
| 그룹 | 예시 키 | 목적 |
|
||||
| --- | --- | --- |
|
||||
| API/CORS | `PORTFOLIOHUB_ALLOWED_ORIGINS` | 브라우저 호출 허용 오리진 제어 |
|
||||
| 인증/권한 | `PORTFOLIOHUB_READ_TOKENS`, `PORTFOLIOHUB_TRADE_WRITE_TOKENS`, `PORTFOLIOHUB_ADMIN_TOKENS` | 정적 토큰 기반 접근 제어 |
|
||||
| 세션/토큰 TTL | `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS` 등 | DB 발급 토큰 만료 정책 |
|
||||
| Bootstrap User | `PORTFOLIOHUB_BOOTSTRAP_USERNAME`, `PORTFOLIOHUB_BOOTSTRAP_PASSWORD` | 초기 admin 사용자 생성 |
|
||||
| DB 연결 | `TIMESCALEDB_*` | Postgres/Timescale 접속 정보 |
|
||||
| Observability | `LOKI_*`, `REDIS_*` | 중앙 로그/캐시 운영 확장 포인트 |
|
||||
|
||||
## 상세 설정 명세
|
||||
|
||||
핵심 설정:
|
||||
|
||||
- `PORTFOLIOHUB_ALLOWED_ORIGINS`: comma-separated CORS origins
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`: users 테이블이 비어 있을 때 생성할 초기 username
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_EMAIL`: bootstrap user의 선택적 email
|
||||
- `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`: 초기 admin password
|
||||
- `PORTFOLIOHUB_READ_TOKENS`: comma-separated read tokens
|
||||
- `PORTFOLIOHUB_TRADE_WRITE_TOKENS`: comma-separated trade-write tokens
|
||||
- `PORTFOLIOHUB_ADMIN_TOKENS`: comma-separated admin tokens
|
||||
- `PORTFOLIOHUB_DB_ENABLED`: `true`일 때 DB repository 활성화
|
||||
- `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS`: login session token TTL(hours)
|
||||
- `PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS`: client token 기본 TTL(days)
|
||||
- `PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS`: bot token 기본 TTL(days)
|
||||
|
||||
DB 활성 설정:
|
||||
|
||||
- `TIMESCALEDB_HOST`
|
||||
- `TIMESCALEDB_PORT`
|
||||
- `TIMESCALEDB_NAME`
|
||||
- `TIMESCALEDB_USER`
|
||||
- `TIMESCALEDB_PASSWORD`
|
||||
- `TIMESCALEDB_SSLMODE`
|
||||
- `TIMESCALEDB_CONNECT_TIMEOUT_SEC`
|
||||
- `TIMESCALEDB_APPLICATION_NAME`
|
||||
|
||||
파생 설정:
|
||||
|
||||
- `active_database_url`은 psycopg PostgreSQL URL 포맷으로 credentials와 query options를 인코딩해 구성한다.
|
||||
|
||||
## CORS 정책
|
||||
|
||||
CORS middleware는 다음 기준으로 동작한다.
|
||||
|
||||
- `allow_origins`: 구성된 origins 목록 사용
|
||||
- `allow_credentials = true`
|
||||
- `allow_methods = ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]`
|
||||
- `allow_headers = ["Authorization", "Content-Type"]`
|
||||
|
||||
브라우저 기반 frontend가 HTTPS로 접근하면 `PORTFOLIOHUB_ALLOWED_ORIGINS`에는 정확한 HTTPS origin만 명시한다.
|
||||
|
||||
예시:
|
||||
|
||||
```bash
|
||||
PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
|
||||
```
|
||||
|
||||
## 운영 규칙
|
||||
- 운영 환경에서는 `.env` 대신 배포 플랫폼의 환경 변수 주입을 우선한다.
|
||||
- `PORTFOLIOHUB_DB_ENABLED=true`일 때 `TIMESCALEDB_*` 필수 키 누락 여부를 시작 시 검증한다.
|
||||
- 민감 값은 문서/샘플에 실제 비밀 값을 기록하지 않는다.
|
||||
|
||||
## Validation Notes
|
||||
- 필수 키 누락 시 명확한 예외 메시지로 기동 실패해야 한다.
|
||||
- CORS 오리진 파싱은 공백/쉼표 변형 입력에도 안정적으로 동작해야 한다.
|
||||
- 부트스트랩 계정 자동 생성은 `users` 테이블이 비어 있을 때만 수행되어야 한다.
|
||||
|
||||
## Open Implementation Points
|
||||
- 설정 스키마를 strict mode로 고정할지(미정의 키 거부) 여부를 확정한다.
|
||||
- Redis/Loki 활성화 플래그와 실패 시 동작(무시/실패)을 명시적으로 분리할지 결정한다.
|
||||
Reference in New Issue
Block a user