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.
Files changed (54) hide show
  1. schwaby-2.6.0/LICENSE +21 -0
  2. schwaby-2.6.0/PKG-INFO +272 -0
  3. schwaby-2.6.0/README.rst +209 -0
  4. schwaby-2.6.0/bin/schwab-generate-token.py +62 -0
  5. schwaby-2.6.0/bin/schwab-order-codegen.py +6 -0
  6. schwaby-2.6.0/schwab/__init__.py +11 -0
  7. schwaby-2.6.0/schwab/_optional.py +47 -0
  8. schwaby-2.6.0/schwab/auth.py +1016 -0
  9. schwaby-2.6.0/schwab/client/__init__.py +2 -0
  10. schwaby-2.6.0/schwab/client/asynchronous.py +62 -0
  11. schwaby-2.6.0/schwab/client/base.py +1406 -0
  12. schwaby-2.6.0/schwab/client/synchronous.py +73 -0
  13. schwaby-2.6.0/schwab/contrib/__init__.py +0 -0
  14. schwaby-2.6.0/schwab/contrib/orders.py +302 -0
  15. schwaby-2.6.0/schwab/contrib/util.py +27 -0
  16. schwaby-2.6.0/schwab/debug.py +195 -0
  17. schwaby-2.6.0/schwab/orders/__init__.py +6 -0
  18. schwaby-2.6.0/schwab/orders/common.py +400 -0
  19. schwaby-2.6.0/schwab/orders/equities.py +571 -0
  20. schwaby-2.6.0/schwab/orders/generic.py +655 -0
  21. schwaby-2.6.0/schwab/orders/options.py +553 -0
  22. schwaby-2.6.0/schwab/scripts/__init__.py +0 -0
  23. schwaby-2.6.0/schwab/scripts/orders_codegen.py +118 -0
  24. schwaby-2.6.0/schwab/streaming.py +3094 -0
  25. schwaby-2.6.0/schwab/utils.py +187 -0
  26. schwaby-2.6.0/schwab/version.py +1 -0
  27. schwaby-2.6.0/schwaby.egg-info/PKG-INFO +272 -0
  28. schwaby-2.6.0/schwaby.egg-info/SOURCES.txt +53 -0
  29. schwaby-2.6.0/schwaby.egg-info/dependency_links.txt +1 -0
  30. schwaby-2.6.0/schwaby.egg-info/requires.txt +27 -0
  31. schwaby-2.6.0/schwaby.egg-info/top_level.txt +2 -0
  32. schwaby-2.6.0/setup.cfg +28 -0
  33. schwaby-2.6.0/setup.py +100 -0
  34. schwaby-2.6.0/tests/__init__.py +0 -0
  35. schwaby-2.6.0/tests/auth_test.py +1641 -0
  36. schwaby-2.6.0/tests/client_test.py +2757 -0
  37. schwaby-2.6.0/tests/contrib/__init__.py +0 -0
  38. schwaby-2.6.0/tests/contrib/orders_test.py +935 -0
  39. schwaby-2.6.0/tests/contrib/util_test.py +15 -0
  40. schwaby-2.6.0/tests/debug_test.py +271 -0
  41. schwaby-2.6.0/tests/http_module_test.py +212 -0
  42. schwaby-2.6.0/tests/orders/__init__.py +0 -0
  43. schwaby-2.6.0/tests/orders/common_test.py +31 -0
  44. schwaby-2.6.0/tests/orders/generic_test.py +1472 -0
  45. schwaby-2.6.0/tests/orders/options_test.py +646 -0
  46. schwaby-2.6.0/tests/orders_test.py +570 -0
  47. schwaby-2.6.0/tests/packaging_test.py +118 -0
  48. schwaby-2.6.0/tests/scripts/__init__.py +0 -0
  49. schwaby-2.6.0/tests/scripts/generate_token_test.py +111 -0
  50. schwaby-2.6.0/tests/scripts/orders_codegen_test.py +533 -0
  51. schwaby-2.6.0/tests/streaming_test.py +8783 -0
  52. schwaby-2.6.0/tests/test.py +7 -0
  53. schwaby-2.6.0/tests/utils.py +229 -0
  54. 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
+
@@ -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,6 @@
1
+ #!/usr/bin/env python
2
+ from schwab.scripts.orders_codegen import latest_order_main
3
+
4
+ if __name__ == '__main__':
5
+ import sys
6
+ sys.exit(latest_order_main(sys.argv[1:]))
@@ -0,0 +1,11 @@
1
+ from . import auth
2
+ from . import client
3
+ #from . import contrib
4
+ from . import debug
5
+ from . import orders
6
+ from . import streaming
7
+ from . import utils
8
+
9
+ from .version import version as __version__
10
+
11
+ LOG_REDACTOR = debug.LogRedactor()
@@ -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
+