33from __future__ import annotations
44
55from typing import TYPE_CHECKING , Any
6+ from urllib .parse import quote
67
78import requests
89
2122BASE_URL = "https://lab.flashalpha.com"
2223
2324
25+ def _seg (s : str ) -> str :
26+ """URL-escape a single path segment (e.g. a ticker) — escapes / ? % etc."""
27+ return quote (s , safe = "" )
28+
29+
2430class FlashAlpha :
2531 """Thin wrapper around the FlashAlpha REST API.
2632
@@ -92,7 +98,7 @@ def _handle(self, resp: requests.Response) -> dict:
9298
9399 def stock_quote (self , ticker : str ) -> dict :
94100 """Live stock quote (bid/ask/mid/last)."""
95- return self ._get (f"/stockquote/{ ticker } " )
101+ return self ._get (f"/stockquote/{ _seg ( ticker ) } " )
96102
97103 def option_quote (
98104 self ,
@@ -110,15 +116,15 @@ def option_quote(
110116 params ["strike" ] = strike
111117 if type :
112118 params ["type" ] = type
113- return self ._get (f"/optionquote/{ ticker } " , params or None )
119+ return self ._get (f"/optionquote/{ _seg ( ticker ) } " , params or None )
114120
115121 def surface (self , symbol : str ) -> dict :
116122 """Volatility surface grid (public, no auth required)."""
117- return self ._get (f"/v1/surface/{ symbol } " )
123+ return self ._get (f"/v1/surface/{ _seg ( symbol ) } " )
118124
119125 def stock_summary (self , symbol : str ) -> dict :
120126 """Comprehensive stock summary (price, vol, exposure, macro)."""
121- return self ._get (f"/v1/stock/{ symbol } /summary" )
127+ return self ._get (f"/v1/stock/{ _seg ( symbol ) } /summary" )
122128
123129 # ── Historical ──────────────────────────────────────────────────
124130
@@ -127,7 +133,7 @@ def historical_stock_quote(self, ticker: str, *, date: str, time: str | None = N
127133 params : dict [str , Any ] = {"date" : date }
128134 if time :
129135 params ["time" ] = time
130- return self ._get (f"/historical/stockquote/{ ticker } " , params )
136+ return self ._get (f"/historical/stockquote/{ _seg ( ticker ) } " , params )
131137
132138 def historical_option_quote (
133139 self ,
@@ -149,7 +155,7 @@ def historical_option_quote(
149155 params ["strike" ] = strike
150156 if type :
151157 params ["type" ] = type
152- return self ._get (f"/historical/optionquote/{ ticker } " , params )
158+ return self ._get (f"/historical/optionquote/{ _seg ( ticker ) } " , params )
153159
154160 # ── Exposure Analytics ──────────────────────────────────────────
155161
@@ -160,40 +166,40 @@ def gex(self, symbol: str, *, expiration: str | None = None, min_oi: int | None
160166 params ["expiration" ] = expiration
161167 if min_oi is not None :
162168 params ["min_oi" ] = min_oi
163- return self ._get (f"/v1/exposure/gex/{ symbol } " , params or None )
169+ return self ._get (f"/v1/exposure/gex/{ _seg ( symbol ) } " , params or None )
164170
165171 def dex (self , symbol : str , * , expiration : str | None = None ) -> dict :
166172 """Delta exposure by strike."""
167173 params : dict [str , Any ] = {}
168174 if expiration :
169175 params ["expiration" ] = expiration
170- return self ._get (f"/v1/exposure/dex/{ symbol } " , params or None )
176+ return self ._get (f"/v1/exposure/dex/{ _seg ( symbol ) } " , params or None )
171177
172178 def vex (self , symbol : str , * , expiration : str | None = None ) -> dict :
173179 """Vanna exposure by strike."""
174180 params : dict [str , Any ] = {}
175181 if expiration :
176182 params ["expiration" ] = expiration
177- return self ._get (f"/v1/exposure/vex/{ symbol } " , params or None )
183+ return self ._get (f"/v1/exposure/vex/{ _seg ( symbol ) } " , params or None )
178184
179185 def chex (self , symbol : str , * , expiration : str | None = None ) -> dict :
180186 """Charm exposure by strike."""
181187 params : dict [str , Any ] = {}
182188 if expiration :
183189 params ["expiration" ] = expiration
184- return self ._get (f"/v1/exposure/chex/{ symbol } " , params or None )
190+ return self ._get (f"/v1/exposure/chex/{ _seg ( symbol ) } " , params or None )
185191
186192 def exposure_summary (self , symbol : str ) -> dict :
187193 """Full exposure summary (GEX/DEX/VEX/CHEX + hedging). Requires Growth+."""
188- return self ._get (f"/v1/exposure/summary/{ symbol } " )
194+ return self ._get (f"/v1/exposure/summary/{ _seg ( symbol ) } " )
189195
190196 def exposure_levels (self , symbol : str ) -> dict :
191197 """Key support/resistance levels from options exposure."""
192- return self ._get (f"/v1/exposure/levels/{ symbol } " )
198+ return self ._get (f"/v1/exposure/levels/{ _seg ( symbol ) } " )
193199
194200 def narrative (self , symbol : str ) -> dict :
195201 """Verbal narrative analysis of exposure. Requires Growth+."""
196- return self ._get (f"/v1/exposure/narrative/{ symbol } " )
202+ return self ._get (f"/v1/exposure/narrative/{ _seg ( symbol ) } " )
197203
198204 def zero_dte (self , symbol : str , * , strike_range : float | None = None ) -> "ZeroDteResponse" :
199205 """Real-time 0DTE analytics: regime, expected move, pin risk, hedging, decay. Requires Growth+.
@@ -205,14 +211,14 @@ def zero_dte(self, symbol: str, *, strike_range: float | None = None) -> "ZeroDt
205211 params : dict [str , Any ] = {}
206212 if strike_range is not None :
207213 params ["strike_range" ] = strike_range
208- return self ._get (f"/v1/exposure/zero-dte/{ symbol } " , params or None )
214+ return self ._get (f"/v1/exposure/zero-dte/{ _seg ( symbol ) } " , params or None )
209215
210216 def exposure_history (self , symbol : str , * , days : int | None = None ) -> dict :
211217 """Daily exposure snapshots for trend analysis. Requires Growth+."""
212218 params : dict [str , Any ] = {}
213219 if days is not None :
214220 params ["days" ] = days
215- return self ._get (f"/v1/exposure/history/{ symbol } " , params or None )
221+ return self ._get (f"/v1/exposure/history/{ _seg ( symbol ) } " , params or None )
216222
217223 # ── Pricing & Sizing ────────────────────────────────────────────
218224
@@ -287,11 +293,11 @@ def kelly(
287293
288294 def volatility (self , symbol : str ) -> dict :
289295 """Comprehensive volatility analysis. Requires Growth+."""
290- return self ._get (f"/v1/volatility/{ symbol } " )
296+ return self ._get (f"/v1/volatility/{ _seg ( symbol ) } " )
291297
292298 def adv_volatility (self , symbol : str ) -> dict :
293299 """Advanced volatility analytics: SVI parameters, variance surface, arbitrage detection, greeks surfaces, variance swap. Requires Alpha+."""
294- return self ._get (f"/v1/adv_volatility/{ symbol } " )
300+ return self ._get (f"/v1/adv_volatility/{ _seg ( symbol ) } " )
295301
296302 # ── Reference Data ──────────────────────────────────────────────
297303
@@ -301,7 +307,7 @@ def tickers(self) -> dict:
301307
302308 def options (self , ticker : str ) -> dict :
303309 """Option chain metadata (expirations + strikes)."""
304- return self ._get (f"/v1/options/{ ticker } " )
310+ return self ._get (f"/v1/options/{ _seg ( ticker ) } " )
305311
306312 def symbols (self ) -> dict :
307313 """Currently queried symbols with live data."""
@@ -332,7 +338,7 @@ def vrp(self, symbol: str) -> dict:
332338
333339 Requires Alpha+.
334340 """
335- return self ._get (f"/v1/vrp/{ symbol } " )
341+ return self ._get (f"/v1/vrp/{ _seg ( symbol ) } " )
336342
337343 # ── Max Pain ────────────────────────────────────────────────────
338344
@@ -351,7 +357,7 @@ def max_pain(self, symbol: str, *, expiration: str | None = None) -> dict:
351357 params : dict [str , Any ] = {}
352358 if expiration :
353359 params ["expiration" ] = expiration
354- return self ._get (f"/v1/maxpain/{ symbol } " , params or None )
360+ return self ._get (f"/v1/maxpain/{ _seg ( symbol ) } " , params or None )
355361
356362 # ── Screener ────────────────────────────────────────────────────
357363
0 commit comments