Metadata-Version: 2.5
Name: bestex-pulse
Version: 1.0.0
Summary: Python client for BestEx Research Pulse Analytics: market impact cost estimates and analytics for futures and US equities.
Project-URL: Documentation, https://mi.bestexresearch.com/api/docs/v2/
Author: BestEx Research
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.6
Provides-Extra: pandas
Requires-Dist: pandas>=2.2; extra == 'pandas'
Description-Content-Type: text/markdown

# 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

```bash
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:

```python
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:

```bash
export PULSE_EMAIL=you@example.com
export PULSE_PASSWORD='...'
```

```python
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

```python
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`.

```python
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:

```python
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.

```python
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`:

```python
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).

```python
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.

```python
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 |

```python
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

```python
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:

```python
with Pulse() as pulse:
    pulse.futures.usage()
```

## Versions

`bestex-pulse` follows [semantic versioning](https://semver.org): 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()`.
