Metadata-Version: 2.4
Name: schwab_sdk_unofficial
Version: 0.3.2
Summary: Cliente ligero para API de Schwab: OAuth, REST y Streaming WebSocket
Author: Schwab SDK Contributors
License-Expression: MIT
Project-URL: Homepage, https://pypi.org/project/schwab-sdk/
Project-URL: Repository, https://github.com/your-org/schwab-sdk
Keywords: schwab,trading,market-data,websocket,sdk,api
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27.0
Requires-Dist: websocket-client>=1.8.0
Requires-Dist: websockets>=13.0
Requires-Dist: fastapi>=0.115.0
Requires-Dist: uvicorn>=0.30.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: cryptography>=41.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: freezegun>=1.4; extra == "dev"
Dynamic: license-file

# Schwab SDK (Python)

Lightweight client for the Schwab API: OAuth, REST (Trader/Market Data), and WebSocket Streaming.

* Focus: thin wrappers, no heavy validation; robust token handling, refresh, and retries.
* Coverage: Accounts, Orders, Market Data, and Streaming (Level One, Book, Chart, Screener, Account Activity).

## Installation

Unofficial PyPI package (distribution name):

```bash
pip install schwab_sdk_unofficial
```

Import in code (module):

```python
from schwab_sdk import Client
```

> **Note:** The package name on PyPI is `schwab_sdk_unofficial`, but the import remains `schwab_sdk` for a clean API.

## Table of Contents

* [Requirements](#requirements)
* [Configuration](#configuration)
* [Quick Start](#quick-start)
* [Authentication (OAuth)](#authentication-oauth)
  * [Token storage options](#token-storage-options)
* [Request & Error Handling (REST)](#request--error-handling-rest)
  * [Exception hierarchy](#exception-hierarchy)
  * [Error handling example](#error-handling-example)
* [Accounts (`accounts.py`)](#accounts-accountspy)
* [Orders (`orders.py`)](#orders-orderspy)
* [Market Data (`market.py`)](#market-data-marketpy)
* [WebSocket Streaming (`streaming.py`)](#websocket-streaming-streamingpy)
  * [Key Formats (quick table)](#key-formats-quick-table)
  * [Service-by-Service Examples](#service-by-service-examples)
  * [Utilities](#utilities)
  * [Recommended Fields](#recommended-fields)
  * [Quick Field Guide (IDs → meaning)](#quick-field-guide-ids--meaning)
  * [Frame Structure](#frame-structure)
* [Enums Reference (`enums.py`)](#enums-reference-enumspy)
  * [Market Data — Request Parameter Enums](#market-data--request-parameter-enums)
  * [Market Data — Response Enums](#market-data--response-enums)
  * [Orders — Enums](#orders--enums)
  * [Accounts — Enums](#accounts--enums)
  * [Streaming — Enums](#streaming--enums)
* [Advanced Troubleshooting](#advanced-troubleshooting)
* [Contributions](#contributions)
* [Disclaimer](#disclaimer)
* [License](#license)

## Requirements

* Python 3.9+
* Dependencies (installed automatically with `pip install schwab_sdk_unofficial`):

| Package | Purpose |
|---|---|
| `httpx` | HTTP client (REST + OAuth) |
| `fastapi` | OAuth callback server |
| `uvicorn` | ASGI server for callback |
| `cryptography` | Adhoc TLS certs & token encryption |
| `python-multipart` | Form POST parsing in callback |
| `websocket-client` | Streaming WebSocket |
| `websockets` | Async streaming |

## Configuration

Create `.env` (or export environment variables):

```env
SCHWAB_CLIENT_ID=your_client_id
SCHWAB_CLIENT_SECRET=your_client_secret
SCHWAB_REDIRECT_URI=https://127.0.0.1:8080/callback
```

Tokens are saved in `schwab_tokens.json` by default and rotate automatically (access ~29 min; re-login notice when refresh expires ~7 d). Set `persist=False` to keep tokens only in memory (useful for DB-backed storage).

## Quick Start

```python
from schwab_sdk import Client
import os

client = Client(
    os.environ['SCHWAB_CLIENT_ID'],
    os.environ['SCHWAB_CLIENT_SECRET'],
    os.environ.get('SCHWAB_REDIRECT_URI', 'https://127.0.0.1:8080/callback'),
    token_path='my_tokens.json',  # Optional: custom token file path
    persist=True,                 # Optional: False to keep tokens only in memory
)

# First use: OAuth login (opens the browser)
client.login()

# REST
quotes = client.market.get_quotes(["AAPL", "MSFT"])  # Market Data
accounts = client.account.get_accounts()               # Accounts

# Streaming (Level One equities)
ws = client.streaming
ws.on_data(lambda f: print("DATA", f))
ws.connect(); ws.login()
ws.equities_subscribe(["AAPL"])
```

## Authentication (OAuth)

* `client.login(timeout=300, auto_open_browser=True, callback_port=None)`
* Handy: `client.has_valid_token()`, `client.refresh_token_now()`, `client.logout()`
* Manual flow: `client.get_auth_url()` + `client.exchange_code(code)` (sync) or `await client.exchange_code(code)` (AsyncClient)
* Internals: adhoc HTTPS callback server (FastAPI + uvicorn), code-for-token exchange, auto-refresh and notice when refresh expires.

### Token storage options

By default tokens are persisted to a JSON file (`schwab_tokens.json`). You can customize this:

```python
# Custom file path
client = Client(client_id, client_secret, redirect_uri, token_path="tokens/my_tokens.json")

# Encrypted at rest (AES-256-GCM)
client = Client(client_id, client_secret, redirect_uri, encryption_key="my-secret")

# In-memory only (no file written) — useful for DB-backed storage
client = Client(client_id, client_secret, redirect_uri, persist=False)

# After login, read tokens to store in your DB:
handler = client.token_handler
db.save(
    access_token=handler.access_token,
    refresh_token=handler.refresh_token,
    access_expires=handler.access_token_expires_at,
    refresh_expires=handler.refresh_token_expires_at,
)
```

### Client constructor parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client_id` | `str` | *required* | Schwab application Client ID |
| `client_secret` | `str` | *required* | Schwab application Client Secret |
| `redirect_uri` | `str` | `https://localhost:8080/callback` | OAuth redirect URI |
| `token_path` | `str \| None` | `"schwab_tokens.json"` | Path to token file |
| `encryption_key` | `str \| None` | `None` | Passphrase for AES-256-GCM token encryption |
| `persist` | `bool` | `True` | `False` to keep tokens only in memory |

## Request & Error Handling (REST)

All REST calls go through `Client._request()` which provides automatic error recovery:

* **Authorization**: Bearer token header injected automatically.
* **401 Unauthorized**: refreshes the access token once and retries immediately.
* **429 / 5xx**: retries up to 3 times with exponential backoff (`0.5 * 2^attempt` seconds).
* **Network errors** (`httpx.RequestError`): same retry logic as 429/5xx.

### Exception hierarchy

HTTP errors raise a subclass of `SchwabAPIError`. Each exception carries:

| Attribute | Type | Description |
|---|---|---|
| `status_code` | `int` | HTTP status code |
| `message` | `str` | Human-readable error from response body |
| `correl_id` | `str \| None` | `Schwab-Client-CorrelId` header (for Schwab support) |
| `errors` | `list[dict]` | Raw error dicts from the response body |
| `response` | `httpx.Response \| None` | Raw response for advanced inspection |

| Exception | HTTP Status | When |
|---|---|---|
| `SchwabValidationError` | 400 | Invalid parameters or malformed request body |
| `SchwabAuthenticationError` | 401 | Access token missing, invalid, or expired |
| `SchwabForbiddenError` | 403 | Valid credentials but insufficient permissions |
| `SchwabNotFoundError` | 404 | Requested resource does not exist |
| `SchwabRateLimitError` | 429 | API rate limit exceeded (after retries) |
| `SchwabServerError` | 500/502/503/504 | Schwab-side failure (after retries) |
| `SchwabTokenExpiredError` | (subclass of 401) | Refresh token expired; full re-login required |
| `SchwabParameterError` | (client-side) | `ValueError` subclass for parameter validation |

### Error handling example

```python
from schwab_sdk import (
    Client,
    SchwabAPIError,
    SchwabNotFoundError,
    SchwabRateLimitError,
    SchwabAuthenticationError,
)

try:
    account = client.account.get_account_by_id("invalid-hash")
except SchwabNotFoundError as e:
    print(f"Not found: {e.message}")
    print(f"Correlation ID: {e.correl_id}")  # for Schwab support
except SchwabRateLimitError as e:
    print(f"Rate limited after retries: {e.message}")
except SchwabAuthenticationError as e:
    print("Token expired, re-login needed")
    client.login()
except SchwabAPIError as e:
    print(f"API error {e.status_code}: {e.message}")
```

### Schwab error response format

```json
{
  "errors": [
    {
      "id": "uuid",
      "status": "400",
      "title": "Bad Request",
      "detail": "Missing header",
      "source": {"header": "Authorization"}
    }
  ]
}
```

The SDK extracts `detail`, `title`, or `message` from each error and joins them into `e.message`. The `Schwab-Client-CorrelId` response header is captured in `e.correl_id`.

### Factory function

`raise_for_schwab_status(response)` inspects an `httpx.Response` and raises the appropriate exception for non-2xx status codes. Used internally by each endpoint module (accounts, orders, market) after calling `_request()`, and also available for custom flows:

```python
from schwab_sdk import raise_for_schwab_status
raise_for_schwab_status(response)  # raises SchwabValidationError, SchwabNotFoundError, etc.
```

---

## Accounts (`accounts.py`)

**Possible exceptions**: `SchwabAuthenticationError` (401), `SchwabForbiddenError` (403), `SchwabNotFoundError` (404), `SchwabServerError` (5xx)

**Related enums**: `AccountField`, `TransactionType`, `AccountInstrumentType`, `TransactionStatus`

### get_account_numbers() -> list[dict]

* GET `/accounts/accountNumbers`
* Returns `accountNumber` and `hashValue` pairs.
* Example response:

```json
[
  {"accountNumber":"12345678","hashValue":"827C...AC12"}
]
```

### get_accounts(fields=None, params=None) -> dict

* GET `/accounts`
* Parameters:

  * `fields` (optional): `List[str]` — use `["positions"]` or `[AccountField.POSITIONS]` to include positions.
  * `params` (optional): additional query parameters.

### get_account_by_id(account_hash, fields=None, params=None) -> dict

* GET `/accounts/{accountNumber}`
* `account_hash`: encrypted account identifier (`hashValue`).
* Raises `SchwabNotFoundError` if `account_hash` is invalid.
* Parameters:

  * `fields` (optional): `List[str]` — `["positions"]` or `[AccountField.POSITIONS]` to include positions.
  * `params` (optional): additional query parameters.

### find_account(last_4_digits: str) -> dict|None

* Helper that uses `get_account_numbers()` and filters by the last 4 digits, then calls `get_account_by_id`.

### get_transactions(account_hash, from_date=None, to_date=None, transaction_types=None, symbol=None, filters=None) -> dict

* GET `/accounts/{accountHash}/transactions`
* **At least one date recommended**: you may pass only `from_date` or only `to_date`. If you pass a single date, the SDK fills in the other for the same day. If both are omitted, the API may return an error or empty results:

  * Short format `YYYY-MM-DD`: start → `YYYY-MM-DDT00:00:00.000Z`, end → `YYYY-MM-DDT23:59:59.000Z`
  * Full ISO UTC `YYYY-MM-DDTHH:MM:SS.ffffffZ`: used as-is; if the other date is missing, it is derived with `00:00:00.000Z` or `23:59:59.000Z` of the same day.
* Parameters:

  * `account_hash` (required): encrypted account identifier (`hashValue`)
  * `from_date` (optional): start date — `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS.ffffffZ`
  * `to_date` (optional): end date — `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS.ffffffZ`
  * `transaction_types` (optional): `List[str]` of `TransactionType` values: `TRADE`, `RECEIVE_AND_DELIVER`, `DIVIDEND_OR_INTEREST`, `ACH_RECEIPT`, `ACH_DISBURSEMENT`, `CASH_RECEIPT`, `CASH_DISBURSEMENT`, `ELECTRONIC_FUND`, `WIRE_OUT`, `WIRE_IN`, `JOURNAL`, `MEMORANDUM`, `MARGIN_CALL`, `MONEY_MARKET`, `SMA_ADJUSTMENT`
  * `symbol` (optional): filter by specific symbol
  * `filters` (optional): additional query parameters dict

**Correct example**:

```python
from datetime import datetime, timezone, timedelta

# Get hashValue
hash_value = client.account.get_account_numbers()[0]['hashValue']

# Create UTC dates
start = datetime.now(timezone.utc) - timedelta(days=7)
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
end = datetime.now(timezone.utc).replace(hour=23, minute=59, second=59, microsecond=999999)

# Proper call
transactions = client.account.get_transactions(
    account_hash=hash_value,  # Use hashValue!
    from_date=start.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
    to_date=end.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
    transaction_types=["TRADE", "DIVIDEND_OR_INTEREST"],  # Optional
)
```

### get_transaction(account_hash: str, transaction_id: str) -> dict

* GET `/accounts/{accountHash}/transactions/{transactionId}`
* Path parameters:

  * `account_hash` (required)
  * `transaction_id` (required): numeric transaction ID
* Returns details of a specific transaction.

### get_user_preferences() -> dict

* GET `/userPreference`
* Returns user preferences and, when applicable, streamer information needed for WebSocket:

  * `streamerSocketUrl`
  * `schwabClientCustomerId`
  * `schwabClientCorrelId`
  * `SchwabClientChannel`
  * `SchwabClientFunctionId`
* Useful to initialize `client.streaming` (LOGIN and subscriptions).

---

## Orders (`orders.py`)

**Possible exceptions**: `SchwabValidationError` (400), `SchwabAuthenticationError` (401), `SchwabForbiddenError` (403), `SchwabNotFoundError` (404), `SchwabServerError` (5xx)

**Related enums**: `OrderType`, `Duration`, `Session`, `OrderStatus`, `EquityInstruction`, `OptionInstruction`, `AssetType`, `OrderStrategyType`, `ComplexOrderStrategyType`, `SpecialInstruction`, `RequestedDestination`, `PriceLinkBasis`, `PriceLinkType`, `StopType`, `TaxLotMethod`, `PositionEffect`, `QuantityType`

All responses include HTTP metadata and the native data:

```json
{
  "status_code": 200,
  "success": true,
  "headers": {"...": "..."},
  "url": "https://...",
  "elapsed_seconds": 0.42,
  "method": "GET|POST|PUT|DELETE",
  "params": {"...": "..."},
  "data": {},
  "order_id": "..."
}
```

### get_orders(account_hash, from_entered_time=None, to_entered_time=None, status=None, max_results=None) -> dict

* GET `/accounts/{accountNumber}/orders`
* API requirements: `fromEnteredTime` and `toEnteredTime` are mandatory (ISO-8601). The SDK defaults them to "last 60 days" if you omit them.
* `status` (case-insensitive): `OrderStatus` enum — normalized to uppercase. See [OrderStatus](#orderstatus) for all values.
* `maxResults` (optional): record limit (API default 3000).
* Datetime format: ISO-8601 `YYYY-MM-DDTHH:MM:SS.000Z`.

### get_all_orders(from_entered_time=None, to_entered_time=None, status=None, max_results=None) -> dict

* GET `/orders`
* API requirements: `fromEnteredTime` and `toEnteredTime` are mandatory (ISO-8601). If you omit them, the SDK uses "last 60 days".
* Filters identical to `get_orders` (including `status` normalization).
* `maxResults` (optional): record limit (API default 3000).

### place_order(account_hash: str, order_data: dict) -> dict

* POST `/accounts/{accountNumber}/orders`
* Extracts `order_id` from the `Location` header when present.

### get_order(account_hash: str, order_id: str) -> dict

* GET `/accounts/{accountNumber}/orders/{orderId}`

### cancel_order(account_hash: str, order_id: str) -> dict

* DELETE `/accounts/{accountNumber}/orders/{orderId}`
* The `order_id` you pass is included in the result dict as `order_id`.

### replace_order(account_hash: str, order_id: str, new_order_data: dict) -> dict

* PUT `/accounts/{accountNumber}/orders/{orderId}`
* Returns `original_order_id` (the id you passed) and new `order_id` (from `Location` header) when the server provides it.

### preview_order(account_hash: str, order_data: dict) -> dict

* POST `/accounts/{accountNumber}/previewOrder`
* Returns preview data; no order is placed.

### Payload helpers

* `build_limit_order(symbol, quantity, price, instruction="BUY")`
* `build_market_order(symbol, quantity, instruction="BUY")`
* `build_bracket_order(symbol, quantity, entry_price, take_profit_price, stop_loss_price)`

Example (preview):

```python
acc = client.account.get_account_numbers()[0]['hashValue']
order = client.orders.build_limit_order("AAPL", 1, 100.00)
preview = client.orders.preview_order(acc, order)
```

---

## Market Data (`market.py`)

**Possible exceptions**: `SchwabValidationError` (400), `SchwabAuthenticationError` (401), `SchwabNotFoundError` (404), `SchwabServerError` (5xx)

**Related enums**: `ContractType`, `Strategy`, `StrikeRange`, `ExpirationMonth`, `OptionType`, `Entitlement`, `PeriodType`, `FrequencyType`, `QuoteField`, `MoverIndex`, `MoverSort`, `MoverFrequency`, `MarketID`, `Projection`

**Response enums**: `AssetMainType`, `AssetSubType`, `SecurityStatus`, `Exchange`, `MICId`, `ExchangeName`, `QuoteType`

### get_quotes(symbols, fields=None, indicative=None, params=None) -> dict

* GET `/quotes?symbols=...`
* Parameters:

  * `symbols` (required): `str` or `List[str]`. E.g.: `"AAPL"` or `["AAPL", "AMZN", "$DJI"]`.
  * `fields` (optional): `str` or `List[str]` — `QuoteField` values: `quote`, `fundamental`, `extended`, `reference`, `regular`, `all`. Default: all.
  * `indicative` (optional): `bool` — include indicative symbol quotes for ETFs.
  * `params` (optional): additional query parameters.

### get_quote(symbol, fields=None, indicative=None, params=None) -> dict

* GET `/{symbol}/quotes`
* Raises `SchwabNotFoundError` if the symbol does not exist.
* Parameters:

  * `symbol` (required): single symbol (e.g., `"TSLA"`).
  * `fields` (optional): `str` or `List[str]` — same `QuoteField` values as `get_quotes`.
  * `indicative` (optional): `bool` — include indicative symbol quotes for ETFs.
  * `params` (optional): additional query parameters.

### get_option_chain(symbol: str, contract_type: str|None=None, strike_count: int|None=None, include_underlying_quote: bool|None=None, strategy: str|None=None, interval: float|None=None, strike: float|None=None, range_type: str|None=None, from_date: str|None=None, to_date: str|None=None, volatility: float|None=None, underlying_price: float|None=None, interest_rate: float|None=None, days_to_expiration: int|None=None, exp_month: str|None=None, option_type: str|None=None, entitlement: str|None=None, params: dict|None=None) -> dict

* GET `/chains`

* Parameters:

  * `symbol` (required): Underlying asset symbol
  * `contract_type` (optional): `ContractType` — `CALL`, `PUT`, `ALL`
  * `strike_count` (optional): Number of strikes above/below ATM
  * `include_underlying_quote` (optional): Include underlying quotes (boolean)
  * `strategy` (optional): `Strategy` — `SINGLE` (default), `ANALYTICAL`, `COVERED`, `VERTICAL`, `CALENDAR`, `STRANGLE`, `STRADDLE`, `BUTTERFLY`, `CONDOR`, `DIAGONAL`, `COLLAR`, `ROLL`
  * `interval` (optional): Strike interval for spread strategy chains
  * `strike` (optional): Strike Price
  * `range_type` (optional): `StrikeRange` — `ITM`, `NTM`, `OTM`, `SAK`, `SBK`, `SNK`, `ALL`
  * `from_date` (optional): From date (`yyyy-MM-dd`)
  * `to_date` (optional): To date (`yyyy-MM-dd`)
  * `volatility` (optional): Volatility (ANALYTICAL strategy only)
  * `underlying_price` (optional): Underlying price (ANALYTICAL strategy only)
  * `interest_rate` (optional): Interest rate (ANALYTICAL strategy only)
  * `days_to_expiration` (optional): Days to expiration (ANALYTICAL strategy only)
  * `exp_month` (optional): `ExpirationMonth` — `JAN`..`DEC`, `ALL`
  * `option_type` (optional): `OptionType` — `STANDARD`, `NON_STANDARD`, `ALL`
  * `entitlement` (optional): `Entitlement` — `PN` (NonPayingPro), `NP` (NonPro), `PP` (PayingPro)
  * `params` (optional): Additional query parameters

### get_expiration_chain(symbol: str, params: dict|None=None) -> dict

* GET `/expirationchain`

* Parameters:

  * `symbol` (required): Underlying asset symbol
  * `params` (optional): Additional query parameters

* Returns: JSON response with option expiration dates for the symbol

### get_price_history(symbol, periodType="month", period=1, frequencyType="daily", frequency=1, startDate=None, endDate=None, need_extended_hours_data=None, need_previous_close=None, params=None) -> dict

* GET `/pricehistory`
* Parameters:

  * `symbol` (required): instrument symbol
  * `periodType`: `PeriodType` — `day`, `month`, `year`, `ytd`
  * `period`: int — number of periods
  * `frequencyType`: `FrequencyType` — `minute`, `daily`, `weekly`, `monthly`
  * `frequency`: int — specific frequency
  * `startDate` / `endDate`: epoch millis (int), `datetime`, `date`, or `"YYYY-MM-DD"` string
  * `need_extended_hours_data` (optional): `bool` — include extended hours data (default: True)
  * `need_previous_close` (optional): `bool` — include previous close price/date
  * `params` (optional): additional query parameters

### get_movers(symbol_id: str, sort: str|None=None, frequency: int|None=None, params: dict|None=None) -> dict

* GET `/movers/{symbol_id}` (e.g., `$DJI`, `$SPX`, `NASDAQ`)
* Parameters:

  * `symbol_id` (required): `MoverIndex` — `$DJI`, `$COMPX`, `$SPX`, `NYSE`, `NASDAQ`, `OTCBB`, `INDEX_ALL`, `EQUITY_ALL`, `OPTION_ALL`, `OPTION_PUT`, `OPTION_CALL`
  * `sort` (optional): `MoverSort` — `VOLUME`, `TRADES`, `PERCENT_CHANGE_UP`, `PERCENT_CHANGE_DOWN`
  * `frequency` (optional): `MoverFrequency` — `0`, `1`, `5`, `10`, `30`, `60` (minutes). Default `0`
  * `params` (optional): Additional query parameters

### get_markets(markets=None, date=None, params=None) -> dict

* GET `/markets`
* Parameters:

  * `markets` (optional): `List[str]` of `MarketID` values — `equity`, `option`, `bond`, `future`, `forex`. If `None`, returns all markets.
  * `date` (optional): `YYYY-MM-DD` (if you send ISO, the SDK trims to date-only)
  * `params` (optional): additional query parameters

### get_market_hours(market_id, date=None, params=None) -> dict

* GET `/markets/{market_id}` — `MarketID`: `equity`, `option`, `bond`, `future`, `forex`
* Parameters:

  * `market_id` (required): validated against `MarketID` enum
  * `date` (optional): `YYYY-MM-DD` (if you send ISO, the SDK trims to date-only)
  * `params` (optional): additional query parameters

### get_instruments(symbols, projection="symbol-search", extra_params=None) -> dict

* GET `/instruments`
* Parameters:

  * `symbols` (required): `str` or `List[str]` — single symbol or list of symbols
  * `projection` (optional): `Projection` — `symbol-search` (default), `symbol-regex`, `desc-search`, `desc-regex`, `search`, `fundamental`
  * `extra_params` (optional): additional query parameters

### get_instrument_by_cusip(cusip_id, params=None) -> dict

* GET `/instruments/{cusip_id}`
* Parameters:

  * `cusip_id` (required): instrument CUSIP identifier
  * `params` (optional): additional query parameters

---

## WebSocket Streaming (`streaming.py`)

**Related enums**: `StreamService`, `StreamCommand`, `StreamResponseCode`

### Callbacks

* `on_data(fn)`  (data frames)
* `on_response(fn)`  (command confirmations/errors)
* `on_notify(fn)`  (heartbeats/notices)

### Basic flow

```python
ws = client.streaming
ws.on_data(lambda f: print("DATA", f))
ws.on_response(lambda f: print("RESP", f))
ws.on_notify(lambda f: print("NOTIFY", f))
ws.connect(); ws.login()  # Authorization = access token without "Bearer"
ws.equities_subscribe(["AAPL","MSFT"])             # LEVELONE_EQUITIES
ws.options_subscribe(["AAPL  250926C00257500"])      # LEVELONE_OPTIONS
ws.nasdaq_book(["MSFT"])                             # NASDAQ_BOOK
ws.chart_equity(["AAPL"])                            # CHART_EQUITY
ws.screener_equity(["NYSE_VOLUME_5"])                # SCREENER_EQUITY
```

### Key Formats (quick table)

| Type           | Format                               | Example                 | Notes                                      |
| -------------- | ------------------------------------ | ----------------------- | ------------------------------------------ |
| Equities       | Ticker                               | `AAPL`, `MSFT`          | Uppercase                                  |
| Options        | `RRRRRRYYMMDDsWWWWWddd`              | `AAPL  251219C00200000` | 6-char symbol, YYMMDD, C/P, 5+3 strike    |
| Futures        | `/<root><month><yy>`                 | `/ESZ25`                | Root/month/year in uppercase               |
| FuturesOptions | `./<root><month><year><C/P><strike>` | `./OZCZ23C565`          | Depends on the feed                        |
| Forex          | `PAIR`                               | `EUR/USD`, `USD/JPY`    | `/` separator                              |
| Screener       | `PREFIX_SORTFIELD_FREQUENCY`         | `NYSE_VOLUME_5`         | Prefix/criterion/frequency                 |

### Service-by-Service Examples

#### Level One Options

```python
ws.options_subscribe(["AAPL  250926C00257500"])  # standard option string
# Default fields: 0,2,3,4,8,16,17,18,20,28,29,30,31,37,44
```

#### Level One Futures

```python
ws.futures_subscribe(["/ESZ25"])  # E-mini S&P 500 Dec 2025
# Default fields: 0,1,2,3,4,5,8,12,13,18,19,20,24,33
```

#### Level One Futures Options

```python
ws.futures_options_subscribe(["./OZCZ23C565"])
# Default fields: 0,1,2,3,4,5,8,12,13,17,18,19,23,24,25,29
```

#### Level One Forex

```python
ws.forex_subscribe(["EUR/USD","USD/JPY"])
# Default fields: 0,1,2,3,4,5,6,7,8,9,10,11,15,16,17,20,21,27,28,29
```

#### Book (Level II)

```python
ws.nasdaq_book(["MSFT"])  # Also: ws.nyse_book, ws.options_book
# Default fields: 0 (Symbol), 1 (BookTime), 2 (Bids), 3 (Asks)
```

#### Chart (Series)

```python
ws.chart_equity(["AAPL"])      # 0..7: key, open, high, low, close, volume, sequence, chartTime
ws.chart_futures(["/ESZ25"])   # 0..6
```

#### Screener

```python
ws.screener_equity(["NYSE_VOLUME_5"])
ws.screener_options(["CBOE_VOLUME_5"])
```

#### Account Activity

```python
ws.account_activity()  # Gets accountHash and subscribes
```

### Utilities

* `connect()` — open the WebSocket connection
* `wait_until_connected(timeout=10.0)` — block until the connection is ready
* `login(...)` — authenticate the stream (sends ADMIN LOGIN)
* `logout()` — send ADMIN LOGOUT
* `disconnect()` — close the WebSocket connection (**AsyncStreaming only**)
* `subscribe(service, keys, fields=None)` — generic SUBS
* `add(service, keys)` — generic ADD
* `unsubscribe(service, keys)` / `unsubscribe_service(service, keys)` — generic UNSUBS
* `view(service, fields)` — generic VIEW

> **AsyncStreaming:** `AsyncClient.streaming` returns an `AsyncStreaming` instance. It mirrors all `Streaming` methods as `async`/`await`, adds `disconnect()`, and is safe for use in async frameworks (FastAPI, etc.).

### Option Symbol Helper

* `create_option_symbol(symbol, expiration, option_type, strike_price)` - Creates Schwab option symbol from components

**Example:**
```python
# Create option symbol from components
option_symbol = ws.create_option_symbol("AAPL", "2025-12-19", "C", 200.0)
# Returns: "AAPL  251219C00200000"

# Use in subscription
ws.options_subscribe([option_symbol])
```

**Parameters:**
* `symbol`: Underlying symbol (e.g., "AAPL")
* `expiration`: Expiration date in "YYYY-MM-DD" format (e.g., "2025-10-03")
* `option_type`: "C" for Call or "P" for Put
* `strike_price`: Strike price (e.g., 257.5)

### Recommended Fields

* LEVELONE_EQUITIES: `0,1,2,3,4,5,8,10,18,42,33,34,35`
* LEVELONE_OPTIONS: `0,2,3,4,8,16,17,18,20,28,29,30,31,37,44`
* LEVELONE_FUTURES: `0,1,2,3,4,5,8,12,13,18,19,20,24,33`
* CHART_EQUITY: `0,1,2,3,4,5,6,7`

#### Quick fields table (copy/paste)

| Service                  | Fields CSV                                        |
| ------------------------ | ------------------------------------------------- |
| LEVELONE_EQUITIES        | 0,1,2,3,4,5,8,10,18,42,33,34,35                   |
| LEVELONE_OPTIONS         | 0,2,3,4,8,16,17,18,20,28,29,30,31,37,44           |
| LEVELONE_FUTURES         | 0,1,2,3,4,5,8,12,13,18,19,20,24,33                |
| LEVELONE_FUTURES_OPTIONS | 0,1,2,3,4,5,8,12,13,17,18,19,23,24,25,29          |
| LEVELONE_FOREX           | 0,1,2,3,4,5,6,7,8,9,10,11,15,16,17,20,21,27,28,29 |
| NASDAQ_BOOK              | 0,1,2,3                                           |
| NYSE_BOOK                | 0,1,2,3                                           |
| OPTIONS_BOOK             | 0,1,2,3                                           |
| CHART_EQUITY             | 0,1,2,3,4,5,6,7                                   |
| CHART_FUTURES            | 0,1,2,3,4,5,6                                     |
| SCREENER_EQUITY          | 0,1,2,3,4                                         |
| SCREENER_OPTION          | 0,1,2,3,4                                         |
| ACCT_ACTIVITY            | 0,1,2,3                                           |

### SUBS / ADD / VIEW / UNSUBS examples by service

```python
ws = client.streaming
ws.on_data(lambda f: print("DATA", f))
ws.on_response(lambda f: print("RESP", f))
ws.connect(); ws.login()

# LEVELONE_EQUITIES
ws.equities_subscribe(["AAPL","TSLA"], fields=[0,1,2,3,4,5,8,10,18,42,33,34,35])
ws.equities_add(["MSFT"])                          # adds without replacing
ws.equities_view([0,1,2,3,5,8,18])                  # changes fields
ws.equities_unsubscribe(["TSLA"])                  # removes symbols

# LEVELONE_OPTIONS
ws.options_subscribe(["AAPL  250926C00257500"], fields=[0,2,3,4,8,16,17,18,20,28,29,30,31,37,44])
ws.options_add(["AAPL  250926P00257500"])
ws.options_view([0,2,3,4,8,16,17,20,28,29,30,31,37,44])
ws.options_unsubscribe(["AAPL  250926C00257500"])

# LEVELONE_FUTURES
ws.futures_subscribe(["/ESZ25"], fields=[0,1,2,3,4,5,8,12,13,18,19,20,24,33])
ws.futures_add(["/NQZ25"])
ws.futures_view([0,1,2,3,4,5,8,12,13,18,19,20,24,33])
ws.futures_unsubscribe(["/ESZ25"])

# LEVELONE_FUTURES_OPTIONS
ws.futures_options_subscribe(["./OZCZ23C565"], fields=[0,1,2,3,4,5,8,12,13,17,18,19,23,24,25,29])
ws.futures_options_add(["./OZCZ23P565"])
ws.futures_options_view([0,1,2,3,4,5,8,12,13,17,18,19,23,24,25,29])
ws.futures_options_unsubscribe(["./OZCZ23C565"])

# LEVELONE_FOREX
ws.forex_subscribe(["EUR/USD","USD/JPY"], fields=[0,1,2,3,4,5,6,7,8,9,10,11,15,16,17,20,21,27,28,29])
ws.forex_add(["GBP/USD"])
ws.forex_view([0,1,2,3,4,5,6,7,8,9,10,11,15,16,17,20,21,27,28,29])
ws.forex_unsubscribe(["USD/JPY"])

# BOOK (Level II)
ws.nasdaq_book(["MSFT"], fields=[0,1,2,3])
ws.add("NASDAQ_BOOK", ["AAPL"])                    # generic ADD
ws.view("NASDAQ_BOOK", [0,1,2,3])                   # generic VIEW
ws.unsubscribe_service("NASDAQ_BOOK", ["MSFT"])    # generic UNSUBS

# CHART (Series)
ws.chart_equity(["AAPL"], fields=[0,1,2,3,4,5,6,7])
ws.add("CHART_EQUITY", ["MSFT"])                  # generic ADD
ws.view("CHART_EQUITY", [0,1,2,3,4,5,6,7])          # generic VIEW
ws.unsubscribe("CHART_EQUITY", ["AAPL"])           # generic UNSUBS

# SCREENER
ws.screener_equity(["EQUITY_ALL_VOLUME_5"], fields=[0,1,2,3,4])
ws.add("SCREENER_EQUITY", ["NYSE_TRADES_1"])
ws.view("SCREENER_EQUITY", [0,1,2,3,4])
ws.unsubscribe("SCREENER_EQUITY", ["EQUITY_ALL_VOLUME_5"])

# ACCT_ACTIVITY
ws.account_activity(fields=[0,1,2,3])                  # subscribe account activity
# For UNSUBS you need the same key used in SUBS (account_hash)
account_hash = getattr(client, "_account_hash", None)
if account_hash:
    ws.unsubscribe_service("ACCT_ACTIVITY", [account_hash])
```

### Quick Field Guide (IDs → meaning)

> Note: exact mappings may vary depending on entitlements/version. Below are practical equivalences observed in frames.

#### LEVELONE_EQUITIES

| ID | Field                      |
| -- | -------------------------- |
| 0  | symbol/key                 |
| 1  | bidPrice                   |
| 2  | askPrice                   |
| 3  | lastPrice                  |
| 4  | bidSize                    |
| 5  | askSize                    |
| 8  | totalVolume                |
| 10 | referencePrice (open/mark) |
| 18 | netChange                  |
| 42 | percentChange              |

#### LEVELONE_OPTIONS

| ID | Field                           |
| -- | ------------------------------- |
| 0  | symbol/key                      |
| 2  | bidPrice                        |
| 3  | askPrice                        |
| 4  | lastPrice                       |
| 8  | totalVolume                     |
| 16 | openInterest                    |
| 17 | daysToExpiration                |
| 20 | strikePrice                     |
| 28 | delta                           |
| 29 | gamma                           |
| 30 | theta                           |
| 31 | vega                            |
| 44 | impliedVolatility (if provided) |

#### LEVELONE_FUTURES

| ID | Field                                      |
| -- | ------------------------------------------ |
| 0  | symbol/key                                 |
| 1  | bidPrice                                   |
| 2  | askPrice                                   |
| 3  | lastPrice                                  |
| 4  | bidSize                                    |
| 5  | askSize                                    |
| 8  | totalVolume                                |
| 12 | openInterest                               |
| 13 | contractDepth/series info (per feed)       |
| 18 | netChange                                  |
| 19 | sessionChange (or days/indicator per feed) |
| 20 | percentChange/ratio (per feed)             |
| 24 | lastSettlement/mark                        |
| 33 | priorSettle                                |

### Frame Structure

* Confirmations (`response`): `{ "response": [ { "service":"ADMIN","command":"LOGIN","content":{"code":0,"msg":"..."}} ] }`
* Data (`data`): `{ "service":"LEVELONE_EQUITIES","timestamp":...,"command":"SUBS","content":[{"key":"AAPL",...}] }`
* Notifications (`notify`): `{ "notify": [ { "heartbeat": "..." } ] }`

### Streamer API Cheat Sheet (parameters and commands)

1. Connection and prerequisites

* **Auth**: use the Access Token from the OAuth flow.
* **Session IDs** (from `GET /userPreference`): `schwabClientCustomerId`, `schwabClientCorrelId`, `SchwabClientChannel`, `SchwabClientFunctionId`.
* **Transport**: JSON WebSocket. One stream per user (if you open more: code 12 CLOSE_CONNECTION).

2. Envelope of each command

* **Common fields**:

  * `service` (req.): `ADMIN`, `LEVELONE_EQUITIES`, `LEVELONE_OPTIONS`, `LEVELONE_FUTURES`, `LEVELONE_FUTURES_OPTIONS`, `LEVELONE_FOREX`, `NYSE_BOOK`, `NASDAQ_BOOK`, `OPTIONS_BOOK`, `CHART_EQUITY`, `CHART_FUTURES`, `SCREENER_EQUITY`, `SCREENER_OPTION`, `ACCT_ACTIVITY`.
  * `command` (req.): `LOGIN`, `SUBS`, `ADD`, `UNSUBS`, `VIEW`, `LOGOUT`.
  * `requestid` (req.): unique request identifier.
  * `SchwabClientCustomerId` and `SchwabClientCorrelId` (recommended): from `userPreference`.
  * `parameters` (optional): depends on service/command.
* **Notes**: `SUBS` overwrites list; `ADD` appends; `UNSUBS` removes; `VIEW` changes `fields`.

3. ADMIN (session)

* `LOGIN` (`service=ADMIN`, `command=LOGIN`)

  * parameters: `Authorization` (token without "Bearer"), `SchwabClientChannel`, `SchwabClientFunctionId`.
* `LOGOUT` (`service=ADMIN`, `command=LOGOUT`)

  * parameters: empty.

4. LEVEL ONE (L1 quotes)

* Common parameters: `keys` (req., CSV list), `fields` (optional, indexes).
* `LEVELONE_EQUITIES`: `keys` uppercase tickers (e.g., `AAPL,TSLA`).
* `LEVELONE_OPTIONS`: `keys` Schwab option format `RRRRRR  YYMMDD[C/P]STRIKE`.
* `LEVELONE_FUTURES`: `keys` `/<root><monthCode><yearCode>` (month codes: F,G,H,J,K,M,N,Q,U,V,X,Z; year two digits), e.g., `/ESZ25`.
* `LEVELONE_FUTURES_OPTIONS`: `keys` `./<root><month><yy><C|P><strike>`, e.g., `./OZCZ23C565`.
* `LEVELONE_FOREX`: `keys` `BASE/QUOTE` pairs CSV (e.g., `EUR/USD,USD/JPY`).

5. BOOK (Level II)

* Services: `NYSE_BOOK`, `NASDAQ_BOOK`, `OPTIONS_BOOK`.
* Parameters: `keys` (req., tickers), `fields` (optional, level indexes).

6. CHART (streaming series)

* `CHART_EQUITY`: `keys` equities; `fields` indexes (OHLCV, time, seq).
* `CHART_FUTURES`: `keys` futures (same format as L1 futures); `fields` indexes.

7. SCREENER (gainers/losers/actives)

* Services: `SCREENER_EQUITY`, `SCREENER_OPTION`.
* `keys` pattern `PREFIX_SORTFIELD_FREQUENCY`, e.g., `EQUITY_ALL_VOLUME_5`.

  * `PREFIX` examples: `$COMPX`, `$DJI`, `$SPX`, `INDEX_ALL`, `NYSE`, `NASDAQ`, `OTCBB`, `EQUITY_ALL`, `OPTION_PUT`, `OPTION_CALL`, `OPTION_ALL`.
  * `SORTFIELD`: `VOLUME`, `TRADES`, `PERCENT_CHANGE_UP`, `PERCENT_CHANGE_DOWN`, `AVERAGE_PERCENT_VOLUME`.
  * `FREQUENCY`: `0,1,5,10,30,60` (min; `0` = full day).
* `fields` (optional): screener field indexes.

8. ACCOUNT (account activity)

* Service: `ACCT_ACTIVITY` (`SUBS`/`UNSUBS`).
* `keys` (req.): arbitrary identifier for your sub; if you send multiple, the first is used.
* `fields` (recommended): `0` (or `0,1,2,3` per example/need).

9. Server responses (`StreamResponseCode`)

* Types: `response` (to your requests), `notify` (heartbeats), `data` (market flow).

| Code | Name | Meaning |
|---|---|---|
| 0 | `SUCCESS` | Command succeeded |
| 3 | `LOGIN_DENIED` | Invalid or expired token |
| 9 | `UNKNOWN_FAILURE` | Unspecified server error |
| 11 | `SERVICE_NOT_AVAILABLE` | Service unavailable for your entitlement |
| 12 | `CLOSE_CONNECTION` | Only one stream per user allowed |
| 19 | `REACHED_SYMBOL_LIMIT` | Too many symbols subscribed |
| 20 | `STREAM_CONN_NOT_FOUND` | Stream connection not found |
| 21 | `BAD_COMMAND_FORMAT` | Malformed command (check Auth format, keys) |
| 22 | `FAILED_COMMAND_SUBS` | SUBS command failed |
| 23 | `FAILED_COMMAND_UNSUBS` | UNSUBS command failed |
| 24 | `FAILED_COMMAND_ADD` | ADD command failed |
| 25 | `FAILED_COMMAND_VIEW` | VIEW command failed |
| 26 | `SUCCEEDED_COMMAND_SUBS` | SUBS succeeded |
| 27 | `SUCCEEDED_COMMAND_UNSUBS` | UNSUBS succeeded |
| 28 | `SUCCEEDED_COMMAND_ADD` | ADD succeeded |
| 29 | `SUCCEEDED_COMMAND_VIEW` | VIEW succeeded |
| 30 | `STOP_STREAMING` | Server stopped streaming |

10. Delivery Types

* `All Sequence`: everything with sequence number.
* `Change`: only changed fields (conflated).
* `Whole`: full messages with throttling.

11. Best practices

* Do `LOGIN` and wait for `code=0` before `SUBS/ADD`.
* To add symbols without losing existing ones, use `ADD` (not `SUBS`).
* Change `fields` with `VIEW` for performance.
* Handle `notify` (heartbeats) and reconnect if they are lost.
* Reuse your `SchwabClientCorrelId` during the session.
* If you see `19` (symbol limit), shard loads by service/session.

---

## Enums Reference (`enums.py`)

All enums are `StrEnum` (or `IntEnum`) subclasses, so they can be passed directly where a `str` is expected. Import from the top-level package:

```python
from schwab_sdk import ContractType, OrderType, StreamService
```

### Market Data — Request Parameter Enums

#### `ContractType`
Option chain contract type filter.

| Value | Description |
|---|---|
| `CALL` | Call contracts only |
| `PUT` | Put contracts only |
| `ALL` | Both calls and puts |

#### `Strategy`
Option chain strategy.

| Value |
|---|
| `SINGLE`, `ANALYTICAL`, `COVERED`, `VERTICAL`, `CALENDAR`, `STRANGLE`, `STRADDLE`, `BUTTERFLY`, `CONDOR`, `DIAGONAL`, `COLLAR`, `ROLL` |

#### `StrikeRange`
Option chain strike range filter.

| Value | API Code | Description |
|---|---|---|
| `IN_THE_MONEY` | `ITM` | In the money |
| `NEAR_THE_MONEY` | `NTM` | Near the money |
| `OUT_OF_THE_MONEY` | `OTM` | Out of the money |
| `STRIKES_ABOVE_MARKET` | `SAK` | Strikes above market |
| `STRIKES_BELOW_MARKET` | `SBK` | Strikes below market |
| `STRIKES_NEAR_MARKET` | `SNK` | Strikes near market |
| `ALL` | `ALL` | All strikes |

#### `ExpirationMonth`
Option chain expiration month filter.

`JAN`, `FEB`, `MAR`, `APR`, `MAY`, `JUN`, `JUL`, `AUG`, `SEP`, `OCT`, `NOV`, `DEC`, `ALL`

#### `OptionType`
Option type filter for chains.

| Value | Description |
|---|---|
| `STANDARD` | Standard contracts |
| `NON_STANDARD` | Non-standard (adjusted) contracts |
| `ALL` | Both |

#### `Entitlement`
Retail token entitlement level.

| Value | Description |
|---|---|
| `PN` | NonPayingPro |
| `NP` | NonPro |
| `PP` | PayingPro |

#### `PeriodType`
Price history period type.

`day`, `month`, `year`, `ytd`

#### `FrequencyType`
Price history frequency type.

`minute`, `daily`, `weekly`, `monthly`

#### `QuoteField`
Quote field groups for GET /quotes.

`quote`, `fundamental`, `extended`, `reference`, `regular`, `all`

#### `MoverIndex`
Index symbols for GET /movers.

`$DJI`, `$COMPX`, `$SPX`, `NYSE`, `NASDAQ`, `OTCBB`, `INDEX_ALL`, `EQUITY_ALL`, `OPTION_ALL`, `OPTION_PUT`, `OPTION_CALL`

#### `MoverSort`
Movers sort attribute.

`VOLUME`, `TRADES`, `PERCENT_CHANGE_UP`, `PERCENT_CHANGE_DOWN`, `AVERAGE_PERCENT_VOLUME`

#### `MoverFrequency` (IntEnum)
Movers frequency in minutes. `0` = real-time snapshot.

`0`, `1`, `5`, `10`, `30`, `60`

#### `MarketID`
Market IDs for market hours endpoints.

`equity`, `option`, `bond`, `future`, `forex`

#### `Projection`
Instrument search projection type.

`symbol-search`, `symbol-regex`, `desc-search`, `desc-regex`, `search`, `fundamental`

### Market Data — Response Enums

#### `AssetMainType`
Primary asset type returned in responses and streaming data.

`BOND`, `EQUITY`, `ETF`, `EXTENDED`, `FOREX`, `FUTURE`, `FUTURE_OPTION`, `FUNDAMENTAL`, `INDEX`, `INDICATOR`, `MUTUAL_FUND`, `OPTION`, `UNKNOWN`

#### `AssetSubType`
Asset sub-type in responses.

| Value | Description |
|---|---|
| `ADR` | American Depositary Receipt |
| `CEF` | Closed-End Fund |
| `COE` | Common Equity |
| `ETF` | Exchange-Traded Fund |
| `ETN` | Exchange-Traded Note |
| `GDR` | Global Depositary Receipt |
| `OEF` | Open-End Fund |
| `PRF` | Preferred Stock |
| `RGT` | Right |
| `UIT` | Unit Investment Trust |
| `WAR` | Warrant |

#### `SecurityStatus`
Security status in quote responses.

`Normal`, `Unknown`, `Halted`, `Closed`

#### `Exchange`
Exchange single-character codes (reference.exchange, streaming field 13).

| Value | Code | Exchange |
|---|---|---|
| `AMEX` | `A` | NYSE American (AMEX) |
| `NASDAQ` | `Q` | NASDAQ |
| `NYSE` | `N` | NYSE |
| `NYSE_ARCA` | `P` | NYSE Arca (Pacific) |
| `OPR` | `o` | Options |
| `OTC_MARKETS` | `9` | Pink Sheets |
| `INDEX` | `0` | Index |
| `XCME` | `@` | CME Futures |
| `GFT` | `T` | Forex |
| `MUTUAL_FUND` | `3` | Mutual Fund |
| `NASDAQ_OTCBB` | `U` | OTCBB |
| `INDICATOR` | `:` | Indicator (realtime) |

#### `MICId`
Market Identifier Codes (ISO 10383) in quote data (askMICId, bidMICId, lastMICId).

`XNYS`, `XNAS`, `ARCX`, `XADF`, `MEMX`, `IEGX`, `EDGX`, `BATS`, `IEXG`, `EPRL`

#### `ExchangeName`
Human-readable exchange names (reference.exchangeName).

`NASDAQ`, `NYSE`, `NYSE Arca`, `OPR`, `OTC Markets`, `Index`, `XCME`, `GFT`, `Mutual Fund`, `Nasdaq OTCBB`

#### `OtcMarketTier`

| Value | Code | Description |
|---|---|---|
| `OTCQX` | `QX` | Best Market |
| `OTCQB` | `QB` | Venture Market |
| `PINK_CURRENT` | `PC` | Pink Current Information |
| `EXPERT_MARKET` | `EM` | Expert Market |

#### Other Response Enums

| Enum | Values | Context |
|---|---|---|
| `QuoteType` | `NBBO` | Quote type in equity/ETF quotes |
| `OptionContractChar` | `C`, `P` | Contract type in option reference |
| `OptionSettlementType` | `P` (PM), `A` (AM) | Settlement type |
| `OptionExpirationType` | `S` (Standard), `W` (Weekly), `Q` (Quarterly), `M` (Mini), `R` (Reduced Value) | UV expiration type |
| `DividendFrequency` | `0` (None), `1` (Annual), `2` (Semi-Annual), `4` (Quarterly), `12` (Monthly) | divFreq in fundamentals |
| `FundStrategy` | `A` (Active), `P` (Passive) | ETF fund strategy |
| `MoverDirection` | `up`, `down` | Direction in movers response |

### Orders — Enums

#### `OrderType`
Order type for placing and querying orders.

`MARKET`, `LIMIT`, `STOP`, `STOP_LIMIT`, `TRAILING_STOP`, `CABINET`, `NON_MARKETABLE`, `MARKET_ON_CLOSE`, `EXERCISE`, `TRAILING_STOP_LIMIT`, `NET_DEBIT`, `NET_CREDIT`, `NET_ZERO`, `LIMIT_ON_CLOSE`

#### `Duration`
Order duration / time-in-force.

| Value | Description |
|---|---|
| `DAY` | Day order |
| `GOOD_TILL_CANCEL` | GTC |
| `FILL_OR_KILL` | FOK |
| `IMMEDIATE_OR_CANCEL` | IOC |
| `END_OF_WEEK` | End of week |
| `END_OF_MONTH` | End of month |
| `NEXT_END_OF_MONTH` | Next end of month |

#### `Session`
Trading session for orders.

| Value | Description |
|---|---|
| `NORMAL` | Regular trading hours |
| `AM` | Pre-market |
| `PM` | After-hours |
| `SEAMLESS` | Extended hours (all sessions) |

#### `OrderStatus`
Order status values for filtering and responses.

`AWAITING_PARENT_ORDER`, `AWAITING_CONDITION`, `AWAITING_STOP_CONDITION`, `AWAITING_MANUAL_REVIEW`, `ACCEPTED`, `AWAITING_UR_OUT`, `PENDING_ACTIVATION`, `QUEUED`, `WORKING`, `REJECTED`, `PENDING_CANCEL`, `CANCELED`, `PENDING_REPLACE`, `REPLACED`, `FILLED`, `EXPIRED`, `NEW`, `AWAITING_RELEASE_TIME`, `PENDING_ACKNOWLEDGEMENT`, `PENDING_RECALL`, `UNKNOWN`

#### `EquityInstruction`
Instruction for equity order legs.

| Value | Description |
|---|---|
| `BUY` | Buy long |
| `SELL` | Sell long position |
| `SELL_SHORT` | Short sell |
| `BUY_TO_COVER` | Cover short position |

#### `OptionInstruction`
Instruction for option order legs.

| Value | Description |
|---|---|
| `BUY_TO_OPEN` | Open new long position |
| `SELL_TO_CLOSE` | Close existing long position |
| `SELL_TO_OPEN` | Open new short position |
| `BUY_TO_CLOSE` | Close existing short position |

#### `AssetType`
Asset type for order placement (instrument.assetType).

`EQUITY`, `OPTION`

#### `OrderStrategyType`
Order strategy type.

| Value | Description |
|---|---|
| `SINGLE` | Single order |
| `OCO` | One-cancels-other |
| `TRIGGER` | Triggered (bracket) order |
| `CANCEL` | Cancel order |
| `RECALL` | Recall order |
| `PAIR` | Pair trade |
| `FLATTEN` | Flatten position |
| `TWO_DAY_SWAP` | Two-day swap |
| `BLAST_ALL` | Blast all |

#### `ComplexOrderStrategyType`
Complex strategy type for multi-leg orders.

`NONE`, `COVERED`, `VERTICAL`, `BACK_RATIO`, `CALENDAR`, `DIAGONAL`, `STRADDLE`, `STRANGLE`, `COLLAR_SYNTHETIC`, `BUTTERFLY`, `CONDOR`, `IRON_CONDOR`, `VERTICAL_ROLL`, `COLLAR_WITH_STOCK`, `DOUBLE_DIAGONAL`, `UNBALANCED_BUTTERFLY`, `UNBALANCED_VERTICAL_ROLL`, `UNBALANCED_CONDOR`, `CUSTOM`

#### Other Order Enums

| Enum | Values | Context |
|---|---|---|
| `SpecialInstruction` | `ALL_OR_NONE`, `DO_NOT_REDUCE`, `ALL_OR_NONE_DO_NOT_REDUCE` | Special order instructions |
| `RequestedDestination` | `INET`, `ECN_ARCA`, `CBOE`, `AMEX`, `PHLX`, `ISE`, `BOX`, `NASDAQ`, `BATS`, `C2`, `AUTO` | Order routing |
| `PriceLinkBasis` | `MANUAL`, `BASE`, `TRIGGER`, `LAST`, `BID`, `ASK`, `ASK_BID`, `MARK`, `AVERAGE` | Stop/limit price basis |
| `PriceLinkType` | `VALUE`, `PERCENT`, `TICK` | Trailing stop type |
| `StopType` | `STANDARD`, `BID`, `ASK`, `LAST`, `MARK` | Stop price type |
| `TaxLotMethod` | `FIFO`, `LIFO`, `HIGH_COST`, `LOW_COST`, `AVERAGE_COST`, `SPECIFIC_LOT`, `LOSS_HARVESTER` | Tax lot selection |
| `OrderLegType` | `EQUITY`, `OPTION`, `INDEX`, `MUTUAL_FUND`, `CASH_EQUIVALENT`, `FIXED_INCOME`, `CURRENCY` | Leg asset type |
| `PositionEffect` | `OPENING`, `CLOSING`, `AUTOMATIC` | Option position effect |
| `QuantityType` | `ALL_SHARES`, `DOLLARS`, `SHARES` | Quantity type |
| `DivCapGains` | `REINVEST`, `PAYOUT` | Dividend reinvestment |
| `ActivityType` | `EXECUTION`, `ORDER_ACTION` | Order activity type |
| `ExecutionType` | `FILL` | Execution type |
| `AdvancedOrderType` | `NONE` | Preview orders |
| `SettlementInstruction` | `REGULAR` | Settlement type |
| `OrderValidationSeverity` | `ACCEPT` | Validation severity |
| `CommissionFeeType` | `COMMISSION` | Commission/fee type |

### Accounts — Enums

#### `AccountField`
Additional fields for account queries.

| Value | Description |
|---|---|
| `POSITIONS` | `"positions"` — include positions in response |

#### `TransactionType`
Transaction types for filtering.

`TRADE`, `RECEIVE_AND_DELIVER`, `DIVIDEND_OR_INTEREST`, `ACH_RECEIPT`, `ACH_DISBURSEMENT`, `CASH_RECEIPT`, `CASH_DISBURSEMENT`, `ELECTRONIC_FUND`, `WIRE_OUT`, `WIRE_IN`, `JOURNAL`, `MEMORANDUM`, `MARGIN_CALL`, `MONEY_MARKET`, `SMA_ADJUSTMENT`

#### `AccountInstrumentType`
Instrument type in account position responses.

`SWEEP_VEHICLE`, `CASH_EQUIVALENT`, `EQUITY`, `OPTION`, `MUTUAL_FUND`, `FIXED_INCOME`, `INDEX`, `CURRENCY`, `COLLECTIVE_INVESTMENT`

#### Other Account Enums

| Enum | Values | Context |
|---|---|---|
| `TransactionStatus` | `VALID` | Transaction status |
| `TransactionSubAccount` | `CASH` | Sub-account type |
| `TransactionActivityType` | `ACTIVITY_CORRECTION` | Transaction activity type |
| `UserType` | `ADVISOR_USER` | User type in transactions |

### Streaming — Enums

#### `StreamService`
WebSocket streaming service names.

`ADMIN`, `LEVELONE_EQUITIES`, `LEVELONE_OPTIONS`, `LEVELONE_FUTURES`, `LEVELONE_FUTURES_OPTIONS`, `LEVELONE_FOREX`, `NASDAQ_BOOK`, `NYSE_BOOK`, `OPTIONS_BOOK`, `CHART_EQUITY`, `CHART_FUTURES`, `SCREENER_EQUITY`, `SCREENER_OPTION`, `ACCT_ACTIVITY`

#### `StreamCommand`
WebSocket streaming commands.

| Value | Description |
|---|---|
| `LOGIN` | Authenticate the stream |
| `LOGOUT` | Close the stream session |
| `SUBS` | Subscribe (replaces existing keys) |
| `UNSUBS` | Unsubscribe keys |
| `ADD` | Add keys (appends to existing) |
| `VIEW` | Change fields for a service |

#### `StreamResponseCode` (IntEnum)
See the [full response code table](#9-server-responses-streamresponsecode) in the Streaming section above.

---

## Advanced Troubleshooting

* `notify.code=12` (Only one connection): close other active WebSocket sessions.
* `response.content.code=3` (Login denied): invalid/expired token → `client.login()`.
* `response.content.code=21` (Bad command formatting): check `Authorization` format (without `Bearer`) and keys (spacing for options, uppercase).
* Persistent REST 401: delete `schwab_tokens.json` (or clear your DB tokens if using `persist=False`) and re-run `client.login()`.
* High latency/lost frames: avoid parallel reconnects; use the SDK's auto-resubscription.

---

## Contributions

Your contributions are welcome! Ideas, issues, and PRs help improve the SDK:

* Open an issue with clear details (environment, steps, expected/actual error).
* Propose endpoint coverage improvements and examples.
* Follow a clear style and add tests or minimal examples when possible.

If you want to hold working sessions or discuss the roadmap, open an issue labeled `discussion`.

## Disclaimer

This project is unofficial and is not affiliated with, sponsored by, or endorsed by Charles Schwab & Co., Inc. "Schwab" and other trademarks are the property of their respective owners. Use of this SDK is subject to the terms and conditions of Schwab APIs and applicable regulations. Use at your own discretion and responsibility.

---

## License

MIT ([LICENSE](LICENSE))
