12 KiB
12 KiB
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과 동기화된 기준 문서다.
마이그레이션 운영 흐름
- 신규 스키마 변경을 Alembic revision으로 추가한다.
migrations/versions/에 revision 파일을 저장한다.- 본 문서에 revision ID와 스키마 변경 내역을 반영한다.
portfoliohub migrate-db또는 Alembicupgrade head로 적용한다.
명령 기준
- 초기 DB 준비:
portfoliohub init-db - 버전 업그레이드:
portfoliohub migrate-db - 상태 점검:
portfoliohub health-check
명령 동작:
init-db: target DB가 없으면 생성 후 Alembichead적용migrate-db: 기존 target DB에 Alembichead적용
스키마 기준
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 |
| 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_usernameon (username) - Unique:
uq_users_emailon (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_hashon (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_nameon (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_configon (name,config) - Unique:
uq_strategy_sets_nickon (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:
timescaledbextension 존재 시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 등)과 마이그레이션 단계를 추가 정의한다.