schwaby 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schwaby-2.6.0/LICENSE +21 -0
- schwaby-2.6.0/PKG-INFO +272 -0
- schwaby-2.6.0/README.rst +209 -0
- schwaby-2.6.0/bin/schwab-generate-token.py +62 -0
- schwaby-2.6.0/bin/schwab-order-codegen.py +6 -0
- schwaby-2.6.0/schwab/__init__.py +11 -0
- schwaby-2.6.0/schwab/_optional.py +47 -0
- schwaby-2.6.0/schwab/auth.py +1016 -0
- schwaby-2.6.0/schwab/client/__init__.py +2 -0
- schwaby-2.6.0/schwab/client/asynchronous.py +62 -0
- schwaby-2.6.0/schwab/client/base.py +1406 -0
- schwaby-2.6.0/schwab/client/synchronous.py +73 -0
- schwaby-2.6.0/schwab/contrib/__init__.py +0 -0
- schwaby-2.6.0/schwab/contrib/orders.py +302 -0
- schwaby-2.6.0/schwab/contrib/util.py +27 -0
- schwaby-2.6.0/schwab/debug.py +195 -0
- schwaby-2.6.0/schwab/orders/__init__.py +6 -0
- schwaby-2.6.0/schwab/orders/common.py +400 -0
- schwaby-2.6.0/schwab/orders/equities.py +571 -0
- schwaby-2.6.0/schwab/orders/generic.py +655 -0
- schwaby-2.6.0/schwab/orders/options.py +553 -0
- schwaby-2.6.0/schwab/scripts/__init__.py +0 -0
- schwaby-2.6.0/schwab/scripts/orders_codegen.py +118 -0
- schwaby-2.6.0/schwab/streaming.py +3094 -0
- schwaby-2.6.0/schwab/utils.py +187 -0
- schwaby-2.6.0/schwab/version.py +1 -0
- schwaby-2.6.0/schwaby.egg-info/PKG-INFO +272 -0
- schwaby-2.6.0/schwaby.egg-info/SOURCES.txt +53 -0
- schwaby-2.6.0/schwaby.egg-info/dependency_links.txt +1 -0
- schwaby-2.6.0/schwaby.egg-info/requires.txt +27 -0
- schwaby-2.6.0/schwaby.egg-info/top_level.txt +2 -0
- schwaby-2.6.0/setup.cfg +28 -0
- schwaby-2.6.0/setup.py +100 -0
- schwaby-2.6.0/tests/__init__.py +0 -0
- schwaby-2.6.0/tests/auth_test.py +1641 -0
- schwaby-2.6.0/tests/client_test.py +2757 -0
- schwaby-2.6.0/tests/contrib/__init__.py +0 -0
- schwaby-2.6.0/tests/contrib/orders_test.py +935 -0
- schwaby-2.6.0/tests/contrib/util_test.py +15 -0
- schwaby-2.6.0/tests/debug_test.py +271 -0
- schwaby-2.6.0/tests/http_module_test.py +212 -0
- schwaby-2.6.0/tests/orders/__init__.py +0 -0
- schwaby-2.6.0/tests/orders/common_test.py +31 -0
- schwaby-2.6.0/tests/orders/generic_test.py +1472 -0
- schwaby-2.6.0/tests/orders/options_test.py +646 -0
- schwaby-2.6.0/tests/orders_test.py +570 -0
- schwaby-2.6.0/tests/packaging_test.py +118 -0
- schwaby-2.6.0/tests/scripts/__init__.py +0 -0
- schwaby-2.6.0/tests/scripts/generate_token_test.py +111 -0
- schwaby-2.6.0/tests/scripts/orders_codegen_test.py +533 -0
- schwaby-2.6.0/tests/streaming_test.py +8783 -0
- schwaby-2.6.0/tests/test.py +7 -0
- schwaby-2.6.0/tests/utils.py +229 -0
- schwaby-2.6.0/tests/utils_test.py +97 -0
schwaby-2.6.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Alex Golec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
schwaby-2.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schwaby
|
|
3
|
+
Version: 2.6.0
|
|
4
|
+
Summary: Unofficial Python client for the Charles Schwab API, built for systematic trading against live accounts
|
|
5
|
+
Home-page: https://github.com/Hu1kSmash/schwaby
|
|
6
|
+
Author: Alex Golec
|
|
7
|
+
Maintainer: Tom Hirt
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Documentation, https://github.com/Hu1kSmash/schwaby/blob/main/docs/index.rst
|
|
10
|
+
Project-URL: Source, https://github.com/Hu1kSmash/schwaby
|
|
11
|
+
Project-URL: Tracker, https://github.com/Hu1kSmash/schwaby/issues
|
|
12
|
+
Project-URL: Upstream, https://github.com/alexgolec/schwab-py
|
|
13
|
+
Keywords: finance trading equities bonds options research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Development Status :: 1 - Planning
|
|
19
|
+
Classifier: Natural Language :: English
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/x-rst
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: authlib>=1.8
|
|
26
|
+
Requires-Dist: httpx2>=2.12.0
|
|
27
|
+
Requires-Dist: websockets>=14.0
|
|
28
|
+
Provides-Extra: login
|
|
29
|
+
Requires-Dist: flask; extra == "login"
|
|
30
|
+
Requires-Dist: multiprocess; extra == "login"
|
|
31
|
+
Requires-Dist: psutil; extra == "login"
|
|
32
|
+
Provides-Extra: codegen
|
|
33
|
+
Requires-Dist: autopep8; extra == "codegen"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: flask; extra == "dev"
|
|
36
|
+
Requires-Dist: multiprocess; extra == "dev"
|
|
37
|
+
Requires-Dist: psutil; extra == "dev"
|
|
38
|
+
Requires-Dist: autopep8; extra == "dev"
|
|
39
|
+
Requires-Dist: callee; extra == "dev"
|
|
40
|
+
Requires-Dist: colorama; extra == "dev"
|
|
41
|
+
Requires-Dist: coverage; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest; extra == "dev"
|
|
43
|
+
Requires-Dist: requests; extra == "dev"
|
|
44
|
+
Requires-Dist: pytz; extra == "dev"
|
|
45
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
46
|
+
Requires-Dist: sphinx_rtd_theme; extra == "dev"
|
|
47
|
+
Requires-Dist: twine; extra == "dev"
|
|
48
|
+
Requires-Dist: wheel; extra == "dev"
|
|
49
|
+
Dynamic: author
|
|
50
|
+
Dynamic: classifier
|
|
51
|
+
Dynamic: description
|
|
52
|
+
Dynamic: description-content-type
|
|
53
|
+
Dynamic: home-page
|
|
54
|
+
Dynamic: keywords
|
|
55
|
+
Dynamic: license
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
Dynamic: maintainer
|
|
58
|
+
Dynamic: project-url
|
|
59
|
+
Dynamic: provides-extra
|
|
60
|
+
Dynamic: requires-dist
|
|
61
|
+
Dynamic: requires-python
|
|
62
|
+
Dynamic: summary
|
|
63
|
+
|
|
64
|
+
``schwaby``: A Charles Schwab API Client for Systematic Trading
|
|
65
|
+
===============================================================
|
|
66
|
+
|
|
67
|
+
``schwaby`` is an unofficial Python client for the Charles Schwab API, built for
|
|
68
|
+
running automated strategies against real accounts.
|
|
69
|
+
|
|
70
|
+
.. note::
|
|
71
|
+
|
|
72
|
+
**Where this came from.** ``schwaby`` began from
|
|
73
|
+
`alexgolec/schwab-py <https://github.com/alexgolec/schwab-py>`__, an excellent
|
|
74
|
+
MIT-licensed library by Alex Golec that gave this project its shape --- the endpoint
|
|
75
|
+
coverage, the order builder, the streaming field tables. He wrote the great majority of
|
|
76
|
+
the code here and his copyright and licence are unchanged.
|
|
77
|
+
|
|
78
|
+
It became a separate project for a practical reason rather than a philosophical one. This
|
|
79
|
+
client runs systematic strategies against funded accounts, and that use imposes
|
|
80
|
+
requirements a general-purpose wrapper has no particular reason to prioritise: prices that
|
|
81
|
+
are never silently altered, a token file that survives a crash mid-refresh, a stream that
|
|
82
|
+
reports what it absorbs instead of going quiet, and failures that are loud and specific
|
|
83
|
+
rather than plausible-looking. Several of those needed changes to behaviour rather than
|
|
84
|
+
additions on top.
|
|
85
|
+
|
|
86
|
+
Those changes were offered upstream first. They were not taken up, so rather than run an
|
|
87
|
+
ever-growing private patch set against someone else's release schedule, they were
|
|
88
|
+
consolidated here and this became its own project with its own release line.
|
|
89
|
+
|
|
90
|
+
**The distribution is** ``schwaby``\ **; the importable package is still** ``schwab``.
|
|
91
|
+
|
|
92
|
+
.. code-block:: shell
|
|
93
|
+
|
|
94
|
+
pip install schwaby
|
|
95
|
+
|
|
96
|
+
.. code-block:: python
|
|
97
|
+
|
|
98
|
+
import schwab
|
|
99
|
+
|
|
100
|
+
Those differ on purpose. Keeping ``schwab`` as the import makes this a drop-in
|
|
101
|
+
replacement --- a consumer changes one line of ``requirements.txt`` and nothing else.
|
|
102
|
+
|
|
103
|
+
.. warning::
|
|
104
|
+
|
|
105
|
+
**The consequence is that** ``schwaby`` **and the original** ``schwab-py`` **cannot be
|
|
106
|
+
installed together.** Both provide the ``schwab`` package, so whichever is installed
|
|
107
|
+
second silently overwrites the other's files. ``pip`` does not warn, nothing fails at
|
|
108
|
+
install time, and the first sign is behaviour from a version you did not choose. If you
|
|
109
|
+
are migrating, uninstall ``schwab-py`` first.
|
|
110
|
+
|
|
111
|
+
The plain install is three packages, because the interactive login flow lives in an
|
|
112
|
+
extra. **Install** ``schwaby[login]`` **if you call** ``easy_client`` **or**
|
|
113
|
+
``client_from_login_flow`` --- including when you already have a token file, since
|
|
114
|
+
``easy_client`` re-authenticates through the login flow once the token passes
|
|
115
|
+
``max_token_age`` (6.5 days by default). Calling either without the extra raises an
|
|
116
|
+
``ImportError`` saying so. ``schwaby[codegen]`` covers the order-code generator.
|
|
117
|
+
Notebook users need neither: there ``easy_client`` uses the manual flow.
|
|
118
|
+
|
|
119
|
+
**Bug reports and questions go to** `the issue tracker
|
|
120
|
+
<https://github.com/Hu1kSmash/schwaby/issues>`__.
|
|
121
|
+
|
|
122
|
+
.. image:: https://github.com/Hu1kSmash/schwaby/workflows/tests/badge.svg
|
|
123
|
+
:target: https://github.com/Hu1kSmash/schwaby/actions?query=workflow%3Atests
|
|
124
|
+
|
|
125
|
+
What is ``schwaby``?
|
|
126
|
+
----------------------
|
|
127
|
+
|
|
128
|
+
``schwaby`` is an unofficial wrapper around the Charles Schwab Consumer APIs.
|
|
129
|
+
It strives to be as thin and unopinionated as possible, offering an elegant
|
|
130
|
+
programmatic interface over each endpoint. Notable functionality includes:
|
|
131
|
+
|
|
132
|
+
* Login and authentication
|
|
133
|
+
* Quotes, fundamentals, and historical pricing data
|
|
134
|
+
* Options chains
|
|
135
|
+
* Streaming quotes and order book depth data
|
|
136
|
+
* Order construction, placement and management
|
|
137
|
+
* Account info
|
|
138
|
+
* Synchronous and ``asyncio`` clients over the same interface
|
|
139
|
+
|
|
140
|
+
How do I use ``schwaby``?
|
|
141
|
+
---------------------------
|
|
142
|
+
|
|
143
|
+
For a full description of ``schwaby``'s functionality, check out the
|
|
144
|
+
`documentation <https://github.com/Hu1kSmash/schwaby/blob/main/docs/index.rst>`__. Meanwhile,
|
|
145
|
+
here's a quick getting started guide:
|
|
146
|
+
|
|
147
|
+
Before you do anything, create an account and an application on the
|
|
148
|
+
`Charles Schwab developer site <https://developer.schwab.com/login>`__.
|
|
149
|
+
You'll receive an API key and app secret, which you can pass to this wrapper.
|
|
150
|
+
You'll also want to take note of your callback URI, as the login flow requires
|
|
151
|
+
it. You app must be approved by Schwab before you can use it (this can take
|
|
152
|
+
several days). You can find more detailed instructions `here
|
|
153
|
+
<https://github.com/Hu1kSmash/schwaby/blob/main/docs/getting-started.rst>`__.
|
|
154
|
+
|
|
155
|
+
Next, install ``schwaby``. Note the distribution is ``schwaby`` while the
|
|
156
|
+
import stays ``schwab`` --- ``pip install schwab-py`` fetches the *original*
|
|
157
|
+
project, which is a different and much older codebase:
|
|
158
|
+
|
|
159
|
+
.. code-block:: shell
|
|
160
|
+
|
|
161
|
+
pip install "schwaby[login]"
|
|
162
|
+
|
|
163
|
+
``[login]`` is there because the example below calls ``easy_client``, which
|
|
164
|
+
opens a browser login flow the first time it runs. Without it, the plain
|
|
165
|
+
``schwaby`` install is the three packages the library always needs. The interactive
|
|
166
|
+
login flow and the order-code generator each need an extra ---
|
|
167
|
+
``schwaby[login]`` and ``schwaby[codegen]`` --- because neither is used by a
|
|
168
|
+
program that loads its token from a file, and a bare install is twelve fewer
|
|
169
|
+
packages on a machine that places trades. Calling either without its extra says
|
|
170
|
+
so, and says what to install.
|
|
171
|
+
|
|
172
|
+
You're good to go! To demonstrate, here's how you can authenticate and fetch
|
|
173
|
+
daily historical price data for the past twenty years:
|
|
174
|
+
|
|
175
|
+
.. code-block:: python
|
|
176
|
+
|
|
177
|
+
from schwab import auth, client
|
|
178
|
+
import json
|
|
179
|
+
|
|
180
|
+
api_key = 'YOUR_API_KEY'
|
|
181
|
+
app_secret = 'YOUR_APP_SECRET'
|
|
182
|
+
callback_url = 'https://127.0.0.1:8182/'
|
|
183
|
+
token_path = '/path/to/token.json'
|
|
184
|
+
|
|
185
|
+
c = auth.easy_client(api_key, app_secret, callback_url, token_path)
|
|
186
|
+
|
|
187
|
+
r = c.get_price_history_every_day('AAPL')
|
|
188
|
+
r.raise_for_status()
|
|
189
|
+
print(json.dumps(r.json(), indent=4))
|
|
190
|
+
|
|
191
|
+
Why should I use ``schwaby``?
|
|
192
|
+
-------------------------------
|
|
193
|
+
|
|
194
|
+
Schwab's API is capable, but several corners of it are tedious to get right and
|
|
195
|
+
unforgiving when you get them wrong. ``schwaby`` takes on those corners and
|
|
196
|
+
stays out of your way everywhere else:
|
|
197
|
+
|
|
198
|
+
1. **Safe authentication.** Schwab's API supports OAuth authentication, but too
|
|
199
|
+
many people online end up rolling their own implementation of the OAuth
|
|
200
|
+
callback flow. This is both unnecessarily complex and dangerous.
|
|
201
|
+
``schwaby`` handles token fetch and refreshing for you.
|
|
202
|
+
|
|
203
|
+
2. **A usable streaming client.** Schwab's streamer is a raw websocket that
|
|
204
|
+
identifies every field by number, and the same number means different things
|
|
205
|
+
on different services --- field ``2`` is the ask price on
|
|
206
|
+
``LEVELONE_EQUITIES`` and the open price on ``CHART_EQUITY``. ``schwaby``
|
|
207
|
+
carries the field tables for all thirteen services and relabels each message
|
|
208
|
+
as it arrives, so you receive ``{'ASK_PRICE': 421.6, ...}`` rather than
|
|
209
|
+
``{'2': 421.6, ...}``. It also handles login and logout, keeps track of which
|
|
210
|
+
response belongs to which request, and lets you register a handler per
|
|
211
|
+
service rather than demultiplexing the stream yourself.
|
|
212
|
+
|
|
213
|
+
3. **Order construction Schwab will accept.** Order JSON is deeply nested, and a
|
|
214
|
+
malformed order comes back rejected with little explanation of what was
|
|
215
|
+
wrong. ``OrderBuilder`` assembles it from named parts and validates the
|
|
216
|
+
values it can, and ``schwab.orders.equities`` and ``schwab.orders.options``
|
|
217
|
+
provide ready-made templates for the common equity orders and option
|
|
218
|
+
strategies. ``schwab.contrib.orders`` runs the process backwards: hand it an
|
|
219
|
+
order you have already placed and it returns the builder that would place it
|
|
220
|
+
again.
|
|
221
|
+
|
|
222
|
+
4. **Enums rather than magic strings.** Each endpoint's legal parameter values
|
|
223
|
+
are enums on the client, so a misspelled projection or an invalid order
|
|
224
|
+
duration fails immediately in Python instead of arriving as an opaque HTTP
|
|
225
|
+
400 in the middle of a session.
|
|
226
|
+
|
|
227
|
+
5. **Minimal wrapping everywhere else.** Unlike some other API wrappers, which
|
|
228
|
+
build in lots of logic and validation, ``schwaby`` takes raw values and
|
|
229
|
+
returns the raw ``httpx2`` response, allowing you to interpret the complex API
|
|
230
|
+
responses as you see fit. Anything you can do with raw HTTP requests you can
|
|
231
|
+
do with ``schwaby``, only more easily.
|
|
232
|
+
|
|
233
|
+
The documentation linked above is worth reading even if you end up calling the
|
|
234
|
+
API directly. Schwab's own developer portal is behind a login, so for a good
|
|
235
|
+
deal of this API those pages are the most accessible description of how it
|
|
236
|
+
actually behaves.
|
|
237
|
+
|
|
238
|
+
Why should I *not* use ``schwaby``?
|
|
239
|
+
-------------------------------------
|
|
240
|
+
|
|
241
|
+
As excellent as Schwab's API is, there are a few popular features it does not
|
|
242
|
+
offer:
|
|
243
|
+
|
|
244
|
+
* While Charles Schwab owns `thinkorswim (AKA TOS)
|
|
245
|
+
<https://www.schwab.com/trading/thinkorswim/desktop>`__, this API is
|
|
246
|
+
unaffiliated with it. You can access and trade against the same accounts as
|
|
247
|
+
TOS, but some of TOS's functionality is not supported by ``schwaby``
|
|
248
|
+
* Paper trading is not supported
|
|
249
|
+
* Historical options pricing data is not available.
|
|
250
|
+
|
|
251
|
+
What else?
|
|
252
|
+
----------
|
|
253
|
+
|
|
254
|
+
Bug reports, suggestions, and patches are welcome. Submit issues
|
|
255
|
+
`here <https://github.com/Hu1kSmash/schwaby/issues>`__ and pull requests `here <https://github.com/Hu1kSmash/schwaby/pulls>`__.
|
|
256
|
+
|
|
257
|
+
If the problem is with behaviour this project shares with
|
|
258
|
+
`alexgolec/schwab-py <https://github.com/alexgolec/schwab-py>`__ and is not one of the
|
|
259
|
+
changes listed in the changelog, it is worth reporting there too --- it will help more
|
|
260
|
+
people than a report here alone.
|
|
261
|
+
|
|
262
|
+
``schwaby`` is released under the
|
|
263
|
+
`MIT license <https://github.com/Hu1kSmash/schwaby/blob/main/LICENSE>`__, and remains
|
|
264
|
+
copyright Alex Golec.
|
|
265
|
+
|
|
266
|
+
**Disclaimer:** *schwaby is an unofficial API wrapper. It is in no way
|
|
267
|
+
endorsed by or affiliated with Charles Schwab or any associated organization.
|
|
268
|
+
Make sure to read and understand the terms of service of the underlying API
|
|
269
|
+
before using this package. This authors accept no responsibility for any
|
|
270
|
+
damage that might stem from use of this package. See the LICENSE file for
|
|
271
|
+
more details.*
|
|
272
|
+
|
schwaby-2.6.0/README.rst
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
``schwaby``: A Charles Schwab API Client for Systematic Trading
|
|
2
|
+
===============================================================
|
|
3
|
+
|
|
4
|
+
``schwaby`` is an unofficial Python client for the Charles Schwab API, built for
|
|
5
|
+
running automated strategies against real accounts.
|
|
6
|
+
|
|
7
|
+
.. note::
|
|
8
|
+
|
|
9
|
+
**Where this came from.** ``schwaby`` began from
|
|
10
|
+
`alexgolec/schwab-py <https://github.com/alexgolec/schwab-py>`__, an excellent
|
|
11
|
+
MIT-licensed library by Alex Golec that gave this project its shape --- the endpoint
|
|
12
|
+
coverage, the order builder, the streaming field tables. He wrote the great majority of
|
|
13
|
+
the code here and his copyright and licence are unchanged.
|
|
14
|
+
|
|
15
|
+
It became a separate project for a practical reason rather than a philosophical one. This
|
|
16
|
+
client runs systematic strategies against funded accounts, and that use imposes
|
|
17
|
+
requirements a general-purpose wrapper has no particular reason to prioritise: prices that
|
|
18
|
+
are never silently altered, a token file that survives a crash mid-refresh, a stream that
|
|
19
|
+
reports what it absorbs instead of going quiet, and failures that are loud and specific
|
|
20
|
+
rather than plausible-looking. Several of those needed changes to behaviour rather than
|
|
21
|
+
additions on top.
|
|
22
|
+
|
|
23
|
+
Those changes were offered upstream first. They were not taken up, so rather than run an
|
|
24
|
+
ever-growing private patch set against someone else's release schedule, they were
|
|
25
|
+
consolidated here and this became its own project with its own release line.
|
|
26
|
+
|
|
27
|
+
**The distribution is** ``schwaby``\ **; the importable package is still** ``schwab``.
|
|
28
|
+
|
|
29
|
+
.. code-block:: shell
|
|
30
|
+
|
|
31
|
+
pip install schwaby
|
|
32
|
+
|
|
33
|
+
.. code-block:: python
|
|
34
|
+
|
|
35
|
+
import schwab
|
|
36
|
+
|
|
37
|
+
Those differ on purpose. Keeping ``schwab`` as the import makes this a drop-in
|
|
38
|
+
replacement --- a consumer changes one line of ``requirements.txt`` and nothing else.
|
|
39
|
+
|
|
40
|
+
.. warning::
|
|
41
|
+
|
|
42
|
+
**The consequence is that** ``schwaby`` **and the original** ``schwab-py`` **cannot be
|
|
43
|
+
installed together.** Both provide the ``schwab`` package, so whichever is installed
|
|
44
|
+
second silently overwrites the other's files. ``pip`` does not warn, nothing fails at
|
|
45
|
+
install time, and the first sign is behaviour from a version you did not choose. If you
|
|
46
|
+
are migrating, uninstall ``schwab-py`` first.
|
|
47
|
+
|
|
48
|
+
The plain install is three packages, because the interactive login flow lives in an
|
|
49
|
+
extra. **Install** ``schwaby[login]`` **if you call** ``easy_client`` **or**
|
|
50
|
+
``client_from_login_flow`` --- including when you already have a token file, since
|
|
51
|
+
``easy_client`` re-authenticates through the login flow once the token passes
|
|
52
|
+
``max_token_age`` (6.5 days by default). Calling either without the extra raises an
|
|
53
|
+
``ImportError`` saying so. ``schwaby[codegen]`` covers the order-code generator.
|
|
54
|
+
Notebook users need neither: there ``easy_client`` uses the manual flow.
|
|
55
|
+
|
|
56
|
+
**Bug reports and questions go to** `the issue tracker
|
|
57
|
+
<https://github.com/Hu1kSmash/schwaby/issues>`__.
|
|
58
|
+
|
|
59
|
+
.. image:: https://github.com/Hu1kSmash/schwaby/workflows/tests/badge.svg
|
|
60
|
+
:target: https://github.com/Hu1kSmash/schwaby/actions?query=workflow%3Atests
|
|
61
|
+
|
|
62
|
+
What is ``schwaby``?
|
|
63
|
+
----------------------
|
|
64
|
+
|
|
65
|
+
``schwaby`` is an unofficial wrapper around the Charles Schwab Consumer APIs.
|
|
66
|
+
It strives to be as thin and unopinionated as possible, offering an elegant
|
|
67
|
+
programmatic interface over each endpoint. Notable functionality includes:
|
|
68
|
+
|
|
69
|
+
* Login and authentication
|
|
70
|
+
* Quotes, fundamentals, and historical pricing data
|
|
71
|
+
* Options chains
|
|
72
|
+
* Streaming quotes and order book depth data
|
|
73
|
+
* Order construction, placement and management
|
|
74
|
+
* Account info
|
|
75
|
+
* Synchronous and ``asyncio`` clients over the same interface
|
|
76
|
+
|
|
77
|
+
How do I use ``schwaby``?
|
|
78
|
+
---------------------------
|
|
79
|
+
|
|
80
|
+
For a full description of ``schwaby``'s functionality, check out the
|
|
81
|
+
`documentation <https://github.com/Hu1kSmash/schwaby/blob/main/docs/index.rst>`__. Meanwhile,
|
|
82
|
+
here's a quick getting started guide:
|
|
83
|
+
|
|
84
|
+
Before you do anything, create an account and an application on the
|
|
85
|
+
`Charles Schwab developer site <https://developer.schwab.com/login>`__.
|
|
86
|
+
You'll receive an API key and app secret, which you can pass to this wrapper.
|
|
87
|
+
You'll also want to take note of your callback URI, as the login flow requires
|
|
88
|
+
it. You app must be approved by Schwab before you can use it (this can take
|
|
89
|
+
several days). You can find more detailed instructions `here
|
|
90
|
+
<https://github.com/Hu1kSmash/schwaby/blob/main/docs/getting-started.rst>`__.
|
|
91
|
+
|
|
92
|
+
Next, install ``schwaby``. Note the distribution is ``schwaby`` while the
|
|
93
|
+
import stays ``schwab`` --- ``pip install schwab-py`` fetches the *original*
|
|
94
|
+
project, which is a different and much older codebase:
|
|
95
|
+
|
|
96
|
+
.. code-block:: shell
|
|
97
|
+
|
|
98
|
+
pip install "schwaby[login]"
|
|
99
|
+
|
|
100
|
+
``[login]`` is there because the example below calls ``easy_client``, which
|
|
101
|
+
opens a browser login flow the first time it runs. Without it, the plain
|
|
102
|
+
``schwaby`` install is the three packages the library always needs. The interactive
|
|
103
|
+
login flow and the order-code generator each need an extra ---
|
|
104
|
+
``schwaby[login]`` and ``schwaby[codegen]`` --- because neither is used by a
|
|
105
|
+
program that loads its token from a file, and a bare install is twelve fewer
|
|
106
|
+
packages on a machine that places trades. Calling either without its extra says
|
|
107
|
+
so, and says what to install.
|
|
108
|
+
|
|
109
|
+
You're good to go! To demonstrate, here's how you can authenticate and fetch
|
|
110
|
+
daily historical price data for the past twenty years:
|
|
111
|
+
|
|
112
|
+
.. code-block:: python
|
|
113
|
+
|
|
114
|
+
from schwab import auth, client
|
|
115
|
+
import json
|
|
116
|
+
|
|
117
|
+
api_key = 'YOUR_API_KEY'
|
|
118
|
+
app_secret = 'YOUR_APP_SECRET'
|
|
119
|
+
callback_url = 'https://127.0.0.1:8182/'
|
|
120
|
+
token_path = '/path/to/token.json'
|
|
121
|
+
|
|
122
|
+
c = auth.easy_client(api_key, app_secret, callback_url, token_path)
|
|
123
|
+
|
|
124
|
+
r = c.get_price_history_every_day('AAPL')
|
|
125
|
+
r.raise_for_status()
|
|
126
|
+
print(json.dumps(r.json(), indent=4))
|
|
127
|
+
|
|
128
|
+
Why should I use ``schwaby``?
|
|
129
|
+
-------------------------------
|
|
130
|
+
|
|
131
|
+
Schwab's API is capable, but several corners of it are tedious to get right and
|
|
132
|
+
unforgiving when you get them wrong. ``schwaby`` takes on those corners and
|
|
133
|
+
stays out of your way everywhere else:
|
|
134
|
+
|
|
135
|
+
1. **Safe authentication.** Schwab's API supports OAuth authentication, but too
|
|
136
|
+
many people online end up rolling their own implementation of the OAuth
|
|
137
|
+
callback flow. This is both unnecessarily complex and dangerous.
|
|
138
|
+
``schwaby`` handles token fetch and refreshing for you.
|
|
139
|
+
|
|
140
|
+
2. **A usable streaming client.** Schwab's streamer is a raw websocket that
|
|
141
|
+
identifies every field by number, and the same number means different things
|
|
142
|
+
on different services --- field ``2`` is the ask price on
|
|
143
|
+
``LEVELONE_EQUITIES`` and the open price on ``CHART_EQUITY``. ``schwaby``
|
|
144
|
+
carries the field tables for all thirteen services and relabels each message
|
|
145
|
+
as it arrives, so you receive ``{'ASK_PRICE': 421.6, ...}`` rather than
|
|
146
|
+
``{'2': 421.6, ...}``. It also handles login and logout, keeps track of which
|
|
147
|
+
response belongs to which request, and lets you register a handler per
|
|
148
|
+
service rather than demultiplexing the stream yourself.
|
|
149
|
+
|
|
150
|
+
3. **Order construction Schwab will accept.** Order JSON is deeply nested, and a
|
|
151
|
+
malformed order comes back rejected with little explanation of what was
|
|
152
|
+
wrong. ``OrderBuilder`` assembles it from named parts and validates the
|
|
153
|
+
values it can, and ``schwab.orders.equities`` and ``schwab.orders.options``
|
|
154
|
+
provide ready-made templates for the common equity orders and option
|
|
155
|
+
strategies. ``schwab.contrib.orders`` runs the process backwards: hand it an
|
|
156
|
+
order you have already placed and it returns the builder that would place it
|
|
157
|
+
again.
|
|
158
|
+
|
|
159
|
+
4. **Enums rather than magic strings.** Each endpoint's legal parameter values
|
|
160
|
+
are enums on the client, so a misspelled projection or an invalid order
|
|
161
|
+
duration fails immediately in Python instead of arriving as an opaque HTTP
|
|
162
|
+
400 in the middle of a session.
|
|
163
|
+
|
|
164
|
+
5. **Minimal wrapping everywhere else.** Unlike some other API wrappers, which
|
|
165
|
+
build in lots of logic and validation, ``schwaby`` takes raw values and
|
|
166
|
+
returns the raw ``httpx2`` response, allowing you to interpret the complex API
|
|
167
|
+
responses as you see fit. Anything you can do with raw HTTP requests you can
|
|
168
|
+
do with ``schwaby``, only more easily.
|
|
169
|
+
|
|
170
|
+
The documentation linked above is worth reading even if you end up calling the
|
|
171
|
+
API directly. Schwab's own developer portal is behind a login, so for a good
|
|
172
|
+
deal of this API those pages are the most accessible description of how it
|
|
173
|
+
actually behaves.
|
|
174
|
+
|
|
175
|
+
Why should I *not* use ``schwaby``?
|
|
176
|
+
-------------------------------------
|
|
177
|
+
|
|
178
|
+
As excellent as Schwab's API is, there are a few popular features it does not
|
|
179
|
+
offer:
|
|
180
|
+
|
|
181
|
+
* While Charles Schwab owns `thinkorswim (AKA TOS)
|
|
182
|
+
<https://www.schwab.com/trading/thinkorswim/desktop>`__, this API is
|
|
183
|
+
unaffiliated with it. You can access and trade against the same accounts as
|
|
184
|
+
TOS, but some of TOS's functionality is not supported by ``schwaby``
|
|
185
|
+
* Paper trading is not supported
|
|
186
|
+
* Historical options pricing data is not available.
|
|
187
|
+
|
|
188
|
+
What else?
|
|
189
|
+
----------
|
|
190
|
+
|
|
191
|
+
Bug reports, suggestions, and patches are welcome. Submit issues
|
|
192
|
+
`here <https://github.com/Hu1kSmash/schwaby/issues>`__ and pull requests `here <https://github.com/Hu1kSmash/schwaby/pulls>`__.
|
|
193
|
+
|
|
194
|
+
If the problem is with behaviour this project shares with
|
|
195
|
+
`alexgolec/schwab-py <https://github.com/alexgolec/schwab-py>`__ and is not one of the
|
|
196
|
+
changes listed in the changelog, it is worth reporting there too --- it will help more
|
|
197
|
+
people than a report here alone.
|
|
198
|
+
|
|
199
|
+
``schwaby`` is released under the
|
|
200
|
+
`MIT license <https://github.com/Hu1kSmash/schwaby/blob/main/LICENSE>`__, and remains
|
|
201
|
+
copyright Alex Golec.
|
|
202
|
+
|
|
203
|
+
**Disclaimer:** *schwaby is an unofficial API wrapper. It is in no way
|
|
204
|
+
endorsed by or affiliated with Charles Schwab or any associated organization.
|
|
205
|
+
Make sure to read and understand the terms of service of the underlying API
|
|
206
|
+
before using this package. This authors accept no responsibility for any
|
|
207
|
+
damage that might stem from use of this package. See the LICENSE file for
|
|
208
|
+
more details.*
|
|
209
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import argparse
|
|
3
|
+
import atexit
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import schwab
|
|
7
|
+
|
|
8
|
+
def main(api_key, app_secret, callback_url, token_path, requested_browser):
|
|
9
|
+
try:
|
|
10
|
+
schwab.auth.client_from_login_flow(
|
|
11
|
+
api_key, app_secret, callback_url, token_path,
|
|
12
|
+
requested_browser=requested_browser, callback_timeout=300)
|
|
13
|
+
return 0
|
|
14
|
+
except ImportError as exc:
|
|
15
|
+
# Printed, then fall through to the manual flow like any other
|
|
16
|
+
# failure. The manual flow needs no optional package, so it really is
|
|
17
|
+
# the right thing to do next -- but reporting this as "failed to fetch
|
|
18
|
+
# a token using a web browser" told the user nothing about the 'login'
|
|
19
|
+
# extra, and they would take the manual flow every time without ever
|
|
20
|
+
# learning why. Both properties matter: say what is wrong, and still
|
|
21
|
+
# get them a token.
|
|
22
|
+
#
|
|
23
|
+
# Not narrowed to the missing-extra message. import_optional re-raises
|
|
24
|
+
# an ImportError from inside an installed package unchanged, and a
|
|
25
|
+
# broken werkzeug under flask is no more a reason to refuse the manual
|
|
26
|
+
# flow than a missing one.
|
|
27
|
+
print(exc, file=sys.stderr)
|
|
28
|
+
print('Falling back to the manual flow.', file=sys.stderr)
|
|
29
|
+
except Exception:
|
|
30
|
+
# Deliberately not a bare 'except:'. That caught KeyboardInterrupt and
|
|
31
|
+
# SystemExit too, so a Ctrl-C at the login prompt fell through into the
|
|
32
|
+
# manual flow rather than quitting.
|
|
33
|
+
print('Failed to fetch a token using a web browser, falling back to '
|
|
34
|
+
'the manual flow')
|
|
35
|
+
|
|
36
|
+
schwab.auth.client_from_manual_flow(api_key, app_secret, callback_url,
|
|
37
|
+
token_path)
|
|
38
|
+
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == '__main__':
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
description='Fetch a new token and write it to a file')
|
|
45
|
+
|
|
46
|
+
required = parser.add_argument_group('required arguments')
|
|
47
|
+
required.add_argument(
|
|
48
|
+
'--token_file', required=True,
|
|
49
|
+
help='Path to token file. Any existing file will be overwritten')
|
|
50
|
+
required.add_argument('--api_key', required=True)
|
|
51
|
+
required.add_argument('--app_secret', required=True)
|
|
52
|
+
required.add_argument('--callback_url', required=True, type=str)
|
|
53
|
+
required.add_argument(
|
|
54
|
+
'--browser', required=False, type=str,
|
|
55
|
+
help='Manually specify a browser in which to start the login '+
|
|
56
|
+
'flow. See here for available options: '+
|
|
57
|
+
'https://docs.python.org/3/library/webbrowser.html#webbrowser.register')
|
|
58
|
+
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
|
|
61
|
+
sys.exit(main(args.api_key, args.app_secret, args.callback_url,
|
|
62
|
+
args.token_file, args.browser))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'''Imports for packages which only one entry point needs.
|
|
2
|
+
|
|
3
|
+
These are declared as extras in ``setup.py`` rather than hard dependencies
|
|
4
|
+
because the common case -- a long-running process which loads a token from a
|
|
5
|
+
file and streams -- never touches them, and a bare install should not pull a
|
|
6
|
+
web framework onto a machine that places trades.
|
|
7
|
+
|
|
8
|
+
Lives in its own module rather than in ``auth``, which is where it started:
|
|
9
|
+
``contrib.orders`` and the code generator need it too, and importing ``auth``
|
|
10
|
+
to reach it drags in ``authlib`` and ``httpx2`` for the sake of a six-line
|
|
11
|
+
helper.
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def import_optional(module_name, extra, needed_for):
|
|
18
|
+
'''Imports ``module_name``, or raises an ImportError which explains itself.
|
|
19
|
+
|
|
20
|
+
The failure has to name the extra: an ImportError for "flask" tells a
|
|
21
|
+
caller nothing about what to install.
|
|
22
|
+
'''
|
|
23
|
+
try:
|
|
24
|
+
return importlib.import_module(module_name)
|
|
25
|
+
except ImportError as exc:
|
|
26
|
+
# Only a genuinely absent module gets the friendly message. A package
|
|
27
|
+
# which is installed but raises ImportError while importing itself --
|
|
28
|
+
# an incompatible werkzeug under flask, a half-removed distribution --
|
|
29
|
+
# would otherwise be reported as missing, and the caller would be told
|
|
30
|
+
# to install something they already have. Let that one through as
|
|
31
|
+
# itself, which at least names the module that actually failed.
|
|
32
|
+
#
|
|
33
|
+
# Compared exactly, not by top-level package. A truncated install can
|
|
34
|
+
# raise ModuleNotFoundError for 'flask.json', and treating that as
|
|
35
|
+
# 'flask' being absent is the same misdirection one level down: the
|
|
36
|
+
# extra is installed, so installing it again fixes nothing.
|
|
37
|
+
if (not isinstance(exc, ModuleNotFoundError)
|
|
38
|
+
or getattr(exc, 'name', None) != module_name):
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
raise ImportError(
|
|
42
|
+
'{} requires the {!r} extra, which is not installed. Install '
|
|
43
|
+
'it with:\n\n'
|
|
44
|
+
' pip install "schwaby[{}]"\n\n'
|
|
45
|
+
'(missing module: {})'.format(
|
|
46
|
+
needed_for, extra, extra, module_name)) from exc
|
|
47
|
+
|