Skip to main content

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 symbol and mic are looked up as of date. Pass bestex_id to 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)

Source distribution for bestex-pulse 1.0.0
File Size Uploaded Provenance
bestex_pulse-1.0.0.tar.gz 48.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for bestex-pulse 1.0.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page