Skip to content

Commit

Permalink
Allows creating multiple watchlists (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
stabacco authored Jul 14, 2022
1 parent 5763b09 commit a7d40c0
Show file tree
Hide file tree
Showing 23 changed files with 1,976 additions and 356 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-python@v1
- uses: pre-commit/[email protected]
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/[email protected]
with:
token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Run image
uses: abatilo/[email protected]
with:
poetry-version: 1.0.10
poetry-version: 1.1.12
- name: Publish python package
run: poetry version ${{ steps.vars.outputs.tag }} && poetry build && poetry publish -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }}
- name: Auto commit pyproject.toml
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9]
poetry-version: [1.1.4]
poetry-version: [1.1.12]
os: [ubuntu-18.04, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
repos:
- repo: "https://github.com/pre-commit/pre-commit-hooks"
rev: v4.1.0
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: requirements-txt-fixer
- repo: "https://github.com/psf/black"
rev: 22.1.0
rev: 22.6.0
hooks:
- id: black
- repo: "https://github.com/pre-commit/mirrors-mypy"
rev: v0.931
rev: v0.961
hooks:
- id: mypy
- repo: "https://github.com/PyCQA/isort"
Expand All @@ -37,7 +37,7 @@ repos:
- "--max-line-length=90"
- "--max-doc-length=90"
- repo: "https://github.com/pre-commit/mirrors-prettier"
rev: v2.5.1
rev: v2.7.1
hooks:
- id: prettier
types_or:
Expand Down
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,68 @@ async def example_stop_sell(symbol='TSLA'):
asyncio.run(example_stop_sell('MSFT'))
```

### Watchlists

These are some examples on how to interact with watchlists:

```python
import stake
import asyncio
from stake.watchlist import Watchlist

async def create_watchlist(name: str) -> "Watchlist":
async with stake.StakeClient() as stake_session:
request= stake.CreateWatchlistRequest(name = name)
new_watchlist = await stake_session.watchlist.create_watchlist(request=request)
return new_watchlist

asyncio.run(create_watchlist(name='My watchlist'))
```

```python
import stake
import asyncio
from stake.watchlist import Watchlist

async def update_watchlist(id: str, tickers: "List[str]") -> "Watchlist":
async with stake.StakeClient() as stake_session:
request= stake.UpdateWatchlistRequest(id=id, tickers=tickers)
watchlist = await stake_session.watchlist.add_to_watchlist(request=request)
return watchlist

asyncio.run(update_watchlist(id=WATCHLIST_ID, tickers=["TSLA", "MSFT", "GOOG"] ))
```

```python
import stake
import asyncio
from stake.watchlist import Watchlist

async def remove_from_watchlist(id: str, tickers: "List[str]") -> "Watchlist":
async with stake.StakeClient() as stake_session:
request= stake.UpdateWatchlistRequest(id=id, tickers=tickers)
watchlist = await stake_session.watchlist.remove_from_watchlist(request=request)
return watchlist

asyncio.run(remove_from_watchlist(id=WATCHLIST_ID, tickers=["TSLA", "GOOG"] ))

```

```python
import stake
import asyncio
from stake.watchlist import Watchlist

async def delete_watchlist(id: str) -> "Watchlist":
async with stake.StakeClient() as stake_session:
request= stake.DeleteWatchlistRequest(id=id)
watchlist = await stake_session.watchlist.delete_watchlist(request=request)
return watchlist

asyncio.run(delete_watchlist(id=WATCHLIST_ID))

```

## Contributors

### Contributors on GitHub
Expand Down
374 changes: 154 additions & 220 deletions poetry.lock

Large diffs are not rendered by default.

38 changes: 26 additions & 12 deletions stake/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import os
from typing import Optional, Union
from urllib.parse import urljoin

import aiohttp
from dotenv import load_dotenv
Expand All @@ -24,9 +24,9 @@

load_dotenv()

__all__ = ["StakeClient", "CredentialsLoginRequest", "SessionTokenLoginRequest"]
logger = logging.getLogger(__name__)

global test_name
__all__ = ["StakeClient", "CredentialsLoginRequest", "SessionTokenLoginRequest"]


class CredentialsLoginRequest(BaseModel):
Expand Down Expand Up @@ -63,12 +63,13 @@ def url(endpoint: str) -> str:
"""Generates a stake api url.
Args:
endpoint (str): the final part of the url
endpoint (str): the full url
Returns:
str: the full url
"""
return urljoin(constant.STAKE_URL, endpoint, allow_fragments=True)
logger.debug("Endpoint %s", endpoint)
return endpoint

@staticmethod
async def get(url: str, payload: dict = None, headers: dict = None) -> dict:
Expand All @@ -92,14 +93,14 @@ async def post(url: str, payload: dict, headers: dict = None) -> dict:
return await response.json()

@staticmethod
async def delete(url: str, payload: dict = None, headers: dict = None) -> bool:
async def delete(url: str, payload: dict = None, headers: dict = None) -> dict:
async with aiohttp.ClientSession(
headers=headers, raise_for_status=True
) as session:
response = await session.delete(
HttpClient.url(url), headers=headers, json=payload
)
return response.status <= 399
return await response.json()


class InvalidLoginException(Exception):
Expand All @@ -110,10 +111,23 @@ class StakeClient:
"""The main client to interact with the Stake API."""

def __init__(
self, request: Union[CredentialsLoginRequest, SessionTokenLoginRequest] = None
self,
request: Union[CredentialsLoginRequest, SessionTokenLoginRequest] = None,
exchange: constant.BaseUrl = constant.NYSE,
):
self.user: Optional[user.User] = None
"""
Args:
request (Union[CredentialsLoginRequest,
SessionTokenLoginRequest], optional):
The authentication method: either a session token or credentials.
Defaults to None.
exchange (constant.BaseUrl, optional):
the stock exchange to be used.
Defaults to constant.NYSE.
"""
self.user: Optional[user.User] = None
self.exchange: constant.BaseUrl = exchange
self.headers = Headers()
self.http_client = HttpClient

Expand Down Expand Up @@ -161,7 +175,7 @@ async def post(self, url: str, payload: dict) -> dict:
url, payload=payload, headers=self.headers.dict(by_alias=True)
)

async def delete(self, url: str, payload: dict = None) -> bool:
async def delete(self, url: str, payload: dict = None) -> dict:
"""Performs an HTTP delete operation.
Args:
Expand Down Expand Up @@ -191,7 +205,7 @@ async def login(
if isinstance(login_request, CredentialsLoginRequest):
try:
data = await self.post(
constant.Url.create_session,
constant.NYSE.create_session,
payload=login_request.dict(by_alias=True),
)

Expand All @@ -201,7 +215,7 @@ async def login(
else:
self.headers.stake_session_token = login_request.token
try:
user_data = await self.get(constant.Url.user)
user_data = await self.get(self.exchange.users)
except aiohttp.client_exceptions.ClientResponseError as error:
raise InvalidLoginException("Invalid Session Token") from error

Expand Down
121 changes: 87 additions & 34 deletions stake/constant.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,87 @@
from enum import Enum


class Url(str, Enum):
"""Contains all the visited stake urls."""

account_balance: str = "cma/getAccountBalance"
account_transactions: str = "users/accounts/accountTransactions"
cancel_order: str = "orders/cancelOrder/${orderId}"
cash_available: str = "users/accounts/cashAvailableForWithdrawal"
create_session: str = "sessions/v2/createSession"
equity_positions: str = "users/accounts/v2/equityPositions"
fund_details: str = "fund/details"
market_status: str = "utils/marketStatus"
orders: str = "users/accounts/v2/orders"
products_suggestions: str = "products/getProductSuggestions/${keyword}"
quick_buy: str = "purchaseorders/v2/quickBuy"
quotes: str = "quotes/marketData/${symbols}"
rate: str = "wallet/rate"
ratings: str = "data/calendar/ratings?tickers=${symbols}&pageSize=${limit}"
sell_orders: str = "sellorders"
symbol: str = "products/searchProduct?symbol=${symbol}&page=1&max=1"
transaction_history: str = "users/accounts/transactionHistory"
transaction_details: str = (
"users/accounts/transactionDetails?"
"reference=${reference}&referenceType=${reference_type}"
)
transactions: str = "users/accounts/transactions"
user: str = "user"
watchlist_modify: str = "instruments/addRemoveInstrumentWatchlist"
watchlist: str = "products/productsWatchlist/${userId}"


STAKE_URL = "https://global-prd-api.hellostake.com/api/"
# sourcery skip: use-fstring-for-concatenation
from urllib.parse import urljoin

from pydantic import BaseModel


class BaseUrl(BaseModel):
"""Contains all the visited stake urls for the NYSE."""

STAKE_URL: str = "https://global-prd-api.hellostake.com/api/"
account_balance: str = urljoin(
STAKE_URL, "cma/getAccountBalance", allow_fragments=True
)
account_transactions: str = urljoin(
STAKE_URL, "users/accounts/accountTransactions", allow_fragments=True
)
cancel_order: str = urljoin(
STAKE_URL, "orders/cancelOrder/{orderId}", allow_fragments=True
)
cash_available: str = urljoin(
STAKE_URL, "users/accounts/cashAvailableForWithdrawal", allow_fragments=True
)
create_session: str = urljoin(
STAKE_URL, "sessions/v2/createSession", allow_fragments=True
)
equity_positions: str = urljoin(
STAKE_URL, "users/accounts/v2/equityPositions", allow_fragments=True
)
fund_details: str = urljoin(STAKE_URL, "fund/details", allow_fragments=True)
market_status: str = urljoin(STAKE_URL, "utils/marketStatus", allow_fragments=True)
orders: str = urljoin(STAKE_URL, "users/accounts/v2/orders", allow_fragments=True)
products_suggestions: str = urljoin(
STAKE_URL, "products/getProductSuggestions/{keyword}", allow_fragments=True
)
quick_buy: str = urljoin(
STAKE_URL, "purchaseorders/v2/quickBuy", allow_fragments=True
)
quotes: str = urljoin(
STAKE_URL, "quotes/marketData/{symbols}", allow_fragments=True
)
rate: str = urljoin(STAKE_URL, "wallet/rate", allow_fragments=True)
ratings: str = urljoin(
STAKE_URL,
"data/calendar/ratings?tickers={symbols}&pageSize={limit}",
allow_fragments=True,
)
sell_orders: str = urljoin(STAKE_URL, "sellorders", allow_fragments=True)
symbol: str = urljoin(
STAKE_URL,
"products/searchProduct?symbol={symbol}&page=1&max=1",
allow_fragments=True,
)
transaction_history: str = urljoin(
STAKE_URL, "users/accounts/transactionHistory", allow_fragments=True
)
transaction_details: str = urljoin(
STAKE_URL,
(
"users/accounts/transactionDetails?"
"reference={reference}&referenceType={reference_type}"
),
allow_fragments=True,
)
transactions: str = urljoin(
STAKE_URL, "users/accounts/transactions", allow_fragments=True
)
users: str = urljoin(STAKE_URL, "user", allow_fragments=True)

# deprecated, use update_watchlist instead
watchlist_modify: str = urljoin(
STAKE_URL, "instruments/addRemoveInstrumentWatchlist", allow_fragments=True
)
# deprecated, use read_watchlist instead
watchlist: str = urljoin(
STAKE_URL, "products/productsWatchlist/{userId}", allow_fragments=True
)

watchlists: str = "https://api.prd.stakeover.io/us/instrument/watchlists"
create_watchlist: str = "https://api.prd.stakeover.io/us/instrument/watchlist"
read_watchlist: str = (
"https://api.prd.stakeover.io/us/instrument/watchlist/{watchlist_id}"
)
update_watchlist: str = read_watchlist + "/items"


# The New York Stock Exchange
NYSE = BaseUrl()
3 changes: 1 addition & 2 deletions stake/equity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from pydantic import BaseModel, Field

from stake.common import BaseClient, SideEnum, camelcase
from stake.constant import Url


class EquityCategory(str, Enum):
Expand Down Expand Up @@ -63,5 +62,5 @@ async def list(self) -> EquityPositions:
Returns:
EquityPositions: The list of your equities.
"""
data = await self._client.get(Url.equity_positions)
data = await self._client.get(self._client.exchange.equity_positions)
return EquityPositions(**data)
Loading

0 comments on commit a7d40c0

Please sign in to comment.