Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notional orders support #207

Merged
merged 1 commit into from
Jan 29, 2025
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
18 changes: 18 additions & 0 deletions docs/orders.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,21 @@ An OCO order is similar, but has no trigger order. It's used to add a profit-tak
resp = account.place_complex_order(session, oco, dry_run=False)

Note that to cancel complex orders, you need to use the ``delete_complex_order`` function, NOT ``delete_order``.

Notional market orders
----------------------

Notional orders are slightly different from normal orders. Since the market will determine both the quantity and the price for you, you need to pass `value` instead of price, and pass `None` for the `quantity` parameter to ``build_leg``.

.. code-block:: python

symbol = Equity.get_equity(session, 'AAPL')
order = NewOrder(
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.NOTIONAL_MARKET,
value=Decimal(-10), # $10 debit, this will result in fractional shares
legs=[
symbol.build_leg(None, OrderAction.BUY_TO_OPEN),
]
)
resp = account.place_order(session, order, dry_run=False)
22 changes: 10 additions & 12 deletions tastytrade/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
PriceEffect,
TastytradeError,
TastytradeJsonDataclass,
_set_sign_for,
set_sign_for,
today_in_new_york,
validate_response,
)
Expand Down Expand Up @@ -97,7 +97,7 @@ def validate_price_effects(cls, data: Any) -> Any:
effect = data.get("unsettled-cryptocurrency-fiat-effect")
if effect == PriceEffect.DEBIT:
data[key] = -abs(Decimal(data[key]))
return _set_sign_for(data, ["pending_cash", "buying_power_adjustment"])
return set_sign_for(data, ["pending_cash", "buying_power_adjustment"])


class AccountBalanceSnapshot(TastytradeJsonDataclass):
Expand Down Expand Up @@ -151,7 +151,7 @@ def validate_price_effects(cls, data: Any) -> Any:
effect = data.get("unsettled-cryptocurrency-fiat-effect")
if effect == PriceEffect.DEBIT:
data[key] = -abs(Decimal(data[key]))
return _set_sign_for(data, ["pending_cash"])
return set_sign_for(data, ["pending_cash"])


class CurrentPosition(TastytradeJsonDataclass):
Expand Down Expand Up @@ -190,7 +190,7 @@ class CurrentPosition(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(data, ["realized_day_gain", "realized_today"])
return set_sign_for(data, ["realized_day_gain", "realized_today"])


class FeesInfo(TastytradeJsonDataclass):
Expand All @@ -199,7 +199,7 @@ class FeesInfo(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(data, ["total_fees"])
return set_sign_for(data, ["total_fees"])


class Lot(TastytradeJsonDataclass):
Expand Down Expand Up @@ -241,7 +241,7 @@ class MarginReportEntry(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"buying_power",
Expand Down Expand Up @@ -275,7 +275,7 @@ class MarginReport(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"maintenance_requirement",
Expand Down Expand Up @@ -437,7 +437,7 @@ class Transaction(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"value",
Expand Down Expand Up @@ -1132,8 +1132,7 @@ async def a_get_effective_margin_requirements(
if symbol:
symbol = symbol.replace("/", "%2F")
data = await session._a_get(
f"/accounts/{self.account_number}/margin-"
f"requirements/{symbol}/effective"
f"/accounts/{self.account_number}/margin-requirements/{symbol}/effective"
)
return MarginRequirement(**data)

Expand All @@ -1150,8 +1149,7 @@ def get_effective_margin_requirements(
if symbol:
symbol = symbol.replace("/", "%2F")
data = session._get(
f"/accounts/{self.account_number}/margin-"
f"requirements/{symbol}/effective"
f"/accounts/{self.account_number}/margin-requirements/{symbol}/effective"
)
return MarginRequirement(**data)

Expand Down
23 changes: 12 additions & 11 deletions tastytrade/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from tastytrade.utils import (
PriceEffect,
TastytradeJsonDataclass,
_get_sign,
_set_sign_for,
get_sign,
set_sign_for,
)


Expand Down Expand Up @@ -149,11 +149,12 @@ class TradeableTastytradeJsonDataclass(TastytradeJsonDataclass):
instrument_type: InstrumentType
symbol: str

def build_leg(self, quantity: Decimal, action: OrderAction) -> Leg:
def build_leg(self, quantity: Optional[Decimal], action: OrderAction) -> Leg:
"""
Builds an order :class:`Leg` from the dataclass.

:param quantity: the quantity of the symbol to trade
:param quantity:
the quantity of the symbol to trade, set this as `None` for notional orders
:param action: :class:`OrderAction` to perform, e.g. BUY_TO_OPEN

:return: a :class:`Leg` object
Expand Down Expand Up @@ -257,12 +258,12 @@ class NewOrder(TastytradeJsonDataclass):
@computed_field
@property
def price_effect(self) -> Optional[PriceEffect]:
return _get_sign(self.price)
return get_sign(self.price)

@computed_field
@property
def value_effect(self) -> Optional[PriceEffect]:
return _get_sign(self.value)
return get_sign(self.value)

@field_serializer("price", "value")
def serialize_fields(self, field: Optional[Decimal]) -> Optional[Decimal]:
Expand Down Expand Up @@ -333,7 +334,7 @@ class PlacedOrder(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(data, ["price", "value"])
return set_sign_for(data, ["price", "value"])


class PlacedComplexOrder(TastytradeJsonDataclass):
Expand Down Expand Up @@ -372,7 +373,7 @@ class BuyingPowerEffect(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"change_in_margin_requirement",
Expand All @@ -398,7 +399,7 @@ class FeeCalculation(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"regulatory_fees",
Expand Down Expand Up @@ -480,7 +481,7 @@ class OrderChainNode(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"total_fees",
Expand Down Expand Up @@ -520,7 +521,7 @@ class ComputedData(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"total_fees",
Expand Down
16 changes: 8 additions & 8 deletions tastytrade/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from tastytrade.utils import (
TastytradeError,
TastytradeJsonDataclass,
_validate_and_parse,
validate_and_parse,
validate_response,
)

Expand Down Expand Up @@ -320,7 +320,7 @@ def __init__(
)
else:
response = self.sync_client.post("/sessions", json=body)
data = _validate_and_parse(response)
data = validate_and_parse(response)
#: The user dict returned by the API; contains basic user information
self.user = User(**data["user"])
#: The session token used to authenticate requests
Expand All @@ -347,11 +347,11 @@ def __init__(

async def _a_get(self, url, **kwargs) -> dict[str, Any]:
response = await self.async_client.get(url, timeout=30, **kwargs)
return _validate_and_parse(response)
return validate_and_parse(response)

def _get(self, url, **kwargs) -> dict[str, Any]:
response = self.sync_client.get(url, timeout=30, **kwargs)
return _validate_and_parse(response)
return validate_and_parse(response)

async def _a_delete(self, url, **kwargs) -> None:
response = await self.async_client.delete(url, **kwargs)
Expand All @@ -363,19 +363,19 @@ def _delete(self, url, **kwargs) -> None:

async def _a_post(self, url, **kwargs) -> dict[str, Any]:
response = await self.async_client.post(url, **kwargs)
return _validate_and_parse(response)
return validate_and_parse(response)

def _post(self, url, **kwargs) -> dict[str, Any]:
response = self.sync_client.post(url, **kwargs)
return _validate_and_parse(response)
return validate_and_parse(response)

async def _a_put(self, url, **kwargs) -> dict[str, Any]:
response = await self.async_client.put(url, **kwargs)
return _validate_and_parse(response)
return validate_and_parse(response)

def _put(self, url, **kwargs) -> dict[str, Any]:
response = self.sync_client.put(url, **kwargs)
return _validate_and_parse(response)
return validate_and_parse(response)

async def a_validate(self) -> bool:
"""
Expand Down
4 changes: 2 additions & 2 deletions tastytrade/streamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
PlacedOrder,
)
from tastytrade.session import Session
from tastytrade.utils import TastytradeError, TastytradeJsonDataclass, _set_sign_for
from tastytrade.utils import TastytradeError, TastytradeJsonDataclass, set_sign_for
from tastytrade.watchlists import Watchlist

CERT_STREAMER_URL = "wss://streamer.cert.tastyworks.com"
Expand Down Expand Up @@ -87,7 +87,7 @@ class UnderlyingYearGainSummary(TastytradeJsonDataclass):
@model_validator(mode="before")
@classmethod
def validate_price_effects(cls, data: Any) -> Any:
return _set_sign_for(
return set_sign_for(
data,
[
"fees",
Expand Down
10 changes: 5 additions & 5 deletions tastytrade/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from typing import Any, Optional
from zoneinfo import ZoneInfo

import pandas_market_calendars as mcal # type: ignore
from httpx._models import Response
from pandas_market_calendars import get_calendar
from pydantic import BaseModel, ConfigDict

NYSE = mcal.get_calendar("NYSE")
NYSE = get_calendar("NYSE")
TZ = ZoneInfo("US/Eastern")


Expand Down Expand Up @@ -269,18 +269,18 @@ def validate_response(response: Response) -> None:
raise TastytradeError(error_message)


def _validate_and_parse(response: Response) -> dict[str, Any]:
def validate_and_parse(response: Response) -> dict[str, Any]:
validate_response(response)
return response.json()["data"]


def _get_sign(value: Optional[Decimal]) -> Optional[PriceEffect]:
def get_sign(value: Optional[Decimal]) -> Optional[PriceEffect]:
if not value:
return None
return PriceEffect.DEBIT if value < 0 else PriceEffect.CREDIT


def _set_sign_for(data: Any, properties: list[str]) -> Any:
def set_sign_for(data: Any, properties: list[str]) -> Any:
"""
Handles setting the sign of a number using the associated "-effect" field.

Expand Down
18 changes: 18 additions & 0 deletions tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ def new_order(session: Session) -> NewOrder:
)


@fixture(scope="module")
def notional_order(session: Session) -> NewOrder:
symbol = Equity.get_equity(session, "AAPL")
leg = symbol.build_leg(None, OrderAction.BUY_TO_OPEN)
return NewOrder(
time_in_force=OrderTimeInForce.DAY,
order_type=OrderType.NOTIONAL_MARKET,
legs=[leg],
value=Decimal(-5),
)


@fixture(scope="module")
def placed_order(
session: Session, account: Account, new_order: NewOrder
Expand All @@ -191,6 +203,12 @@ def test_place_order(placed_order: PlacedOrder):
pass


def test_place_notional_order(
session: Session, account: Account, notional_order: NewOrder
):
account.place_order(session, notional_order, dry_run=True)


def test_get_order(session: Session, account: Account, placed_order: PlacedOrder):
sleep(3)
assert account.get_order(session, placed_order.id).id == placed_order.id
Expand Down
Loading