schwab-py

repository·main·Indexed 17 days ago

https://github.com/alexgolec/schwab-py

An unofficial Python wrapper for the Charles Schwab Consumer APIs, providing a thin, unopinionated interface for trading and market data. It includes tools for OAuth2 authentication such as easy_client and the schwab-generate-token.py CLI, automatic token refreshing, and support for both synchronous and asyncio clients.

Tokens
14.2K
Snippets
34
Records
81
Agent score
66%

What's inside schwab-py

  1. Overview of schwab-py functionality

    main

    schwab-py is an unofficial, thin, and unopinionated wrapper for the Charles Schwab Consumer APIs. It provides a programmatic interface for:

    • Authentication: Safe OAuth handling, including token fetch and refreshing.
    • Market Data: Quotes, fundamentals, and historical pricing data.
    • Options: Access to options chains.
    • Streaming: Streaming quotes and order book depth data.
    • Trading: Trades and trade management.
    • Accounts: Access to account information.

    Note that this library is not affiliated with thinkorswim (TOS). While you can trade against the same accounts, some TOS-specific functionality is not supported. Additionally, paper trading and historical options pricing data are currently unavailable.

  2. Handle API return values and errors

    main

    All methods return an httpx.Response object. To handle responses and errors correctly, use the following pattern:

    1. Check status_code to ensure the request was successful.
    2. Use r.raise_for_status() to raise an exception if the response indicates an error.
    3. Use r.json() to access the returned data as pure Python data structures.

    You can convert the resulting JSON data into a pandas DataFrame using pandas.DataFrame.from_dict().

    r = client.some_endpoint()
    assert r.status_code == 200, r.raise_for_status()
    data = r.json()
  3. Map account numbers to account hashes

    main

    The Schwab API does not accept raw account numbers for most methods; instead, it requires account hashes. You must use the get_account_numbers() method to retrieve a mapping of raw account numbers to their corresponding hashValue.

    Example mapping structure:

    [
      {
        "accountNumber": "123456789",
        "hashValue": "123ABCXYZ"
      }
    ]
    import httpx
    from schwab.auth import easy_client
    from schwab.orders.equities import equity_buy_market
    
    # ... setup client ...
    c = easy_client(
            token_path='/path/to/token.json',
            api_key='api-key',
            app_secret='app-secret',
            callback_url='https://callback.com',
            webdriver_func=make_webdriver)
    
    # 1. Fetch the account numbers/hashes
    resp = c.get_account_numbers()
    assert resp.status_code == httpx.codes.OK
    
    # 2. Extract the hash for the desired account
    # Note: This example takes the first available hash
    account_hash = resp.json()[0]['hashValue']
    
    # 3. Use the hash in subsequent API calls
    c.place_order(account_hash, equity_buy_market('AAPL', 1))
  4. Create Composite Orders (OCO and FIFO)

    main

    The schwab.orders.common module provides utility methods for complex multi-order strategies like "One Cancels Other" (OCO) and "First Triggers Second" (FIFO).

    Critical Usage Rule: Do not pass already-executed orders to these methods. You must pass the OrderBuilder objects returned by the templates before they are built/placed. Passing orders that have already been placed via client.place_order() will result in both constituent orders being executed immediately, causing the composite order attempt to fail.

    Available Methods:

    • one_cancels_other(order_one, order_two)
    • first_triggers_second(order_one, order_two)
    from schwab.orders.common import one_cancels_other, first_triggers_second
    from schwab.orders.options import option_buy_to_open_limit, option_sell_to_close_limit
    
    # CORRECT: Pass the builders, not the results of place_order()
    composite_order = first_triggers_second(
        option_buy_to_open_limit(trade_symbol, contracts, safety_ask),
        option_sell_to_close_limit(trade_symbol, half, double)
    )
    
    client.place_order(account_id, composite_order.build())
  5. Handle token expiration and refreshing

    main

    Schwab tokens consist of an access token (valid for 30 minutes) and a refresh token (valid for 7 days).

    • Automatic Refreshing: schwab-py automatically manages the refresh process. If it detects an expired access token, it uses the refresh token to get a new one invisibly.
    • The 7-Day Limit: Refresh tokens expire after 7 days. Once this happens, you will receive an error and must delete your old token file and create a new one via the login flow.
    • Proactive Management: You can check the age of your token using Client.token_age. A common pattern is to recreate the token on Sunday before markets open to ensure a fresh week of trading.
  6. Use the Utils class for miscellaneous utilities

    main

    The schwab.utils.Utils class provides miscellaneous utility methods for interacting with the Schwab API. To use these utilities, you must instantiate the Utils class by passing your Client instance and the account_hash for the specific account you are working with.

    from schwab.utils import Utils
    
    # Initialize the utility class
    utils = Utils(client, account_hash)
  7. Subscribe, add, and unsubscribe from streams

    main

    Stream operations follow specific naming patterns. Note that these methods are not thread-safe and should be called in series.

    • Subscribing: Use <SERVICE_NAME>_subs(symbols) to enable a data stream. It is recommended not to call a subscription function more than once for a given stream, as behavior for multiple calls is undocumented and may clear previous subscriptions.
    • Adding Symbols: Use <SERVICE_NAME>_add(symbols) to add symbols to an existing subscription. This is supported by certain services like equity_charts and futures_charts.
    • Unsubscribing: Use <SERVICE_NAME>_unsubs(symbols) to disable streaming for specific symbols. Note that symbols not explicitly unsubscribed may remain subscribed.
  8. Handle changes to symbol formats

    main

    Two major symbol format changes have occurred during the transition to Schwab:

    1. Options Symbols: The format has changed from TDAmeritrade. Use the schwab-py helper class (see options_symbols documentation) to parse and generate these symbols correctly.
    2. Equity Index Symbols: TDAmeritrade used a .X suffix (e.g., $SPX.X for S&P 500). Schwab uses the symbol without the suffix (e.g., $SPX).
  9. Manage shorter token lifetimes

    main

    Schwab enforces much shorter token lifetimes than TDAmeritrade. While tda-api tokens could effectively last indefinitely via refresh mechanisms, Schwab tokens are valid for only seven days.

    After seven days, tokens must be deleted and regenerated using the standard login flow. To avoid service interruptions during trading hours, it is recommended to preemptively cycle your token on Sundays before the market opens.

  10. Best practices for managing token files

    main

    The library manages token creation and refreshing automatically. To avoid parsing failures or errors, follow these rules:

    1. Never create the token file manually. If you don't have a token, pass a non-existent file path to client_from_login_flow or easy_client. These methods will trigger the login flow and create the file for you.
    2. Never modify the token file. Modifying the file will likely break the authentication logic.
    3. Never share the token file. Sharing a token file between different applications can cause race conditions where one application locks the other out of refreshing.

    Security Warning: Treat your token file as sensitively as your Schwab username and password. Never share it with anyone, including library developers.

  11. Understand data field relabeling

    main

    The raw Schwab API returns JSON objects where data fields are represented by numerical keys (e.g., "1": 421.65).

    schwab-py automatically relabels these numerical keys into human-readable strings (e.g., "OPEN_PRICE": 421.65) to make the data easier to work with.

    To find the specific string mappings for a service, investigate the enum classes in the library that end with ***Fields (e.g., ChartEquityFields).

    // Raw API format (Numerical keys)
    {
      "service": "CHART_EQUITY",
      "content": [{
        "key": "MSFT",
        "1": 779,
        "2": 421.65
      }]
    }
    
    // schwab-py relabeled format
    {
      "service": "CHART_EQUITY",
      "content": [{
        "key": "MSFT",
        "SEQUENCE": 779,
        "OPEN_PRICE": 421.65
      }]
    }
  12. Understand the structure of Schwab order specifications

    main

    The Client.place_order() method requires a complex JSON object. While schwab-py provides templates for common trades, understanding the underlying structure is necessary for advanced users creating complex multi-leg or multi-asset orders.

    Key components of an order specification include:

    • orderType: e.g., LIMIT, STOP_LIMIT.
    • orderStrategyType: e.g., SINGLE (for non-composite orders), TRIGGER (to hold an order until a condition is met), or OCO (One-Cancels-Other).
    • orderLegCollection: A list of legs containing instruction (BUY/SELL), instrument (with assetType and symbol), and quantity.
    • price: Specified at the top level of the order, not inside the individual legs. This is critical for composite options orders.
    • childOrderStrategies: Used for nested or conditional orders (like OCO or TRIGGER) that are activated based on the execution of a parent order.
    {
        "session": "NORMAL",
        "duration": "DAY",
        "orderType": "LIMIT",
        "price": "190.90",
        "orderLegCollection": [
            {
                "instruction": "BUY",
                "instrument": {
                    "assetType": "EQUITY",
                    "symbol": "MSFT"
                },
                "quantity": 1
            }
        ],
        "orderStrategyType": "SINGLE"
    }