feat(strategy): expose configurable nick contract
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import NoReturn
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
|
||||
@@ -11,7 +12,11 @@ from app.core.config import Settings, get_settings
|
||||
from app.domain.models import StrategySet
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.schemas.portfolio import CreateStrategySetRequest, UpdateStrategySetRequest
|
||||
from app.schemas.v1 import StrategyCreateRequest, StrategyResponse, StrategyUpdateRequest
|
||||
from app.schemas.v1 import (
|
||||
StrategyCreateRequest,
|
||||
StrategyResponse,
|
||||
StrategyUpdateRequest,
|
||||
)
|
||||
from app.services.strategy_set_service import (
|
||||
StrategyConflictError,
|
||||
StrategyInactiveError,
|
||||
@@ -44,6 +49,7 @@ def _response(strategy: StrategySet) -> StrategyResponse:
|
||||
parsed = {}
|
||||
return StrategyResponse(
|
||||
strategy_id=strategy.id,
|
||||
nick=strategy.nick,
|
||||
name=strategy.name,
|
||||
config=parsed if isinstance(parsed, dict) else {},
|
||||
status=strategy.status,
|
||||
@@ -53,7 +59,7 @@ def _response(strategy: StrategySet) -> StrategyResponse:
|
||||
)
|
||||
|
||||
|
||||
def _raise_strategy_error(exc: ValueError) -> None:
|
||||
def _raise_strategy_error(exc: ValueError) -> NoReturn:
|
||||
if isinstance(exc, StrategyNotFoundError):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
if isinstance(exc, (StrategyInactiveError, StrategyInUseError, StrategyConflictError)):
|
||||
@@ -129,14 +135,18 @@ def create_admin_strategy(
|
||||
) -> StrategyResponse:
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
strategy = service.create(
|
||||
CreateStrategySetRequest(
|
||||
name=body.name,
|
||||
config=_canonical_config(body.config),
|
||||
status=body.status,
|
||||
try:
|
||||
strategy = service.create(
|
||||
CreateStrategySetRequest(
|
||||
name=body.name,
|
||||
nick=body.nick,
|
||||
config=_canonical_config(body.config),
|
||||
status=body.status,
|
||||
)
|
||||
)
|
||||
)
|
||||
return _response(strategy)
|
||||
return _response(strategy)
|
||||
except ValueError as exc:
|
||||
_raise_strategy_error(exc)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
@@ -155,6 +165,7 @@ def update_admin_strategy(
|
||||
strategy_id,
|
||||
UpdateStrategySetRequest(
|
||||
name=body.name,
|
||||
nick=body.nick,
|
||||
config=_canonical_config(body.config) if body.config is not None else None,
|
||||
status=body.status,
|
||||
),
|
||||
|
||||
@@ -243,7 +243,14 @@ class PortfolioRepository(Protocol):
|
||||
|
||||
def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: ...
|
||||
|
||||
def create_strategy_set(self, name: str, config: str, *, status: str = "active") -> StrategySet: ...
|
||||
def create_strategy_set(
|
||||
self,
|
||||
name: str,
|
||||
config: str,
|
||||
*,
|
||||
nick: str | None = None,
|
||||
status: str = "active",
|
||||
) -> StrategySet: ...
|
||||
|
||||
def get_strategy_set(self, id: str) -> StrategySet | None: ...
|
||||
|
||||
@@ -254,6 +261,7 @@ class PortfolioRepository(Protocol):
|
||||
id: str,
|
||||
*,
|
||||
name: str | None = None,
|
||||
nick: str | None = None,
|
||||
config: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> StrategySet: ...
|
||||
|
||||
@@ -1360,28 +1360,38 @@ class PostgresPortfolioRepository:
|
||||
def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet:
|
||||
return self.create_strategy_set(name, config)
|
||||
|
||||
def create_strategy_set(self, name: str, config: str, *, status: str = "active") -> StrategySet:
|
||||
def create_strategy_set(
|
||||
self,
|
||||
name: str,
|
||||
config: str,
|
||||
*,
|
||||
nick: str | None = None,
|
||||
status: str = "active",
|
||||
) -> StrategySet:
|
||||
self.ensure_schema()
|
||||
nick = f"{name}:{hashlib.sha1(config.encode('utf-8')).hexdigest()[:12]}"
|
||||
with self._connect().cursor() as cur:
|
||||
new_id = str(uuid.uuid4())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO strategy_sets (id, name, config, nick, status)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
ON CONFLICT (name, config) DO NOTHING
|
||||
""",
|
||||
(new_id, name, config, nick, status),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, config, nick, status, revision, created_at, updated_at
|
||||
FROM strategy_sets
|
||||
WHERE name = %s AND config = %s
|
||||
""",
|
||||
(name, config),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
final_nick = nick or f"{name}:{hashlib.sha1(config.encode('utf-8')).hexdigest()[:12]}"
|
||||
try:
|
||||
with self._connect().cursor() as cur:
|
||||
new_id = str(uuid.uuid4())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO strategy_sets (id, name, config, nick, status)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
ON CONFLICT (name, config) DO NOTHING
|
||||
""",
|
||||
(new_id, name, config, final_nick, status),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, config, nick, status, revision, created_at, updated_at
|
||||
FROM strategy_sets
|
||||
WHERE name = %s AND config = %s
|
||||
""",
|
||||
(name, config),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
except psycopg.errors.UniqueViolation as exc:
|
||||
raise ValueError("동일한 nick 전략이 이미 존재합니다") from exc
|
||||
return self._row_to_strategy_set(row)
|
||||
|
||||
def get_strategy_set(self, id: str) -> StrategySet | None:
|
||||
@@ -1414,6 +1424,7 @@ class PostgresPortfolioRepository:
|
||||
id: str,
|
||||
*,
|
||||
name: str | None = None,
|
||||
nick: str | None = None,
|
||||
config: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> StrategySet:
|
||||
@@ -1435,15 +1446,16 @@ class PostgresPortfolioRepository:
|
||||
raise ValueError("전략을 찾을 수 없습니다")
|
||||
current = self._row_to_strategy_set(row)
|
||||
final_name = name if name is not None else current.name
|
||||
final_nick = nick if nick is not None else current.nick
|
||||
final_config = config if config is not None else current.config
|
||||
final_status = status if status is not None else current.status
|
||||
if (
|
||||
final_name == current.name
|
||||
and final_nick == current.nick
|
||||
and final_config == current.config
|
||||
and final_status == current.status
|
||||
):
|
||||
return current
|
||||
nick = f"{final_name}:{hashlib.sha1(final_config.encode('utf-8')).hexdigest()[:12]}"
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE strategy_sets
|
||||
@@ -1456,11 +1468,11 @@ class PostgresPortfolioRepository:
|
||||
WHERE id = %s
|
||||
RETURNING id, name, config, nick, status, revision, created_at, updated_at
|
||||
""",
|
||||
(final_name, final_config, nick, final_status, id),
|
||||
(final_name, final_config, final_nick, final_status, id),
|
||||
)
|
||||
updated = cur.fetchone()
|
||||
except psycopg.errors.UniqueViolation as exc:
|
||||
raise ValueError("동일한 name/config 전략이 이미 존재합니다") from exc
|
||||
raise ValueError("동일한 name/config 또는 nick 전략이 이미 존재합니다") from exc
|
||||
if updated is None:
|
||||
raise ValueError("전략을 찾을 수 없습니다")
|
||||
return self._row_to_strategy_set(updated)
|
||||
|
||||
@@ -186,6 +186,7 @@ class CreateStrategySetRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str
|
||||
nick: str | None = Field(default=None, min_length=1)
|
||||
config: str
|
||||
status: Literal["active", "inactive"] = "active"
|
||||
|
||||
@@ -194,6 +195,7 @@ class UpdateStrategySetRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = None
|
||||
nick: str | None = Field(default=None, min_length=1)
|
||||
config: str | None = None
|
||||
status: Literal["active", "inactive"] | None = None
|
||||
|
||||
|
||||
@@ -305,6 +305,7 @@ StrategyStatus = Literal["active", "inactive"]
|
||||
|
||||
class StrategyResponse(BaseModel):
|
||||
strategy_id: str
|
||||
nick: str
|
||||
name: str
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
status: StrategyStatus
|
||||
@@ -317,6 +318,7 @@ class StrategyCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(..., min_length=1)
|
||||
nick: str | None = Field(default=None, min_length=1)
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
status: StrategyStatus = "active"
|
||||
|
||||
@@ -325,6 +327,7 @@ class StrategyUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = Field(default=None, min_length=1)
|
||||
nick: str | None = Field(default=None, min_length=1)
|
||||
config: dict[str, Any] | None = None
|
||||
status: StrategyStatus | None = None
|
||||
|
||||
|
||||
@@ -46,26 +46,49 @@ class StrategySetService:
|
||||
return self.get(strategy_set_id, active_only=True)
|
||||
|
||||
def create(self, payload: CreateStrategySetRequest) -> StrategySet:
|
||||
create_strategy = cast(
|
||||
Callable[..., StrategySet] | None,
|
||||
getattr(self._repository, "create_strategy_set", None),
|
||||
)
|
||||
if create_strategy is not None:
|
||||
strategy = create_strategy(payload.name, payload.config, status=payload.status)
|
||||
else:
|
||||
strategy = self._repository.get_or_create_strategy_set(payload.name, payload.config)
|
||||
if payload.status != strategy.status:
|
||||
strategy = self.update(
|
||||
strategy.id,
|
||||
UpdateStrategySetRequest(status=payload.status),
|
||||
try:
|
||||
create_strategy = cast(
|
||||
Callable[..., StrategySet] | None,
|
||||
getattr(self._repository, "create_strategy_set", None),
|
||||
)
|
||||
return strategy
|
||||
if create_strategy is not None:
|
||||
strategy = create_strategy(
|
||||
payload.name,
|
||||
payload.config,
|
||||
nick=payload.nick,
|
||||
status=payload.status,
|
||||
)
|
||||
else:
|
||||
strategy = self._repository.get_or_create_strategy_set(payload.name, payload.config)
|
||||
if payload.nick is not None and payload.nick != strategy.nick:
|
||||
strategy = self.update(
|
||||
strategy.id,
|
||||
UpdateStrategySetRequest(nick=payload.nick),
|
||||
)
|
||||
if payload.status != strategy.status:
|
||||
strategy = self.update(
|
||||
strategy.id,
|
||||
UpdateStrategySetRequest(status=payload.status),
|
||||
)
|
||||
return strategy
|
||||
except StrategyConflictError:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
raise StrategyConflictError(str(exc)) from exc
|
||||
|
||||
def update(self, strategy_set_id: str, payload: UpdateStrategySetRequest) -> StrategySet:
|
||||
try:
|
||||
if payload.nick is None:
|
||||
return self._repository.update_strategy_set(
|
||||
strategy_set_id,
|
||||
name=payload.name,
|
||||
config=payload.config,
|
||||
status=payload.status,
|
||||
)
|
||||
return self._repository.update_strategy_set(
|
||||
strategy_set_id,
|
||||
name=payload.name,
|
||||
nick=payload.nick,
|
||||
config=payload.config,
|
||||
status=payload.status,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user