feat: update strategy set schema fields

This commit is contained in:
2026-04-03 08:03:27 +00:00
parent 3eae90a9ab
commit ebd4559939
13 changed files with 131 additions and 37 deletions
+3 -2
View File
@@ -95,8 +95,9 @@ def print_portfolio_detail(console: Console, detail: PortfolioDetailResponse) ->
"\n".join( "\n".join(
[ [
f"id: {detail.strategy_set.id}", f"id: {detail.strategy_set.id}",
f"name: {detail.strategy_set.strategy_name}", f"name: {detail.strategy_set.name}",
f"config: {detail.strategy_set.strategy_config}", f"config: {detail.strategy_set.config}",
f"nick: {detail.strategy_set.nick}",
] ]
), ),
title="Strategy Set", title="Strategy Set",
+3 -2
View File
@@ -10,8 +10,9 @@ from typing import Any, Protocol, runtime_checkable
@dataclass @dataclass
class StrategySet: class StrategySet:
id: str id: str
strategy_name: str name: str
strategy_config: str config: str
nick: str
created_at: datetime | None = None created_at: datetime | None = None
+1 -1
View File
@@ -45,7 +45,7 @@ class PortfolioDomainRepository(Protocol):
def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio: ... def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio: ...
def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: ... def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: ...
def get_strategy_set(self, id: str) -> StrategySet | None: ... def get_strategy_set(self, id: str) -> StrategySet | None: ...
def get_or_create_strategy_set(self, strategy_name: str, strategy_config: str) -> StrategySet: ... def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: ...
def save( def save(
self, self,
portfolio: Portfolio, portfolio: Portfolio,
+1 -1
View File
@@ -43,7 +43,7 @@ class PortfolioRepository(Protocol):
def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: ... def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: ...
def get_or_create_strategy_set(self, strategy_name: str, strategy_config: str) -> StrategySet: ... def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: ...
def get_strategy_set(self, id: str) -> StrategySet | None: ... def get_strategy_set(self, id: str) -> StrategySet | None: ...
+17 -14
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import json import json
import uuid import uuid
from dataclasses import dataclass from dataclasses import dataclass
@@ -82,10 +83,11 @@ class PostgresPortfolioRepository:
""" """
CREATE TABLE IF NOT EXISTS strategy_sets ( CREATE TABLE IF NOT EXISTS strategy_sets (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
strategy_name TEXT NOT NULL, name TEXT NOT NULL,
strategy_config TEXT NOT NULL DEFAULT '', config TEXT NOT NULL DEFAULT '',
nick TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (strategy_name, strategy_config) UNIQUE (name, config)
) )
""" """
) )
@@ -352,40 +354,41 @@ class PostgresPortfolioRepository:
rows = cur.fetchall() rows = cur.fetchall()
return [self._row_to_portfolio(row) for row in rows] return [self._row_to_portfolio(row) for row in rows]
def get_or_create_strategy_set(self, strategy_name: str, strategy_config: str) -> StrategySet: def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
self.ensure_schema() self.ensure_schema()
nick = f"{name}:{hashlib.sha1(config.encode('utf-8')).hexdigest()[:12]}"
with self._connect().cursor() as cur: with self._connect().cursor() as cur:
new_id = str(uuid.uuid4()) new_id = str(uuid.uuid4())
cur.execute( cur.execute(
""" """
INSERT INTO strategy_sets (id, strategy_name, strategy_config) INSERT INTO strategy_sets (id, name, config, nick)
VALUES (%s, %s, %s) VALUES (%s, %s, %s, %s)
ON CONFLICT (strategy_name, strategy_config) DO NOTHING ON CONFLICT (name, config) DO NOTHING
""", """,
(new_id, strategy_name, strategy_config), (new_id, name, config, nick),
) )
cur.execute( cur.execute(
""" """
SELECT id, strategy_name, strategy_config, created_at SELECT id, name, config, nick, created_at
FROM strategy_sets FROM strategy_sets
WHERE strategy_name = %s AND strategy_config = %s WHERE name = %s AND config = %s
""", """,
(strategy_name, strategy_config), (name, config),
) )
row = cur.fetchone() row = cur.fetchone()
return StrategySet(id=row[0], strategy_name=row[1], strategy_config=row[2], created_at=row[3]) return StrategySet(id=row[0], name=row[1], config=row[2], nick=row[3], created_at=row[4])
def get_strategy_set(self, id: str) -> StrategySet | None: def get_strategy_set(self, id: str) -> StrategySet | None:
self.ensure_schema() self.ensure_schema()
with self._connect().cursor() as cur: with self._connect().cursor() as cur:
cur.execute( cur.execute(
"SELECT id, strategy_name, strategy_config, created_at FROM strategy_sets WHERE id = %s", "SELECT id, name, config, nick, created_at FROM strategy_sets WHERE id = %s",
(id,), (id,),
) )
row = cur.fetchone() row = cur.fetchone()
if row is None: if row is None:
return None return None
return StrategySet(id=row[0], strategy_name=row[1], strategy_config=row[2], created_at=row[3]) return StrategySet(id=row[0], name=row[1], config=row[2], nick=row[3], created_at=row[4])
def save( def save(
self, self,
+3 -2
View File
@@ -10,8 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field
class StrategySetResponse(BaseModel): class StrategySetResponse(BaseModel):
id: str id: str
strategy_name: str name: str
strategy_config: str config: str
nick: str
created_at: datetime | None = None created_at: datetime | None = None
+3 -2
View File
@@ -65,8 +65,9 @@ def _to_strategy_set_response(strategy_set: StrategySet | None):
return None return None
return StrategySetResponse( return StrategySetResponse(
id=strategy_set.id, id=strategy_set.id,
strategy_name=strategy_set.strategy_name, name=strategy_set.name,
strategy_config=strategy_set.strategy_config, config=strategy_set.config,
nick=strategy_set.nick,
created_at=strategy_set.created_at, created_at=strategy_set.created_at,
) )
+3 -2
View File
@@ -114,8 +114,9 @@ Response 200:
], ],
"strategy_set": { "strategy_set": {
"id": "stv-001", "id": "stv-001",
"strategy_name": "target_value", "name": "target_value",
"strategy_config": "{}", "config": "{}",
"nick": "target_value:99914b932bd3",
"created_at": "2026-04-03T00:00:00Z" "created_at": "2026-04-03T00:00:00Z"
}, },
"live_quantity": null "live_quantity": null
+10 -5
View File
@@ -5,8 +5,10 @@
This document defines the database schema in an AI-agent-friendly format. This document defines the database schema in an AI-agent-friendly format.
- Source of truth: Alembic revisions in `migrations/versions` - Source of truth: Alembic revisions in `migrations/versions`
- Current baseline revision: `20260403_0001` - Current baseline revision: `20260403_0002`
- Baseline file: `migrations/versions/20260403_0001_initial_portfolio_schema.py` - Baseline files:
- `migrations/versions/20260403_0001_initial_portfolio_schema.py`
- `migrations/versions/20260403_0002_strategy_set_columns.py`
## 2. Scope ## 2. Scope
@@ -38,14 +40,16 @@ Columns:
| Column | Type | Null | Default | Constraints / Notes | | Column | Type | Null | Default | Constraints / Notes |
|---|---|---|---|---| |---|---|---|---|---|
| id | Text | NO | - | Primary Key | | id | Text | NO | - | Primary Key |
| strategy_name | Text | NO | - | Part of unique pair | | name | Text | NO | - | Strategy name, part of unique pair |
| strategy_config | Text | NO | "" | 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 | | created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp |
Constraints: Constraints:
- Primary key: `id` - Primary key: `id`
- Unique: `uq_strategy_sets_name_config` on (`strategy_name`, `strategy_config`) - Unique: `uq_strategy_sets_name_config` on (`name`, `config`)
- Unique: `uq_strategy_sets_nick` on (`nick`)
### 4.2 portfolios ### 4.2 portfolios
@@ -155,3 +159,4 @@ When schema changes:
| Revision ID | File | Summary | | 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_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` |
+8
View File
@@ -275,6 +275,14 @@ Base routes:
- `strategy_set: StrategySetResponse | null` - `strategy_set: StrategySetResponse | null`
- `live_quantity: float | null` - `live_quantity: float | null`
`StrategySetResponse`
- `id: str`
- `name: str`
- `config: str`
- `nick: str`
- `created_at: datetime | null`
`LedgerEntryResponse` `LedgerEntryResponse`
- Snapshot-oriented ledger response including balances after each action - Snapshot-oriented ledger response including balances after each action
@@ -0,0 +1,73 @@
"""rename strategy_set columns and add nick
Revision ID: 20260403_0002
Revises: 20260403_0001
Create Date: 2026-04-03 00:30:00
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "20260403_0002"
down_revision = "20260403_0001"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("strategy_sets", sa.Column("name", sa.Text(), nullable=True))
op.add_column("strategy_sets", sa.Column("config", sa.Text(), nullable=True, server_default=""))
op.add_column("strategy_sets", sa.Column("nick", sa.Text(), nullable=True))
op.execute(
sa.text(
"""
UPDATE strategy_sets
SET
name = strategy_name,
config = strategy_config,
nick = strategy_name || ':' || substr(md5(strategy_config), 1, 12)
"""
)
)
op.alter_column("strategy_sets", "name", nullable=False)
op.alter_column("strategy_sets", "config", nullable=False)
op.alter_column("strategy_sets", "nick", nullable=False)
op.drop_constraint("uq_strategy_sets_name_config", "strategy_sets", type_="unique")
op.create_unique_constraint("uq_strategy_sets_name_config", "strategy_sets", ["name", "config"])
op.create_unique_constraint("uq_strategy_sets_nick", "strategy_sets", ["nick"])
op.drop_column("strategy_sets", "strategy_name")
op.drop_column("strategy_sets", "strategy_config")
def downgrade() -> None:
op.add_column("strategy_sets", sa.Column("strategy_name", sa.Text(), nullable=True))
op.add_column("strategy_sets", sa.Column("strategy_config", sa.Text(), nullable=True, server_default=""))
op.execute(
sa.text(
"""
UPDATE strategy_sets
SET
strategy_name = name,
strategy_config = config
"""
)
)
op.alter_column("strategy_sets", "strategy_name", nullable=False)
op.alter_column("strategy_sets", "strategy_config", nullable=False)
op.drop_constraint("uq_strategy_sets_nick", "strategy_sets", type_="unique")
op.drop_constraint("uq_strategy_sets_name_config", "strategy_sets", type_="unique")
op.create_unique_constraint("uq_strategy_sets_name_config", "strategy_sets", ["strategy_name", "strategy_config"])
op.drop_column("strategy_sets", "nick")
op.drop_column("strategy_sets", "name")
op.drop_column("strategy_sets", "config")
+3 -3
View File
@@ -15,7 +15,7 @@ class FakeRepo:
def __init__(self, portfolio: Portfolio | None = None) -> None: def __init__(self, portfolio: Portfolio | None = None) -> None:
self._portfolio = portfolio self._portfolio = portfolio
self.entries: list[LedgerEntry] = [] self.entries: list[LedgerEntry] = []
self.strategy_set = StrategySet(id="set-1", strategy_name="target_value", strategy_config="{}") self.strategy_set = StrategySet(id="set-1", name="target_value", config="{}", nick="target_value:99914b932bd3")
self.closed = False self.closed = False
def close(self) -> None: def close(self) -> None:
@@ -99,8 +99,8 @@ class FakeRepo:
return [] return []
return [self._portfolio] return [self._portfolio]
def get_or_create_strategy_set(self, strategy_name: str, strategy_config: str) -> StrategySet: def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
self.strategy_set = StrategySet(id="set-1", strategy_name=strategy_name, strategy_config=strategy_config) self.strategy_set = StrategySet(id="set-1", name=name, config=config, nick=f"{name}:99914b932bd3")
return self.strategy_set return self.strategy_set
def get_strategy_set(self, id: str) -> StrategySet | None: def get_strategy_set(self, id: str) -> StrategySet | None:
+3 -3
View File
@@ -12,7 +12,7 @@ class FakeRepo:
def __init__(self, portfolio: Portfolio | None = None) -> None: def __init__(self, portfolio: Portfolio | None = None) -> None:
self._portfolio = portfolio self._portfolio = portfolio
self.entries: list[LedgerEntry] = [] self.entries: list[LedgerEntry] = []
self.strategy_set = StrategySet(id="set-1", strategy_name="target_value", strategy_config="{}") self.strategy_set = StrategySet(id="set-1", name="target_value", config="{}", nick="target_value:99914b932bd3")
self.last_delete: tuple[str, bool] | None = None self.last_delete: tuple[str, bool] | None = None
def get(self, portfolio_id: str) -> Portfolio: def get(self, portfolio_id: str) -> Portfolio:
@@ -60,8 +60,8 @@ class FakeRepo:
def get_strategy_set(self, id: str) -> StrategySet | None: def get_strategy_set(self, id: str) -> StrategySet | None:
return self.strategy_set if id == self.strategy_set.id else None return self.strategy_set if id == self.strategy_set.id else None
def get_or_create_strategy_set(self, strategy_name: str, strategy_config: str) -> StrategySet: def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
self.strategy_set = StrategySet(id="set-1", strategy_name=strategy_name, strategy_config=strategy_config) self.strategy_set = StrategySet(id="set-1", name=name, config=config, nick=f"{name}:99914b932bd3")
return self.strategy_set return self.strategy_set
def save( def save(