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
+17 -17
View File
@@ -201,7 +201,7 @@ class FakeRepo:
def get_strategy_set(self, id: str) -> StrategySet | None:
return self._strategy_sets.get(id)
def list_strategy_sets(self) -> list[StrategySet]:
def list_strategy_sets(self, *, include_inactive: bool = True) -> list[StrategySet]:
return list(self._strategy_sets.values())
def delete_strategy_set(self, id: str) -> None:
@@ -350,7 +350,7 @@ def test_create_portfolio_assigns_uuid_id() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code == 0, result.output
@@ -371,7 +371,7 @@ def test_create_returns_uuid_in_response() -> None:
result = runner.invoke(
runner.app,
["--format", "json", "portfolio", "create", "MY_PORTFOLIO", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["--format", "json", "portfolio", "create", "MY_PORTFOLIO", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code == 0, result.output
@@ -389,7 +389,7 @@ def test_status_resolves_by_name_and_user() -> None:
create_result = runner.invoke(
runner.app,
["--format", "json", "portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["--format", "json", "portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert create_result.exit_code == 0, create_result.output
@@ -437,7 +437,7 @@ def test_create_without_user_fails() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--force"],
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--strategy-id", "set-1", "--force"],
)
assert result.exit_code != 0
@@ -450,7 +450,7 @@ def test_create_with_unknown_user_fails() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--user", "ghost", "--force"],
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--user", "ghost", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code == 1
@@ -464,7 +464,7 @@ def test_name_validation_rejects_invalid_characters() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "invalid name!", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "invalid name!", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code == 1
@@ -478,7 +478,7 @@ def test_name_validation_rejects_empty_name() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code != 0
@@ -492,11 +492,11 @@ def test_alice_and_bob_can_have_same_portfolio_name() -> None:
alice_result = runner.invoke(
runner.app,
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
bob_result = runner.invoke(
runner.app,
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "bob", "--force"],
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "bob", "--strategy-id", "set-1", "--force"],
)
assert alice_result.exit_code == 0, alice_result.output
@@ -513,7 +513,7 @@ def test_buy_resolves_portfolio_by_name_and_user() -> None:
runner.invoke(
runner.app,
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--cash-balance", "1000000", "--user", "alice", "--force"],
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--cash-balance", "1000000", "--user", "alice", "--strategy-id", "set-1", "--force"],
)
buy_result = runner.invoke(
runner.app,
@@ -532,7 +532,7 @@ def test_delete_resolves_portfolio_by_name_and_user() -> None:
runner.invoke(
runner.app,
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "MY_PORT", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
delete_result = runner.invoke(
runner.app,
@@ -552,7 +552,7 @@ def test_alice_cannot_delete_bobs_portfolio() -> None:
runner.invoke(
runner.app,
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "bob", "--force"],
["portfolio", "create", "SAMSUNG", "--asset-id", _TEST_ASSET_ID, "--user", "bob", "--strategy-id", "set-1", "--force"],
)
delete_result = runner.invoke(
runner.app,
@@ -575,7 +575,7 @@ def test_name_validation_accepts_korean() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "삼성전자 포트폴리오", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "삼성전자 포트폴리오", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code == 0, result.output
@@ -589,7 +589,7 @@ def test_name_validation_accepts_ascii_unchanged() -> None:
result = runner.invoke(
runner.app,
["portfolio", "create", "my-portfolio_v1:test.01", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "my-portfolio_v1:test.01", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
assert result.exit_code == 0, result.output
@@ -602,7 +602,7 @@ def test_name_validation_rejects_over_100_chars() -> None:
long_name = "가" * 101
with pytest.raises(Exception):
CreatePortfolioRequest(name=long_name, asset_id="some-uuid")
CreatePortfolioRequest(name=long_name, asset_id="some-uuid", strategy_id="set-1")
def test_update_new_name_renames_portfolio() -> None:
@@ -612,7 +612,7 @@ def test_update_new_name_renames_portfolio() -> None:
runner.invoke(
runner.app,
["portfolio", "create", "OLD_NAME", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"],
["portfolio", "create", "OLD_NAME", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--strategy-id", "set-1", "--force"],
)
rename_result = runner.invoke(
runner.app,