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
+55 -7
View File
@@ -79,6 +79,9 @@ class _FakeRepo:
def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
return self._strategy_set
def get_strategy_set(self, id: str) -> StrategySet | None:
return self._strategy_set if id == self._strategy_set.id else None
def create_portfolio(
self,
portfolio_id: str,
@@ -190,7 +193,7 @@ def test_post_admin_creates_for_target_user() -> None:
with _with_repo(repo):
resp = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "user_id": _OTHER_USER_ID},
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001", "user_id": _OTHER_USER_ID},
headers=_auth("admin-tok"),
)
assert resp.status_code == 201
@@ -209,7 +212,7 @@ def test_post_user_creates_for_self() -> None:
with _with_repo(repo):
resp = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom"},
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001"},
headers=_auth("user-tok"),
)
assert resp.status_code == 201
@@ -230,7 +233,7 @@ def test_post_service_token_returns_403() -> None:
with _with_repo(repo, db_auth=True):
resp = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom"},
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001"},
headers=_auth("svc-raw-token"),
)
assert resp.status_code == 403
@@ -244,7 +247,7 @@ def test_post_inactive_asset_returns_422() -> None:
with _with_repo(repo):
resp = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom"},
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001"},
headers=_auth("admin-tok"),
)
assert resp.status_code == 422
@@ -259,7 +262,7 @@ def test_post_missing_asset_returns_422() -> None:
with _with_repo(repo):
resp = client.post(
"/v1/portfolios",
json={"asset_id": "no-such-uuid", "broker": "kiwoom"},
json={"asset_id": "no-such-uuid", "broker": "kiwoom", "strategy_id": "strat-001"},
headers=_auth("admin-tok"),
)
assert resp.status_code == 422
@@ -268,7 +271,7 @@ def test_post_missing_asset_returns_422() -> None:
def test_post_no_bearer_returns_401() -> None:
"""REQ-025 AC-6: no token → HTTP 401."""
client = _make_client()
resp = client.post("/v1/portfolios", json={"asset_id": _ASSET_UUID, "broker": "kiwoom"})
resp = client.post("/v1/portfolios", json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001"})
assert resp.status_code == 401
@@ -280,7 +283,7 @@ def test_post_non_admin_user_id_ignored() -> None:
with _with_repo(repo):
resp = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "user_id": _OTHER_USER_ID},
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001", "user_id": _OTHER_USER_ID},
headers=_auth("user-tok"),
)
assert resp.status_code == 201
@@ -499,3 +502,48 @@ def test_delete_already_disabled_is_idempotent() -> None:
resp = client.delete(f"/v1/portfolios/{_PORTFOLIO_UUID}", headers=_auth("admin-tok"))
assert resp.status_code == 200
assert resp.json()["status"] == "disabled"
# Issue #26: strategy catalog references on portfolio mutations.
def test_post_strategy_reference_errors_and_required_contract() -> None:
repo = _FakeRepo(asset=_make_asset(_ASSET_UUID, "active"))
client = _make_client()
with _with_repo(repo):
missing = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "missing"},
headers=_auth("admin-tok"),
)
invalid = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom"},
headers=_auth("admin-tok"),
)
repo._strategy_set.status = "inactive"
inactive = client.post(
"/v1/portfolios",
json={"asset_id": _ASSET_UUID, "broker": "kiwoom", "strategy_id": "strat-001"},
headers=_auth("admin-tok"),
)
assert missing.status_code == 404
assert invalid.status_code == 422
assert inactive.status_code == 409
def test_patch_strategy_reference_errors_are_404_and_409() -> None:
repo = _repo_with_portfolio(owner=_OTHER_USER_ID)
client = _make_client()
with _with_repo(repo):
missing = client.patch(
f"/v1/portfolios/{_PORTFOLIO_UUID}",
json={"strategy_id": "missing"},
headers=_auth("admin-tok"),
)
repo._strategy_set.status = "inactive"
inactive = client.patch(
f"/v1/portfolios/{_PORTFOLIO_UUID}",
json={"strategy_id": "strat-001"},
headers=_auth("admin-tok"),
)
assert missing.status_code == 404
assert inactive.status_code == 409