bestex-pulse
Python client for BestEx Research Pulse Analytics: pre-trade market impact cost estimates and trading analytics for futures and US equities.
Results are typed objects, so your editor completes field names and a type checker catches mistakes. Lists
and tables turn into pandas DataFrames with .to_pandas(). The full API reference, with every field
explained, is at https://mi.bestexresearch.com/api/docs/v2/.
Install
pip install bestex-pulse
pip install "bestex-pulse[pandas]" # also installs pandas, for .to_pandas()
Python 3.10 or later. You need a Pulse account with access to the asset classes you call.
Logging in
In a notebook or terminal, just create a client. If no credentials are set, you're asked for them, and a mistyped password can be retyped:
from bestex_pulse import Pulse
pulse = Pulse()
# Pulse email: you@example.com
# Pulse password:
# Logged in as you@example.com
For scripts and scheduled jobs, set your credentials in the environment instead of in code:
export PULSE_EMAIL=you@example.com
export PULSE_PASSWORD='...'
pulse = Pulse(interactive=False) # never prompts; fails at once if the variables are missing
Without a terminal (cron, CI, schedulers, most containers) the client never prompts anyway.
interactive=False also covers a job you start from a terminal and leave running in the background.
Your password is used only to log in. The session token the API returns is kept in memory and never written to disk; if it expires, the client logs in again once by itself. Use one client per process: it is safe to share between threads.
Quickstart
from bestex_pulse import Pulse
pulse = Pulse()
# What would 500 E-mini S&P contracts cost, worked over an hour?
est = pulse.futures.costs.fixed_duration(
exchange_symbol="ESU26",
exchange_mic_code="XCME",
date="2026-06-23",
ordersize=500,
start_time="14:30",
end_time="15:30", # UTC
)
print(est.total_bps)
# And 200,000 shares of Apple?
est = pulse.equities.costs.fixed_duration(
symbol="AAPL",
mic="XNAS",
date="2026-06-23",
shares=200_000,
start_time="14:00",
end_time="15:00", # UTC
)
print(est.predicted_total_cost_bps)
Futures
Times are UTC. Dates are datetime.date or "YYYY-MM-DD"; times are datetime.time or "HH:MM"
(seconds are ignored: futures estimates work to the minute). Identify a contract by exchange_symbol and
exchange_mic_code, or by bloomberg_id.
import datetime as dt
est = pulse.futures.costs.fixed_duration(
exchange_symbol="ESU26",
exchange_mic_code="XCME",
date=dt.date(2026, 6, 23),
ordersize=500,
start_time="14:30",
end_time="15:30", # UTC
)
est.total_bps # total estimated cost: market impact + VWAP slippage
est.cost_permanent_bps # the size-driven part
est.fit_confidence
| Method | Returns |
|---|---|
futures.instruments.supported_symbols() |
covered base symbols (list) |
futures.instruments.active(date=, futures_base_symbol=, exchange_mic_code=) |
contracts that traded that day (list) |
futures.costs.fixed_duration(...) |
cost of an order worked between two times |
futures.costs.fixed_participation(..., participation=) |
cost at a fixed participation rate |
futures.costs.optimal_with_alpha_decay(..., order_size=, adf=) |
the cheapest pace given alpha decay; .grid_to_pandas() |
futures.analytics.trading_stats(start_date=, end_date=, ...) |
daily statistics (list) |
futures.analytics.realized_stats(...) / estimated_stats(...) |
interval statistics |
futures.analytics.volume_estimate(...) |
interval and day volume |
futures.usage() |
your API usage and limits |
Cost estimates carry total_bps, total_ticks, total_pct_spread and total_contract: market impact
plus VWAP slippage, the total estimated cost. Values the API could not compute are None.
To find the contract to ask about, list the ones that traded on a day:
contracts = pulse.futures.instruments.active(date="2026-06-22", futures_base_symbol="ES", exchange_mic_code="XCME")
front = min(contracts, key=lambda c: c.volume_rank) # the most traded
front.exchange_symbol
US equities
Times are UTC, as for futures. Identify an instrument by bestex_id, or by symbol and mic, which the
client looks up for you, as of the request's date, and remembers.
est = pulse.equities.costs.fixed_duration(
symbol="AAPL",
mic="XNAS",
date="2026-06-23",
shares=200_000,
start_time="14:00",
end_time="15:00", # UTC
)
est.predicted_total_cost_bps
est.confidence_level, est.confidence_reasons
bars = pulse.equities.history.bars(symbol="AAPL", mic="XNAS", date="2026-06-23", fields=["close_price", "volume"])
bars.to_pandas() # one row per minute
| Group | Methods |
|---|---|
equities.instruments |
resolve(), get(bestex_id), list(country=...) |
equities.costs |
fixed_duration(), fixed_participation(pov=...), closing_auction() |
equities.analytics |
intraday_profile(), liquidity_forecast() |
equities.history |
bars(), bars_enhanced(), eod(), eod_latest(bestex_ids=[...]) |
equities.reference |
calendar(), countries(), exchanges(), trading_hours() |
equities.meta |
availability(), cost_model(), usage() |
Participation here is pov (a fraction, 0-1) and the volume cap is vc, as in the API. eod_latest
takes any number of ids and sends them in chunks of 500. Session times from trading_hours() are UTC too.
A symbol lookup is an ordinary API call: it counts towards your usage (and any trial allowance) and the rate
limit, once per new symbol, MIC and date, and the client remembers it. In a loop over many dates, look the
instrument up once and pass its bestex_id:
days = ["2026-06-22", "2026-06-23"]
aapl = pulse.equities.instruments.resolve(symbol="AAPL", mic="XNAS")
for day in days:
pulse.equities.costs.fixed_duration(bestex_id=aapl.bestex_id, date=day, shares=200_000)
Baskets (batch requests)
Price many orders at once. Each order takes the same fields as the single method; keyword arguments apply to every order that doesn't give its own. Symbols are looked up in bulk, and any number of orders can be passed (they are sent 500 at a time).
basket = pulse.equities.costs.fixed_duration_batch(
[
{"symbol": "AAPL", "mic": "XNAS", "shares": 200_000},
{"symbol": "MSFT", "mic": "XNAS", "shares": 150_000},
{"bestex_id": 10276, "shares": 50_000, "start_time": "15:00"}, # its own start time
],
date="2026-06-23",
start_time="14:00",
end_time="15:30", # UTC, for every order
)
basket.ok # the orders that were priced
basket.errors # the ones that weren't, each with .error.error_code and .error.message
basket.to_pandas() # one row per order, in the order given, failures included
for item in basket:
if item.error is not None:
print(item.index, item.error.error_code, item.error.message)
else:
print(item.index, item.value.predicted_total_cost_bps)
A failed order doesn't fail the basket: it comes back as that order's error. An unknown symbol, for example,
is IDENTIFIER_NOT_FOUND. A malformed order (an unknown field, a badly formatted date) raises ValueError
before anything is sent, naming the item: orders[2]: ... (requests[2] and items[2] in the forecast and
lookup batches). In .to_pandas(), the status column is "ok" or "error".
| Method | Each item takes the fields of |
|---|---|
equities.costs.fixed_duration_batch(orders, ...) |
costs.fixed_duration() |
equities.costs.fixed_participation_batch(orders, ...) |
costs.fixed_participation() |
equities.costs.closing_auction_batch(orders, ...) |
costs.closing_auction() |
equities.analytics.liquidity_forecast_batch(requests, ...) |
analytics.liquidity_forecast() |
equities.instruments.resolve_batch(items, ...) |
instruments.resolve() |
Tables
Lists of results have .to_pandas(), which needs pandas 2.2 or later: pip install "bestex-pulse[pandas]".
Without pandas everything else works; only .to_pandas() asks you to install it.
symbols = pulse.futures.instruments.supported_symbols()
symbols[0].futures_base_symbol # typed results
symbols.to_pandas() # a DataFrame
profile = pulse.equities.analytics.intraday_profile(symbol="AAPL", mic="XNAS", date="2026-06-23")
df = profile.to_pandas() # one row per bar
df.attrs # the day-level values, e.g. the instrument
Responses that hold one value per bar or per day (bars, end-of-day history, the intraday profile) become one
row per bar or day, with the values that describe the whole response in df.attrs. The alpha-decay optimum's
search grid is optimum.grid_to_pandas(). Missing values are pandas' <NA> (NaT in date columns), and
dates are real date columns.
Errors
Every error the client raises is a PulseError:
| Exception | When |
|---|---|
AuthenticationError |
wrong email or password, or the session was rejected |
PermissionDeniedError |
your account can't use this endpoint: no access to the asset class, a trial that has ended, or a network location your account isn't allowed to call from |
RateLimitError |
too many requests, after the client's own retries |
ServerError |
the API failed on its side or is temporarily unavailable |
PulseAPIError |
any other error answer, such as a request the API rejects (the parent of the four above) |
PulseConnectionError |
the API couldn't be reached, after retries |
ResponseValidationError |
the answer didn't match what this version expects: upgrading bestex-pulse usually fixes it |
from bestex_pulse import PulseAPIError
try:
pulse.equities.costs.fixed_duration(symbol="AAPL", mic="XNAS", date="2026-06-23", shares=200_000)
except PulseAPIError as exc:
print(exc.status_code, exc.error_code, exc.message, exc.details)
error_code is a stable, machine-readable code where the API gives one (for example
IDENTIFIER_NOT_FOUND), and details holds the specifics. Arguments of the wrong type or format raise
TypeError or ValueError before anything is sent.
Things to know
- All times are UTC, for both asset classes, in requests and in results.
- A ticker means what it meant on the request's date. Tickers get reused, so
symbolandmicare looked up as ofdate. Passbestex_idto pin an instrument. - Some fields can be
None. For example, a futures estimate for a day that hasn't traded yet has no realized volume or prices. Check before doing arithmetic. - Each call counts towards your usage and the rate limit, lookups included.
pulse.futures.usage()shows where you stand.
Timeouts, retries and proxies
import httpx
pulse = Pulse(
timeout=60, # seconds to wait for each response (default 30)
max_retries=5, # retries after throttling or a temporary failure (default 3)
)
# Behind a corporate proxy, or with your own certificate bundle:
pulse = Pulse(http_client=httpx.Client(proxy="http://proxy.example.com:8080", verify="/path/to/ca-bundle.pem"))
Throttled requests (HTTP 429) and temporary failures (502, 503, 504, network errors) are retried, waiting as
the API asks or, failing that, half a second, then one second, then two. Other errors are not retried. The
client only talks https:// and never follows a redirect, so your password can't be sent anywhere else. If you
pass your own http_client, close it yourself; otherwise use pulse.close(), or a with block:
with Pulse() as pulse:
pulse.futures.usage()
Versions
bestex-pulse follows semantic versioning: upgrades within a major version (1.x) won't
break your code. A breaking change needs a new major version (2.0), and the changes below say what changed and how
to move. To take fixes and new features but never a breaking change, pin the major version: bestex-pulse>=1,<2.
New fields the API adds are ignored by older versions, so upgrading is needed only to use them. If a field your
version requires disappears or changes type, you get a ResponseValidationError that says to upgrade. Fields
that can legitimately be empty read as None when the API doesn't send them, so treat None as "no value",
not as zero.
Changes
1.0.0
First release: futures and US equities, single requests and batches, typed results with .to_pandas().
Metadata
Release files for bestex-pulse 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| bestex_pulse-1.0.0.tar.gz | 48.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bestex_pulse-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.0 kB
Release files / bestex_pulse-1.0.0.tar.gz
| Download URL | bestex_pulse-1.0.0.tar.gz |
|---|---|
| Size | 48.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ed8416d426931a016ac25e172fac12b3c63b12141621e99f0c0caa085a11a42f
|
|
BLAKE2b-256 checksum How to use checksums |
78f161a4a0eb94ba3b69f25c20aa2f9743f7e862c8b3b35617ebb6de4b4995ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / bestex_pulse-1.0.0-py3-none-any.whl
| Download URL | bestex_pulse-1.0.0-py3-none-any.whl |
|---|---|
| Size | 56.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9bc691f721418be08415fdaecb2e68a6dba76107326a28f2506f0cbeb2761574
|
|
BLAKE2b-256 checksum How to use checksums |
01e611cfa7b6a9a55b05b7bd102ad0e59ac1ae17a0090072260ce9163c7ce716
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for bestex-pulse 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.