Skip to content

Commit

Permalink
add notional orders (#207)
Browse files Browse the repository at this point in the history
  • Loading branch information
Graeme22 authored Jan 29, 2025
1 parent 10555bd commit 0091777
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 38 deletions.
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

0 comments on commit 0091777

Please sign in to comment.