Skip to content

Commit 0091777

Browse files
authored
add notional orders (#207)
1 parent 10555bd commit 0091777

File tree

7 files changed

+73
-38
lines changed

7 files changed

+73
-38
lines changed

Diff for: docs/orders.rst

+18
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,21 @@ An OCO order is similar, but has no trigger order. It's used to add a profit-tak
127127
resp = account.place_complex_order(session, oco, dry_run=False)
128128
129129
Note that to cancel complex orders, you need to use the ``delete_complex_order`` function, NOT ``delete_order``.
130+
131+
Notional market orders
132+
----------------------
133+
134+
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``.
135+
136+
.. code-block:: python
137+
138+
symbol = Equity.get_equity(session, 'AAPL')
139+
order = NewOrder(
140+
time_in_force=OrderTimeInForce.DAY,
141+
order_type=OrderType.NOTIONAL_MARKET,
142+
value=Decimal(-10), # $10 debit, this will result in fractional shares
143+
legs=[
144+
symbol.build_leg(None, OrderAction.BUY_TO_OPEN),
145+
]
146+
)
147+
resp = account.place_order(session, order, dry_run=False)

Diff for: tastytrade/account.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
PriceEffect,
2525
TastytradeError,
2626
TastytradeJsonDataclass,
27-
_set_sign_for,
27+
set_sign_for,
2828
today_in_new_york,
2929
validate_response,
3030
)
@@ -97,7 +97,7 @@ def validate_price_effects(cls, data: Any) -> Any:
9797
effect = data.get("unsettled-cryptocurrency-fiat-effect")
9898
if effect == PriceEffect.DEBIT:
9999
data[key] = -abs(Decimal(data[key]))
100-
return _set_sign_for(data, ["pending_cash", "buying_power_adjustment"])
100+
return set_sign_for(data, ["pending_cash", "buying_power_adjustment"])
101101

102102

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

156156

157157
class CurrentPosition(TastytradeJsonDataclass):
@@ -190,7 +190,7 @@ class CurrentPosition(TastytradeJsonDataclass):
190190
@model_validator(mode="before")
191191
@classmethod
192192
def validate_price_effects(cls, data: Any) -> Any:
193-
return _set_sign_for(data, ["realized_day_gain", "realized_today"])
193+
return set_sign_for(data, ["realized_day_gain", "realized_today"])
194194

195195

196196
class FeesInfo(TastytradeJsonDataclass):
@@ -199,7 +199,7 @@ class FeesInfo(TastytradeJsonDataclass):
199199
@model_validator(mode="before")
200200
@classmethod
201201
def validate_price_effects(cls, data: Any) -> Any:
202-
return _set_sign_for(data, ["total_fees"])
202+
return set_sign_for(data, ["total_fees"])
203203

204204

205205
class Lot(TastytradeJsonDataclass):
@@ -241,7 +241,7 @@ class MarginReportEntry(TastytradeJsonDataclass):
241241
@model_validator(mode="before")
242242
@classmethod
243243
def validate_price_effects(cls, data: Any) -> Any:
244-
return _set_sign_for(
244+
return set_sign_for(
245245
data,
246246
[
247247
"buying_power",
@@ -275,7 +275,7 @@ class MarginReport(TastytradeJsonDataclass):
275275
@model_validator(mode="before")
276276
@classmethod
277277
def validate_price_effects(cls, data: Any) -> Any:
278-
return _set_sign_for(
278+
return set_sign_for(
279279
data,
280280
[
281281
"maintenance_requirement",
@@ -437,7 +437,7 @@ class Transaction(TastytradeJsonDataclass):
437437
@model_validator(mode="before")
438438
@classmethod
439439
def validate_price_effects(cls, data: Any) -> Any:
440-
return _set_sign_for(
440+
return set_sign_for(
441441
data,
442442
[
443443
"value",
@@ -1132,8 +1132,7 @@ async def a_get_effective_margin_requirements(
11321132
if symbol:
11331133
symbol = symbol.replace("/", "%2F")
11341134
data = await session._a_get(
1135-
f"/accounts/{self.account_number}/margin-"
1136-
f"requirements/{symbol}/effective"
1135+
f"/accounts/{self.account_number}/margin-requirements/{symbol}/effective"
11371136
)
11381137
return MarginRequirement(**data)
11391138

@@ -1150,8 +1149,7 @@ def get_effective_margin_requirements(
11501149
if symbol:
11511150
symbol = symbol.replace("/", "%2F")
11521151
data = session._get(
1153-
f"/accounts/{self.account_number}/margin-"
1154-
f"requirements/{symbol}/effective"
1152+
f"/accounts/{self.account_number}/margin-requirements/{symbol}/effective"
11551153
)
11561154
return MarginRequirement(**data)
11571155

Diff for: tastytrade/order.py

+12-11
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from tastytrade.utils import (
1010
PriceEffect,
1111
TastytradeJsonDataclass,
12-
_get_sign,
13-
_set_sign_for,
12+
get_sign,
13+
set_sign_for,
1414
)
1515

1616

@@ -149,11 +149,12 @@ class TradeableTastytradeJsonDataclass(TastytradeJsonDataclass):
149149
instrument_type: InstrumentType
150150
symbol: str
151151

152-
def build_leg(self, quantity: Decimal, action: OrderAction) -> Leg:
152+
def build_leg(self, quantity: Optional[Decimal], action: OrderAction) -> Leg:
153153
"""
154154
Builds an order :class:`Leg` from the dataclass.
155155
156-
:param quantity: the quantity of the symbol to trade
156+
:param quantity:
157+
the quantity of the symbol to trade, set this as `None` for notional orders
157158
:param action: :class:`OrderAction` to perform, e.g. BUY_TO_OPEN
158159
159160
:return: a :class:`Leg` object
@@ -257,12 +258,12 @@ class NewOrder(TastytradeJsonDataclass):
257258
@computed_field
258259
@property
259260
def price_effect(self) -> Optional[PriceEffect]:
260-
return _get_sign(self.price)
261+
return get_sign(self.price)
261262

262263
@computed_field
263264
@property
264265
def value_effect(self) -> Optional[PriceEffect]:
265-
return _get_sign(self.value)
266+
return get_sign(self.value)
266267

267268
@field_serializer("price", "value")
268269
def serialize_fields(self, field: Optional[Decimal]) -> Optional[Decimal]:
@@ -333,7 +334,7 @@ class PlacedOrder(TastytradeJsonDataclass):
333334
@model_validator(mode="before")
334335
@classmethod
335336
def validate_price_effects(cls, data: Any) -> Any:
336-
return _set_sign_for(data, ["price", "value"])
337+
return set_sign_for(data, ["price", "value"])
337338

338339

339340
class PlacedComplexOrder(TastytradeJsonDataclass):
@@ -372,7 +373,7 @@ class BuyingPowerEffect(TastytradeJsonDataclass):
372373
@model_validator(mode="before")
373374
@classmethod
374375
def validate_price_effects(cls, data: Any) -> Any:
375-
return _set_sign_for(
376+
return set_sign_for(
376377
data,
377378
[
378379
"change_in_margin_requirement",
@@ -398,7 +399,7 @@ class FeeCalculation(TastytradeJsonDataclass):
398399
@model_validator(mode="before")
399400
@classmethod
400401
def validate_price_effects(cls, data: Any) -> Any:
401-
return _set_sign_for(
402+
return set_sign_for(
402403
data,
403404
[
404405
"regulatory_fees",
@@ -480,7 +481,7 @@ class OrderChainNode(TastytradeJsonDataclass):
480481
@model_validator(mode="before")
481482
@classmethod
482483
def validate_price_effects(cls, data: Any) -> Any:
483-
return _set_sign_for(
484+
return set_sign_for(
484485
data,
485486
[
486487
"total_fees",
@@ -520,7 +521,7 @@ class ComputedData(TastytradeJsonDataclass):
520521
@model_validator(mode="before")
521522
@classmethod
522523
def validate_price_effects(cls, data: Any) -> Any:
523-
return _set_sign_for(
524+
return set_sign_for(
524525
data,
525526
[
526527
"total_fees",

Diff for: tastytrade/session.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tastytrade.utils import (
1010
TastytradeError,
1111
TastytradeJsonDataclass,
12-
_validate_and_parse,
12+
validate_and_parse,
1313
validate_response,
1414
)
1515

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

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

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

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

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

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

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

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

380380
async def a_validate(self) -> bool:
381381
"""

Diff for: tastytrade/streamer.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
PlacedOrder,
4242
)
4343
from tastytrade.session import Session
44-
from tastytrade.utils import TastytradeError, TastytradeJsonDataclass, _set_sign_for
44+
from tastytrade.utils import TastytradeError, TastytradeJsonDataclass, set_sign_for
4545
from tastytrade.watchlists import Watchlist
4646

4747
CERT_STREAMER_URL = "wss://streamer.cert.tastyworks.com"
@@ -87,7 +87,7 @@ class UnderlyingYearGainSummary(TastytradeJsonDataclass):
8787
@model_validator(mode="before")
8888
@classmethod
8989
def validate_price_effects(cls, data: Any) -> Any:
90-
return _set_sign_for(
90+
return set_sign_for(
9191
data,
9292
[
9393
"fees",

Diff for: tastytrade/utils.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from typing import Any, Optional
55
from zoneinfo import ZoneInfo
66

7-
import pandas_market_calendars as mcal # type: ignore
87
from httpx._models import Response
8+
from pandas_market_calendars import get_calendar
99
from pydantic import BaseModel, ConfigDict
1010

11-
NYSE = mcal.get_calendar("NYSE")
11+
NYSE = get_calendar("NYSE")
1212
TZ = ZoneInfo("US/Eastern")
1313

1414

@@ -269,18 +269,18 @@ def validate_response(response: Response) -> None:
269269
raise TastytradeError(error_message)
270270

271271

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

276276

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

282282

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

Diff for: tests/test_account.py

+18
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,18 @@ def new_order(session: Session) -> NewOrder:
180180
)
181181

182182

183+
@fixture(scope="module")
184+
def notional_order(session: Session) -> NewOrder:
185+
symbol = Equity.get_equity(session, "AAPL")
186+
leg = symbol.build_leg(None, OrderAction.BUY_TO_OPEN)
187+
return NewOrder(
188+
time_in_force=OrderTimeInForce.DAY,
189+
order_type=OrderType.NOTIONAL_MARKET,
190+
legs=[leg],
191+
value=Decimal(-5),
192+
)
193+
194+
183195
@fixture(scope="module")
184196
def placed_order(
185197
session: Session, account: Account, new_order: NewOrder
@@ -191,6 +203,12 @@ def test_place_order(placed_order: PlacedOrder):
191203
pass
192204

193205

206+
def test_place_notional_order(
207+
session: Session, account: Account, notional_order: NewOrder
208+
):
209+
account.place_order(session, notional_order, dry_run=True)
210+
211+
194212
def test_get_order(session: Session, account: Account, placed_order: PlacedOrder):
195213
sleep(3)
196214
assert account.get_order(session, placed_order.id).id == placed_order.id

0 commit comments

Comments
 (0)