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
+24 -42
View File
@@ -36,6 +36,7 @@ from app.schemas.portfolio import (
TransferRequest,
UpdatePortfolioRequest,
)
from app.services.strategy_set_service import StrategyInactiveError, StrategyNotFoundError
def _infer_symbol(portfolio_id: str) -> str:
@@ -59,6 +60,9 @@ def _to_portfolio_summary(portfolio: Portfolio):
price=portfolio.price,
profit_buffer=portfolio.profit_buffer,
active_strategy_id=portfolio.active_strategy_id,
strategy_name=portfolio.strategy_name,
strategy_params=portfolio.strategy_params,
strategy_revision=portfolio.strategy_revision,
avabile_strategies=portfolio.avabile_strategies,
managed_by_daemon_id=portfolio.managed_by_daemon_id,
created_at=portfolio.created_at,
@@ -92,7 +96,10 @@ def _to_strategy_set_response(strategy_set: StrategySet | None):
name=strategy_set.name,
config=strategy_set.config,
nick=strategy_set.nick,
status=strategy_set.status,
revision=strategy_set.revision,
created_at=strategy_set.created_at,
updated_at=strategy_set.updated_at,
)
@@ -213,14 +220,12 @@ class PortfolioService:
if asset.status != "active":
raise ValueError(f"asset is inactive: {payload.asset_id}")
active_strategy_id = payload.active_strategy_id
if active_strategy_id is None:
strategy_set = self._repository.get_or_create_strategy_set(
payload.strategy_name,
json.dumps(payload.strategy_params, sort_keys=True, ensure_ascii=True),
)
active_strategy_id = strategy_set.id
avabile_strategies = _merge_strategy_ids(active_strategy_id, payload.avabile_strategies)
strategy_set = self._repository.get_strategy_set(payload.strategy_id)
if strategy_set is None:
raise StrategyNotFoundError(f"strategy not found: {payload.strategy_id}")
if strategy_set.status != "active":
raise StrategyInactiveError(f"strategy is inactive: {payload.strategy_id}")
active_strategy_id = strategy_set.id
portfolio = self._repository.create_portfolio(
str(uuid.uuid4()),
name=payload.name,
@@ -233,7 +238,7 @@ class PortfolioService:
asset_price=payload.asset_price,
profit_buffer=payload.profit_buffer,
active_strategy_id=active_strategy_id,
avabile_strategies=avabile_strategies,
avabile_strategies=[active_strategy_id],
)
return self.get_portfolio_status(portfolio_id=portfolio.portfolio_id, recent_trades=5)
@@ -241,42 +246,19 @@ class PortfolioService:
fields = payload.model_dump(exclude_none=True)
current = self._repository.get(portfolio_id)
if "strategy_name" in fields or "strategy_params" in fields:
current_strategy_set = None
if current.active_strategy_id:
current_strategy_set = self._repository.get_strategy_set(current.active_strategy_id)
strategy_name = fields.get("strategy_name")
if strategy_name is None and current_strategy_set is not None:
strategy_name = current_strategy_set.name
if strategy_name is None:
raise ValueError("strategy_name is required when no active strategy is set")
strategy_params = fields.get("strategy_params")
if strategy_params is None and current_strategy_set is not None:
strategy_params = _parse_strategy_config(current_strategy_set.config)
if strategy_params is None:
strategy_params = {}
strategy_set = self._repository.get_or_create_strategy_set(
strategy_name,
json.dumps(strategy_params, sort_keys=True, ensure_ascii=True),
)
fields["active_strategy_id"] = strategy_set.id
if "strategy_id" in fields:
strategy_id = str(fields.pop("strategy_id"))
strategy_set = self._repository.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_id
fields["avabile_strategies"] = _merge_strategy_ids(
strategy_set.id,
fields.get("avabile_strategies", current.avabile_strategies),
strategy_id,
current.avabile_strategies,
)
elif "active_strategy_id" in fields:
fields["avabile_strategies"] = _merge_strategy_ids(
fields.get("active_strategy_id"),
fields.get("avabile_strategies", current.avabile_strategies),
)
fields.pop("strategy_name", None)
fields.pop("strategy_params", None)
self._repository.update_portfolio(portfolio_id, **fields)
return self.get_portfolio_status(portfolio_id=portfolio_id, recent_trades=5)