Skip to content

Commit

Permalink
Merge pull request #73 from alpacahq/trading-orders-2
Browse files Browse the repository at this point in the history
Trading API: Adds All Order Routes
  • Loading branch information
haxdds authored May 27, 2022
2 parents 7d15129 + 087da28 commit 02d9875
Show file tree
Hide file tree
Showing 6 changed files with 482 additions and 11 deletions.
4 changes: 2 additions & 2 deletions alpaca/common/models/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class Order(BaseModel):
Attributes:
id (UUID): Order ID generated by Alpaca.
client_order_id (UUID): Client unique order ID
client_order_id (str): Client unique order ID
created_at (datetime): Timestamp when the Order was created.
updated_at (datetime): Timestamp when the Order was last updated.
submitted_at (datetime): Timestamp when the Order was submitted.
Expand Down Expand Up @@ -147,7 +147,7 @@ class Order(BaseModel):
"""

id: UUID
client_order_id: UUID
client_order_id: str
created_at: datetime
updated_at: datetime
submitted_at: datetime
Expand Down
143 changes: 140 additions & 3 deletions alpaca/trading/client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
from uuid import UUID
from pydantic import parse_obj_as
from alpaca.broker.client import validate_uuid_id_param
from alpaca.common.rest import RESTClient
from typing import Optional
from typing import Optional, List, Union
from alpaca.common.enums import BaseURL
from alpaca.common.models import Order
from .models import OrderRequest
from .models import (
OrderRequest,
GetOrdersRequest,
ReplaceOrderRequest,
GetOrderByIdRequest,
CancelOrderResponse,
)
from ..common import APIError


class TradingClient(RESTClient):
"""
A client to interact with the trading API, in both paper and live mode.
"""

def __init__(
self,
api_key: Optional[str] = None,
Expand All @@ -30,8 +44,131 @@ def __init__(
)

def submit_order(self, order_data: OrderRequest) -> Order:
""" """
"""Creates an order to buy or sell an asset.
Args:
order_data (OrderCreationRequest): The request data for creating a new order.
Returns:
Order: The resulting submitted order.
"""
data = order_data.to_request_fields()
response = self.post("/orders", data)

return Order(**response)

def get_orders(self, filter: Optional[GetOrdersRequest] = None) -> List[Order]:
"""
Returns all orders. Orders can be filtered by parameters.
Args:
filter (Optional[GetOrdersRequest]): The parameters to filter the orders with.
Returns:
List[Order]: The queried orders.
"""
# checking to see if we specified at least one param
params = filter.to_request_fields() if filter is not None else {}

if "symbols" in params and type(params["symbols"]) is List:
params["symbols"] = ",".join(params["symbols"])

response = self.get("/orders", params)

return parse_obj_as(List[Order], response)

def get_order_by_id(
self, order_id: Union[UUID, str], filter: Optional[GetOrderByIdRequest] = None
) -> Order:
"""
Returns a specific order by its order id.
Args:
order_id (Union[UUID, str]): The unique uuid identifier for the order.
filter (Optional[GetOrderByIdRequest]): The parameters for the query.
Returns:
Order: The order that was queried.
"""
# checking to see if we specified at least one param
params = filter.to_request_fields() if filter is not None else {}

order_id = validate_uuid_id_param(order_id, "order_id")

response = self.get(f"/orders/{order_id}", params)

return Order(**response)

def get_order_by_client_id(self, client_id: Union[UUID, str]) -> Order:
"""
Returns a specific order by its client order id.
Args:
client_id (Union[UUID, str]): The unique client order id identifier for the order.
Returns:
Order: The queried order.
"""
client_id = validate_uuid_id_param(client_id, "client_id")

response = self.get(f"/orders/{client_id}")

return Order(**response)

def replace_order(
self,
order_id: Union[UUID, str],
order_data: Optional[ReplaceOrderRequest] = None,
) -> Order:
"""
Updates an order with new parameters.
Args:
order_id (Union[UUID, str]): The unique uuid identifier for the order being replaced.
order_data (Optional[ReplaceOrderRequest]): The parameters we wish to update.
Returns:
Order: The updated order.
"""
# checking to see if we specified at least one param
params = order_data.to_request_fields() if order_data is not None else {}

order_id = validate_uuid_id_param(order_id, "order_id")

response = self.patch(f"/orders/{order_id}", params)

return Order(**response)

def cancel_orders(self) -> List[CancelOrderResponse]:
"""
Cancels all orders.
Returns:
List[CancelOrderResponse]: The list of HTTP statuses for each order attempted to be cancelled.
"""
response = self.delete(f"/orders")

return parse_obj_as(List[CancelOrderResponse], response)

def cancel_order_by_id(self, order_id: Union[UUID, str]) -> CancelOrderResponse:
"""
Cancels a specific order by its order id.
Args:
order_id (Union[UUID, str]): The unique uuid identifier of the order being cancelled.
Returns:
CancelOrderResponse: The HTTP response from the cancel request.
"""
order_id = validate_uuid_id_param(order_id, "order_id")

cancel_data = {"id": order_id}

# handle error responses so that we can return it to the user
try:
response = self.delete(f"/orders/{order_id}")
cancel_data["status"] = response.status_code
except APIError as error:
cancel_data["status"] = error.status_code

return CancelOrderResponse(**cancel_data)
84 changes: 81 additions & 3 deletions alpaca/trading/models/requests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
from typing import Optional, Any

from alpaca.common.enums import OrderSide, OrderType, TimeInForce, OrderClass
from datetime import datetime
from typing import List, Optional, Any
from uuid import UUID

from alpaca.common.enums import (
OrderSide,
OrderStatus,
OrderType,
Sort,
TimeInForce,
OrderClass,
)
from alpaca.common.models import NonEmptyRequest

from alpaca.common.models import ValidateBaseModel as BaseModel

from pydantic import root_validator


Expand Down Expand Up @@ -178,3 +189,70 @@ def root_validator(cls, values: dict) -> dict:
raise ValueError("Both trail_percent and trail_price cannot be set.")

return values


class GetOrdersRequest(NonEmptyRequest):
"""Contains data for submitting a request to retrieve orders.
Attributes:
status (Optional[OrderStatus]): Order status to be queried. open, closed or all. Defaults to open.
limit (Optional[int]): The maximum number of orders in response. Defaults to 50 and max is 500.
after (Optional[datetime]): The response will include only ones submitted after this timestamp.
until (Optional[datetime]): The response will include only ones submitted until this timestamp.
direction (Optional[Sort]): The chronological order of response based on the submission time. asc or desc. Defaults to desc.
nested (Optional[bool]): If true, the result will roll up multi-leg orders under the legs field of primary order.
side (Optional[OrderSide]): Filters down to orders that have a matching side field set.
symbols (Optional[List[str]]): List of symbols to filter by.
"""

status: Optional[OrderStatus]
limit: Optional[int] # not pagination
after: Optional[datetime]
until: Optional[datetime]
direction: Optional[Sort]
nested: Optional[bool]
side: Optional[OrderSide]
symbols: Optional[List[str]]


class GetOrderByIdRequest(NonEmptyRequest):
"""Contains data for submitting a request to retrieve a single order by its order id.
Attributes:
nested (bool): If true, the result will roll up multi-leg orders under the legs field of primary order.
"""

nested: bool


class ReplaceOrderRequest(NonEmptyRequest):
"""Contains data for submitting a request to replace an order.
Attributes:
qty (Optional[int]): Number of shares to trade
time_in_force (Optional[TimeInForce]): The new expiration logic of the order.
limit_price (Optional[float]): Required if type of order being replaced is limit or stop_limit
stop_price (Optional[float]): Required if type of order being replaced is stop or stop_limit
trail (Optional[float]): The new value of the trail_price or trail_percent value (works only for type=“trailing_stop”)
client_order_id (Optional[str]): A unique identifier for the order.
"""

qty: Optional[int]
time_in_force: Optional[TimeInForce]
limit_price: Optional[float]
stop_price: Optional[float]
trail: Optional[float]
client_order_id: Optional[str]


class CancelOrderResponse(BaseModel):
"""
Data returned after requesting to cancel an order. It contains the cancel status of an order.
Attributes:
id (UUID): The order id
status (int): The HTTP status returned after attempting to cancel the order.
"""

id: UUID
status: int
1 change: 0 additions & 1 deletion tests/common/test_common_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ def test_order_uuids():
order = create_dummy_order()

assert isinstance(order.id, UUID)
assert isinstance(order.client_order_id, UUID)
assert isinstance(order.replaced_by, UUID)
assert isinstance(order.replaces, UUID)
assert isinstance(order.asset_id, UUID)
Expand Down
9 changes: 7 additions & 2 deletions tests/trading/test_trading_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from alpaca.common.enums import OrderSide, OrderType, TimeInForce
from alpaca.trading.models import MarketOrderRequest, TrailingStopOrderRequest

from alpaca.trading.models import (
MarketOrderRequest,
TrailingStopOrderRequest,
ReplaceOrderRequest,
CancelOrderResponse,
)
from alpaca.trading.models.requests import GetOrdersRequest
import pytest


Expand Down
Loading

0 comments on commit 02d9875

Please sign in to comment.