Add ProofLine criteria documentation and Strategy Catalog v1 implementation
- Add acceptance criteria documentation (AC-0013 through AC-0023) - Add Line-0003 discovery documentation - Implement Strategy Catalog v1 with new database migration - Update portfolio API endpoints and schemas - Refactor strategy and portfolio services - Add strategy management CLI commands - Update domain models with strategy catalog support - Add comprehensive tests for v1 strategies - Update design documentation (data model, interfaces, runtime flows)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""strategy_sets: active status, revision, and default strategy
|
||||
|
||||
Revision ID: 20260814_0029
|
||||
Revises: 20260715_0028
|
||||
Create Date: 2026-08-14 00:00:00
|
||||
|
||||
The migration is additive and idempotent for databases where ensure_schema()
|
||||
may already have created the columns. Existing strategy IDs and portfolio
|
||||
references are preserved. The canonical target_value/{} strategy is reused
|
||||
when present, otherwise one stable default row is inserted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
revision = "20260814_0029"
|
||||
down_revision = "20260715_0028"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_DEFAULT_STRATEGY_ID = "00000000-0000-0000-0000-000000000029"
|
||||
_DEFAULT_NAME = "target_value"
|
||||
_DEFAULT_CONFIG = "{}"
|
||||
_DEFAULT_NICK = "target_value:bf21a9e8fbc5"
|
||||
|
||||
|
||||
def _column_exists(inspector: sa.engine.Inspector, table_name: str, column_name: str) -> bool:
|
||||
return any(column["name"] == column_name for column in inspector.get_columns(table_name))
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
if "strategy_sets" not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
if not _column_exists(inspector, "strategy_sets", "status"):
|
||||
op.add_column(
|
||||
"strategy_sets",
|
||||
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'active'")),
|
||||
)
|
||||
if not _column_exists(inspector, "strategy_sets", "revision"):
|
||||
op.add_column(
|
||||
"strategy_sets",
|
||||
sa.Column("revision", sa.Integer(), nullable=False, server_default=sa.text("1")),
|
||||
)
|
||||
if not _column_exists(inspector, "strategy_sets", "updated_at"):
|
||||
op.add_column(
|
||||
"strategy_sets",
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("NOW()")),
|
||||
)
|
||||
|
||||
bind.execute(text("UPDATE strategy_sets SET status = 'active' WHERE status IS NULL"))
|
||||
bind.execute(text("UPDATE strategy_sets SET revision = 1 WHERE revision IS NULL OR revision < 1"))
|
||||
bind.execute(text("UPDATE strategy_sets SET updated_at = created_at WHERE updated_at IS NULL"))
|
||||
bind.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO strategy_sets (id, name, config, nick, status, revision)
|
||||
SELECT :id, :name, :config, :nick, 'active', 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM strategy_sets WHERE name = :name AND config = :config
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": _DEFAULT_STRATEGY_ID,
|
||||
"name": _DEFAULT_NAME,
|
||||
"config": _DEFAULT_CONFIG,
|
||||
"nick": _DEFAULT_NICK,
|
||||
},
|
||||
)
|
||||
|
||||
# The same (name, config) row is the canonical default; ensure it is selectable.
|
||||
bind.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE strategy_sets
|
||||
SET status = 'active', updated_at = CASE WHEN status = 'active' THEN updated_at ELSE NOW() END
|
||||
WHERE name = :name AND config = :config AND status <> 'active'
|
||||
"""
|
||||
),
|
||||
{"name": _DEFAULT_NAME, "config": _DEFAULT_CONFIG},
|
||||
)
|
||||
|
||||
# PostgreSQL has no ADD CONSTRAINT IF NOT EXISTS, so guard by catalog lookup.
|
||||
bind.execute(
|
||||
text(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_strategy_sets_status'
|
||||
AND conrelid = 'strategy_sets'::regclass
|
||||
) THEN
|
||||
ALTER TABLE strategy_sets
|
||||
ADD CONSTRAINT ck_strategy_sets_status
|
||||
CHECK (status IN ('active', 'inactive'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_strategy_sets_revision_positive'
|
||||
AND conrelid = 'strategy_sets'::regclass
|
||||
) THEN
|
||||
ALTER TABLE strategy_sets
|
||||
ADD CONSTRAINT ck_strategy_sets_revision_positive
|
||||
CHECK (revision >= 1);
|
||||
END IF;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Forward-only data migration; existing references and revisions are retained."""
|
||||
return
|
||||
Reference in New Issue
Block a user