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.
- fidelity_trader/__init__.py +22 -0
- fidelity_trader/_http.py +75 -0
- fidelity_trader/alerts/__init__.py +4 -0
- fidelity_trader/alerts/price_triggers.py +140 -0
- fidelity_trader/alerts/subscription.py +147 -0
- fidelity_trader/async_client.py +216 -0
- fidelity_trader/auth/__init__.py +3 -0
- fidelity_trader/auth/auto_refresh.py +120 -0
- fidelity_trader/auth/security_context.py +26 -0
- fidelity_trader/auth/session.py +211 -0
- fidelity_trader/auth/session_keepalive.py +38 -0
- fidelity_trader/cli/__init__.py +3 -0
- fidelity_trader/cli/_app.py +67 -0
- fidelity_trader/cli/_auth.py +145 -0
- fidelity_trader/cli/_config.py +25 -0
- fidelity_trader/cli/_errors.py +50 -0
- fidelity_trader/cli/_market_data.py +187 -0
- fidelity_trader/cli/_options.py +303 -0
- fidelity_trader/cli/_orders.py +293 -0
- fidelity_trader/cli/_output.py +109 -0
- fidelity_trader/cli/_portfolio.py +191 -0
- fidelity_trader/cli/_research.py +135 -0
- fidelity_trader/cli/_session.py +139 -0
- fidelity_trader/cli/_stream.py +204 -0
- fidelity_trader/client.py +162 -0
- fidelity_trader/credentials.py +182 -0
- fidelity_trader/exceptions.py +21 -0
- fidelity_trader/market_data/__init__.py +4 -0
- fidelity_trader/market_data/chart.py +105 -0
- fidelity_trader/market_data/fastquote.py +48 -0
- fidelity_trader/models/__init__.py +3 -0
- fidelity_trader/models/_parsers.py +39 -0
- fidelity_trader/models/account.py +15 -0
- fidelity_trader/models/account_detail.py +119 -0
- fidelity_trader/models/alerts.py +196 -0
- fidelity_trader/models/analytics.py +87 -0
- fidelity_trader/models/auth.py +40 -0
- fidelity_trader/models/available_market.py +97 -0
- fidelity_trader/models/balance.py +407 -0
- fidelity_trader/models/cancel_order.py +49 -0
- fidelity_trader/models/cancel_replace.py +263 -0
- fidelity_trader/models/chart.py +96 -0
- fidelity_trader/models/closed_position.py +252 -0
- fidelity_trader/models/conditional_order.py +357 -0
- fidelity_trader/models/equity_order.py +271 -0
- fidelity_trader/models/fastquote.py +222 -0
- fidelity_trader/models/holiday_calendar.py +42 -0
- fidelity_trader/models/loaned_securities.py +115 -0
- fidelity_trader/models/option_order.py +355 -0
- fidelity_trader/models/option_summary.py +268 -0
- fidelity_trader/models/order.py +182 -0
- fidelity_trader/models/position.py +206 -0
- fidelity_trader/models/preferences.py +47 -0
- fidelity_trader/models/price_trigger.py +214 -0
- fidelity_trader/models/research.py +118 -0
- fidelity_trader/models/screener.py +93 -0
- fidelity_trader/models/search.py +33 -0
- fidelity_trader/models/security_context.py +69 -0
- fidelity_trader/models/single_option_order.py +284 -0
- fidelity_trader/models/staged_order.py +80 -0
- fidelity_trader/models/streaming.py +13 -0
- fidelity_trader/models/tax_lot.py +134 -0
- fidelity_trader/models/transaction.py +153 -0
- fidelity_trader/models/watchlist.py +121 -0
- fidelity_trader/orders/__init__.py +8 -0
- fidelity_trader/orders/cancel.py +61 -0
- fidelity_trader/orders/cancel_replace.py +68 -0
- fidelity_trader/orders/conditional.py +73 -0
- fidelity_trader/orders/equity.py +65 -0
- fidelity_trader/orders/options.py +75 -0
- fidelity_trader/orders/single_option.py +71 -0
- fidelity_trader/orders/staged.py +69 -0
- fidelity_trader/orders/status.py +49 -0
- fidelity_trader/portfolio/__init__.py +7 -0
- fidelity_trader/portfolio/accounts.py +52 -0
- fidelity_trader/portfolio/balances.py +74 -0
- fidelity_trader/portfolio/closed_positions.py +68 -0
- fidelity_trader/portfolio/loaned_securities.py +29 -0
- fidelity_trader/portfolio/option_summary.py +37 -0
- fidelity_trader/portfolio/positions.py +63 -0
- fidelity_trader/portfolio/tax_lots.py +34 -0
- fidelity_trader/portfolio/transactions.py +68 -0
- fidelity_trader/reference/__init__.py +0 -0
- fidelity_trader/reference/holiday_calendar.py +26 -0
- fidelity_trader/reference/markets.py +29 -0
- fidelity_trader/research/__init__.py +6 -0
- fidelity_trader/research/analytics.py +49 -0
- fidelity_trader/research/data.py +44 -0
- fidelity_trader/research/screener.py +111 -0
- fidelity_trader/research/search.py +25 -0
- fidelity_trader/retry.py +105 -0
- fidelity_trader/settings/__init__.py +0 -0
- fidelity_trader/settings/preferences.py +89 -0
- fidelity_trader/streaming/__init__.py +4 -0
- fidelity_trader/streaming/mdds.py +330 -0
- fidelity_trader/streaming/mdds_fields.py +152 -0
- fidelity_trader/streaming/news.py +25 -0
- fidelity_trader/trading/__init__.py +0 -0
- fidelity_trader/watchlists/__init__.py +3 -0
- fidelity_trader/watchlists/watchlists.py +88 -0
- fidelity_trader_api-0.1.0.dist-info/METADATA +830 -0
- fidelity_trader_api-0.1.0.dist-info/RECORD +105 -0
- fidelity_trader_api-0.1.0.dist-info/WHEEL +4 -0
- fidelity_trader_api-0.1.0.dist-info/entry_points.txt +2 -0
- 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"
|
fidelity_trader/_http.py
ADDED
|
@@ -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,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
|
+
)
|