Files
portfolio-hub/docs/portfoliohub-spec.md
T

17 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):

  1. Environment variables
  2. .env file (loaded at startup if present)
  3. Code defaults

Core settings:

  • PORTFOLIOHUB_ALLOWED_ORIGINS: comma-separated CORS origins
  • PORTFOLIOHUB_BOOTSTRAP_USERNAME: initial username created on startup when users table is empty
  • PORTFOLIOHUB_BOOTSTRAP_EMAIL: optional email for the bootstrap user
  • PORTFOLIOHUB_BOOTSTRAP_PASSWORD: initial password for the bootstrap admin user
  • PORTFOLIOHUB_READ_TOKENS: comma-separated read tokens
  • PORTFOLIOHUB_TRADE_WRITE_TOKENS: comma-separated write tokens
  • PORTFOLIOHUB_ADMIN_TOKENS: comma-separated admin tokens
  • PORTFOLIOHUB_DB_ENABLED: enables DB repository selection when true
  • PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS: login session token TTL in hours
  • PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS: default client token TTL in days
  • PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS: default bot token TTL in days

Database settings (active source):

  • TIMESCALEDB_HOST
  • TIMESCALEDB_PORT
  • TIMESCALEDB_NAME
  • TIMESCALEDB_USER
  • TIMESCALEDB_PASSWORD
  • TIMESCALEDB_SSLMODE
  • TIMESCALEDB_CONNECT_TIMEOUT_SEC
  • TIMESCALEDB_APPLICATION_NAME

Derived setting:

  • active_database_url uses 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:

  • read
  • trade-write
  • admin

Authorization hierarchy:

  • admin can access all endpoints
  • trade-write can access trade-write + read
  • read can 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 of PortfolioListItem

Get Portfolio Status

  • GET /portfolios/{portfolio_id}
  • Auth: read
  • Query params:
    • recent_trades (int, default 5)
  • 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, default false)
  • Response 204: no content

Create Comment

  • POST /portfolios/{portfolio_id}/comments
  • Auth: trade-write
  • Body: CommentCreateRequest (content: str)
  • Response 201: CommentResponse
  • Response 404: portfolio not found

List Comments

  • GET /portfolios/{portfolio_id}/comments
  • Auth: read
  • Response 200: array of CommentResponse, sorted by created_at DESC
  • Response 404: portfolio not found

Update Comment

  • PATCH /portfolios/{portfolio_id}/comments/{comment_id}
  • Auth: trade-write
  • Body: CommentUpdateRequest (content: str)
  • Response 200: CommentResponse
  • Response 404: portfolio or comment not found

Delete Comment

  • DELETE /portfolios/{portfolio_id}/comments/{comment_id}
  • Auth: trade-write
  • Response 204: no content
  • Response 404: portfolio or comment not found

7.3 Strategy Set Endpoints

List Strategy Sets

  • GET /strategy-sets
  • Auth: read
  • Response 200: array of StrategySetResponse

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.md for 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: read or higher with DB-issued token/session
  • Response 200: array of AuthTokenResponse

Create Client Token

  • POST /auth/tokens
  • Auth: read or 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: read or 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 of BotPrincipalResponse

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 of AuthTokenResponse

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 of UserResponse

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_USERNAME and PORTFOLIOHUB_BOOTSTRAP_PASSWORD are configured, application startup creates the bootstrap admin user only when the users table 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: str
  • broker: str = "kis"
  • status: str = "enabled"
  • name: str | null (when null, defaults to symbol)
  • 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 (all fields optional)

  • 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

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

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
  • price: float > 0
  • fee: float >= 0
  • tax: float >= 0
  • note: str = ""

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

  • content: str

CommentUpdateRequest

  • content: str

CreateStrategySetRequest

  • name: str
  • config: str

UpdateStrategySetRequest (all fields optional)

  • name: str | null
  • config: str | null

8.2 Response Models

PortfolioListItem

  • Includes portfolio summary fields and 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

LedgerEntryResponse

  • Snapshot-oriented ledger response including balances after each action

ErrorResponse

  • detail: str

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

9. CLI Specification

Global option:

  • --format [table|json] (default: table)

Commands:

  • portfoliohub init-db [--maintenance-db postgres]
  • portfoliohub migrate-db
  • portfoliohub health-check

Portfolio subcommands:

  • 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]

Portfolio comment subcommands:

  • 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]

Strategy subcommands:

  • portfoliohub strategy list
  • portfoliohub 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.
  • --force bypasses confirmation prompts.

10. Persistence and Migration Specification

Repository selection:

  • If PORTFOLIOHUB_DB_ENABLED=false: an unimplemented placeholder repository raises FeatureNotReadyError on access.
  • If PORTFOLIOHUB_DB_ENABLED=true: PostgresPortfolioRepository is used.

Migration commands:

  • init-db: create target DB if missing, then apply Alembic head
  • migrate-db: apply Alembic head to 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_origins from configured origins list
  • allow_credentials = true
  • allow_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