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:
2026-08-15 11:18:31 +09:00
parent 5fc389dea8
commit 6e02d92282
48 changed files with 2114 additions and 393 deletions
@@ -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