diff --git a/finam_trade_api/base_client/models.py b/finam_trade_api/base_client/models.py index b91320e..4bcb0d8 100644 --- a/finam_trade_api/base_client/models.py +++ b/finam_trade_api/base_client/models.py @@ -21,6 +21,4 @@ class FinamMoney(BaseModel): """ A custom money type for Finam API responses. """ - units: str = "0" - currency_code: str = "RUB" - nanos: int = 0 + value: str = "0" diff --git a/finam_trade_api/order/__init__.py b/finam_trade_api/order/__init__.py index ec4ca40..d8f8556 100644 --- a/finam_trade_api/order/__init__.py +++ b/finam_trade_api/order/__init__.py @@ -1 +1,16 @@ +from finam_trade_api.order.model import ( + CancelOrderRequest, + GetOrderRequest, + Leg, + Order, + OrdersRequest, + OrdersResponse, + OrderState, + OrderStatus, + OrderType, + Side, + StopCondition, + TimeInForce, + ValidBefore, +) from finam_trade_api.order.order import OrderClient diff --git a/finam_trade_api/order/model.py b/finam_trade_api/order/model.py index a6eeeaa..00ea0f5 100644 --- a/finam_trade_api/order/model.py +++ b/finam_trade_api/order/model.py @@ -1,11 +1,141 @@ +from datetime import datetime from enum import Enum +from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field + +from finam_trade_api.base_client.models import FinamDecimal, FinamMoney + + +class Side(str, Enum): + """Сторона заявки""" + BUY = "SIDE_BUY" + SELL = "SIDE_SELL" + + +class OrderStatus(str, Enum): + """Статус заявки""" + UNSPECIFIED = "ORDER_STATUS_UNSPECIFIED" + NEW = "ORDER_STATUS_NEW" + PARTIALLY_FILLED = "ORDER_STATUS_PARTIALLY_FILLED" + FILLED = "ORDER_STATUS_FILLED" + DONE_FOR_DAY = "ORDER_STATUS_DONE_FOR_DAY" + CANCELED = "ORDER_STATUS_CANCELED" + REPLACED = "ORDER_STATUS_REPLACED" + PENDING_CANCEL = "ORDER_STATUS_PENDING_CANCEL" + REJECTED = "ORDER_STATUS_REJECTED" + SUSPENDED = "ORDER_STATUS_SUSPENDED" + PENDING_NEW = "ORDER_STATUS_PENDING_NEW" + EXPIRED = "ORDER_STATUS_EXPIRED" + FAILED = "ORDER_STATUS_FAILED" + FORWARDING = "ORDER_STATUS_FORWARDING" + WAIT = "ORDER_STATUS_WAIT" + DENIED_BY_BROKER = "ORDER_STATUS_DENIED_BY_BROKER" + REJECTED_BY_EXCHANGE = "ORDER_STATUS_REJECTED_BY_EXCHANGE" + WATCHING = "ORDER_STATUS_WATCHING" + EXECUTED = "ORDER_STATUS_EXECUTED" + DISABLED = "ORDER_STATUS_DISABLED" + LINK_WAIT = "ORDER_STATUS_LINK_WAIT" + SL_GUARD_TIME = "ORDER_STATUS_SL_GUARD_TIME" + SL_EXECUTED = "ORDER_STATUS_SL_EXECUTED" + SL_FORWARDING = "ORDER_STATUS_SL_FORWARDING" + TP_GUARD_TIME = "ORDER_STATUS_TP_GUARD_TIME" + TP_EXECUTED = "ORDER_STATUS_TP_EXECUTED" + TP_CORRECTION = "ORDER_STATUS_TP_CORRECTION" + TP_FORWARDING = "ORDER_STATUS_TP_FORWARDING" + TP_CORR_GUARD_TIME = "ORDER_STATUS_TP_CORR_GUARD_TIME" + + +class OrderType(str, Enum): + """Тип заявки""" + UNSPECIFIED = "ORDER_TYPE_UNSPECIFIED" + MARKET = "ORDER_TYPE_MARKET" + LIMIT = "ORDER_TYPE_LIMIT" + STOP = "ORDER_TYPE_STOP" + STOP_LIMIT = "ORDER_TYPE_STOP_LIMIT" + MULTI_LEG = "ORDER_TYPE_MULTI_LEG" + + +class StopCondition(str, Enum): + """Условие срабатывания стоп заявки""" + UNSPECIFIED = "STOP_CONDITION_UNSPECIFIED" + LAST_UP = "STOP_CONDITION_LAST_UP" + LAST_DOWN = "STOP_CONDITION_LAST_DOWN" + + +class TimeInForce(str, Enum): + """Срок действия заявки""" + UNSPECIFIED = "TIME_IN_FORCE_UNSPECIFIED" + DAY = "TIME_IN_FORCE_DAY" + GOOD_TILL_CANCEL = "TIME_IN_FORCE_GOOD_TILL_CANCEL" + GOOD_TILL_CROSSING = "TIME_IN_FORCE_GOOD_TILL_CROSSING" + EXT = "TIME_IN_FORCE_EXT" + ON_OPEN = "TIME_IN_FORCE_ON_OPEN" + ON_CLOSE = "TIME_IN_FORCE_ON_CLOSE" + IOC = "TIME_IN_FORCE_IOC" + FOK = "TIME_IN_FORCE_FOK" + + +class ValidBefore(str, Enum): + """Срок действия условной заявки""" + UNSPECIFIED = "VALID_BEFORE_UNSPECIFIED" + END_OF_DAY = "VALID_BEFORE_END_OF_DAY" + GOOD_TILL_CANCEL = "VALID_BEFORE_GOOD_TILL_CANCEL" + GOOD_TILL_DATE = "VALID_BEFORE_GOOD_TILL_DATE" + + +class Leg(BaseModel): + """Лег""" + symbol: str + quantity: FinamDecimal + side: Side + + +class Order(BaseModel): + """Информация о заявке""" + account_id: str + symbol: str + quantity: FinamDecimal + side: Side + type: OrderType + time_in_force: Optional[TimeInForce] = None + limit_price: Optional[FinamMoney] = None + stop_price: Optional[FinamMoney] = None + stop_condition: Optional[StopCondition] = None + legs: Optional[list[Leg]] = Field(default=None) + client_order_id: Optional[str] = Field(default=None, max_length=50) + valid_before: Optional[ValidBefore] = None + comment: Optional[str] = Field(default=None, max_length=128) + + +class CancelOrderRequest(BaseModel): + """Запрос отмены торговой заявки""" + account_id: str + order_id: str + + +class GetOrderRequest(BaseModel): + """Запрос на получение конкретного ордера""" + account_id: str + order_id: str + + +class OrdersRequest(BaseModel): + """Запрос получения списка торговых заявок""" + account_id: str class OrderState(BaseModel): - orderId: str + """Состояние заявки""" + order_id: str + exec_id: str + status: OrderStatus + order: Order + transact_at: datetime + accept_at: Optional[datetime] = None + withdraw_at: Optional[datetime] = None class OrdersResponse(BaseModel): + """Список торговых заявок""" orders: list[OrderState] diff --git a/finam_trade_api/order/order.py b/finam_trade_api/order/order.py index 0f97eb1..0626010 100644 --- a/finam_trade_api/order/order.py +++ b/finam_trade_api/order/order.py @@ -1,5 +1,15 @@ from finam_trade_api import TokenManager from finam_trade_api.base_client import BaseClient +from finam_trade_api.exceptions import FinamTradeApiError +from finam_trade_api.models import ErrorModel +from finam_trade_api.order.model import ( + CancelOrderRequest, + GetOrderRequest, + Order, + OrdersRequest, + OrdersResponse, + OrderState, +) class OrderClient(BaseClient): @@ -7,14 +17,106 @@ def __init__(self, token_manager: TokenManager): super().__init__(token_manager) self._url = "/accounts" - async def get_orders(self): - raise NotImplementedError - async def get_order(self): - raise NotImplementedError + async def get_orders(self, params: OrdersRequest) -> OrdersResponse: + """ + Получение списка заявок для аккаунта. - async def place_order(self): - raise NotImplementedError + Args: + params (OrdersRequest): Параметры запроса с идентификатором аккаунта. - async def cancel_order(self): - raise NotImplementedError + Returns: + OrdersResponse: Список торговых заявок. + + Raises: + FinamTradeApiError: Если запрос завершился с ошибкой. + """ + response, ok = await self._exec_request( + self.RequestMethod.GET, + f"{self._url}/{params.account_id}/orders", + ) + + if not ok: + err = ErrorModel(**response) + raise FinamTradeApiError(f"code={err.code} | message={err.message} | details={err.details}") + + return OrdersResponse(**response) + + + async def get_order(self, params: GetOrderRequest) -> OrderState: + """ + Получение информации о конкретном ордере. + + Args: + params (GetOrderRequest): Параметры запроса с идентификатором аккаунта и заявки. + + Returns: + OrderState: Состояние заявки. + + Raises: + FinamTradeApiError: Если запрос завершился с ошибкой. + """ + response, ok = await self._exec_request( + self.RequestMethod.GET, + f"{self._url}/{params.account_id}/orders/{params.order_id}", + ) + + if not ok: + err = ErrorModel(**response) + raise FinamTradeApiError(f"code={err.code} | message={err.message} | details={err.details}") + + return OrderState(**response) + + + async def place_order(self, order: Order) -> OrderState: + """ + Выставление биржевой заявки. + + Args: + order (Order): Информация о заявке для выставления. + + Returns: + OrderState: Состояние созданной заявки. + + Raises: + FinamTradeApiError: Если запрос завершился с ошибкой. + """ + # Используем model_dump с mode='json' для корректной сериализации enum'ов + payload = order.model_dump(mode="json", exclude_none=True) + + response, ok = await self._exec_request( + self.RequestMethod.POST, + f"{self._url}/{order.account_id}/orders", + payload=payload, + ) + + if not ok: + err = ErrorModel(**response) + raise FinamTradeApiError(f"code={err.code} | message={err.message} | details={err.details}") + + return OrderState(**response) + + + async def cancel_order(self, params: CancelOrderRequest) -> OrderState: + """ + Отмена биржевой заявки. + + Args: + params (CancelOrderRequest): Параметры запроса с идентификатором аккаунта и заявки. + + Returns: + OrderState: Состояние отмененной заявки. + + Raises: + FinamTradeApiError: Если запрос завершился с ошибкой. + """ + response, ok = await self._exec_request( + self.RequestMethod.DELETE, + f"{self._url}/{params.account_id}/orders/{params.order_id}", + ) + + if not ok: + err = ErrorModel(**response) + raise FinamTradeApiError(f"code={err.code} | message={err.message} | details={err.details}") + + return OrderState(**response)