diff --git a/Schwab Trader API Authentication and Order Examples Guide.pdf b/Schwab Trader API Authentication and Order Examples Guide.pdf new file mode 100644 index 0000000..7f05f62 Binary files /dev/null and b/Schwab Trader API Authentication and Order Examples Guide.pdf differ diff --git a/cschwabpy/SchwabAsyncClient.py b/cschwabpy/SchwabAsyncClient.py index 6f476b2..7c2ab0c 100644 --- a/cschwabpy/SchwabAsyncClient.py +++ b/cschwabpy/SchwabAsyncClient.py @@ -117,7 +117,6 @@ def __auth_header(self) -> Mapping[str, str]: async def get_account_numbers_async(self) -> List[AccountNumberWithHashID]: await self._ensure_valid_access_token() - import json target_url = f"{SCHWAB_TRADER_API_BASE_URL}/accounts/accountNumbers" client = httpx.AsyncClient() if self.__client is None else self.__client @@ -218,15 +217,13 @@ async def place_order_async( await self._ensure_valid_access_token() target_url = f"{SCHWAB_TRADER_API_BASE_URL}/accounts/{account_number_hash.hashValue}/orders" client = httpx.AsyncClient() if self.__client is None else self.__client - try: _header = self.__auth_header() _header["Content-Type"] = "application/json" - print("order to place: ", json.dumps(order.to_json())) response = await client.post( url=target_url, - json=json.dumps(order.to_json()), - headers=self.__auth_header(), + data=json.dumps(order.to_json()), + headers=_header, ) if response.status_code == 201: return True diff --git a/cschwabpy/SchwabClient.py b/cschwabpy/SchwabClient.py index 42b5ef8..d6f9e2d 100644 --- a/cschwabpy/SchwabClient.py +++ b/cschwabpy/SchwabClient.py @@ -119,7 +119,6 @@ def __auth_header(self) -> Mapping[str, str]: def get_account_numbers(self) -> List[AccountNumberWithHashID]: self._ensure_valid_access_token() - import json target_url = f"{SCHWAB_TRADER_API_BASE_URL}/accounts/accountNumbers" client = httpx.Client() if self.__client is None else self.__client @@ -224,11 +223,10 @@ def place_order( try: _header = self.__auth_header() _header["Content-Type"] = "application/json" - print("order to place: ", json.dumps(order.to_json())) response = client.post( url=target_url, json=json.dumps(order.to_json()), - headers=self.__auth_header(), + headers=_header, ) if response.status_code == 201: return True diff --git a/cschwabpy/models/__init__.py b/cschwabpy/models/__init__.py index 432b66d..cd31263 100644 --- a/cschwabpy/models/__init__.py +++ b/cschwabpy/models/__init__.py @@ -29,8 +29,45 @@ class JSONSerializableBaseModel(BaseModel): model_config = ConfigDict(use_enum_values=True, populate_by_name=True) - def to_json(self) -> Mapping[str, Any]: - return self.model_dump(by_alias=True) + def to_json(self, drop_null_value: bool = True) -> Mapping[str, Any]: + """Converts the object to a JSON dictionary with options to drop null values from dictionary.""" + super_json = self.model_dump(by_alias=True) + if not drop_null_value: + return super_json + + result: MutableMapping[str, Any] = {} + for k, v in list(super_json.items()): + if v is not None: + result[k] = self.__handle_item(v) + + return result + + def __handle_item(self, item: Any) -> Any: + """Handle item in the dictionary, list or primitive values.""" + if isinstance(item, dict): + return self.__del_none(item) + elif isinstance(item, List): + result = [] + for itm in list(item): + result.append(self.__handle_item(itm)) + return result + elif isinstance(item, set): + result = set() + for itm in set(item): + result.add(self.__handle_item(itm)) + return result + else: + return item + + def __del_none(self, d: MutableMapping[str, Any]) -> MutableMapping[str, Any]: + """Recursively remove None values from a dictionary.""" + for key, value in list(d.items()): + if value is None: + del d[key] + elif isinstance(value, MutableMapping): + d[key] = self.__del_none(value) + + return d class ErrorMessage(JSONSerializableBaseModel): diff --git a/cschwabpy/models/trade_models.py b/cschwabpy/models/trade_models.py index 3aedaaf..948f629 100644 --- a/cschwabpy/models/trade_models.py +++ b/cschwabpy/models/trade_models.py @@ -1,5 +1,5 @@ from cschwabpy.models import JSONSerializableBaseModel, OptionContractType -from typing import Optional, List, Any +from typing import Optional, List, Any, Mapping, MutableMapping from pydantic import Field from enum import Enum @@ -289,11 +289,11 @@ class MarginInitialBalance(MarginBalance): class AccountInstrument(JSONSerializableBaseModel): assetType: Optional[AssetType] = None - cusip: Optional[str] = "" - description: Optional[str] = "" - instrumentId: Optional[int] = 0 + cusip: Optional[str] = None + description: Optional[str] = None + instrumentId: Optional[int] = None symbol: Optional[str] = None - netChange: Optional[float] = 0 + netChange: Optional[float] = None class AccountEquity(AccountInstrument): @@ -331,12 +331,12 @@ class OrderLeg(JSONSerializableBaseModel): class OrderLegCollection(JSONSerializableBaseModel): orderLegType: Optional[AssetType] = None - legId: Optional[int] = 0 + legId: Optional[int] = None instrument: Optional[AccountInstrument] = None # e.g. AccountOption instruction: Optional[OrderLegInstruction] = None positionEffect: Optional[PositionEffect] = None quantity: Optional[float] = None - quantityType: Optional[QuantityType] = QuantityType.SHARES + quantityType: Optional[QuantityType] = None # QuantityType.SHARES # toSymbol: Optional[str] = None #TODO @@ -366,7 +366,7 @@ class Position(JSONSerializableBaseModel): class Account(JSONSerializableBaseModel): type_: Optional[AccountType] = Field(None, alias="type") accountNumber: str - roundTrips: Optional[int] = 0 + roundTrips: Optional[int] = None isDayTrader: Optional[bool] = False isClosingOnlyRestricted: Optional[bool] = False pfcbFlag: Optional[bool] = False @@ -415,38 +415,38 @@ class OrderActivity(JSONSerializableBaseModel): class Order(JSONSerializableBaseModel): - session: Optional[Session] = None - duration: Optional[Duration] = None - orderType: Optional[OrderType] = OrderType.LIMIT + session: Session + duration: Duration = Duration.DAY + orderType: OrderType = OrderType.LIMIT cancelTime: Optional[str] = None complexOrderStrategyType: Optional[ComplexOrderStrategyType] = None - quantity: Optional[float] = 0 - filledQuantity: Optional[float] = 0 - remainingQuantity: Optional[float] = 0 - requestedDestination: Optional[Destination] = Destination.AUTO + quantity: Optional[float] = None + filledQuantity: Optional[float] = None + remainingQuantity: Optional[float] = None + requestedDestination: Optional[Destination] = None destinationLinkName: Optional[str] = "AUTO" releaseTime: Optional[str] = None stopPrice: Optional[float] = None - stopPriceLinkBasis: Optional[PriceLinkBasis] = PriceLinkBasis.AVERAGE - stopPriceLinkType: Optional[PriceLinkType] = PriceLinkType.VALUE - stopPriceOffset: Optional[float] = 0 - stopType: Optional[StopType] = StopType.MARK - priceLinkBasis: Optional[PriceLinkBasis] = PriceLinkBasis.AVERAGE - priceLinkType: Optional[PriceLinkType] = PriceLinkType.VALUE - price: Optional[float] = None - taxLotMethod: Optional[TaxLotMethod] = TaxLotMethod.FIFO + stopPriceLinkBasis: Optional[PriceLinkBasis] = None + stopPriceLinkType: Optional[PriceLinkType] = None + stopPriceOffset: Optional[float] = None + stopType: Optional[StopType] = None + priceLinkBasis: Optional[PriceLinkBasis] = None + priceLinkType: Optional[PriceLinkType] = None + price: float + taxLotMethod: Optional[TaxLotMethod] = None orderLegCollection: List[OrderLegCollection] = [] - activationPrice: Optional[float] = 0 - specialInstruction: Optional[SpecialInstruction] = SpecialInstruction.ALL_OR_NONE + activationPrice: Optional[float] = None + specialInstruction: Optional[SpecialInstruction] = None orderStrategyType: Optional[OrderStrategyType] = OrderStrategyType.SINGLE - orderId: Optional[int] = 0 - cancelable: Optional[bool] = False - editable: Optional[bool] = False + orderId: Optional[int] = None + cancelable: Optional[bool] = None + editable: Optional[bool] = None status: Optional[OrderStatus] = None enteredTime: Optional[str] = None closeTime: Optional[str] = None - tag: Optional[str] = "" - accountNumber: Optional[int] = 0 # or str ? + tag: Optional[str] = None + accountNumber: Optional[int] = None orderActivityCollection: List[OrderActivity] = [] replacingOrderCollection: List[str] = [] childOrderStrategies: List[str] = [] diff --git a/pyproject.toml b/pyproject.toml index f66e39a..27ab06b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cschwabpy" -version = "0.1.0" +version = "0.1.1" description = "" authors = ["Tony Wang "] readme = "README.md" diff --git a/setup.py b/setup.py index 0ea84d1..33a4d00 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="CSchwabPy", - version="0.1.0", + version="0.1.1", description="Charles Schwab Stock & Option Trade API Client for Python.", long_description=long_description, long_description_content_type="text/markdown",