annotate schwab_trader/schwab_client.py @ 242:543df0fe7168

[tools] Add full HLS player support
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 13:14:41 -0700
parents eb8b4230fdb9
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
220
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
1 from __future__ import annotations
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
2
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
3 import base64
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
4 import json
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
5 import os
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
6 import stat
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
7 import time
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
8 import urllib.error
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
9 import urllib.parse
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
10 import urllib.request
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
11 from dataclasses import dataclass
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
12 from pathlib import Path
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
13 from typing import Any
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
14
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
15
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
16 AUTH_URL = "https://api.schwabapi.com/v1/oauth/authorize"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
17 TOKEN_URL = "https://api.schwabapi.com/v1/oauth/token"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
18 TRADER_BASE_URL = "https://api.schwabapi.com/trader/v1"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
19 DEFAULT_TOKEN_FILE = "~/.config/zenbu/schwab_tokens.json"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
20
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
21
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
22 class SchwabError(RuntimeError):
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
23 pass
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
24
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
25
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
26 @dataclass(frozen=True)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
27 class SchwabConfig:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
28 app_key: str
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
29 app_secret: str
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
30 redirect_uri: str
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
31 token_file: Path
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
32
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
33 @classmethod
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
34 def from_env(cls) -> "SchwabConfig":
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
35 app_key = os.environ.get("SCHWAB_APP_KEY", "").strip()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
36 app_secret = os.environ.get("SCHWAB_APP_SECRET", "").strip()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
37 redirect_uri = os.environ.get("SCHWAB_REDIRECT_URI", "").strip()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
38 token_file = Path(os.environ.get("SCHWAB_TOKEN_FILE", DEFAULT_TOKEN_FILE)).expanduser()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
39
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
40 missing = [
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
41 name
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
42 for name, value in (
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
43 ("SCHWAB_APP_KEY", app_key),
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
44 ("SCHWAB_APP_SECRET", app_secret),
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
45 ("SCHWAB_REDIRECT_URI", redirect_uri),
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
46 )
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
47 if not value
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
48 ]
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
49 if missing:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
50 raise SchwabError("Missing required environment variables: " + ", ".join(missing))
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
51
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
52 return cls(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
53 app_key=app_key,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
54 app_secret=app_secret,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
55 redirect_uri=redirect_uri,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
56 token_file=token_file,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
57 )
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
58
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
59
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
60 @dataclass(frozen=True)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
61 class ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
62 status: int
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
63 headers: dict[str, str]
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
64 body: Any
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
65 raw_body: str
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
66
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
67
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
68 def build_authorization_url(app_key: str, redirect_uri: str, state: str | None = None) -> str:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
69 params = {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
70 "response_type": "code",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
71 "client_id": app_key,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
72 "redirect_uri": redirect_uri,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
73 }
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
74 if state:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
75 params["state"] = state
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
76 return AUTH_URL + "?" + urllib.parse.urlencode(params)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
77
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
78
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
79 def extract_authorization_code(code_or_url: str) -> str:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
80 value = code_or_url.strip()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
81 if not value:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
82 raise SchwabError("Authorization code is empty")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
83
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
84 parsed = urllib.parse.urlparse(value)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
85 if parsed.scheme and parsed.netloc:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
86 query = urllib.parse.parse_qs(parsed.query)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
87 codes = query.get("code")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
88 if not codes or not codes[0]:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
89 raise SchwabError("No code= parameter found in callback URL")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
90 return codes[0]
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
91
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
92 return urllib.parse.unquote(value)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
93
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
94
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
95 def exchange_code_for_tokens(config: SchwabConfig, code_or_url: str) -> dict[str, Any]:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
96 code = extract_authorization_code(code_or_url)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
97 return _token_request(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
98 config,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
99 {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
100 "grant_type": "authorization_code",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
101 "code": code,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
102 "redirect_uri": config.redirect_uri,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
103 },
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
104 )
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
105
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
106
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
107 def refresh_tokens(config: SchwabConfig, refresh_token: str | None = None) -> dict[str, Any]:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
108 token_value = refresh_token
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
109 if token_value is None:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
110 existing = load_tokens(config.token_file)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
111 token_value = existing.get("refresh_token")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
112 if not token_value:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
113 raise SchwabError("No refresh token available")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
114
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
115 return _token_request(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
116 config,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
117 {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
118 "grant_type": "refresh_token",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
119 "refresh_token": token_value,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
120 },
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
121 )
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
122
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
123
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
124 def save_tokens(path: Path, tokens: dict[str, Any]) -> None:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
125 payload = dict(tokens)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
126 payload["saved_at"] = int(time.time())
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
127 path.parent.mkdir(parents=True, exist_ok=True)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
128 path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
129 path.chmod(stat.S_IRUSR | stat.S_IWUSR)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
130
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
131
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
132 def load_tokens(path: Path) -> dict[str, Any]:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
133 if not path.exists():
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
134 raise SchwabError(f"Token file does not exist: {path}")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
135 return json.loads(path.read_text(encoding="utf-8"))
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
136
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
137
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
138 def get_account_numbers(access_token: str) -> ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
139 return _api_request("GET", "/accounts/accountNumbers", access_token)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
140
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
141
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
142 def get_accounts(access_token: str, fields: str | None = None) -> ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
143 query = ""
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
144 if fields:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
145 query = "?" + urllib.parse.urlencode({"fields": fields})
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
146 return _api_request("GET", "/accounts" + query, access_token)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
147
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
148
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
149 def get_account(access_token: str, account_hash: str, fields: str | None = None) -> ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
150 query = ""
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
151 if fields:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
152 query = "?" + urllib.parse.urlencode({"fields": fields})
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
153 return _api_request("GET", f"/accounts/{urllib.parse.quote(account_hash)}{query}", access_token)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
154
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
155
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
156 def build_equity_order(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
157 action: str,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
158 symbol: str,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
159 quantity: float,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
160 order_type: str = "MARKET",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
161 price: float | None = None,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
162 duration: str = "DAY",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
163 session: str = "NORMAL",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
164 ) -> dict[str, Any]:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
165 normalized_action = action.upper()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
166 normalized_symbol = symbol.upper()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
167 normalized_order_type = order_type.upper()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
168 normalized_duration = duration.upper()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
169 normalized_session = session.upper()
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
170
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
171 if normalized_action not in {"BUY", "SELL"}:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
172 raise SchwabError("action must be BUY or SELL")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
173 if not normalized_symbol:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
174 raise SchwabError("symbol is required")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
175 if quantity <= 0:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
176 raise SchwabError("quantity must be greater than zero")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
177 if normalized_order_type not in {"MARKET", "LIMIT"}:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
178 raise SchwabError("order_type must be MARKET or LIMIT")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
179 if normalized_order_type == "LIMIT" and price is None:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
180 raise SchwabError("LIMIT orders require --price")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
181 if normalized_order_type == "MARKET" and price is not None:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
182 raise SchwabError("MARKET orders cannot include --price")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
183
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
184 order: dict[str, Any] = {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
185 "orderType": normalized_order_type,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
186 "session": normalized_session,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
187 "duration": normalized_duration,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
188 "orderStrategyType": "SINGLE",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
189 "orderLegCollection": [
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
190 {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
191 "instruction": normalized_action,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
192 "quantity": quantity,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
193 "instrument": {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
194 "symbol": normalized_symbol,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
195 "assetType": "EQUITY",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
196 },
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
197 }
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
198 ],
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
199 }
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
200 if price is not None:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
201 order["price"] = f"{price:.2f}"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
202 return order
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
203
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
204
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
205 def place_order(access_token: str, account_hash: str, order: dict[str, Any]) -> ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
206 path = f"/accounts/{urllib.parse.quote(account_hash)}/orders"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
207 return _api_request("POST", path, access_token, order)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
208
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
209
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
210 def _token_request(config: SchwabConfig, form: dict[str, str]) -> dict[str, Any]:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
211 credentials = f"{config.app_key}:{config.app_secret}".encode("utf-8")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
212 headers = {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
213 "Authorization": "Basic " + base64.b64encode(credentials).decode("ascii"),
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
214 "Content-Type": "application/x-www-form-urlencoded",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
215 "Accept": "application/json",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
216 }
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
217 data = urllib.parse.urlencode(form).encode("utf-8")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
218 request = urllib.request.Request(TOKEN_URL, data=data, headers=headers, method="POST")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
219 response = _open_request(request)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
220 if not isinstance(response.body, dict):
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
221 raise SchwabError("Token endpoint did not return a JSON object")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
222 return response.body
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
223
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
224
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
225 def _api_request(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
226 method: str,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
227 path: str,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
228 access_token: str,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
229 body: dict[str, Any] | None = None,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
230 ) -> ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
231 headers = {
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
232 "Authorization": f"Bearer {access_token}",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
233 "Accept": "application/json",
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
234 }
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
235 data = None
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
236 if body is not None:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
237 data = json.dumps(body).encode("utf-8")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
238 headers["Content-Type"] = "application/json"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
239
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
240 request = urllib.request.Request(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
241 TRADER_BASE_URL + path,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
242 data=data,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
243 headers=headers,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
244 method=method,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
245 )
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
246 return _open_request(request)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
247
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
248
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
249 def _open_request(request: urllib.request.Request) -> ApiResponse:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
250 try:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
251 with urllib.request.urlopen(request, timeout=30) as response:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
252 raw = response.read().decode("utf-8")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
253 return ApiResponse(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
254 status=response.status,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
255 headers=dict(response.headers.items()),
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
256 body=_parse_json_or_text(raw),
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
257 raw_body=raw,
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
258 )
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
259 except urllib.error.HTTPError as error:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
260 raw = error.read().decode("utf-8", errors="replace")
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
261 raise SchwabError(
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
262 f"Schwab API request failed with HTTP {error.code}: {raw or error.reason}"
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
263 ) from error
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
264 except urllib.error.URLError as error:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
265 raise SchwabError(f"Schwab API request failed: {error.reason}") from error
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
266
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
267
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
268 def _parse_json_or_text(raw: str) -> Any:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
269 if not raw:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
270 return None
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
271 try:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
272 return json.loads(raw)
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
273 except json.JSONDecodeError:
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
274 return raw
eb8b4230fdb9 [schwab-trader] Add guarded trading experiment
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
275