diff --git a/cschwabpy/models/trade_models.py b/cschwabpy/models/trade_models.py index 76cb2b2..dd878a5 100644 --- a/cschwabpy/models/trade_models.py +++ b/cschwabpy/models/trade_models.py @@ -1,6 +1,108 @@ from cschwabpy.models import JSONSerializableBaseModel +from typing import Optional, List, Any +from pydantic import Field +from enum import Enum + + +class AccountType(str, Enum): + MARGIN = "MARGIN" + CASH = "CASH" + IRA = "IRA" class AccountNumberModel(JSONSerializableBaseModel): accountNumber: str hashValue: str + + +class MarginBalance(JSONSerializableBaseModel): + availableFunds: Optional[float] = None + availableFundsNonMarginableTrade: Optional[float] = None + buyingPower: Optional[float] = None + buyingPowerNonMarginableTrade: Optional[float] = None + dayTradingBuyingPower: Optional[float] = None + dayTradingBuyingPowerCall: Optional[float] = None + dayTradingEquityCall: Optional[float] = None + equity: Optional[float] = None + equityPercentage: Optional[float] = None + longMarginValue: Optional[float] = None + longOptionMarketValue: Optional[float] = None + longStockValue: Optional[float] = None + maintenanceCall: Optional[float] = None + maintenanceRequirement: Optional[float] = None + margin: Optional[float] = None + marginEquity: Optional[float] = None + moneyMarketFund: Optional[float] = None + mutualFundValue: Optional[float] = None + sma: Optional[float] = None + stockBuyingPower: Optional[float] = None + optionBuyingPower: Optional[float] = None + regTCall: Optional[float] = None + shortMarginValue: Optional[float] = None + shortOptionMarketValue: Optional[float] = None + shortStockValue: Optional[float] = None + totalCash: Optional[float] = None + isInCall: Optional[bool] = None + unsettledCash: Optional[float] = None + pendingDeposits: Optional[float] = None + marginBalance: Optional[float] = None + shortBalance: Optional[float] = None + accountValue: Optional[float] = None + + +class MarginInitialBalance(MarginBalance): + accruedInterest: Optional[float] = None + availableFundsNonMarginableTrade: Optional[float] = None + bondValue: Optional[float] = None + cashBalance: Optional[float] = None + cashAvailableForTrading: Optional[float] = None + cashReceipts: Optional[float] = None + liquidationValue: Optional[float] = None + + +class Position(JSONSerializableBaseModel): + shortQuantity: Optional[float] = None + averagePrice: Optional[float] = None + currentDayProfitLoss: Optional[float] = None + currentDayProfitLossPercentage: Optional[float] = None + longQuantity: Optional[float] = None + settledLongQuantity: Optional[float] = None + settledShortQuantity: Optional[float] = None + agedQuantity: Optional[float] = None + instrument: Optional[Any] = None # TODO: AccountInstrument type + marketValue: Optional[float] = None + maintenanceRequirement: Optional[float] = None + averageLongPrice: Optional[float] = None + averageShortPrice: Optional[float] = None + taxLotAverageLongPrice: Optional[float] = None + taxLotAverageShortPrice: Optional[float] = None + longOpenProfitLoss: Optional[float] = None + shortOpenProfitLoss: Optional[float] = None + previousSessionLongQuantity: Optional[float] = None + previousSessionShortQuantity: Optional[float] = None + currentDayCost: Optional[float] = None + + +class Account(JSONSerializableBaseModel): + type_: Optional[AccountType] = Field(None, alias="type") + accountNumber: str + roundTrips: Optional[int] = 0 + isDayTrader: Optional[bool] = False + isClosingOnlyRestricted: Optional[bool] = False + pfcbFlag: Optional[bool] = False + positions: List[Position] = [] + initialBalances: Optional[MarginInitialBalance] = None + currentBalances: Optional[MarginBalance] = None + projectedBalances: Optional[MarginBalance] = None + + +class MarginAccount(Account): + type_: AccountType = Field(AccountType.MARGIN, alias="type") + + +class CashAccount(Account): + type_: AccountType = Field(AccountType.CASH, alias="type") + + +class SecuritiesAccount(JSONSerializableBaseModel): + securitiesAccount: Account diff --git a/tests/data/mock_schwab_api_resp.json b/tests/data/mock_schwab_api_resp.json index 5b9198b..6a7d1bf 100644 --- a/tests/data/mock_schwab_api_resp.json +++ b/tests/data/mock_schwab_api_resp.json @@ -1,4 +1,123 @@ { + "securities_account": [ + { + "securitiesAccount": { + "accountNumber": "123", + "roundTrips": 0, + "isDayTrader": false, + "isClosingOnlyRestricted": false, + "pfcbFlag": false, + "positions": [ + { + "shortQuantity": 0, + "averagePrice": 0, + "currentDayProfitLoss": 0, + "currentDayProfitLossPercentage": 0, + "longQuantity": 0, + "settledLongQuantity": 0, + "settledShortQuantity": 0, + "agedQuantity": 0, + "instrument": { + "cusip": "123", + "symbol": "spy", + "description": "sfsdfsadf", + "instrumentId": 0, + "netChange": 0, + "type": "SWEEP_VEHICLE" + }, + "marketValue": 0, + "maintenanceRequirement": 0, + "averageLongPrice": 0, + "averageShortPrice": 0, + "taxLotAverageLongPrice": 0, + "taxLotAverageShortPrice": 0, + "longOpenProfitLoss": 0, + "shortOpenProfitLoss": 0, + "previousSessionLongQuantity": 0, + "previousSessionShortQuantity": 0, + "currentDayCost": 0 + } + ], + "initialBalances": { + "accruedInterest": 0, + "availableFundsNonMarginableTrade": 0, + "bondValue": 0, + "buyingPower": 0, + "cashBalance": 0, + "cashAvailableForTrading": 0, + "cashReceipts": 0, + "dayTradingBuyingPower": 0, + "dayTradingBuyingPowerCall": 0, + "dayTradingEquityCall": 0, + "equity": 0, + "equityPercentage": 0, + "liquidationValue": 0, + "longMarginValue": 0, + "longOptionMarketValue": 0, + "longStockValue": 0, + "maintenanceCall": 0, + "maintenanceRequirement": 0, + "margin": 0, + "marginEquity": 0, + "moneyMarketFund": 0, + "mutualFundValue": 0, + "regTCall": 0, + "shortMarginValue": 0, + "shortOptionMarketValue": 0, + "shortStockValue": 0, + "totalCash": 0, + "isInCall": 0, + "unsettledCash": 0, + "pendingDeposits": 0, + "marginBalance": 0, + "shortBalance": 0, + "accountValue": 0 + }, + "currentBalances": { + "availableFunds": 0, + "availableFundsNonMarginableTrade": 0, + "buyingPower": 0, + "buyingPowerNonMarginableTrade": 0, + "dayTradingBuyingPower": 0, + "dayTradingBuyingPowerCall": 0, + "equity": 0, + "equityPercentage": 0, + "longMarginValue": 0, + "maintenanceCall": 0, + "maintenanceRequirement": 0, + "marginBalance": 0, + "regTCall": 0, + "shortBalance": 0, + "shortMarginValue": 0, + "sma": 0, + "isInCall": 0, + "stockBuyingPower": 0, + "optionBuyingPower": 0 + }, + "projectedBalances": { + "availableFunds": 0, + "availableFundsNonMarginableTrade": 0, + "buyingPower": 0, + "buyingPowerNonMarginableTrade": 0, + "dayTradingBuyingPower": 0, + "dayTradingBuyingPowerCall": 0, + "equity": 0, + "equityPercentage": 0, + "longMarginValue": 0, + "maintenanceCall": 0, + "maintenanceRequirement": 0, + "marginBalance": 0, + "regTCall": 0, + "shortBalance": 0, + "shortMarginValue": 0, + "sma": 0, + "isInCall": 0, + "stockBuyingPower": 0, + "optionBuyingPower": 0 + } + } + } + ], "account_numbers": [ { "accountNumber": "123456789", diff --git a/tests/test_models.py b/tests/test_models.py index ed74644..6533e12 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -11,6 +11,7 @@ OptionContractType, OptionContractStrategy, ) +from cschwabpy.models.trade_models import SecuritiesAccount, MarginAccount, CashAccount from cschwabpy.models.token import Tokens, LocalTokenStore from cschwabpy.SchwabAsyncClient import SchwabAsyncClient @@ -50,6 +51,24 @@ def test_option_chain_parsing() -> None: print(df.put_df.head(5)) +def test_parsing_securities_account(): + json_mock = get_mock_response()["securities_account"] + accounts: typing.List[SecuritiesAccount] = [] + for sec_account in json_mock: + securities_account = SecuritiesAccount(**sec_account).securitiesAccount + accounts.append(securities_account) + assert securities_account is not None + assert securities_account.accountNumber == "123" + # assert securities_account.accountType == "MARGIN" + assert securities_account.isDayTrader == False + assert securities_account.roundTrips == 0 + assert securities_account.positions is not None + assert len(securities_account.positions) == 1 + assert securities_account.initialBalances is not None + + assert len(accounts) == 1 + + @pytest.mark.asyncio async def test_download_option_chain(httpx_mock: HTTPXMock): mock_option_chain_resp = get_mock_response()