feat: update strategy set schema fields
This commit is contained in:
@@ -95,8 +95,9 @@ def print_portfolio_detail(console: Console, detail: PortfolioDetailResponse) ->
|
||||
"\n".join(
|
||||
[
|
||||
f"id: {detail.strategy_set.id}",
|
||||
f"name: {detail.strategy_set.strategy_name}",
|
||||
f"config: {detail.strategy_set.strategy_config}",
|
||||
f"name: {detail.strategy_set.name}",
|
||||
f"config: {detail.strategy_set.config}",
|
||||
f"nick: {detail.strategy_set.nick}",
|
||||
]
|
||||
),
|
||||
title="Strategy Set",
|
||||
|
||||
@@ -10,8 +10,9 @@ from typing import Any, Protocol, runtime_checkable
|
||||
@dataclass
|
||||
class StrategySet:
|
||||
id: str
|
||||
strategy_name: str
|
||||
strategy_config: str
|
||||
name: str
|
||||
config: str
|
||||
nick: str
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class PortfolioDomainRepository(Protocol):
|
||||
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 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(
|
||||
self,
|
||||
portfolio: Portfolio,
|
||||
|
||||
@@ -43,7 +43,7 @@ class PortfolioRepository(Protocol):
|
||||
|
||||
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: ...
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
@@ -82,10 +83,11 @@ class PostgresPortfolioRepository:
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS strategy_sets (
|
||||
id TEXT PRIMARY KEY,
|
||||
strategy_name TEXT NOT NULL,
|
||||
strategy_config TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
config TEXT NOT NULL DEFAULT '',
|
||||
nick TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (strategy_name, strategy_config)
|
||||
UNIQUE (name, config)
|
||||
)
|
||||
"""
|
||||
)
|
||||
@@ -352,40 +354,41 @@ class PostgresPortfolioRepository:
|
||||
rows = cur.fetchall()
|
||||
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()
|
||||
nick = f"{name}:{hashlib.sha1(config.encode('utf-8')).hexdigest()[:12]}"
|
||||
with self._connect().cursor() as cur:
|
||||
new_id = str(uuid.uuid4())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO strategy_sets (id, strategy_name, strategy_config)
|
||||
VALUES (%s, %s, %s)
|
||||
ON CONFLICT (strategy_name, strategy_config) DO NOTHING
|
||||
INSERT INTO strategy_sets (id, name, config, nick)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (name, config) DO NOTHING
|
||||
""",
|
||||
(new_id, strategy_name, strategy_config),
|
||||
(new_id, name, config, nick),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, strategy_name, strategy_config, created_at
|
||||
SELECT id, name, config, nick, created_at
|
||||
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()
|
||||
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:
|
||||
self.ensure_schema()
|
||||
with self._connect().cursor() as cur:
|
||||
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,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is 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(
|
||||
self,
|
||||
|
||||
@@ -10,8 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
class StrategySetResponse(BaseModel):
|
||||
id: str
|
||||
strategy_name: str
|
||||
strategy_config: str
|
||||
name: str
|
||||
config: str
|
||||
nick: str
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ def _to_strategy_set_response(strategy_set: StrategySet | None):
|
||||
return None
|
||||
return StrategySetResponse(
|
||||
id=strategy_set.id,
|
||||
strategy_name=strategy_set.strategy_name,
|
||||
strategy_config=strategy_set.strategy_config,
|
||||
name=strategy_set.name,
|
||||
config=strategy_set.config,
|
||||
nick=strategy_set.nick,
|
||||
created_at=strategy_set.created_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -114,8 +114,9 @@ Response 200:
|
||||
],
|
||||
"strategy_set": {
|
||||
"id": "stv-001",
|
||||
"strategy_name": "target_value",
|
||||
"strategy_config": "{}",
|
||||
"name": "target_value",
|
||||
"config": "{}",
|
||||
"nick": "target_value:99914b932bd3",
|
||||
"created_at": "2026-04-03T00:00:00Z"
|
||||
},
|
||||
"live_quantity": null
|
||||
|
||||
+10
-5
@@ -5,8 +5,10 @@
|
||||
This document defines the database schema in an AI-agent-friendly format.
|
||||
|
||||
- Source of truth: Alembic revisions in `migrations/versions`
|
||||
- Current baseline revision: `20260403_0001`
|
||||
- Baseline file: `migrations/versions/20260403_0001_initial_portfolio_schema.py`
|
||||
- Current baseline revision: `20260403_0002`
|
||||
- Baseline files:
|
||||
- `migrations/versions/20260403_0001_initial_portfolio_schema.py`
|
||||
- `migrations/versions/20260403_0002_strategy_set_columns.py`
|
||||
|
||||
## 2. Scope
|
||||
|
||||
@@ -38,14 +40,16 @@ Columns:
|
||||
| Column | Type | Null | Default | Constraints / Notes |
|
||||
|---|---|---|---|---|
|
||||
| id | Text | NO | - | Primary Key |
|
||||
| strategy_name | Text | NO | - | Part of unique pair |
|
||||
| strategy_config | Text | NO | "" | Part of unique pair |
|
||||
| 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 (`strategy_name`, `strategy_config`)
|
||||
- Unique: `uq_strategy_sets_name_config` on (`name`, `config`)
|
||||
- Unique: `uq_strategy_sets_nick` on (`nick`)
|
||||
|
||||
### 4.2 portfolios
|
||||
|
||||
@@ -155,3 +159,4 @@ When schema changes:
|
||||
| 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` |
|
||||
|
||||
@@ -275,6 +275,14 @@ Base routes:
|
||||
- `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
|
||||
|
||||
@@ -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
@@ -15,7 +15,7 @@ class FakeRepo:
|
||||
def __init__(self, portfolio: Portfolio | None = None) -> None:
|
||||
self._portfolio = portfolio
|
||||
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
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -99,8 +99,8 @@ class FakeRepo:
|
||||
return []
|
||||
return [self._portfolio]
|
||||
|
||||
def get_or_create_strategy_set(self, strategy_name: str, strategy_config: str) -> StrategySet:
|
||||
self.strategy_set = StrategySet(id="set-1", strategy_name=strategy_name, strategy_config=strategy_config)
|
||||
def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
|
||||
self.strategy_set = StrategySet(id="set-1", name=name, config=config, nick=f"{name}:99914b932bd3")
|
||||
return self.strategy_set
|
||||
|
||||
def get_strategy_set(self, id: str) -> StrategySet | None:
|
||||
|
||||
@@ -12,7 +12,7 @@ class FakeRepo:
|
||||
def __init__(self, portfolio: Portfolio | None = None) -> None:
|
||||
self._portfolio = portfolio
|
||||
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
|
||||
|
||||
def get(self, portfolio_id: str) -> Portfolio:
|
||||
@@ -60,8 +60,8 @@ class FakeRepo:
|
||||
def get_strategy_set(self, id: str) -> StrategySet | 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:
|
||||
self.strategy_set = StrategySet(id="set-1", strategy_name=strategy_name, strategy_config=strategy_config)
|
||||
def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
|
||||
self.strategy_set = StrategySet(id="set-1", name=name, config=config, nick=f"{name}:99914b932bd3")
|
||||
return self.strategy_set
|
||||
|
||||
def save(
|
||||
|
||||
Reference in New Issue
Block a user