Troubleshooting
May 29, 2026 · View on GitHub
Every exception the bridge throws carries a stable ErrorCode (FA-AUTH-001, FA-AUTH-002, FA-RATE-001, FA-NET-001). Each code maps onto an anchor on this page; clicking through from a stack trace lands you on the right section. The anchors are lowercased — e.g. FA-AUTH-001 → #fa-auth-001 — and the exception's DocUrl property points at this file with the right fragment already attached.
This doc covers:
- FA-AUTH-001 — Missing API key
- FA-AUTH-002 — Unauthorized
- FA-RATE-001 — Rate limited
- FA-NET-001 — Network error
- Why two symbols? (custom-data vs equity)
- Bar fields are null
- Tier-restricted endpoints
- Cold-cache slowness on
adv-volatility
fa-auth-001
Missing API key. Thrown as FlashAlphaAuthMissingException when the key resolution chain — explicit override → QC Cloud GetParameter → FLASHALPHA_API_KEY env var — finds nothing.
Symptoms
- Algorithm dies on the first FlashAlpha bar request, before any
OnDatafires. - Exception message:
FlashAlpha API key not found. Set FLASHALPHA_API_KEY env var, FlashAlphaConfig.ApiKey, or QC parameter 'flashalpha-api-key'. - QC Cloud: the cloud log shows the same message under the algorithm's runtime errors.
Causes
- The env var is set in your shell but not exported to LEAN's process —
lean backtestlaunched from a different shell, IDE run config, or systemd unit doesn't inherit it. - On QC Cloud, the parameter is named differently (e.g.
flashalpha_api_keywith underscores) or has trailing whitespace. - The explicit override is being set after the first
AddDatacall, so the bridge has already failed.
Fixes
- Confirm the env var is actually visible to LEAN. From the same shell you launch LEAN with:
echo "$FLASHALPHA_API_KEY". Empty? Export it again. - Match the QC parameter name exactly. It is
flashalpha-api-key— hyphens, lowercase. No leading or trailing spaces. Recreate it if in doubt. - Set the explicit override at the top of
Initialize. Setting it in a custom helper called fromOnDatais too late — the firstAddDataline resolves the key. - Mind whitespace on copy/paste. A key with a leading or trailing space won't compare equal to the resolver's
string.IsNullOrEmptycheck on the wire; the API will reject it withFA-AUTH-002, notFA-AUTH-001. If you see both errors flip-flopping, the value is non-empty but malformed.
fa-auth-002
Unauthorized. Thrown as FlashAlphaUnauthorizedException when the API returns HTTP 401 / 403 after a request reaches the wire.
Symptoms
- Algorithm starts, the first FlashAlpha request goes out, then the bridge raises after the SDK call returns.
- Exception message includes the endpoint and the last four characters of the key, never the full value:
FlashAlpha rejected the API key (…AbCd) on endpoint exposure/gex. See troubleshooting docs. - The dashboard on flashalpha.com shows a recent failed request for the key.
Causes
- Wrong key. Typo, stale key from a prior environment, picked the wrong one out of a password manager.
- Revoked key. A previous admin rotated it.
- Plan tier doesn't cover the endpoint.
vrp,adv-volatility, andstock/summaryreturn 403 for free-tier keys. - Geo / IP restriction. Some accounts ship with allow-listed IPs; CI runners outside the allow list get 403.
Fixes
- Confirm the key in the FlashAlpha dashboard. Last four characters should match the
(…AbCd)in the exception. If they don't, you're not picking up the key you think you are — re-check the resolution chain. - Check plan tier. If only certain endpoints fail (e.g.
vrp403,gex200) you've outgrown the free tier — upgrade or remove theAddData<…>for the restricted bar. - Allow-list the runner IP if you've configured IP restrictions on the key, or remove the restriction for CI runs.
- Rotate the key if there's any chance it leaked. The dashboard can issue a fresh one in seconds.
fa-rate-001
Rate limited (after SDK retries exhausted). Thrown as FlashAlphaRateLimitedException when the API returns HTTP 429 and the SDK's built-in exponential-backoff retry loop fails to recover.
Symptoms
- Algorithm runs for a while, then dies mid-backtest on a particular bar request.
- Exception message:
FlashAlpha rate-limited the request to <endpoint> after retries exhausted. - The bridge surfaces the underlying SDK exception via
InnerException.
Causes
- Backtest is at minute resolution. ~390 calls per ticker per day; a 252-day SPY backtest is ~98k calls — easy to brush against tier limits.
- Many tickers in the universe. A 500-name universe with daily GEX is 500 calls per day for that bar alone; mix in DEX / VEX and you triple the cost.
- Concurrent backtests on the same key. Local LEAN backtest, CI run, and a teammate's QC Cloud run on the same key collide on the per-second window.
Fixes
- Drop to
Resolution.Dailyunless you genuinely need intraday snapshots. One daily call has the same headlinenet_gex,gamma_flip, andnet_gex_labelyou'd read off any minute. - Cache locally. The bridge has a per-process cache so multiple subscriptions on
(endpoint, ticker, date)reuse the same response. Move shared logic into a single bar where you can. For research, dump the bars to disk and replay them. - Throttle the universe. Gate
FlashAlphaTickersUniverseoncoverage.healthy_daysso you only pull the well-covered names. See docs/recipes/filter-universe-by-gex-regime.md. - Increase
MaxRetriesas a last resort:FlashAlphaConfig.MaxRetries = 5(C#) /config.max_retries = 5(Python). This trades latency for resilience. - Upgrade your plan if you've genuinely outgrown the rate.
fa-net-001
Network error. Thrown as FlashAlphaNetworkException when the SDK can't reach historical.flashalpha.com — DNS failure, TLS handshake failure, connection reset.
Symptoms
- Exception message:
Network error talking to FlashAlpha at <endpoint>. InnerExceptionis typicallyHttpRequestException(C#) orhttpx.ConnectError/httpx.ReadTimeout(Python).- Other internet traffic from the same host succeeds — only
historical.flashalpha.comfails.
Causes
- Corporate proxy or firewall. Egress to
*.flashalpha.com:443is blocked. - DNS misconfiguration. Local resolver can't see
historical.flashalpha.com. - TLS / cert validation failure. Out-of-date system trust store, or a MITM proxy injecting its own cert.
- Server-side hiccup. Rare; FlashAlpha publishes incidents.
Fixes
- Curl from the same host.
curl -I https://historical.flashalpha.com/v1/ping— should return200 OK. If it doesn't, the issue is below the bridge. - Configure an HTTPS proxy if your network requires one — both Python (
HTTPS_PROXYenv var) and .NET (HttpClient's default proxy detection) respect the standard env vars. - Bump the timeout.
FlashAlphaConfig.HttpTimeout = TimeSpan.FromSeconds(60)(C#) /config.http_timeout_s = 60.0(Python) for slow corporate networks. - Update root certs if TLS validation is failing — Ubuntu
apt install --reinstall ca-certificates, macOS automatic via system update, Windows via Windows Update. - Check status.flashalpha.com for an active incident.
why-two-symbols
The most common surprise on this bridge: a FlashAlpha custom-data Symbol is not the same Symbol you get from AddEquity. They look identical when printed (both say SPY), but they are distinct LEAN identities, and Slice lookups by one will not return data for the other.
What's happening
LEAN's custom-data subscription system mints a fresh Symbol for each AddData<TBar>(ticker, …) call, distinct from the equity Symbol that AddEquity(ticker, …) returns. The two symbols share a ticker string but are entirely separate subscriptions internally — they show up in different slots of the Slice and must be queried independently.
Diagnostic
If slice.ContainsKey(myEquitySymbol) returns true but slice.Get<FlashAlphaGexBar>(myEquitySymbol) returns null, this is what's biting you. Same the other direction.
Fix — hold both symbols as fields
private Symbol _spy; // from AddEquity
private Symbol _gex; // from AddData / AddFlashAlphaGex
public override void Initialize()
{
_spy = AddEquity("SPY", Resolution.Daily).Symbol;
_gex = this.AddFlashAlphaGex("SPY").Symbol;
}
public override void OnData(Slice slice)
{
if (!slice.ContainsKey(_gex)) return;
var gex = slice.Get<FlashAlphaGexBar>(_gex);
// SetHoldings on the EQUITY symbol, not the GEX symbol.
SetHoldings(_spy, gex.NetGexLabel == "positive" ? 1.0m : 0m);
}
For multi-ticker setups, pair the two by ticker string with a Dictionary<string, Symbol>:
private readonly Dictionary<string, Symbol> _equity = new();
private readonly Dictionary<string, Symbol> _gex = new();
public override void Initialize()
{
foreach (var t in new[] { "SPY", "QQQ", "IWM" })
{
_equity[t] = AddEquity(t, Resolution.Daily).Symbol;
_gex[t] = this.AddFlashAlphaGex(t).Symbol;
}
}
There's a full multi-ticker walkthrough in docs/recipes/combine-flashalpha-with-equity-data.md.
Why doesn't the bridge unify the symbols?
QC's API doesn't let you. AddData<T> returns a BaseData-typed subscription whose Symbol is owned by the custom-data subsystem; we have no hook to attach it to an existing equity Symbol. Every custom-data provider in the LEAN ecosystem has the same constraint.
bar-fields-null
A bar arrives in OnData but key fields read as null / None / 0.
Causes by bar
-
GEX / DEX / VEX / CHEX.
Strikesper-rowCallVolumeandPutVolumeare placeholders on historical — the minute table doesn't retain intraday volume. Use theCallOi/PutOifields for the historical positioning view. -
Option-quote. Per-row
BidSize/AskSize/Volumeare always 0 on historical;SviVolis alwaysnullwithSviVolGated == "backtest_mode". Documented in the bar docstring. -
Stock summary.
OptionsFlow.TotalCallVolume,TotalPutVolume, andPcRatioVolumeare 0 / null — no minute volume on replay.Macro.VixFuturesandMacro.FearAndGreedare always null on historical (CME futures and the CNN index are not historically reconstructible). -
VRP.
StrategyScoresandNetHarvestScoreare null on early historical timestamps with insufficient warmup — check theWarningslist. Also:ZScoreandPercentilelive onbar.Vrp, not the top level;NetGexlives onbar.Regime. These look like null at the top level but are populated one nesting in. -
Zero-DTE. On names with no same-day expiry the entire bar is "thin" —
NoZeroDte = true,Messagepopulated, every other blocknull,NextZeroDteExpirypointing at the next available expiry. Null-check explicitly:if bar.NoZeroDte: self.Debug(f"No 0DTE today — next is {bar.NextZeroDteExpiry}") return
General null-safety
Always null-check nested blocks before drilling in. The Slice.Get<T> indexer can return null if the bar didn't arrive on this slice (e.g. weekends, holidays), and the bar itself can have nullable nested blocks even on a successful response.
var bar = slice.Get<FlashAlphaZeroDteBar>(_zeroDte);
if (bar?.PinRisk == null) return;
var pinScore = bar.PinRisk.Score;
bar = slice[self.zero_dte]
pin_risk = bar.PinRisk or {}
pin_score = pin_risk.get("score")
if pin_score is None:
return
tier-restricted
The API returned a 403 with the body {"error":"tier_restricted"}. The bridge surfaces this as FlashAlphaUnauthorizedException — same error code path as a bad key.
Endpoints currently gated by plan tier:
| Endpoint | Required tier |
|---|---|
vrp | Alpha or above |
adv-volatility | Alpha or above |
stock/summary | Alpha or above |
Fix: upgrade in the FlashAlpha dashboard, or remove the AddData<…> for the restricted bar. The other 14 endpoint families are free-tier.
cold-cache-adv-vol
The adv-volatility endpoint runs SVI calibration plus full surface arbitrage checks on each request. Cold-cache responses can take ~1.5s. First request after a process restart, or on a date that's never been requested before, blows past the default 30-second HTTP timeout only on pathological surfaces — but per-bar latency is noticeably higher than gex or surface.
If you're hitting timeouts:
- Bump
FlashAlphaConfig.HttpTimeoutto 60 seconds. - Don't subscribe
adv-volatilityminute-by-minute. The endpoint is daily-cadence in spirit; minute subscriptions multiply cost without giving you fresh SVI parameters between bars. - Pre-warm the cache by hitting the date range in a research notebook (
lean research) before running the backtest.