fidelity-trader-api 0.1.0__py3-none-any.whl

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 (105) hide show
  1. fidelity_trader/__init__.py +22 -0
  2. fidelity_trader/_http.py +75 -0
  3. fidelity_trader/alerts/__init__.py +4 -0
  4. fidelity_trader/alerts/price_triggers.py +140 -0
  5. fidelity_trader/alerts/subscription.py +147 -0
  6. fidelity_trader/async_client.py +216 -0
  7. fidelity_trader/auth/__init__.py +3 -0
  8. fidelity_trader/auth/auto_refresh.py +120 -0
  9. fidelity_trader/auth/security_context.py +26 -0
  10. fidelity_trader/auth/session.py +211 -0
  11. fidelity_trader/auth/session_keepalive.py +38 -0
  12. fidelity_trader/cli/__init__.py +3 -0
  13. fidelity_trader/cli/_app.py +67 -0
  14. fidelity_trader/cli/_auth.py +145 -0
  15. fidelity_trader/cli/_config.py +25 -0
  16. fidelity_trader/cli/_errors.py +50 -0
  17. fidelity_trader/cli/_market_data.py +187 -0
  18. fidelity_trader/cli/_options.py +303 -0
  19. fidelity_trader/cli/_orders.py +293 -0
  20. fidelity_trader/cli/_output.py +109 -0
  21. fidelity_trader/cli/_portfolio.py +191 -0
  22. fidelity_trader/cli/_research.py +135 -0
  23. fidelity_trader/cli/_session.py +139 -0
  24. fidelity_trader/cli/_stream.py +204 -0
  25. fidelity_trader/client.py +162 -0
  26. fidelity_trader/credentials.py +182 -0
  27. fidelity_trader/exceptions.py +21 -0
  28. fidelity_trader/market_data/__init__.py +4 -0
  29. fidelity_trader/market_data/chart.py +105 -0
  30. fidelity_trader/market_data/fastquote.py +48 -0
  31. fidelity_trader/models/__init__.py +3 -0
  32. fidelity_trader/models/_parsers.py +39 -0
  33. fidelity_trader/models/account.py +15 -0
  34. fidelity_trader/models/account_detail.py +119 -0
  35. fidelity_trader/models/alerts.py +196 -0
  36. fidelity_trader/models/analytics.py +87 -0
  37. fidelity_trader/models/auth.py +40 -0
  38. fidelity_trader/models/available_market.py +97 -0
  39. fidelity_trader/models/balance.py +407 -0
  40. fidelity_trader/models/cancel_order.py +49 -0
  41. fidelity_trader/models/cancel_replace.py +263 -0
  42. fidelity_trader/models/chart.py +96 -0
  43. fidelity_trader/models/closed_position.py +252 -0
  44. fidelity_trader/models/conditional_order.py +357 -0
  45. fidelity_trader/models/equity_order.py +271 -0
  46. fidelity_trader/models/fastquote.py +222 -0
  47. fidelity_trader/models/holiday_calendar.py +42 -0
  48. fidelity_trader/models/loaned_securities.py +115 -0
  49. fidelity_trader/models/option_order.py +355 -0
  50. fidelity_trader/models/option_summary.py +268 -0
  51. fidelity_trader/models/order.py +182 -0
  52. fidelity_trader/models/position.py +206 -0
  53. fidelity_trader/models/preferences.py +47 -0
  54. fidelity_trader/models/price_trigger.py +214 -0
  55. fidelity_trader/models/research.py +118 -0
  56. fidelity_trader/models/screener.py +93 -0
  57. fidelity_trader/models/search.py +33 -0
  58. fidelity_trader/models/security_context.py +69 -0
  59. fidelity_trader/models/single_option_order.py +284 -0
  60. fidelity_trader/models/staged_order.py +80 -0
  61. fidelity_trader/models/streaming.py +13 -0
  62. fidelity_trader/models/tax_lot.py +134 -0
  63. fidelity_trader/models/transaction.py +153 -0
  64. fidelity_trader/models/watchlist.py +121 -0
  65. fidelity_trader/orders/__init__.py +8 -0
  66. fidelity_trader/orders/cancel.py +61 -0
  67. fidelity_trader/orders/cancel_replace.py +68 -0
  68. fidelity_trader/orders/conditional.py +73 -0
  69. fidelity_trader/orders/equity.py +65 -0
  70. fidelity_trader/orders/options.py +75 -0
  71. fidelity_trader/orders/single_option.py +71 -0
  72. fidelity_trader/orders/staged.py +69 -0
  73. fidelity_trader/orders/status.py +49 -0
  74. fidelity_trader/portfolio/__init__.py +7 -0
  75. fidelity_trader/portfolio/accounts.py +52 -0
  76. fidelity_trader/portfolio/balances.py +74 -0
  77. fidelity_trader/portfolio/closed_positions.py +68 -0
  78. fidelity_trader/portfolio/loaned_securities.py +29 -0
  79. fidelity_trader/portfolio/option_summary.py +37 -0
  80. fidelity_trader/portfolio/positions.py +63 -0
  81. fidelity_trader/portfolio/tax_lots.py +34 -0
  82. fidelity_trader/portfolio/transactions.py +68 -0
  83. fidelity_trader/reference/__init__.py +0 -0
  84. fidelity_trader/reference/holiday_calendar.py +26 -0
  85. fidelity_trader/reference/markets.py +29 -0
  86. fidelity_trader/research/__init__.py +6 -0
  87. fidelity_trader/research/analytics.py +49 -0
  88. fidelity_trader/research/data.py +44 -0
  89. fidelity_trader/research/screener.py +111 -0
  90. fidelity_trader/research/search.py +25 -0
  91. fidelity_trader/retry.py +105 -0
  92. fidelity_trader/settings/__init__.py +0 -0
  93. fidelity_trader/settings/preferences.py +89 -0
  94. fidelity_trader/streaming/__init__.py +4 -0
  95. fidelity_trader/streaming/mdds.py +330 -0
  96. fidelity_trader/streaming/mdds_fields.py +152 -0
  97. fidelity_trader/streaming/news.py +25 -0
  98. fidelity_trader/trading/__init__.py +0 -0
  99. fidelity_trader/watchlists/__init__.py +3 -0
  100. fidelity_trader/watchlists/watchlists.py +88 -0
  101. fidelity_trader_api-0.1.0.dist-info/METADATA +830 -0
  102. fidelity_trader_api-0.1.0.dist-info/RECORD +105 -0
  103. fidelity_trader_api-0.1.0.dist-info/WHEEL +4 -0
  104. fidelity_trader_api-0.1.0.dist-info/entry_points.txt +2 -0
  105. fidelity_trader_api-0.1.0.dist-info/licenses/LICENSE +191 -0
@@ -0,0 +1,22 @@
1
+ from fidelity_trader.client import FidelityClient
2
+ from fidelity_trader.async_client import AsyncFidelityClient
3
+ from fidelity_trader.exceptions import (
4
+ FidelityError,
5
+ AuthenticationError,
6
+ SessionExpiredError,
7
+ CSRFTokenError,
8
+ APIError,
9
+ DryRunError,
10
+ )
11
+
12
+ __all__ = [
13
+ "FidelityClient",
14
+ "AsyncFidelityClient",
15
+ "FidelityError",
16
+ "AuthenticationError",
17
+ "SessionExpiredError",
18
+ "CSRFTokenError",
19
+ "APIError",
20
+ "DryRunError",
21
+ ]
22
+ __version__ = "0.1.0"
@@ -0,0 +1,75 @@
1
+ import uuid
2
+ import httpx
3
+
4
+ from fidelity_trader.retry import RetryTransport
5
+
6
+ BASE_URL = "https://digital.fidelity.com"
7
+ AUTH_URL = "https://ecaap.fidelity.com"
8
+
9
+ REQUEST_HEADERS = {
10
+ "User-Agent": (
11
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
12
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
13
+ "Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0 "
14
+ "ATPNext/4.4.1.7 FTPlusDesktop/4.4.1.7"
15
+ ),
16
+ "AppId": "RETAIL-CC-LOGIN-SDK",
17
+ "AppName": "PILoginExperience",
18
+ "Content-Type": "application/json",
19
+ "Accept": "*/*",
20
+ "Origin": BASE_URL,
21
+ "Referer": f"{BASE_URL}/",
22
+ "Accept-Token-Type": "ET",
23
+ "Accept-Token-Location": "HEADER",
24
+ "Token-Location": "HEADER",
25
+ "Cache-Control": "no-cache, no-store, must-revalidate",
26
+ }
27
+
28
+ # Data/Trading API host (from captured traffic)
29
+ DPSERVICE_URL = "https://dpservice.fidelity.com"
30
+ ALERTS_URL = "https://ecawsgateway.fidelity.com"
31
+ STREAMING_NEWS_URL = "https://streaming-news.mds.fidelity.com"
32
+ FASTQUOTE_URL = "https://fastquote.fidelity.com"
33
+
34
+ # Headers used by Fidelity Trader+ desktop app for data APIs
35
+ # (different from login headers which use RETAIL-CC-LOGIN-SDK)
36
+ ATP_HEADERS = {
37
+ "AppId": "AP149323",
38
+ "AppName": "Active Trader Desktop for Windows",
39
+ "User-Agent": "ATPNext/4.4.1.7 FTPlusDesktop/4.4.1.7",
40
+ "Content-Type": "application/json; charset=utf-8",
41
+ "Accept": "application/json",
42
+ }
43
+
44
+ def create_session(timeout: float = 30.0) -> httpx.Client:
45
+ return httpx.Client(follow_redirects=True, timeout=timeout, headers=REQUEST_HEADERS)
46
+
47
+ def create_atp_session(
48
+ timeout: float = 30.0,
49
+ max_retries: int = 0,
50
+ retry_delay: float = 1.0,
51
+ backoff_factor: float = 2.0,
52
+ ) -> httpx.Client:
53
+ """Create a pre-configured httpx client for Fidelity Trader+ data APIs.
54
+
55
+ When *max_retries* > 0, the underlying transport is wrapped in a
56
+ :class:`RetryTransport` that retries on transient failures (connection
57
+ errors, timeouts, 429/5xx status codes) with exponential backoff.
58
+ The default (*max_retries=0*) preserves existing behaviour with no retry.
59
+ """
60
+ transport: httpx.BaseTransport | None = None
61
+ if max_retries > 0:
62
+ transport = RetryTransport(
63
+ max_retries=max_retries,
64
+ retry_delay=retry_delay,
65
+ backoff_factor=backoff_factor,
66
+ )
67
+ return httpx.Client(
68
+ follow_redirects=True,
69
+ timeout=timeout,
70
+ headers=ATP_HEADERS,
71
+ transport=transport,
72
+ )
73
+
74
+ def make_req_id() -> str:
75
+ return f"REQ{uuid.uuid4().hex}"
@@ -0,0 +1,4 @@
1
+ """Alerts subscription module for Fidelity Trader+."""
2
+ from fidelity_trader.alerts.subscription import AlertsAPI
3
+
4
+ __all__ = ["AlertsAPI"]
@@ -0,0 +1,140 @@
1
+ """Price triggers API — list, create, and delete price-based alert triggers."""
2
+ from __future__ import annotations
3
+
4
+ from typing import List, Optional
5
+
6
+ import httpx
7
+
8
+ from fidelity_trader._http import DPSERVICE_URL
9
+ from fidelity_trader.models.price_trigger import (
10
+ DEFAULT_DEVICES,
11
+ PriceTriggerCreateRequest,
12
+ PriceTriggerCreateResponse,
13
+ PriceTriggerDeleteRequest,
14
+ PriceTriggerDeleteResponse,
15
+ PriceTriggerDevice,
16
+ PriceTriggersResponse,
17
+ )
18
+
19
+ _PRICE_TRIGGERS_BASE = (
20
+ "/ftgw/dp/retail-price-triggers/v1"
21
+ "/investments/research/alert/price-triggers"
22
+ )
23
+
24
+ _PRICE_TRIGGERS_PATH = f"{_PRICE_TRIGGERS_BASE}/list"
25
+ _PRICE_TRIGGERS_CREATE_PATH = f"{_PRICE_TRIGGERS_BASE}/create"
26
+ _PRICE_TRIGGERS_DELETE_PATH = f"{_PRICE_TRIGGERS_BASE}/delete"
27
+
28
+
29
+ class PriceTriggersAPI:
30
+ """Client for the Fidelity price triggers endpoints (list, create, delete)."""
31
+
32
+ def __init__(self, http: httpx.Client) -> None:
33
+ self._http = http
34
+
35
+ def get_price_triggers(
36
+ self,
37
+ symbol: str,
38
+ status: str = "active",
39
+ offset: int = 0,
40
+ ) -> PriceTriggersResponse:
41
+ """Fetch the list of price triggers for a given symbol.
42
+
43
+ GETs ``/ftgw/dp/retail-price-triggers/v1/investments/research/alert/
44
+ price-triggers/list`` with query parameters ``symbol``, ``status``,
45
+ and ``offset``.
46
+
47
+ Args:
48
+ symbol: Ticker symbol to query (e.g. ``"QS"``).
49
+ status: Filter by trigger status (default ``"active"``).
50
+ offset: Pagination offset (default ``0``).
51
+
52
+ Returns:
53
+ A :class:`~fidelity_trader.models.price_trigger.PriceTriggersResponse`.
54
+
55
+ Raises:
56
+ httpx.HTTPStatusError: on non-2xx responses.
57
+ """
58
+ params = {
59
+ "symbol": symbol,
60
+ "status": status,
61
+ "offset": offset,
62
+ }
63
+ resp = self._http.get(
64
+ f"{DPSERVICE_URL}{_PRICE_TRIGGERS_PATH}",
65
+ params=params,
66
+ )
67
+ resp.raise_for_status()
68
+ return PriceTriggersResponse.from_api_response(resp.json())
69
+
70
+ def create_price_trigger(
71
+ self,
72
+ symbol: str,
73
+ operator: str,
74
+ value: float,
75
+ currency: str = "USD",
76
+ notes: str = "",
77
+ devices: Optional[List[PriceTriggerDevice]] = None,
78
+ ) -> PriceTriggerCreateResponse:
79
+ """Create a new price trigger.
80
+
81
+ POSTs to ``/ftgw/dp/retail-price-triggers/v1/investments/research/
82
+ alert/price-triggers/create``.
83
+
84
+ Args:
85
+ symbol: Ticker symbol (e.g. ``"SPY"``).
86
+ operator: Trigger operator. Captured values:
87
+ ``"lessThanPercent"``, ``"greaterThanPercent"``,
88
+ ``"lessThan"``, ``"greaterThan"``.
89
+ value: Trigger threshold value.
90
+ currency: Currency code (default ``"USD"``).
91
+ notes: Optional note text (default ``""``).
92
+ devices: Notification devices. Defaults to
93
+ Active Trader Pro and Fidelity mobile applications.
94
+
95
+ Returns:
96
+ A :class:`~fidelity_trader.models.price_trigger.PriceTriggerCreateResponse`.
97
+
98
+ Raises:
99
+ httpx.HTTPStatusError: on non-2xx responses.
100
+ """
101
+ request = PriceTriggerCreateRequest(
102
+ symbol=symbol,
103
+ operator=operator,
104
+ value=value,
105
+ currency=currency,
106
+ notes=notes,
107
+ devices=devices if devices is not None else list(DEFAULT_DEVICES),
108
+ )
109
+ resp = self._http.post(
110
+ f"{DPSERVICE_URL}{_PRICE_TRIGGERS_CREATE_PATH}",
111
+ json=request.to_api_payload(),
112
+ )
113
+ resp.raise_for_status()
114
+ return PriceTriggerCreateResponse.from_api_response(resp.json())
115
+
116
+ def delete_price_triggers(
117
+ self,
118
+ trigger_ids: List[str],
119
+ ) -> PriceTriggerDeleteResponse:
120
+ """Delete one or more price triggers by ID.
121
+
122
+ POSTs to ``/ftgw/dp/retail-price-triggers/v1/investments/research/
123
+ alert/price-triggers/delete``.
124
+
125
+ Args:
126
+ trigger_ids: List of trigger ID strings to delete.
127
+
128
+ Returns:
129
+ A :class:`~fidelity_trader.models.price_trigger.PriceTriggerDeleteResponse`.
130
+
131
+ Raises:
132
+ httpx.HTTPStatusError: on non-2xx responses.
133
+ """
134
+ request = PriceTriggerDeleteRequest(trigger_ids=trigger_ids)
135
+ resp = self._http.post(
136
+ f"{DPSERVICE_URL}{_PRICE_TRIGGERS_DELETE_PATH}",
137
+ json=request.to_api_payload(),
138
+ )
139
+ resp.raise_for_status()
140
+ return PriceTriggerDeleteResponse.from_api_response(resp.json())
@@ -0,0 +1,147 @@
1
+ """Alerts subscription API — wraps the ATBTSubscription SOAP endpoint."""
2
+ from __future__ import annotations
3
+
4
+ import xml.etree.ElementTree as ET
5
+
6
+ import httpx
7
+
8
+ from fidelity_trader._http import ALERTS_URL
9
+ from fidelity_trader.models.alerts import AlertActivation, AlertsResponse
10
+
11
+ _SUBSCRIBE_PATH = "/ftgw/alerts/services/ATBTSubscription"
12
+ _ALERTS_PATH = "/ftgw/alerts/services/ATBTAlerts"
13
+
14
+ # XML namespaces (match the captured request exactly)
15
+ _NS_SOAP = "http://schemas.xmlsoap.org/soap/envelope/"
16
+ _NS_PROD = "http://xmlns.fmr.com/institutional/common/headers/2011/08/ProductIdentity"
17
+ _NS_PRIN = "http://xmlns.fmr.com/institutional/common/headers/2012/09/PrincipalIdentity"
18
+ _NS_AUT = "http://xmlns.fmr.com/institutional/eca/fens/2014/06/AutoSubscription"
19
+
20
+
21
+ def _build_get_alerts_envelope(msg_from: int = 1, msg_to: int = 100) -> bytes:
22
+ """Build the GetAlerts SOAP request body as UTF-8 bytes.
23
+
24
+ Uses a raw XML template matching the captured request format.
25
+ """
26
+ return (
27
+ "<soapenv:Envelope"
28
+ " xmlns:prin='http://xmlns.fmr.com/institutional/common/headers/2012/09/PrincipalIdentity'"
29
+ " xmlns:prod='http://xmlns.fmr.com/institutional/common/headers/2011/08/ProductIdentity'"
30
+ " xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'"
31
+ " xmlns:xsd='http://www.w3.org/2001/XMLSchema'"
32
+ " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>"
33
+ "<soapenv:Header>"
34
+ "<prin:PrincipalIdentity>"
35
+ "<prin:RequestorId>Fidelity</prin:RequestorId>"
36
+ "<prin:AuthMethod>Basic</prin:AuthMethod>"
37
+ "<prin:PrincipalDomain>Retail</prin:PrincipalDomain>"
38
+ "<prin:RequestorType>Standard</prin:RequestorType>"
39
+ "<prin:PrincipalRole>Owner</prin:PrincipalRole>"
40
+ "</prin:PrincipalIdentity>"
41
+ "<prod:ProductIdentity>"
42
+ "<prod:AppId>AP002304</prod:AppId>"
43
+ "<prod:AppName>ATP</prod:AppName>"
44
+ "<prod:AppVersion>4.5.1</prod:AppVersion>"
45
+ "<prod:ProductId>ATP</prod:ProductId>"
46
+ "<prod:SubSystem>ActiveTrader</prod:SubSystem>"
47
+ "</prod:ProductIdentity>"
48
+ "</soapenv:Header>"
49
+ "<soapenv:Body>"
50
+ "<GetAlerts xmlns='http://xmlns.fmr.com/brokerage/fens/service/ALERTS/2009-09'>"
51
+ f"<MsgIndexFrom>{msg_from}</MsgIndexFrom>"
52
+ f"<MsgIndexTo>{msg_to}</MsgIndexTo>"
53
+ "</GetAlerts>"
54
+ "</soapenv:Body>"
55
+ "</soapenv:Envelope>"
56
+ ).encode("utf-8")
57
+
58
+
59
+ def _build_soap_envelope() -> bytes:
60
+ """Build the CustomerSignOn SOAP request body as UTF-8 bytes."""
61
+ # Register namespace prefixes so ElementTree uses clean tags
62
+ ET.register_namespace("soapenv", _NS_SOAP)
63
+ ET.register_namespace("prod", _NS_PROD)
64
+ ET.register_namespace("prin", _NS_PRIN)
65
+ ET.register_namespace("aut", _NS_AUT)
66
+
67
+ envelope = ET.Element(f"{{{_NS_SOAP}}}Envelope")
68
+
69
+ # --- Header ---
70
+ header = ET.SubElement(envelope, f"{{{_NS_SOAP}}}Header")
71
+
72
+ principal = ET.SubElement(header, f"{{{_NS_PRIN}}}PrincipalIdentity")
73
+ ET.SubElement(principal, f"{{{_NS_PRIN}}}RequestorId").text = "fidelity"
74
+ ET.SubElement(principal, f"{{{_NS_PRIN}}}AuthMethod").text = "Basic"
75
+ ET.SubElement(principal, f"{{{_NS_PRIN}}}PrincipalDomain").text = "Retail"
76
+ ET.SubElement(principal, f"{{{_NS_PRIN}}}PrincipalRole").text = "Owner"
77
+
78
+ product = ET.SubElement(header, f"{{{_NS_PROD}}}ProductIdentity")
79
+ ET.SubElement(product, f"{{{_NS_PROD}}}AppId").text = "AP002304"
80
+ ET.SubElement(product, f"{{{_NS_PROD}}}AppName").text = "ATP"
81
+ ET.SubElement(product, f"{{{_NS_PROD}}}AppVersion").text = "0.0.1"
82
+ ET.SubElement(product, f"{{{_NS_PROD}}}ProductId").text = "ATP"
83
+ ET.SubElement(product, f"{{{_NS_PROD}}}SubSystem").text = "ActiveTrader"
84
+
85
+ # --- Body ---
86
+ body = ET.SubElement(envelope, f"{{{_NS_SOAP}}}Body")
87
+ sign_on = ET.SubElement(body, f"{{{_NS_AUT}}}CustomerSignOn")
88
+ ET.SubElement(sign_on, f"{{{_NS_AUT}}}AlertCode").text = "ATBT"
89
+
90
+ return ET.tostring(envelope, encoding="unicode", xml_declaration=False).encode("utf-8")
91
+
92
+
93
+ class AlertsAPI:
94
+ """Client for the Fidelity alerts subscription (ATBTSubscription) SOAP service."""
95
+
96
+ def __init__(self, http: httpx.Client) -> None:
97
+ self._http = http
98
+
99
+ def subscribe(self) -> AlertActivation:
100
+ """Subscribe to ATP/ATBT alerts.
101
+
102
+ POSTs the ``CustomerSignOn`` SOAP envelope to the ATBTSubscription
103
+ endpoint. Returns an :class:`~fidelity_trader.models.alerts.AlertActivation`
104
+ containing the STOMP/JMS credentials and server URL needed to connect
105
+ to the real-time alert stream.
106
+
107
+ Raises:
108
+ httpx.HTTPStatusError: on non-2xx responses.
109
+ ValueError: if the SOAP response cannot be parsed.
110
+ """
111
+ soap_body = _build_soap_envelope()
112
+ resp = self._http.post(
113
+ f"{ALERTS_URL}{_SUBSCRIBE_PATH}",
114
+ content=soap_body,
115
+ headers={
116
+ "Content-Type": "application/x-www-form-urlencoded",
117
+ "SOAPAction": "CustomerSignOn",
118
+ },
119
+ )
120
+ resp.raise_for_status()
121
+ return AlertActivation.from_xml(resp.content)
122
+
123
+ def get_alerts(self, msg_from: int = 1, msg_to: int = 100) -> AlertsResponse:
124
+ """Retrieve alert messages (order fills, cancellations, etc.).
125
+
126
+ POSTs the ``GetAlerts`` SOAP envelope to the ATBTAlerts endpoint.
127
+ Returns an :class:`~fidelity_trader.models.alerts.AlertsResponse`
128
+ containing the total message count and parsed alert messages.
129
+
130
+ Args:
131
+ msg_from: Starting message index (1-based, default 1).
132
+ msg_to: Ending message index (default 100).
133
+
134
+ Raises:
135
+ httpx.HTTPStatusError: on non-2xx responses.
136
+ ValueError: if the SOAP response cannot be parsed.
137
+ """
138
+ soap_body = _build_get_alerts_envelope(msg_from, msg_to)
139
+ resp = self._http.post(
140
+ f"{ALERTS_URL}{_ALERTS_PATH}",
141
+ content=soap_body,
142
+ headers={
143
+ "Content-Type": "application/x-www-form-urlencoded",
144
+ },
145
+ )
146
+ resp.raise_for_status()
147
+ return AlertsResponse.from_soap_response(resp.content)
@@ -0,0 +1,216 @@
1
+ """Async wrapper around FidelityClient using asyncio.to_thread."""
2
+
3
+ import asyncio
4
+
5
+ from fidelity_trader.client import FidelityClient
6
+
7
+
8
+ class AsyncFidelityClient:
9
+ """Async variant of FidelityClient.
10
+
11
+ Wraps the sync client and delegates calls to a thread executor
12
+ via asyncio.to_thread, so they don't block the event loop.
13
+
14
+ Usage:
15
+ async with AsyncFidelityClient() as client:
16
+ await client.login(username, password)
17
+ positions = await client.get_positions(["Z12345678"])
18
+ """
19
+
20
+ def __init__(self, **kwargs) -> None:
21
+ self._sync = FidelityClient(**kwargs)
22
+
23
+ # ------------------------------------------------------------------
24
+ # Auth lifecycle
25
+ # ------------------------------------------------------------------
26
+
27
+ async def login(
28
+ self, username: str, password: str, totp_secret: str = None
29
+ ) -> dict:
30
+ return await asyncio.to_thread(
31
+ self._sync.login, username, password, totp_secret
32
+ )
33
+
34
+ async def logout(self) -> None:
35
+ await asyncio.to_thread(self._sync.logout)
36
+
37
+ @property
38
+ def is_authenticated(self) -> bool:
39
+ return self._sync.is_authenticated
40
+
41
+ # ------------------------------------------------------------------
42
+ # Auto-refresh (sync — just delegates, no I/O)
43
+ # ------------------------------------------------------------------
44
+
45
+ def enable_auto_refresh(self, interval: int = 300) -> None:
46
+ self._sync.enable_auto_refresh(interval)
47
+
48
+ def disable_auto_refresh(self) -> None:
49
+ self._sync.disable_auto_refresh()
50
+
51
+ # ------------------------------------------------------------------
52
+ # Close / context manager
53
+ # ------------------------------------------------------------------
54
+
55
+ async def close(self) -> None:
56
+ await asyncio.to_thread(self._sync.close)
57
+
58
+ async def __aenter__(self):
59
+ return self
60
+
61
+ async def __aexit__(self, *args):
62
+ await self.close()
63
+
64
+ # ------------------------------------------------------------------
65
+ # Module accessors — expose the sync module objects directly.
66
+ # Users can wrap individual calls via asyncio.to_thread themselves:
67
+ # result = await asyncio.to_thread(client.research.get_earnings, ...)
68
+ # ------------------------------------------------------------------
69
+
70
+ @property
71
+ def positions(self):
72
+ return self._sync.positions
73
+
74
+ @property
75
+ def balances(self):
76
+ return self._sync.balances
77
+
78
+ @property
79
+ def option_summary(self):
80
+ return self._sync.option_summary
81
+
82
+ @property
83
+ def transactions(self):
84
+ return self._sync.transactions
85
+
86
+ @property
87
+ def order_status(self):
88
+ return self._sync.order_status
89
+
90
+ @property
91
+ def equity_orders(self):
92
+ return self._sync.equity_orders
93
+
94
+ @property
95
+ def option_orders(self):
96
+ return self._sync.option_orders
97
+
98
+ @property
99
+ def cancel_order(self):
100
+ return self._sync.cancel_order
101
+
102
+ @property
103
+ def single_option_orders(self):
104
+ return self._sync.single_option_orders
105
+
106
+ @property
107
+ def cancel_replace(self):
108
+ return self._sync.cancel_replace
109
+
110
+ @property
111
+ def research(self):
112
+ return self._sync.research
113
+
114
+ @property
115
+ def search(self):
116
+ return self._sync.search
117
+
118
+ @property
119
+ def streaming(self):
120
+ return self._sync.streaming
121
+
122
+ @property
123
+ def watchlists(self):
124
+ return self._sync.watchlists
125
+
126
+ @property
127
+ def accounts(self):
128
+ return self._sync.accounts
129
+
130
+ @property
131
+ def option_chain(self):
132
+ return self._sync.option_chain
133
+
134
+ @property
135
+ def chart(self):
136
+ return self._sync.chart
137
+
138
+ @property
139
+ def option_analytics(self):
140
+ return self._sync.option_analytics
141
+
142
+ @property
143
+ def alerts(self):
144
+ return self._sync.alerts
145
+
146
+ @property
147
+ def closed_positions(self):
148
+ return self._sync.closed_positions
149
+
150
+ @property
151
+ def loaned_securities(self):
152
+ return self._sync.loaned_securities
153
+
154
+ @property
155
+ def tax_lots(self):
156
+ return self._sync.tax_lots
157
+
158
+ @property
159
+ def available_markets(self):
160
+ return self._sync.available_markets
161
+
162
+ @property
163
+ def preferences(self):
164
+ return self._sync.preferences
165
+
166
+ @property
167
+ def security_context(self):
168
+ return self._sync.security_context
169
+
170
+ @property
171
+ def session_keepalive(self):
172
+ return self._sync.session_keepalive
173
+
174
+ @property
175
+ def holiday_calendar(self):
176
+ return self._sync.holiday_calendar
177
+
178
+ @property
179
+ def staged_orders(self):
180
+ return self._sync.staged_orders
181
+
182
+ @property
183
+ def price_triggers(self):
184
+ return self._sync.price_triggers
185
+
186
+ @property
187
+ def conditional_orders(self):
188
+ return self._sync.conditional_orders
189
+
190
+ @property
191
+ def screener(self):
192
+ return self._sync.screener
193
+
194
+ # ------------------------------------------------------------------
195
+ # Async convenience methods for the most common operations.
196
+ # For everything else, access the module property and wrap the call:
197
+ # result = await asyncio.to_thread(client.research.get_earnings, ...)
198
+ # ------------------------------------------------------------------
199
+
200
+ async def get_positions(self, account_numbers, **kwargs):
201
+ """Async shortcut for positions.get_positions()."""
202
+ return await asyncio.to_thread(
203
+ self._sync.positions.get_positions, account_numbers, **kwargs
204
+ )
205
+
206
+ async def get_balances(self, account_numbers, **kwargs):
207
+ """Async shortcut for balances.get_balances()."""
208
+ return await asyncio.to_thread(
209
+ self._sync.balances.get_balances, account_numbers, **kwargs
210
+ )
211
+
212
+ async def get_order_status(self, account_numbers, **kwargs):
213
+ """Async shortcut for order_status.get_order_status()."""
214
+ return await asyncio.to_thread(
215
+ self._sync.order_status.get_order_status, account_numbers, **kwargs
216
+ )
@@ -0,0 +1,3 @@
1
+ from fidelity_trader.auth.session import AuthSession
2
+
3
+ __all__ = ["AuthSession"]