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
+18 -8
View File
@@ -17,7 +17,10 @@ class StrategySetResponse(BaseModel):
name: str
config: str
nick: str
status: Literal["active", "inactive"] = "active"
revision: int = 1
created_at: datetime | None = None
updated_at: datetime | None = None
class LedgerEntryResponse(BaseModel):
@@ -55,6 +58,9 @@ class PortfolioSummary(BaseModel):
price: float = 0.0
profit_buffer: float = 0.0
active_strategy_id: str | None = None
strategy_name: str | None = None
strategy_params: dict[str, Any] = Field(default_factory=dict)
strategy_revision: int | None = Field(default=None, ge=1)
avabile_strategies: list[str] | None = None
managed_by_daemon_id: str | None = None
created_at: datetime | None = None
@@ -74,12 +80,11 @@ class PortfolioDetailResponse(BaseModel):
class CreatePortfolioRequest(PortfolioBase):
model_config = ConfigDict(extra="forbid")
name: str # required, overrides Optional from PortfolioBase; used as (owner_user_id, name) identifier
asset_id: str
strategy_name: str = "target_value"
strategy_params: dict[str, Any] = Field(default_factory=dict)
active_strategy_id: str | None = None
avabile_strategies: list[str] | None = None
strategy_id: str = Field(..., min_length=1)
cash_balance: float = Field(default=0.0, ge=0.0)
asset_quantity: float = Field(default=0.0, ge=0.0)
asset_price: float = Field(default=0.0, ge=0.0)
@@ -96,13 +101,12 @@ class CreatePortfolioRequest(PortfolioBase):
class UpdatePortfolioRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
broker: str | None = None
status: str | None = None
name: str | None = None
strategy_name: str | None = None
strategy_params: dict[str, Any] | None = None
active_strategy_id: str | None = None
avabile_strategies: list[str] | None = None
strategy_id: str | None = Field(default=None, min_length=1)
asset_price: float | None = Field(default=None, ge=0.0)
managed_by_daemon_id: str | None = None
@@ -179,11 +183,17 @@ class ErrorResponse(BaseModel):
class CreateStrategySetRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
config: str
status: Literal["active", "inactive"] = "active"
class UpdateStrategySetRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str | None = None
config: str | None = None
status: Literal["active", "inactive"] | None = None
+46 -4
View File
@@ -233,6 +233,8 @@ class PortfolioReturnSummaryResponse(BaseModel):
# ---------------------------------------------------------------------------
class PortfolioCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
asset_id: str = Field(..., min_length=1)
broker: str = Field(..., min_length=1)
name: str | None = None
@@ -240,18 +242,18 @@ class PortfolioCreateRequest(BaseModel):
asset_quantity: float = Field(default=0.0, ge=0.0)
asset_price: float = Field(default=0.0, ge=0.0)
profit_buffer: float = Field(default=0.0, ge=0.0)
strategy_name: str | None = None
strategy_params: dict[str, Any] = Field(default_factory=dict)
strategy_id: str = Field(..., min_length=1)
managed_by_daemon_id: str | None = None
user_id: str | None = None # admin-only: override owner
class PortfolioUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
broker: str | None = None
status: str | None = None
name: str | None = None
strategy_name: str | None = None
strategy_params: dict[str, Any] | None = None
strategy_id: str | None = Field(default=None, min_length=1)
asset_price: float | None = Field(default=None, ge=0.0)
managed_by_daemon_id: str | None = None
@@ -285,6 +287,7 @@ class PortfolioItem(BaseModel):
active_strategy_id: str | None = None
strategy_name: str | None = None
strategy_params: dict[str, Any] = Field(default_factory=dict)
strategy_revision: int | None = Field(default=None, ge=1)
markets: list[str] = Field(default_factory=list)
@@ -293,6 +296,45 @@ class PortfolioListResponse(BaseModel):
total: int
# ---------------------------------------------------------------------------
# GET /v1/strategies, CRUD /v1/admin/strategies
# ---------------------------------------------------------------------------
StrategyStatus = Literal["active", "inactive"]
class StrategyResponse(BaseModel):
strategy_id: str
name: str
config: dict[str, Any] = Field(default_factory=dict)
status: StrategyStatus
revision: int = Field(ge=1)
created_at: datetime | None = None
updated_at: datetime | None = None
class StrategyCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str = Field(..., min_length=1)
config: dict[str, Any] = Field(default_factory=dict)
status: StrategyStatus = "active"
class StrategyUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str | None = Field(default=None, min_length=1)
config: dict[str, Any] | None = None
status: StrategyStatus | None = None
@model_validator(mode="after")
def require_change(self) -> "StrategyUpdateRequest":
if not self.model_fields_set:
raise ValueError("at least one field is required")
return self
# ---------------------------------------------------------------------------
# GET /v1/portfolios/{id}/tags, CRUD /v1/portfolios/{id}/comments (REQ-030, REQ-031)
# ---------------------------------------------------------------------------