Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pytest-coverage = "^0.0"
pytest-mock = "^3.3.0"
faker = "^4.14.0"
pytest-recording = "^0.13.4"
types-pyyaml = "^6.0.12.20260518"


[tool.isort]
Expand Down
13 changes: 8 additions & 5 deletions stake/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class NYSEUrl(BaseModel):
"""Contains all the visited stake urls for the NYSE."""

STAKE_URL: str = "https://api2.prd.hellostake.com/"
STAKE_OVER_URL: str = "https://api.prd.hellostake.com/"
account_balance: str = urljoin(
STAKE_URL, "api/cma/getAccountBalance", allow_fragments=True
)
Expand Down Expand Up @@ -47,7 +48,7 @@ class NYSEUrl(BaseModel):
STAKE_URL, "api/purchaseorders/v2/quickBuy", allow_fragments=True
)
quotes: str = urljoin(
STAKE_URL, "api/quotes/marketData/{symbols}", allow_fragments=True
STAKE_OVER_URL, "us/pricing/quotes/marketData", allow_fragments=True
)
rate: str = urljoin(STAKE_URL, "api/wallet/rate", allow_fragments=True)
ratings: str = urljoin(
Expand Down Expand Up @@ -78,16 +79,18 @@ class NYSEUrl(BaseModel):
users: str = urljoin(STAKE_URL, "api/user", allow_fragments=True)

watchlists: str = urljoin(
STAKE_URL, "us/instrument/watchlists", allow_fragments=True
STAKE_OVER_URL, "us/instrument/watchlists", allow_fragments=True
)
create_watchlist: str = urljoin(
STAKE_URL, "us/instrument/watchlist", allow_fragments=True
STAKE_OVER_URL, "us/instrument/watchlist", allow_fragments=True
)
read_watchlist: str = urljoin(
STAKE_URL, "us/instrument/watchlist/{watchlist_id}", allow_fragments=True
STAKE_OVER_URL, "us/instrument/watchlist/{watchlist_id}", allow_fragments=True
)
update_watchlist: str = urljoin(
STAKE_URL, "us/instrument/watchlist/{watchlist_id}/items", allow_fragments=True
STAKE_OVER_URL,
"us/instrument/watchlist/{watchlist_id}/items",
allow_fragments=True,
)

statement: str = urljoin(
Expand Down
73 changes: 65 additions & 8 deletions stake/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from stake.ratings import Rating
from stake.statement import Statement

__all__ = ["ProductSearchByName"]
__all__ = ["ProductSearchByName", "ProductQuote"]


class ProductSearchByName(BaseModel):
Expand All @@ -30,6 +30,27 @@ class Instrument(BaseModel):
model_config = ConfigDict(alias_generator=camelcase)


class ProductQuote(BaseModel):
symbol: str
bid: Optional[float] = None
ask: Optional[float] = None
close: Optional[float] = None
close_bid: Optional[float] = None
close_ask: Optional[float] = None
last_trade: Optional[float] = None
pre_post_market_last_trade: Optional[float] = None
prior_close: Optional[float] = None
open: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
market_status: Optional[str] = None
trading_status: Optional[str] = None
stake_instrument_id: Optional[uuid.UUID] = None
trade_timestamp: Optional[datetime] = None
volume: Optional[int] = None
model_config = ConfigDict(alias_generator=camelcase)


class Product(BaseModel):
id: uuid.UUID
instrument_type_id: Optional[str] = Field(None, alias="instrumentTypeID")
Expand All @@ -56,6 +77,22 @@ class Product(BaseModel):
trade_status: Optional[int] = None
encoded_name: str
period: str
bid: Optional[float] = None
ask: Optional[float] = None
close: Optional[float] = None
close_bid: Optional[float] = None
close_ask: Optional[float] = None
last_trade: Optional[float] = None
pre_post_market_last_trade: Optional[float] = None
prior_close: Optional[float] = None
open: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
market_status: Optional[str] = None
trading_status: Optional[str] = None
stake_instrument_id: Optional[uuid.UUID] = None
trade_timestamp: Optional[datetime] = None
volume: Optional[int] = None
inception_date: Optional[datetime] = None
instrument_tags: List[Any]
child_instruments: List[Instrument]
Expand Down Expand Up @@ -84,6 +121,17 @@ async def statements(self, start_date: date | None = None) -> "List[Statement]":


class ProductsClient(BaseClient):
async def quotes(self, symbols: List[str]) -> List[ProductQuote]:
"""Return market quote data for US symbols."""
data = await self._client.post(
self._client.exchange.quotes, {"symbols": symbols}
)
return [ProductQuote.model_validate(quote) for quote in data]

async def quote(self, symbol: str) -> Optional[ProductQuote]:
quotes = await self.quotes([symbol])
return quotes[0] if quotes else None

async def get(self, symbol: str) -> Optional[Product]:
"""Given a symbol it will return the matching product.

Expand All @@ -94,13 +142,22 @@ async def get(self, symbol: str) -> Optional[Product]:
self._client.exchange.symbol.format(symbol=symbol)
)

return (
Product.model_validate(
data["products"][0], context=dict(client=self._client)
)
if data["products"]
else None
)
if not data["products"]:
return None

product_data = data["products"][0]
try:
quote = await self.quote(symbol)
except Exception:
quote = None

if quote:
product_data = {
**product_data,
**quote.model_dump(by_alias=True, exclude_none=True),
}

return Product.model_validate(product_data, context=dict(client=self._client))

async def search(self, request: ProductSearchByName) -> List[Instrument]:
products = await self._client.get(
Expand Down
Loading
Loading