Skip to content
Open
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
36 changes: 36 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,39 @@ Order Module
orderTerm=orderTerm,
marketSession=marketSession,
)

# place multi-leg option order:
quantity = 1
strikePrice1 = 200
strikePrice2 = 210
expiryDate = "2022-02-18"

legs=[
{
"symbol": "PLTR",
"orderAction": "BUY_OPEN",
"callPut": "CALL",
"strikePrice": strikePrice1,
"expiryDate": expiryDate,
"quantity": quantity
},
{
"symbol": "PLTR",
"orderAction": "SELL_OPEN",
"callPut": "CALL",
"strikePrice": strikePrice2,
"expiryDate": expiryDate,
"quantity": quantity
}]

resp = orders.place_option_order(
resp_format="json",
accountIdKey = accountIDKey,
legs=legs,
clientOrderId=clientOrderId,
priceType= priceType,
limitPrice=limitPrice,
allOrNone=False,
orderTerm=orderTerm,
marketSession=marketSession
)
174 changes: 106 additions & 68 deletions pyetrade/order.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from datetime import datetime
from typing import Union
from copy import deepcopy

import dateutil.parser
import xmltodict
Expand Down Expand Up @@ -309,28 +310,49 @@ def find_option_orders(
if symbol == opt_sym:
results.append(o)
return results

@staticmethod
def check_order(**kwargs):
""":description: Check that required params for preview or place order are there and correct

(Used internally)
"""
# For multi-leg orders
if "legs" in kwargs:
mandatory = [
"accountIdKey",
"clientOrderId",
"priceType",
"orderTerm",
"marketSession",
"legs"
]

missing = [param for param in mandatory if param not in kwargs]
if missing:
raise OrderException("Missing required parameter for multi-leg order.", params=missing)

for i, leg in enumerate(kwargs["legs"]):
required_leg_keys = {"symbol", "orderAction", "callPut", "strikePrice", "expiryDate", "quantity"}
missing_leg = [k for k in required_leg_keys if k not in leg]
if missing_leg:
raise OrderException(f"Missing leg fields in leg {i}", params=missing_leg)
else:
# Single-leg orders
mandatory = [
"accountIdKey",
"symbol",
"orderAction",
"clientOrderId",
"priceType",
"quantity",
"orderTerm",
"marketSession",
]

mandatory = [
"accountIdKey",
"symbol",
"orderAction",
"clientOrderId",
"priceType",
"quantity",
"orderTerm",
"marketSession",
]

missing = [param for param in mandatory if param not in kwargs]
if missing:
raise OrderException(params=missing)
missing = [param for param in mandatory if param not in kwargs]
if missing:
raise OrderException("Missing required parameter for single-leg order.", params=missing)

if kwargs["priceType"] == "STOP" and "stopPrice" not in kwargs:
raise OrderException(params=["stopPrice"])
Expand All @@ -342,69 +364,85 @@ def check_order(**kwargs):
and "stopPrice" not in kwargs
):
raise OrderException(params=["limitPrice", "stopPrice"])

@staticmethod
def build_order_payload(order_type: str, **kwargs) -> dict:
""":description: Builds the POST payload of a preview or place order
(Used internally)

:param order_type: PreviewOrderRequest or PlaceOrderRequest
:type order_type: str, required
:securityType: EQ or OPTN
:orderAction: for OPTN: BUY_OPEN, SELL_CLOSE
:callPut: CALL or PUT
:expiryDate: string, e.g. "2022-02-18"
:return: Builds Order Payload
:rtype: ``xml`` or ``json`` based on ``resp_format``
:EtradeRef: https://apisb.etrade.com/docs/api/order/api-order-v1.html
"""
Builds the POST payload for preview or place order (including support for multi-leg spreads).

:param order_type: "PreviewOrderRequest" or "PlaceOrderRequest"
:return: dict payload ready for submission
"""
securityType = kwargs.get("securityType", "EQ") # EQ by default
product = {"securityType": securityType, "symbol": kwargs["symbol"]}

if securityType == "OPTN":
expiryDate = dateutil.parser.parse(
kwargs.pop("expiryDate")
) # dateutil can handle most date formats
product.update(
{
"expiryDay": expiryDate.day,
"expiryMonth": expiryDate.month,
"expiryYear": expiryDate.year,
"callPut": kwargs["callPut"],
"strikePrice": kwargs["strikePrice"],
payload_order = deepcopy(kwargs)
instruments = []

# Check if multi-leg order
if "legs" in kwargs and isinstance(kwargs["legs"], list):
payload_order["orderType"] = "SPREADS"
for leg in kwargs["legs"]:
expiry = dateutil.parser.parse(leg["expiryDate"])
product = {
"securityType": "OPTN",
"symbol": leg["symbol"],
"expiryDay": expiry.day,
"expiryMonth": expiry.month,
"expiryYear": expiry.year,
"callPut": leg["callPut"],
"strikePrice": leg["strikePrice"]
}
)

instrument = {
"Product": product,
"orderAction": kwargs["orderAction"],
"quantityType": "QUANTITY",
"quantity": kwargs["quantity"],
}
instrument = {
"Product": product,
"orderAction": leg["orderAction"],
"quantityType": "QUANTITY",
"quantity": leg["quantity"]
}
instruments.append(instrument)

order = kwargs
order["Instrument"] = instrument
payload_order["Instrument"] = instruments
payload_order.pop("legs", None) # Remove the key here
else:
# Single leg fallback
payload_order["orderType"] = "OPTN"
expiry = dateutil.parser.parse(kwargs["expiryDate"])
product = {
"securityType": "OPTN",
"symbol": kwargs["symbol"],
"expiryDay": expiry.day,
"expiryMonth": expiry.month,
"expiryYear": expiry.year,
"callPut": kwargs["callPut"],
"strikePrice": kwargs["strikePrice"]
}
instrument = {
"Product": product,
"orderAction": kwargs["orderAction"],
"quantityType": "QUANTITY",
"quantity": kwargs["quantity"]
}
instruments.append(instrument)

def remove_invalid_price_from_kwargs(key: str) -> None:
if float(kwargs.get(key, 0)) <= 0:
kwargs.pop(key, 0)
payload_order["Instrument"] = instruments

remove_invalid_price_from_kwargs("stopPrice")
remove_invalid_price_from_kwargs("limitPrice")
# Clean up invalid pricing
for key in ["limitPrice", "stopPrice"]:
try:
if float(kwargs.get(key, 0)) <= 0:
payload_order.pop(key, None)
except:
payload_order.pop(key, None)

# Optional: Format stopPrice
if "stopPrice" in kwargs:
stopPrice = float(kwargs["stopPrice"])
round_down = "SELL" == kwargs["orderAction"][:4]
spstr = to_decimal_str(stopPrice, round_down)

order["stopPrice"] = spstr
stop = float(kwargs["stopPrice"])
round_down = kwargs.get("orderAction", "").startswith("SELL")
payload_order["stopPrice"] = to_decimal_str(stop, round_down)

# Build final payload
payload = {
order_type: {
"orderType": securityType,
"orderType": payload_order["orderType"],
"clientOrderId": kwargs["clientOrderId"],
"Order": order,
"Order": payload_order
}
}

Expand Down Expand Up @@ -583,13 +621,13 @@ def change_preview_equity_order(
payload = self.build_order_payload("PreviewOrderRequest", **kwargs)

return self.perform_request(self.session.put, api_url, payload, "xml")

def place_option_order(self, **kwargs) -> dict:
""":description: Places Option Order, only single leg CALL or PUT is supported for now
:return: Returns confirmation of the equity order
""":description: Places Option Order, now supports multi-leg strategies
:return: Returns confirmation of the option order
"""
kwargs["securityType"] = "OPTN"

return self.place_equity_order(**kwargs)

def place_equity_order(self, **kwargs) -> dict:
Expand Down