# PortfolioHub DB Schema Specification ## 1. Purpose This document defines the database schema in an AI-agent-friendly format. - Source of truth: Alembic revisions in `migrations/versions` - Current baseline revision: `20260403_0007` - Baseline files: - `migrations/versions/20260403_0001_initial_portfolio_schema.py` - `migrations/versions/20260403_0002_strategy_set_columns.py` - `migrations/versions/20260403_0003_portfolio_strategy_refs.py` - `migrations/versions/20260403_0004_users_table.py` - `migrations/versions/20260403_0005_auth_user_fields_and_tokens.py` - `migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py` - `migrations/versions/20260403_0007_bot_principal_permission_only.py` ## 2. Scope Schema objects covered by baseline revision: - `strategy_sets` - `users` - `auth_tokens` - `bot_principals` - `portfolios` - `portfolio_ledger` - secondary indexes - Timescale hypertable conversion rule for `portfolio_ledger` ## 3. Conventions - Data type names follow SQLAlchemy/Alembic declaration. - `NULL` means nullable. - Default means DB-level `server_default` where defined. - Relationship columns are documented even when no FK constraint exists. ## 4. Tables ### 4.1 users Purpose: - Stores admin-managed user records for operational ownership and future identity mapping. Columns: | 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`) ### 4.2 auth_tokens Purpose: - Stores DB-issued session tokens and client tokens. Columns: | 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 for issued token | | 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`) ### 4.3 bot_principals Purpose: - Stores external bot identities for admin-managed API key issuance and lifecycle control. Columns: | 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 operator description | | is_active | Boolean | NO | TRUE | When false, bot tokens are rejected | | 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`) ### 4.3 strategy_sets Purpose: - Stores normalized strategy configuration snapshots. Columns: | Column | Type | Null | Default | Constraints / Notes | |---|---|---|---|---| | id | Text | NO | - | Primary Key | | name | Text | NO | - | Strategy name, part of unique pair | | config | Text | NO | "" | Strategy config payload, part of unique pair | | nick | Text | NO | - | Unique strategy set 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`) ### 4.4 portfolios Purpose: - Stores current portfolio state and strategy linkage. Current baseline note: - The columns below describe the currently migrated schema through revision `20260403_0005`. Columns: | Column | Type | Null | Default | Constraints / Notes | |---|---|---|---|---| | portfolio_id | Text | NO | - | Primary Key | | broker | Text | NO | "kis" | Broker code | | status | Text | NO | "enabled" | Portfolio status | | 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` values | | 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 Constraint notes: - `active_strategy_id` is a nullable logical reference to `strategy_sets.id` - `avabile_strategies` stores zero or more selectable strategy set IDs when present ### 4.5 portfolio_ledger Purpose: - Stores append-like ledger snapshots per portfolio action. Columns: | Column | Type | Null | Default | Constraints / Notes | |---|---|---|---|---| | portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` (no FK) | | 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 ## 5. Indexes ### 5.1 ix_users_created_at - Table: `users` - Columns/expressions: (`created_at`) - Unique: no - Intent: speed operational listing by recency. ### 5.2 idx_auth_tokens_user_created - Table: `auth_tokens` - Columns/expressions: (`user_id`, `created_at`) - Unique: no - Intent: fetch current user's issued tokens efficiently. ### 5.3 idx_auth_tokens_hash_active - Table: `auth_tokens` - Columns/expressions: (`token_hash`) - Unique: no - Intent: resolve bearer tokens efficiently. ### 5.4 idx_auth_tokens_bot_created - Table: `auth_tokens` - Columns/expressions: (`bot_principal_id`, `created_at`) - Unique: no - Intent: fetch bot-issued token lists efficiently. ### 5.5 idx_bot_principals_active_created - Table: `bot_principals` - Columns/expressions: (`is_active`, `created_at`) - Unique: no - Intent: speed operational filtering of active/inactive bots. ### 5.6 idx_portfolio_ledger_portfolio_time - Table: `portfolio_ledger` - Columns/expressions: (`portfolio_id`, `recorded_at DESC`) - Unique: no - Intent: fetch recent ledger entries per portfolio efficiently. ### 5.7 idx_portfolios_broker_status_updated - Table: `portfolios` - Columns/expressions: (`broker`, `status`, `updated_at DESC`) - Unique: no - Intent: speed list/filter queries by broker/status and recency. ## 6. TimescaleDB Behavior Conditional behavior in migration upgrade: - Checks whether extension `timescaledb` exists. - If present, executes: `create_hypertable('portfolio_ledger', 'recorded_at', if_not_exists => TRUE)` Implication: - `portfolio_ledger` acts as a hypertable in environments with TimescaleDB extension enabled. ## 7. Integrity Notes Current baseline intentionally has no DB-level FK constraints. - Logical relations are managed by application/repository logic. - When adding FK constraints in future migrations, update this document and include migration IDs. ## 8. Change Management Rule When schema changes: 1. Add/modify Alembic revision under `migrations/versions`. 2. Update this document in the same commit. 3. Keep revision mapping table below in sync. ## 9. 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`->`name`, `strategy_config`->`config`, add unique `nick`, handle legacy unnamed unique constraint | | 20260403_0003 | migrations/versions/20260403_0003_portfolio_strategy_refs.py | Remove embedded portfolio 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 password/permission/login fields to `users` and create `auth_tokens` table | | 20260403_0006 | migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py | Add `bot_principals` table and `auth_tokens.bot_principal_id` for bot API key management | | 20260403_0007 | migrations/versions/20260403_0007_bot_principal_permission_only.py | Remove `bot_principals.owner_user_id` and enforce bot-local `permission` model |