From cd6c6905e0ff897e0aef2b59d5e45196a0176a7e Mon Sep 17 00:00:00 2001 From: Vadim Rogoza Date: Mon, 22 Dec 2025 22:36:27 +0300 Subject: [PATCH 1/3] OrdersService requests implementation --- finam_trade_api/base_client/models.py | 7 +- finam_trade_api/order/__init__.py | 15 +++ finam_trade_api/order/model.py | 136 +++++++++++++++++++++++++- finam_trade_api/order/order.py | 118 ++++++++++++++++++++-- 4 files changed, 263 insertions(+), 13 deletions(-) diff --git a/finam_trade_api/base_client/models.py b/finam_trade_api/base_client/models.py index b91320e..27844d2 100644 --- a/finam_trade_api/base_client/models.py +++ b/finam_trade_api/base_client/models.py @@ -21,6 +21,7 @@ class FinamMoney(BaseModel): """ A custom money type for Finam API responses. """ - units: str = "0" - currency_code: str = "RUB" - nanos: int = 0 + value: str = "0" + #units: str = "0" + #currency_code: str = "RUB" + #nanos: int = 0 diff --git a/finam_trade_api/order/__init__.py b/finam_trade_api/order/__init__.py index ec4ca40..a8cfcbf 100644 --- a/finam_trade_api/order/__init__.py +++ b/finam_trade_api/order/__init__.py @@ -1 +1,16 @@ from finam_trade_api.order.order import OrderClient +from finam_trade_api.order.model import ( + CancelOrderRequest, + GetOrderRequest, + Leg, + Order, + OrdersRequest, + OrdersResponse, + OrderState, + OrderStatus, + OrderType, + Side, + StopCondition, + TimeInForce, + ValidBefore, +) diff --git a/finam_trade_api/order/model.py b/finam_trade_api/order/model.py index a6eeeaa..7686f09 100644 --- a/finam_trade_api/order/model.py +++ b/finam_trade_api/order/model.py @@ -1,11 +1,143 @@ +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): + """Сторона заявки""" + #LONG = "long" + #SHORT = "short" + 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..4ba2e02 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) From 693f21425958a77fb3a2697a31aadf6f14869e51 Mon Sep 17 00:00:00 2001 From: Vadim Rogoza Date: Tue, 23 Dec 2025 17:32:19 +0300 Subject: [PATCH 2/3] OrderService Review 1 --- finam_trade_api/base_client/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/finam_trade_api/base_client/models.py b/finam_trade_api/base_client/models.py index 27844d2..4bcb0d8 100644 --- a/finam_trade_api/base_client/models.py +++ b/finam_trade_api/base_client/models.py @@ -22,6 +22,3 @@ class FinamMoney(BaseModel): A custom money type for Finam API responses. """ value: str = "0" - #units: str = "0" - #currency_code: str = "RUB" - #nanos: int = 0 From a12fa130ba5c2ea10bceb7b4e2f6e2311e8ef156 Mon Sep 17 00:00:00 2001 From: Vadim Rogoza Date: Tue, 23 Dec 2025 17:57:38 +0300 Subject: [PATCH 3/3] OrderService Review 2 --- finam_trade_api/order/__init__.py | 2 +- finam_trade_api/order/model.py | 4 +--- finam_trade_api/order/order.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/finam_trade_api/order/__init__.py b/finam_trade_api/order/__init__.py index a8cfcbf..d8f8556 100644 --- a/finam_trade_api/order/__init__.py +++ b/finam_trade_api/order/__init__.py @@ -1,4 +1,3 @@ -from finam_trade_api.order.order import OrderClient from finam_trade_api.order.model import ( CancelOrderRequest, GetOrderRequest, @@ -14,3 +13,4 @@ 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 7686f09..00ea0f5 100644 --- a/finam_trade_api/order/model.py +++ b/finam_trade_api/order/model.py @@ -8,9 +8,7 @@ class Side(str, Enum): - """Сторона заявки""" - #LONG = "long" - #SHORT = "short" + """Сторона заявки""" BUY = "SIDE_BUY" SELL = "SIDE_SELL" diff --git a/finam_trade_api/order/order.py b/finam_trade_api/order/order.py index 4ba2e02..0626010 100644 --- a/finam_trade_api/order/order.py +++ b/finam_trade_api/order/order.py @@ -82,7 +82,7 @@ async def place_order(self, order: Order) -> OrderState: FinamTradeApiError: Если запрос завершился с ошибкой. """ # Используем model_dump с mode='json' для корректной сериализации enum'ов - payload = order.model_dump(mode='json', exclude_none=True) + payload = order.model_dump(mode="json", exclude_none=True) response, ok = await self._exec_request( self.RequestMethod.POST,