Skip to content

almost finished schwab, testing #742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 189 additions & 85 deletions docsrc/brokers.schwab.rst
Original file line number Diff line number Diff line change
@@ -1,108 +1,212 @@
Schwab API Setup
===============
Schwab
======

Charles Schwab provides API access for automated trading and market data through its developer platform. This guide outlines how to set up your environment variables and complete the initial OAuth login process, which works seamlessly with any strategy once configured.
Charles Schwab is a major US brokerage that now offers a modern Trader API for equities, options, and **futures *data* only (no trade endpoints yet)**. Lumibot supports Schwab for both live trading and data retrieval, using the official `schwab-py` library.

.. note::
Schwab API access requires a developer account and application approval. You must apply for API access and set up your app in the Schwab Developer Portal.

API Credentials
---------------

To use Schwab with Lumibot, you need to set the following environment variables in your `.env` file:

.. list-table:: Schwab API Credentials
:widths: 30 50 20
:header-rows: 1

* - **Variable**
- **Description**
- **Example**
* - `SCHWAB_API_KEY`
- Your Schwab API key (Consumer Key) from the Developer Portal.
- `abc123xyz`
* - `SCHWAB_SECRET`
- Your Schwab API secret.
- `supersecret`
* - `SCHWAB_ACCOUNT_NUMBER`
- Your Schwab brokerage account number.
- `12345678`
* - `SCHWAB_REDIRECT_URI`
- (Optional) OAuth2 callback URI (default: `https://127.0.0.1:8182`)
- `https://yourdomain.com/callback`
* - `TRADING_BROKER`
- (Optional) Set to `schwab` to force Schwab as the broker.
- `schwab`
* - `SCHWAB_TOKEN_PATH`
- (Optional) Path to store OAuth token.json (default: strategy dir).
- `./token.json`

Creating an App & Getting Keys
------------------------------

1. Register on the `Schwab Developer Portal <https://developer.schwab.com/>`_.
2. Go to **Dashboard → Apps → Create App**.
3. Enter an app name and a **Callback URL** (must be HTTPS, ≤ 256 chars, matches exactly).
4. Request the **Trader API** product, accept terms, and submit.
5. Wait for manual approval (typically 1–3 business days).
6. Once approved, copy your **API Key (Consumer Key)** and **API Secret** from the app details.

OAuth2 Authentication Flow
--------------------------

Before proceeding, ensure you have created a developer account at `developer.schwab.com <https://developer.schwab.com>`_ and registered an application to obtain your API credentials.

Developer Account and Application Setup
--------------------------------------

To use the Schwab API, you first need to create a developer account and register an application. This process will provide you with an **API Key** and **API Secret**, which are essential for authentication.

1. **Create a Developer Account**

- Visit `developer.schwab.com <https://developer.schwab.com>`_ and click **Sign Up**.
- Follow the instructions to create your account.
- Log in to access your dashboard.

2. **Create and Configure an Application**

- In your Schwab developer dashboard, navigate to the **Applications** section and click **Create Application**.
- Fill in the required fields (app name, description, API Product, etc.). We recommend selecting **Accounts and Trading Production** unless you have a specific reason to choose otherwise.
- Specify an **Order Limit** (the default is often 120 orders per minute).
- Set your **Callback URL** to:

``https://127.0.0.1:8182``

.. important::
Do not include a trailing slash.

- Submit your application and wait for it to be approved (the status should eventually change to **Ready For Use**).
- Once approved, retrieve your **API Key** and **API Secret** from the application details.

Environment Variables Setup
Schwab uses OAuth2 for authentication. The first time you run your strategy, a browser window will open for you to log in and approve access. A `token.json` file will be created in your strategy directory (or at `SCHWAB_TOKEN_PATH` if set).

- **Access tokens** last 30 minutes; **refresh tokens** last 7 days.
- The `schwab-py` library will auto-refresh tokens as needed.
- If running on a server, run the login flow once locally and copy `token.json` to the server.
- For headless/cloud environments, use the CLI/manual login helper (`schwab.auth.client_from_manual_flow`), which prints a URL to paste into any browser.
- Keep `token.json` secure and out of version control.
- If you delete or move `token.json`, you will need to re-authorize.

.. warning::
If your refresh token expires (after 7 days without re-auth), you must repeat the browser login flow.

**One-click cloud login (Replit · Render · any PaaS)**

Deploy your bot and watch the logs.
The first time Lumibot detects that `token.json` is missing, it prints a URL like:

https://my-bot.repl.co/schwab-login

1. Open that link once, sign in to Schwab, click **Allow**.
2. You’ll see “✅ Schwab token saved”.
3. Restart the service (or let Replit auto-restart). Done.

As long as your bot checks Schwab at least once per day, the token
auto-refreshes and you will *not* be asked to log in again.
If the service is stopped for 7+ days, redeploy and repeat the link.

(Optional) override the callback route with
`SCHWAB_REDIRECT_URI=https://YOUR_DOMAIN/schwab-login`.

Sandbox vs Production
---------------------

Schwab offers a **Sandbox** environment for safe testing with synthetic accounts and data.

- Enable Sandbox when creating your app, or promote your app later in the Developer Portal.
- Use the same credentials; only the API base URL changes.
- Use separate apps for production and sandbox to avoid confusion.

Supported Assets & Order Types
------------------------------

.. list-table:: Supported Asset Classes and Order Types
:widths: 20 15 15 15 15 20
:header-rows: 1

* - **Asset**
- **Market**
- **Limit**
- **Stop**
- **Stop-Limit**
- **Advanced (OCO/Bracket)**
* - Stocks/ETFs
- ✔
- ✔
- ✔
- ✔
- ✖ (not yet)
* - Options
- ✔ (buy/sell, open/close)
- ✔
- —
- —
- ✖ (not yet)
* - Futures
- ✖ (quotes only)
- ✖
- ✖
- ✖
- ✖

- Multi-leg/spread options and advanced orders are not yet implemented in Lumibot.
- **Futures trading is not supported; only streaming quotes are available.**

Market Data
-----------

- Real-time quotes, option chains, and historical bars (up to 15 years daily, 6 months intraday for equities/options).
- **Level-I/II streaming quotes are available for equities, options, and futures; historical bars only for equities/ETFs.**
- No extra entitlements required for individual developers.
- Futures quotes available; historical futures bars not yet supported.

Rate Limits & Token Expiry
--------------------------

For Schwab API integration, you must set the following environment variables in your ``.env`` file (located in the same directory as your strategy). These variables allow your application to authenticate with Schwab's API seamlessly with any strategy you use.

.. list-table:: Schwab Configuration
:widths: 25 50 25
:header-rows: 1

* - **Variable**
- **Description**
- **Example**
* - SCHWAB_API_KEY
- Your Schwab API key obtained from the developer dashboard.
- your_api_key_here
* - SCHWAB_SECRET
- Your Schwab API secret obtained from the developer dashboard.
- your_api_secret_here
* - SCHWAB_ACCOUNT_NUMBER
- Your Schwab account number used for trading.
- 123456789
- **~120 requests/minute** for data; **2–4 trade requests/sec**.
- Exceeding limits returns HTTP 429 errors.
- Error codes: `429-001` = rate, `429-005` = burst; back-off 60 seconds if hit.
- Access tokens expire after 30 minutes; refresh tokens after 7 days.

.. important::

Double-check that the API key, secret, and callback URL are entered exactly as specified on the Schwab developer portal to avoid authentication issues.
Known Issues & Best Practices
-----------------------------

OAuth Process and Token Creation
-------------------------------
- Initial OAuth requires browser login every 7 days.
- `token.json` must be unique per account/app.
- Advanced orders (OCO/OTO/Bracket) not yet supported.
- Callback URL must match exactly (including trailing slash).
- Refresh tokens proactively (every 28–29 min) to avoid expiry.
- Secure `token.json` (chmod 600) and rotate secrets regularly.
- Use separate apps for sandbox and production.
- **Attempting to place a futures order returns HTTP 400 “Unsupported instrument”.**
- **No official docs for futures endpoints—implementation subject to change.**

When you run your trading application for the first time, an OAuth flow will be initiated to securely log you into your Schwab account. During this process, you will see output in your terminal similar to:
Example Strategy
----------------

.. code-block:: text
You can provide your Schwab credentials in several ways:
- By creating a `.env` file in the same directory as your strategy (recommended for local development).
- By setting them as secrets in Replit, or as environment variables in cloud platforms like Render.
- By exporting them as environment variables in your shell.

This is the browser-assisted login and token creation flow for
schwab-py. This flow automatically opens the login page on your
browser, captures the resulting OAuth callback, and creates a token
using the result. The authorization URL is:
**Example `.env` file:**

https://api.schwabapi.com/v1/oauth/authorize?response_type=code&client_id=RfUVxotUc8p6CbeCwFmophgNZSat0TLv&redirect_uri=https%3A%2F%2F127.0.0.1%3A8182&state=6pYvtte5gHRZKXRyrQjkjHNIYuO2Ra
.. code-block:: bash

IMPORTANT: Your browser will give you a security warning about an
invalid certificate prior to issuing the redirect. This is because
schwab-py has started a server on your machine to receive the OAuth
redirect using a self-signed SSL certificate. You can ignore that
warning, but make sure to first check that the URL matches your
callback URL (ignoring URL parameters). As a reminder, your callback URL
is:
# .env
TRADING_BROKER=schwab
SCHWAB_API_KEY=YOUR_APP_KEY
SCHWAB_SECRET=YOUR_APP_SECRET
SCHWAB_ACCOUNT_NUMBER=XXXXXXXX

https://127.0.0.1:8182
Then, create your `main.py` (or `strategy.py`) file:

See here to learn more about self-signed SSL certificates:
https://schwab-py.readthedocs.io/en/latest/auth.html#ssl-errors
.. code-block:: python

If you encounter any issues, see here for troubleshooting:
https://schwab-py.readthedocs.io/en/latest/auth.html#troubleshooting
from lumibot.traders import Trader
from lumibot.strategies.strategy import Strategy

class MyStrategy(Strategy):
def initialize(self):
self.sleeptime = "1D"
self.symbol = "SPY"

Press ENTER to open the browser. Note you can call this method with interactive=False to skip this input.
def on_trading_iteration(self):
last = self.get_last_price(self.symbol)
self.log_message(f"Last price for {self.symbol}: {last}")
asset = self.create_asset(self.symbol)
order = self.create_order(asset, 1, "buy")
self.submit_order(order)

After completing the OAuth flow:
trader = Trader()
strategy = MyStrategy()
trader.add_strategy(strategy)
trader.run_all()

- A ``token.json`` file will be created and saved on your system. This file stores your login details (access tokens) so that you do not need to complete the OAuth process every time you run your application.
- Ensure you keep this file secure, as it contains sensitive authentication details.
Support & Contact
-----------------

Summary
------
- Schwab Developer Portal: https://developer.schwab.com/
- API Documentation: https://schwab-py.readthedocs.io/
- Support: Developer Portal → Support → Create Ticket, or email api-development@schwab.com

1. **Environment Variables**: Set ``SCHWAB_API_KEY``, ``SCHWAB_SECRET``, and ``SCHWAB_ACCOUNT_NUMBER`` in your ``.env`` file.
2. **OAuth Flow**: On the first run, you'll complete a browser-assisted login process. A ``token.json`` file will be created to store your session tokens.
3. **Callback URL**: Use ``https://127.0.0.1:8182`` exactly as specified when creating your application.
.. note::
For advanced usage and troubleshooting, see the `schwab-py documentation <https://schwab-py.readthedocs.io/>`_ and the Lumibot source code for `Schwab` broker and `SchwabData` data source.

.. important::
The example above shows what *our strategy* did in a sandbox environment; it is **not** investment advice.

By following these steps, your Schwab API integration should be up and running with any trading strategy you choose to deploy. Happy trading!
.. disclaimer::
This integration is for educational purposes only. Please consult with a financial advisor before using any trading strategy with real funds.
46 changes: 42 additions & 4 deletions lumibot/brokers/schwab.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
import re
from datetime import datetime, timedelta
from pytz import timezone
import threading
from threading import Thread
from flask import Flask

from termcolor import colored
from lumibot.brokers import Broker
from lumibot.entities import Order, Asset, Position
from lumibot.data_sources import SchwabData

# Import Schwab specific libraries
from schwab.auth import easy_client
from schwab.auth import easy_client, client_from_login_flow
from schwab.client import Client
from schwab.streaming import StreamClient

# Import PollingStream class
from lumibot.trading_builtins import PollingStream
from threading import Thread # Add this import

class Schwab(Broker):
"""
Expand Down Expand Up @@ -81,13 +83,22 @@ def __init__(
logging.error(colored("Missing Schwab API credentials. Ensure SCHWAB_API_KEY, SCHWAB_SECRET, and SCHWAB_ACCOUNT_NUMBER are set in .env file.", "red"))
raise ValueError("Missing Schwab API credentials")

# Smart redirect URI detection
redirect_uri = (
os.getenv("SCHWAB_REDIRECT_URI") or
(f"https://{os.getenv('REPLIT_APP_URL')}/schwab-login"
if os.getenv('REPLIT_APP_URL') else None) or
(f"https://{os.getenv('RENDER_EXTERNAL_URL')}/schwab-login"
if os.getenv('RENDER_EXTERNAL_URL') else None) or
'https://127.0.0.1:8182'
)
# Get the current folder for token path
current_folder = os.path.dirname(os.path.realpath(__file__))
token_path = os.path.join(current_folder, 'token.json')

try:
# Create Schwab API client
self.client = easy_client(api_key, secret, 'https://127.0.0.1:8182', token_path)
self.client = easy_client(api_key, secret, redirect_uri, token_path)
self._ensure_cloud_login(redirect_uri, token_path)

# Get account numbers and find the hash value for the specified account number
response = self.client.get_account_numbers()
Expand Down Expand Up @@ -1623,3 +1634,30 @@ def sync_positions(self, strategy):
self._filled_positions.append(position)

logging.debug(f"Synchronized {len(new_positions)} positions for strategy {strategy_name if strategy_name else 'None'}")

def _ensure_cloud_login(self, redirect_uri: str, token_path: str):
"""Spin up a one-time /schwab-login web route if token.json is missing."""
if os.path.exists(token_path):
return

app = Flask("schwab-login")

@app.route("/schwab-login")
def schwab_login():
client_from_login_flow(
api_key = self.client.api_key,
app_secret = self.client.app_secret,
callback_url = redirect_uri,
token_path = token_path,
interactive = False
)

This comment was marked as resolved.

return "✅ Schwab token saved. You can close this tab."

logging.info(
colored(f"[Schwab] First-time setup: open {redirect_uri} "
"in your browser, complete login, then restart the bot.", "green")
)
threading.Thread(
target=lambda: app.run(host="0.0.0.0", port=8080, debug=False),
daemon=True
).start()
4 changes: 2 additions & 2 deletions lumibot/strategies/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@ def get_portfolio_value(self):
float
The current portfolio value, which is the sum of the cash and net equity. This is the total value of your account, which is the amount of money you would have if you sold all your assets and closed all your positions. For crypto assets, this is the total value of your account in the quote asset (eg. USDT if that is your quote asset).
"""
return self.portfolio_value
return self._portfolio_value

def get_cash(self):
"""Get the current cash value in your account.
Expand All @@ -1107,7 +1107,7 @@ def get_cash(self):
float
The current cash value. This is the amount of cash you have in your account, which is the amount of money you can use to buy assets. For crypto assets, this is the amount of the quote asset you have in your account (eg. USDT if that is your quote asset).
"""
return self.cash
return self._cash

def get_positions(self, include_cash_positions: bool = False):
"""Get all positions for the account.
Expand Down
Loading
Loading