FMP client migration: stockbee Batch C (#495)
The following standalone skills now vendor a generated scripts/fmp_client.py
from scripts/fmp_client/registry.py, the shared family-B core with API-call
budget, and the stockbee_stable extension:
stockbee-20pct-studystockbee-episodic-pivot-analyzerstockbee-exhaustion-hammer-screenerstockbee-momentum-burst-screenerstockbee-setup-fluency-trainer
Each main script keeps a thin module-level FMPClient adapter that preserves
its public class name, constructor, methods, return shapes and stats keys. The
adapter loads the sibling generated client by file path under a per-skill
module name, so the five bundles never share a fmp_client module. HTTP,
authentication, redaction, rate limiting, JSON retry, 429 handling and budget
accounting run in the generated client. The existing seventeen generated clients
are unchanged: the core template and redaction fragment are untouched.
The extension adds three stable-only primitives on top of the shared
_rate_limited_get:
fetch_screener_rows(params)for/stable/company-screenerfetch_eod_rows(symbol, days, pad_days=10)for/stable/historical-price-eod/fullfetch_quote_row(symbol)for/stable/quote
They return the provider list unchanged ([] when the provider answered
without rows) or None on a transport error, an HTTP error or a non-list 200
body. They never call _record_endpoint_failure or consult
_disabled_endpoints: an empty answer for a delisted or new ticker is data,
not an endpoint failure, so no streak of empty symbols can disable a scan.
Preserved behavior
CLI --api-key / FMP_API_KEY precedence, report formats, universe filters,
sorting, normalizers (normalize_fmp_historical_response,
normalize_price_bars, normalize_bars), quote-bar construction and every
non-client function are unchanged. Offline paths (--prices-json, event JSON,
model-book files) still run without requests, and the existing “requests
missing” errors are kept. Each adapter keeps its local budget exception class
and translates the shared ApiCallBudgetExceeded into it. Episodic-pivot keeps
its soft budget behavior: get_historical_prices returns []. Each skill keeps its own default budget.
Non-budget exceptions from the shared layer, such as the tzdata
RuntimeError, propagate.
Episodic-pivot’s unused get_profile was deleted with its tests. It is the
only public method removed.
Intentional transport changes
- Stable only. A 402, 403 or empty stable response no longer tries a v3 URL.
The quote call in the hammer screener moves from v3
quote/{symbol}to/stable/quote. - A 429 waits 60 seconds and retries once; a repeated 429 sets
rate_limit_reached. The shared core resets its retry counter only on an HTTP 200, so after a 429 whose retry ended in another error, the next 429 stops immediately without waiting. This is inherited core behavior shared by all generated clients. Momentum and hammer previously stopped immediately; 20pct, fluency and episodic-pivot did not track 429. - A malformed 200 JSON body is retried once, then fails soft.
- History windows use the America/New_York date. The hammer default bar date uses the same helper.
- 20pct switches from query-parameter auth to header auth. Hammer drops its duplicate query-parameter key.
- Episodic-pivot’s counter counts completed responses plus retries, and no longer counts requests that raised a transport exception.
- HTTP-error diagnostics are redacted instead of printing the raw
response.text[:200]. History and universe failures print their status to stderr in all five skills. Episodic-pivot was silent before and now prints them too. The best-effort hammer quote override stays quiet. - Fluency no longer propagates a raw
RequestException. Because failures are now soft,command_updatefails closed. A request fails when the shared fetch returnsNone, which covers transport errors, HTTP errors and the 429 short-circuit;[]is not a failure. If all N symbols fail, it printsERROR: FMP price fetch failed for all N symbols; model book not updatedto stderr, returns exit code 1, saves no model book and writes no report. If only some fail, the model book is saved and the report carriesfailed_requests.--prices-jsonruns are unaffected, and budget exhaustion still raises. - 20pct’s fetch loop no longer swallows
RuntimeError.
Gate coverage
The generated client drift gate covers 22 targets. CI, pre-commit and the PR
template package checks include the five new archives, and the provider
contracts list the stockbee skills as owners of historical-price-eod-full
(all five), quote (hammer) and company-screener (momentum, hammer, 20pct).
The company-screener contract notes now say which consumers are stable-only
and which three still fall back to v3 /stock-screener. Repo tests cover the extension rendering only into these five
clients, no circuit breaker in the extension block, and fake-transport checks of
the three primitives: stable URL and params, the ET window, None on HTTP error
or non-list body, [] passthrough, and no request to /api/v3/. Each skill
suite runs in its own process because the bundles share the module name
fmp_client. scripts/tests/test_stockbee_fmp_migration.py loads each
bundle in a fresh interpreter. It asserts that the adapter uses its own
sibling client, never imports the bare fmp_client name, and sends requests
only to /stable.
This PR completes Batch C, not the remaining #495 batches.