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
+68 -4
View File
@@ -469,9 +469,40 @@ class FakeRepo:
def get_strategy_set(self, id: str) -> StrategySet | None:
return self.strategy_set if self.strategy_set.id == id else None
def list_strategy_sets(self) -> list[StrategySet]:
def list_strategy_sets(self, *, include_inactive: bool = True) -> list[StrategySet]:
from datetime import datetime, timezone
return sorted(self._strategy_sets.values(), key=lambda s: s.created_at or datetime.now(timezone.utc), reverse=True)
values = list(self._strategy_sets.values())
if not include_inactive:
values = [item for item in values if item.status == "active"]
return sorted(values, key=lambda s: s.created_at or datetime.now(timezone.utc), reverse=True)
def update_strategy_set(
self,
id: str,
*,
name: str | None = None,
config: str | None = None,
status: str | None = None,
) -> StrategySet:
strategy = self._strategy_sets.get(id)
if strategy is None:
raise ValueError("전략을 찾을 수 없습니다")
final_name = name if name is not None else strategy.name
final_config = config if config is not None else strategy.config
final_status = status if status is not None else strategy.status
if (final_name, final_config, final_status) == (
strategy.name,
strategy.config,
strategy.status,
):
return strategy
strategy.name = final_name
strategy.config = final_config
strategy.status = final_status
strategy.nick = f"{strategy.name}:updated"
strategy.revision += 1
strategy.updated_at = datetime.now(timezone.utc)
return strategy
def delete_strategy_set(self, id: str) -> None:
if id in self._strategy_sets:
@@ -670,8 +701,8 @@ def test_cli_create_buy_and_delete_flow() -> None:
_TEST_ASSET_ID,
"--cash-balance",
"1000000",
"--strategy-params",
'{"target_asset_ratio": 0.5}',
"--strategy-id",
"set-1",
"--force",
],
)
@@ -717,6 +748,8 @@ def test_cli_write_command_prompts_before_create() -> None:
_TEST_ASSET_ID,
"--cash-balance",
"1000000",
"--strategy-id",
"set-1",
],
input="y\n",
)
@@ -740,6 +773,8 @@ def test_cli_write_command_can_abort() -> None:
_TEST_ASSET_ID,
"--cash-balance",
"1000000",
"--strategy-id",
"set-1",
],
input="n\n",
)
@@ -1434,3 +1469,32 @@ def test_cli_service_token_rebind_rejects_cross_user_daemon() -> None:
assert result.exit_code == 1
assert "해당 토큰 사용자에 속하지 않습니다" in result.stdout
def test_cli_strategy_commands_show_status_revision_and_preserve_id() -> None:
repo = FakeRepo()
runner = build_runner(repo)
listed = runner.invoke(runner.app, ["strategy", "list"])
updated = runner.invoke(
runner.app,
["strategy", "update", "set-1", "--status", "inactive", "--name", "target_value_v2"],
)
no_change = runner.invoke(runner.app, ["strategy", "update", "set-1", "--status", "inactive"])
assert listed.exit_code == 0
assert "Status" in listed.output and "Revision" in listed.output
assert updated.exit_code == 0
assert "ID: set-1" in updated.output
assert "Status: inactive" in updated.output
assert "Revision: 2" in updated.output
assert no_change.exit_code == 0
assert "Revision: 2" in no_change.output
def test_cli_strategy_update_requires_a_field() -> None:
repo = FakeRepo()
runner = build_runner(repo)
result = runner.invoke(runner.app, ["strategy", "update", "set-1"])
assert result.exit_code == 1
assert "at least one" in result.output