Cliente ligero para API de Schwab: OAuth, REST y Streaming WebSocket
Project description
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):
pip install schwab_sdk_unofficial
Import in code (module):
from schwab_sdk import Client, AsyncClient
⚠️ Note: The package name on PyPI is schwab_sdk_unofficial, but the import remains schwab_sdk for a clean API.
Table of Contents
Requirements
- Python 3.9+
- Install dependencies:
pip install requests websocket-client flask
Configuration
Create .env (or export environment variables):
SCHWAB_CLIENT_ID=your_client_id
SCHWAB_CLIENT_SECRET=your_client_secret
SCHWAB_REDIRECT_URI=https://127.0.0.1:8080/callback
Token storage modes
token_pathes obligatorio en los clientes (Sync/Async) y controla la persistencia de tokens.- Modo archivo: pasa una ruta (p.ej.
"schwab_tokens.json"). El SDK guarda/lee un JSON y rota tokens automáticamente (access ~29 min; aviso de re-login cuando el refresh expira ~7 d). - Modo memoria: pasa
":memory:". El SDK no escribe en disco y mantiene los tokens solo en memoria; puedes inicializar o refrescar contoken_datay consultar el estado conclient.token_handler.get_token_payload().
Quick Start
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', # Requerido: archivo o ":memory:"
# En modo memoria puedes inicializar tokens desde un dict (refresh/boot)
token_data={
# Minimal example: provide current tokens
# 'access_token': '...',
# 'refresh_token': '...',
# either expires_in (seconds) OR access_token_expires_at (ISO)
# 'expires_in': 1800,
# 'access_token_expires_at': '2025-10-01T12:00:00',
# optional refresh token expiration (ISO)
# 'refresh_token_expires_at': '2025-10-07T11:00:00'
}
)
# 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"])
Async Support
The SDK also provides async versions of all endpoints using AsyncClient:
import asyncio
from schwab_sdk import AsyncClient
async def main():
# Initialize async client
async with AsyncClient(
os.environ['SCHWAB_CLIENT_ID'],
os.environ['SCHWAB_CLIENT_SECRET'],
token_path='async_tokens.json' # Requerido: archivo o ":memory:"
) as client:
# Login (async wrapper)
await client.login()
# Async REST calls
quotes = await client.market.get_quotes(["AAPL", "MSFT"])
accounts = await client.account.get_accounts()
orders = await client.orders.get_all_orders()
# Async streaming
await client.streaming.connect()
await client.streaming.login()
await client.streaming.equities_subscribe(["AAPL"])
# Set up callbacks
client.streaming.on_data(lambda data: print("DATA:", data))
client.streaming.on_response(lambda resp: print("RESPONSE:", resp))
# Keep running to receive data
await asyncio.sleep(10) # Example: run for 10 seconds
await client.streaming.disconnect()
# Run async function
asyncio.run(main())
Async Benefits
- Non-blocking: Multiple API calls can run concurrently
- Better performance: Especially for multiple simultaneous requests
- Context manager: Automatic session cleanup with
async with - Same API: All methods have async equivalents with
await
Async vs Sync
| Feature | Sync | Async |
|---|---|---|
| Import | from schwab_sdk import Client |
from schwab_sdk import AsyncClient |
| Usage | client.method() |
await client.method() |
| Context | client = Client(...) |
async with AsyncClient(...) |
| Performance | Sequential | Concurrent |
| Dependencies | requests, websocket-client |
aiohttp, websockets |
Async Streaming
The async streaming client provides non-blocking WebSocket connections:
import asyncio
from schwab_sdk import AsyncClient
async def streaming_example():
async with AsyncClient(client_id, client_secret, token_path=":memory:") as client:
await client.login()
# Connect to streaming
await client.streaming.connect()
await client.streaming.login()
# Set up callbacks
client.streaming.on_data(lambda data: print("Market data:", data))
client.streaming.on_response(lambda resp: print("Response:", resp))
client.streaming.on_notify(lambda notify: print("Notification:", notify))
# Subscribe to data streams
await client.streaming.equities_subscribe(["AAPL", "MSFT"])
await client.streaming.options_subscribe([
client.streaming.create_option_symbol("AAPL", "2025-12-19", "C", 200.0)
])
await client.streaming.account_activity_subscribe("your_account_hash")
# Keep running
await asyncio.sleep(30) # Run for 30 seconds
# Cleanup
await client.streaming.disconnect()
asyncio.run(streaming_example())
Streaming Services Available
| Service | Method | Description |
|---|---|---|
| Equities | equities_subscribe(symbols) |
Stock quotes |
| Options | options_subscribe(option_symbols) |
Option quotes |
| Futures | futures_subscribe(symbols) |
Futures quotes |
| Forex | forex_subscribe(pairs) |
Currency pairs |
| Account | account_activity_subscribe(account_hash) |
Account activity |
Async Accounts Module
Async version of account and transaction endpoints:
Methods
get_accounts(fields=None)- Get all accountsget_account_by_id(account_hash, fields=None)- Get specific accountget_transactions(account_hash, from_date=None, to_date=None, symbol=None, types=None)- Get transactionsget_transaction(account_hash, transaction_id)- Get specific transactionget_preferences(account_hash)- Get account preferences
Example
async with AsyncClient(client_id, client_secret) as client:
await client.login()
# Get all accounts
accounts = await client.account.get_accounts()
# Get account with positions
account = await client.account.get_account_by_id("123456789", fields="positions")
# Get transactions for date range
transactions = await client.account.get_transactions(
"123456789",
from_date="2025-01-01",
to_date="2025-01-31"
)
Async Orders Module
Async version of order management endpoints:
Methods
get_orders(account_hash, max_results=None, from_entered_time=None, to_entered_time=None, status=None)- Get ordersget_all_orders(max_results=None, from_entered_time=None, to_entered_time=None, status=None)- Get all ordersplace_order(account_hash, order_data)- Place new orderget_order(account_hash, order_id)- Get specific ordercancel_order(account_hash, order_id)- Cancel orderreplace_order(account_hash, order_id, order_data)- Replace orderpreview_order(account_hash, order_data)- Preview order
Example
async with AsyncClient(client_id, client_secret) as client:
await client.login()
# Get orders for account (various date formats)
orders = await client.orders.get_orders("123456789", status="FILLED")
# Get orders with date range (YYYY-MM-DD format)
orders = await client.orders.get_orders(
"123456789",
from_entered_time="2025-01-01",
to_entered_time="2025-01-31"
)
# Get orders with full ISO format (passed through as-is)
orders = await client.orders.get_orders(
"123456789",
from_entered_time="2025-01-01T09:00:00.000Z",
to_entered_time="2025-01-01T17:00:00.000Z"
)
# Mixed formats also work
orders = await client.orders.get_orders(
"123456789",
from_entered_time="2025-01-01", # YYYY-MM-DD (auto-converted)
to_entered_time="2025-01-31T23:59:59.000Z" # Full ISO (passed through)
)
# Place a market order
order_data = {
"orderType": "MARKET",
"session": "NORMAL",
"duration": "DAY",
"orderStrategyType": "SINGLE",
"orderLegCollection": [{
"instruction": "BUY",
"quantity": 100,
"instrument": {"symbol": "AAPL", "assetType": "EQUITY"}
}]
}
result = await client.orders.place_order("123456789", order_data)
Async Market Module
Async version of market data endpoints:
Methods
get_quote(symbol)- Get single quoteget_quotes(symbols)- Get multiple quotesget_movers(symbol_id, sort=None, frequency=None, params=None)- Get market moversget_option_chain(symbol, contract_type=None, ...)- Get option chainget_expiration_chain(symbol, params=None)- Get expiration datesget_markets(date=None)- Get market hoursget_market_hours(date=None)- Get market hours for date
Example
async with AsyncClient(client_id, client_secret) as client:
await client.login()
# Get quotes
quotes = await client.market.get_quotes(["AAPL", "MSFT", "GOOGL"])
# Get option chain
options = await client.market.get_option_chain(
"AAPL",
contract_type="CALL",
strike_count=5,
from_date="2025-01-01",
to_date="2025-12-31"
)
# Get market movers
movers = await client.market.get_movers("$SPX.X", sort="PERCENT_CHANGE")
Authentication (OAuth)
client.login(timeout=300, auto_open_browser=True)- Handy:
client.has_valid_token(),client.refresh_token_now(),client.logout() - Internals: adhoc HTTPS callback server (dev), code-for-token exchange, auto-refresh and notice when refresh expires.
Request & Error Handling (REST)
All REST calls use Client._request() with:
- Automatic Authorization headers
- Refresh retry on 401 (once) and immediate resend
- Retries with backoff for 429/5xx (exponential with factor 0.5)
Accounts (accounts.py)
get_account_numbers() -> List[dict]
- GET
/accounts/accountNumbers - Returns
accountNumberandhashValuepairs. - Example response:
[
{"accountNumber":"12345678","hashValue":"827C...AC12"}
]
get_accounts(params: dict|None=None) -> dict
-
GET
/accounts -
Query parameters:
fields(optional): the API currently acceptspositionsto return positions. E.g.:fields=positions.
get_account_by_id(account_hash: str, params: dict|None=None) -> dict
-
GET
/accounts/{accountNumber} -
account_hash: encrypted account identifier (hashValue). -
Query parameters:
fields(optional):positionsto include positions. E.g.:fields=positions.
find_account(last_4_digits: str) -> dict|None
- Helper that uses
get_account_numbers()and filters by the last 4 digits, then callsget_account_by_id.
get_transactions(account_hash: str, from_date: str|None, to_date: str|None, filters: dict|None=None) -> dict
-
GET
/accounts/{accountHash}/transactions -
ONE DATE REQUIRED: you may pass only
from_dateor onlyto_date. If you pass a single date, the SDK fills in the other for the same day:- 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 with00:00:00.000Zor23:59:59.000Zof the same day.
- Short format
-
Params:
-
startDate: ISO UTC -YYYY-MM-DDTHH:MM:SS.ffffffZ(or shortYYYY-MM-DD) -
endDate: ISO UTC -YYYY-MM-DDTHH:MM:SS.ffffffZ(or shortYYYY-MM-DD) -
filters: optional dict:types: string with valid types:TRADE,RECEIVE_AND_DELIVER,DIVIDEND_OR_INTEREST,ACH_RECEIPT,ACH_DISBURSEMENT,CASH_RECEIPT,CASH_DISBURSEMENT,ELECTRONIC_FUND,WIRE_OUT,WIRE_IN,JOURNALsymbol: specific symbolstatus: transaction status
Note: In some API configurations,
typesmay be considered mandatory. The SDK does not require it and treats it as an optional filter. -
Correct example:
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'),
filters={"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:
streamerSocketUrlschwabClientCustomerIdschwabClientCorrelIdSchwabClientChannelSchwabClientFunctionId
-
Useful to initialize
client.streaming(LOGIN and subscriptions).
Orders (orders.py)
All responses include HTTP metadata and the native data:
{
"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 - Date format: Accepts both
YYYY-MM-DD(auto-converted to ISO) or full ISO-8601 format. If omitted, defaults to "last 60 days". status(case-insensitive): normalized to uppercase. Values accepted by the API: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.maxResults(optional): record limit (API default 3000).- Date conversion:
YYYY-MM-DD→YYYY-MM-DDT00:00:00.000Z(start) orYYYY-MM-DDT23:59:59.000Z(end)
get_all_orders(from_entered_time=None, to_entered_time=None, status=None, max_results=None) -> dict
- GET
/orders - Date format: Accepts both
YYYY-MM-DD(auto-converted to ISO) or full ISO-8601 format. If omitted, defaults to "last 60 days". - Filters identical to
get_orders(includingstatusnormalization). maxResults(optional): record limit (API default 3000).- Date conversion:
YYYY-MM-DD→YYYY-MM-DDT00:00:00.000Z(start) orYYYY-MM-DDT23:59:59.000Z(end)
place_order(account_hash: str, order_data: dict) -> dict
- POST
/accounts/{accountNumber}/orders - Extracts
order_idfrom theLocationheader 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} - Tries to extract
order_idfromLocationif the server returns it.
replace_order(account_hash: str, order_id: str, new_order_data: dict) -> dict
- PUT
/accounts/{accountNumber}/orders/{orderId} - Returns new
order_id(fromLocation) when applicable.
preview_order(account_hash: str, order_data: dict) -> dict
- POST
/accounts/{accountNumber}/previewOrder - Tries to extract
order_idfromLocationif the server returns it.
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):
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)
get_quotes(symbols: str|List[str], params: dict|None=None) -> dict
-
GET
/quotes?symbols=... -
Parameters:
-
symbols(required): str or list of symbols separated by commas. E.g.:AAPL,AMZN,$DJI,/ESH23. -
params(optional):fields: subset of data. Values:quote,fundamental,extended,reference,regular. Default: all.indicative: boolean (true|false) to include indicative quotes (e.g., ETFs). Example:indicative=false.
-
get_quote(symbol: str, params: dict|None=None) -> dict
-
GET
/{symbol}/quotes -
Parameters:
-
symbol(required): single symbol (e.g.,TSLA). -
params(optional):fields: same as inget_quotes.
-
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 symbolcontract_type(optional): Contract Type. Available values:CALL,PUT,ALLstrike_count(optional): The Number of strikes to return above or below the at-the-money priceinclude_underlying_quote(optional): Underlying quotes to be included (boolean)strategy(optional): OptionChain strategy. Default is SINGLE. Available values:SINGLE,ANALYTICAL,COVERED,VERTICAL,CALENDAR,STRANGLE,STRADDLE,BUTTERFLY,CONDOR,DIAGONAL,COLLAR,ROLLinterval(optional): Strike interval for spread strategy chains (see strategy param)strike(optional): Strike Pricerange_type(optional): Range(ITM/NTM/OTM etc.)from_date(optional): From date (pattern: yyyy-MM-dd)to_date(optional): To date (pattern: yyyy-MM-dd)volatility(optional): Volatility to use in calculations. Applies only to ANALYTICAL strategy chainsunderlying_price(optional): Underlying price to use in calculations. Applies only to ANALYTICAL strategy chainsinterest_rate(optional): Interest rate to use in calculations. Applies only to ANALYTICAL strategy chainsdays_to_expiration(optional): Days to expiration to use in calculations. Applies only to ANALYTICAL strategy chainsexp_month(optional): Expiration month. Available values:JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC,ALLoption_type(optional): Option Typeentitlement(optional): Applicable only if its retail token, entitlement of client PP-PayingPro, NP-NonPro and PN-NonPayingPro. Available values:PN,NP,PPparams(optional): Additional query parameters
get_expiration_chain(symbol: str, params: dict|None=None) -> dict
-
GET
/expirationchain -
Parameters:
symbol(required): Underlying asset symbolparams(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, params=None) -> dict
-
GET
/pricehistory -
Parameters:
periodType:day|month|year|ytdperiod: intfrequencyType:minute|daily|weekly|monthlyfrequency: intstartDate/endDate(ms since epoch)- Additional optionals (
needExtendedHoursData, etc., depending on entitlements)
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): Index Symbol. Available values:$DJI,$COMPX,$SPX,NYSE,NASDAQ,OTCBB,INDEX_ALL,EQUITY_ALL,OPTION_ALL,OPTION_PUT,OPTION_CALLsort(optional): Sort by a particular attribute. Available values:VOLUME,TRADES,PERCENT_CHANGE_UP,PERCENT_CHANGE_DOWNfrequency(optional): To return movers with the specified directions of up or down. Available values:0,1,5,10,30,60(min). Default0params(optional): Additional query parameters
get_markets(params: dict|None=None) -> dict
-
GET
/markets -
Query parameters:
markets(required by API): array ofequity,option,bond,future,forex(the SDK acceptsparams={"markets": ...})date(optional):YYYY-MM-DD(if you send ISO, the SDK trims to date)
get_market_hours(market_id: str, params: dict|None=None) -> dict
-
GET
/markets/{market_id}(equity,option,bond,forex) -
Query parameters:
date(optional):YYYY-MM-DD(if you send ISO, the SDK trims to date)
get_instruments(symbols: str|List[str], projection: str, extra_params: dict|None=None) -> dict
-
GET
/instruments -
Parameters:
symbols(required): single symbol or comma-separated listprojection(required by API):symbol-search,symbol-regex,desc-search,desc-regex,search,fundamentalNote: the SDK does not require it in the signature, but it is recommended to provide it.
get_instrument_by_cusip(cusip_id: str, params: dict|None=None) -> dict
- GET
/instruments/{cusip_id}
WebSocket Streaming (streaming.py)
Callbacks
on_data(fn)(data frames)on_response(fn)(command confirmations/errors)on_notify(fn)(heartbeats/notices)
Basic flow
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
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
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 Forex
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)
ws.nasdaq_book(["MSFT"]) # Also: ws.nyse_book, ws.options_book
# Default fields: 0 (Symbol), 1 (BookTime), 2 (Bids), 3 (Asks)
Chart (Series)
ws.chart_equity(["AAPL"]) # 0..7: key, open, high, low, close, volume, sequence, chartTime
ws.chart_futures(["/ESZ25"]) # 0..5
Screener
ws.screener_equity(["NYSE_VOLUME_5"])
ws.screener_options(["CBOE_VOLUME_5"])
Account Activity
ws.account_activity() # Gets accountHash and subscribes
Utilities
subscribe(service, keys, fields=None)add(service, keys)unsubscribe(service, keys)/unsubscribe_service(service, keys)view(service, fields)
Option Symbol Helper
create_option_symbol(symbol, expiration, option_type, strike_price)- Creates Schwab option symbol from components
Example:
# 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 Putstrike_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,18,19,20,24,33 |
| 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 |
| SCREENER_EQUITY | 0,1,2,3,4 |
| SCREENER_OPTION | 0,1,2,3,4 |
| ACCT_ACTIVITY | 0,1,2 |
SUBS / ADD / VIEW / UNSUBS examples by service
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"])
# 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]) # 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)
- 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).
- 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.SchwabClientCustomerIdandSchwabClientCorrelId(recommended): fromuserPreference.parameters(optional): depends on service/command.
-
Notes:
SUBSoverwrites list;ADDappends;UNSUBSremoves;VIEWchangesfields.
- ADMIN (session)
-
LOGIN(service=ADMIN,command=LOGIN)- parameters:
Authorization(token without "Bearer"),SchwabClientChannel,SchwabClientFunctionId.
- parameters:
-
LOGOUT(service=ADMIN,command=LOGOUT)- parameters: empty.
- LEVEL ONE (L1 quotes)
- Common parameters:
keys(req., CSV list),fields(optional, indexes). LEVELONE_EQUITIES:keysuppercase tickers (e.g.,AAPL,TSLA).LEVELONE_OPTIONS:keysSchwab option formatRRRRRR 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:keysBASE/QUOTEpairs CSV (e.g.,EUR/USD,USD/JPY).
- BOOK (Level II)
- Services:
NYSE_BOOK,NASDAQ_BOOK,OPTIONS_BOOK. - Parameters:
keys(req., tickers),fields(optional, level indexes).
- CHART (streaming series)
CHART_EQUITY:keysequities;fieldsindexes (OHLCV, time, seq).CHART_FUTURES:keysfutures (same format as L1 futures);fieldsindexes.
- SCREENER (gainers/losers/actives)
-
Services:
SCREENER_EQUITY,SCREENER_OPTION. -
keyspatternPREFIX_SORTFIELD_FREQUENCY, e.g.,EQUITY_ALL_VOLUME_5.PREFIXexamples:$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.
- 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(or0,1,2,3per example/need).
- Server responses
- Types:
response(to your requests),notify(heartbeats),data(market flow). - Key codes:
0SUCCESS,3LOGIN_DENIED,11SERVICE_NOT_AVAILABLE,12CLOSE_CONNECTION,19REACHED_SYMBOL_LIMIT,20STREAM_CONN_NOT_FOUND,21BAD_COMMAND_FORMAT,26/27/28/29successes forSUBS/UNSUBS/ADD/VIEW.
- Delivery Types
All Sequence: everything with sequence number.Change: only changed fields (conflated).Whole: full messages with throttling.
- Best practices
- Do
LOGINand wait forcode=0beforeSUBS/ADD. - To add symbols without losing existing ones, use
ADD(notSUBS). - Change
fieldswithVIEWfor performance. - Handle
notify(heartbeats) and reconnect if they are lost. - Reuse your
SchwabClientCorrelIdduring the session. - If you see
19(symbol limit), shard loads by service/session.
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): checkAuthorizationformat (withoutBearer) and keys (spacing for options, uppercase).- Persistent REST 401: delete
schwab_tokens.jsonand re-runclient.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)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file schwab_sdk_unofficial-0.2.4.tar.gz.
File metadata
- Download URL: schwab_sdk_unofficial-0.2.4.tar.gz
- Upload date:
- Size: 73.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6713ea6787c6fe3f68da87d92ed240dc4987740a3231f561ab6eb6f4680e6502
|
|
| MD5 |
3e37f1ca7f955a5da68fcc10bf32f9ce
|
|
| BLAKE2b-256 |
d32d4355f12bcdb65cc3aa2a25084e0bbd2a25d5ed575b4f54ef2482af55d141
|
File details
Details for the file schwab_sdk_unofficial-0.2.4-py3-none-any.whl.
File metadata
- Download URL: schwab_sdk_unofficial-0.2.4-py3-none-any.whl
- Upload date:
- Size: 65.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
144e205ac80d7846f1ff0bc9c7cc4fd7dd4c67d73ab2b6625d8dc94887812693
|
|
| MD5 |
612752286f3c931d99b709b635245654
|
|
| BLAKE2b-256 |
d755a526c27ab011aeb1e6f3b31dd30a309ca2a2f8a4993f4425fcee118d7b53
|