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
+22 -13
View File
@@ -8,7 +8,6 @@ REQ-043: GET /v1/portfolios/{id}/holdings/cost-basis — current holding WAC cos
from __future__ import annotations
import json
import logging
import uuid
from datetime import date, datetime, timedelta, timezone
@@ -44,6 +43,7 @@ from app.schemas.v1 import (
TradeHistoryItem,
TradeHistoryResponse,
)
from app.services.strategy_set_service import StrategyInactiveError, StrategyNotFoundError
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1/portfolios", tags=["v1"])
@@ -96,6 +96,7 @@ def _portfolio_to_item(
active_strategy_id=portfolio.active_strategy_id,
strategy_name=portfolio.strategy_name,
strategy_params=portfolio.strategy_params,
strategy_revision=portfolio.strategy_revision,
markets=portfolio.markets,
)
@@ -160,10 +161,11 @@ def create_portfolio_v1(
)
name = body.name or f"{body.broker}-{body.asset_id[:8]}"
strategy_set = repo.get_or_create_strategy_set(
body.strategy_name or "target_value",
json.dumps(body.strategy_params or {}, sort_keys=True, ensure_ascii=True),
)
strategy_set = repo.get_strategy_set(body.strategy_id)
if strategy_set is None:
raise StrategyNotFoundError(f"strategy not found: {body.strategy_id}")
if strategy_set.status != "active":
raise StrategyInactiveError(f"strategy is inactive: {body.strategy_id}")
portfolio = repo.create_portfolio(
str(uuid.uuid4()),
@@ -181,6 +183,10 @@ def create_portfolio_v1(
)
except HTTPException:
raise
except StrategyNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except StrategyInactiveError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
finally:
@@ -215,21 +221,24 @@ def update_portfolio_v1(
fields = body.model_dump(exclude_none=True)
if fields:
if "strategy_name" in fields or "strategy_params" in fields:
strategy_set = repo.get_or_create_strategy_set(
fields.pop("strategy_name", None) or "target_value",
json.dumps(fields.pop("strategy_params", None) or {}, sort_keys=True, ensure_ascii=True),
)
if "strategy_id" in fields:
strategy_id = str(fields.pop("strategy_id"))
strategy_set = repo.get_strategy_set(strategy_id)
if strategy_set is None:
raise StrategyNotFoundError(f"strategy not found: {strategy_id}")
if strategy_set.status != "active":
raise StrategyInactiveError(f"strategy is inactive: {strategy_id}")
fields["active_strategy_id"] = strategy_set.id
fields["avabile_strategies"] = [strategy_set.id]
else:
fields.pop("strategy_name", None)
fields.pop("strategy_params", None)
repo.update_portfolio(portfolio_id, **fields)
portfolio = repo.get(portfolio_id)
except HTTPException:
raise
except StrategyNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except StrategyInactiveError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
finally: