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(
|
"\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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: ...
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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` |
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user