15 KiB
PortfolioHub Specification
1. Overview
PortfolioHub is a FastAPI-based portfolio lifecycle service with a Typer CLI.
- Runtime: Python 3.11+
- API framework: FastAPI
- CLI framework: Typer
- DB driver: Psycopg v3
- Migration: Alembic
Primary goals:
- Manage portfolio lifecycle (create/update/delete/reset)
- Record trade and balance-affecting actions (buy/sell/invest/transfer)
- Expose status and ledger snapshots through API and CLI
- Keep API and CLI behavior aligned through a shared service/repository layer
2. Scope and Non-Goals
In scope:
- Authenticated portfolio read and write operations
- Admin-managed user CRUD operations
- Role-based access control (read, trade-write, admin)
- Optional PostgreSQL/Timescale-backed repository via feature flag
- Operational commands for DB bootstrap and migration
Current non-goals / limitations:
- In-memory fallback repository is not implemented for production usage
- External broker execution is not handled here (service focuses on portfolio state)
- Advanced observability wiring for Redis/Loki is not yet implemented in runtime paths
3. Architecture
Layered components:
- API layer: HTTP routes and request validation
- CLI layer: command parsing and output rendering
- Service layer: orchestration and response shaping
- Domain layer: business logic rules
- Repository layer: persistence abstraction and concrete Postgres implementation
Key design principle:
- API and CLI call the same
PortfolioService, so behavior is consistent regardless of entrypoint.
4. Entrypoints
HTTP service:
- 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
5. Configuration Specification
Configuration precedence (high to low):
- Environment variables
.envfile (loaded at startup if present)- Code defaults
Core settings:
PORTFOLIOHUB_ALLOWED_ORIGINS: comma-separated CORS originsPORTFOLIOHUB_BOOTSTRAP_USERNAME: initial username created on startup when users table is emptyPORTFOLIOHUB_BOOTSTRAP_EMAIL: optional email for the bootstrap userPORTFOLIOHUB_BOOTSTRAP_PASSWORD: initial password for the bootstrap admin userPORTFOLIOHUB_READ_TOKENS: comma-separated read tokensPORTFOLIOHUB_TRADE_WRITE_TOKENS: comma-separated write tokensPORTFOLIOHUB_ADMIN_TOKENS: comma-separated admin tokensPORTFOLIOHUB_DB_ENABLED: enables DB repository selection whentruePORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS: login session token TTL in hoursPORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS: default client token TTL in daysPORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS: default bot token TTL in days
Database settings (active source):
TIMESCALEDB_HOSTTIMESCALEDB_PORTTIMESCALEDB_NAMETIMESCALEDB_USERTIMESCALEDB_PASSWORDTIMESCALEDB_SSLMODETIMESCALEDB_CONNECT_TIMEOUT_SECTIMESCALEDB_APPLICATION_NAME
Derived setting:
active_database_urluses PostgreSQL psycopg URL format with encoded credentials and query options.
6. Authentication and Authorization
Auth scheme:
Authorization: Bearer <token>required for protected API routes
Permission levels:
readtrade-writeadmin
Authorization hierarchy:
admincan access all endpointstrade-writecan accesstrade-write+readreadcan access only read endpoints
Failure responses:
- Missing/invalid bearer header:
401 - Unknown token:
401 - Insufficient permission:
403
7. HTTP API Specification
Base routes:
- Health:
/health - Auth:
/auth - Portfolio:
/portfolios - Users:
/users
7.1 Health Endpoint
GET /health- Auth: none
- Response
200:
{"status": "ok"}
7.2 Portfolio Endpoints
List Portfolios
GET /portfolios- Auth:
read - Query params:
broker(optional string)status_filter(optional string)
- Response
200: array ofPortfolioListItem
Get Portfolio Status
GET /portfolios/{portfolio_id}- Auth:
read - Query params:
recent_trades(int, default5)
- Response
200:PortfolioDetailResponse
Create Portfolio
POST /portfolios- Auth:
trade-write - Body:
CreatePortfolioRequest - Response
201:PortfolioDetailResponse
Update Portfolio
PATCH /portfolios/{portfolio_id}- Auth:
trade-write - Body:
UpdatePortfolioRequest - Response
200:PortfolioDetailResponse
Buy
POST /portfolios/{portfolio_id}/buy- Auth:
trade-write - Body:
BuyRequest - Response
200:PortfolioDetailResponse
Sell
POST /portfolios/{portfolio_id}/sell- Auth:
trade-write - Body:
SellRequest - Response
200:PortfolioDetailResponse
Invest
POST /portfolios/{portfolio_id}/invest- Auth:
trade-write - Body:
InvestRequest - Response
200:PortfolioDetailResponse
Transfer
POST /portfolios/{portfolio_id}/transfer- Auth:
trade-write - Body:
TransferRequest - Response
200:PortfolioDetailResponse
Reset
POST /portfolios/{portfolio_id}/reset- Auth:
trade-write - Body:
ResetPortfolioRequest - Response
200:PortfolioDetailResponse
Delete
DELETE /portfolios/{portfolio_id}- Auth:
admin - Query params:
keep_ledger(bool, defaultfalse)
- Response
204: no content
7.3 Strategy Set Endpoints
List Strategy Sets
GET /strategy-sets- Auth:
read - Response
200: array ofStrategySetResponse
Create Strategy Set
POST /strategy-sets- Auth:
trade-write - Body:
CreateStrategySetRequest - Response
201:StrategySetResponse
Note: Uses get_or_create_strategy_set logic, so if (name, config) pair already exists, returns existing strategy set.
Update Strategy Set
PATCH /strategy-sets/{strategy_set_id}- Auth:
trade-write - Body:
UpdateStrategySetRequest - Response
200:StrategySetResponse
Note: Since strategy sets are identified by (name, config) uniqueness, updating may result in a new strategy set ID if name or config changes.
Delete Strategy Set
DELETE /strategy-sets/{strategy_set_id}- Auth:
trade-write - Response
204: no content - Response
409: Conflict if strategy set is in use by any portfolio
7.4 Example Requests and Responses
- See
docs/api-examples.mdfor maintained request/response examples.
7.5 Auth Endpoints
Login
POST /auth/login- Auth: none
- Body:
LoginRequest - Response
200:TokenResponse - Response
401: invalid username/password
List Current Tokens
GET /auth/tokens- Auth:
reador higher with DB-issued token/session - Response
200: array ofAuthTokenResponse
Create Client Token
POST /auth/tokens- Auth:
reador higher with DB-issued token/session - Body:
CreateClientTokenRequest - Response
201:IssuedClientTokenResponse - Response
403: requested token permission exceeds current permission
Revoke Client Token
DELETE /auth/tokens/{token_id}- Auth:
reador higher with DB-issued token/session - Response
204: no content - Response
404: token not found
Create Bot Principal
POST /auth/bots- Auth:
admin - Body:
CreateBotPrincipalRequest - Response
201:BotPrincipalResponse
List Bot Principals
GET /auth/bots- Auth:
admin - Query params:
is_active(optional bool)
- Response
200: array ofBotPrincipalResponse
Get Bot Principal
GET /auth/bots/{bot_id}- Auth:
admin - Response
200:BotPrincipalResponse - Response
404: bot not found
Deactivate Bot Principal
DELETE /auth/bots/{bot_id}- Auth:
admin - Response
200:BotPrincipalResponse - Behavior: deactivates bot and revokes all active tokens linked to the bot
Issue Bot Token
POST /auth/bots/{bot_id}/tokens- Auth:
admin - Body:
CreateBotTokenRequest - Response
201:IssuedBotTokenResponse
List Bot Tokens
GET /auth/bots/{bot_id}/tokens- Auth:
admin - Response
200: array ofAuthTokenResponse
Revoke Bot Token
DELETE /auth/bots/{bot_id}/tokens/{token_id}- Auth:
admin - Response
204: no content
7.6 User Endpoints
All user management endpoints are admin-only.
List Users
GET /users- Auth:
admin - Response
200: array ofUserResponse
Get User
GET /users/{user_id}- Auth:
admin - Response
200:UserResponse - Response
404: user not found
Create User
POST /users- Auth:
admin - Body:
CreateUserRequest - Response
201:UserResponse - Response
409: duplicate username or email
Bootstrap rule:
- If DB-backed persistence is enabled and both
PORTFOLIOHUB_BOOTSTRAP_USERNAMEandPORTFOLIOHUB_BOOTSTRAP_PASSWORDare configured, application startup creates the bootstrap admin user only when theuserstable is empty.
Update User
PATCH /users/{user_id}- Auth:
admin - Body:
UpdateUserRequest - Response
200:UserResponse - Response
404: user not found - Response
409: duplicate username or email
Delete User
DELETE /users/{user_id}- Auth:
admin - Response
204: no content - Response
404: user not found
8. Schema Specification
8.1 Request Models
CreatePortfolioRequest
portfolio_id: strbroker: str = "kis"status: str = "enabled"symbol: 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 (all fields optional)
broker: str | nullstatus: 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
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
LoginRequest
username: strpassword: str
CreateClientTokenRequest
name: strpermission: "read" | "trade-write" | "admin" | nullexpires_in_days: int | null
CreateBotPrincipalRequest
owner_user_id: strname: strdescription: str | null
CreateBotTokenRequest
name: strpermission: "read" | "trade-write" | "admin" | nullexpires_in_days: int | nullprice: float > 0fee: float >= 0tax: float >= 0note: str = ""
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
CreateStrategySetRequest
name: strconfig: str
UpdateStrategySetRequest (all fields optional)
name: str | nullconfig: str | null
8.2 Response Models
PortfolioListItem
- Includes portfolio summary fields and computed
total_value
PortfolioSummary
portfolio_id: strbroker: strstatus: 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
LedgerEntryResponse
- Snapshot-oriented ledger response including balances after each action
ErrorResponse
detail: str
BotPrincipalResponse
id: strowner_user_id: strname: strdescription: str | nullis_active: boolcreated_at: datetime | nullupdated_at: datetime | nulldeactivated_at: datetime | null
9. CLI Specification
Global option:
--format [table|json](default:table)
Commands:
portfoliohub init-db [--maintenance-db postgres]portfoliohub migrate-dbportfoliohub health-check
Portfolio subcommands:
portfoliohub portfolio list [--broker ...] [--status ...]portfoliohub portfolio status <portfolio_id> [--recent 5]portfoliohub portfolio create <portfolio_id> --symbol ... [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]
Strategy subcommands:
portfoliohub strategy listportfoliohub strategy create --name ... --config ...portfoliohub strategy update <strategy_set_id> [--name ...] [--config ...]portfoliohub strategy delete <strategy_set_id> [--force]
Confirmation behavior:
- Mutating portfolio commands request confirmation by default.
--forcebypasses confirmation prompts.
10. Persistence and Migration Specification
Repository selection:
- If
PORTFOLIOHUB_DB_ENABLED=false: an unimplemented placeholder repository raisesFeatureNotReadyErroron access. - If
PORTFOLIOHUB_DB_ENABLED=true:PostgresPortfolioRepositoryis used.
Migration commands:
init-db: create target DB if missing, then apply Alembicheadmigrate-db: apply Alembicheadto existing target DB
Migration source of truth:
migrations/versions
DB schema specification:
- Detailed table/column/index constraints are documented in
docs/db-schema-spec.md. - The documentation is generated from and must stay consistent with Alembic revisions under
migrations/versions.
11. Error Handling
Application-level errors are translated through registered exception handlers.
Typical error classes include:
- authorization failures (
401,403) - domain/repository-level failures surfaced by service operations
- feature-not-ready errors when DB mode is disabled but persistence is required
12. CORS Policy
CORS middleware is enabled with:
allow_originsfrom configured origins listallow_credentials = trueallow_methods = ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]allow_headers = ["Authorization", "Content-Type"]
13. Testing Specification
Test framework:
pytest
Configured behavior:
- test paths:
tests - recursion excluded:
temp,.git,.venv
Current test modules include API, auth, CLI, and domain service coverage.
14. Versioning and Compatibility
- Current package version:
0.1.0 - API version exposure follows application version setting (
app_version) - Minimum Python version:
3.11
15. Future Extension Points
- Redis-backed cache/session integration
- Loki logging pipeline integration
- Additional brokers and order-execution adapters
- Enhanced operational/admin endpoints