Metadata-Version: 2.5
Name: vanda-api
Version: 1.1.1
Summary: Official Python SDK for Vanda Analytics Data API
Project-URL: Homepage, https://gitlab.com/yourusername/vanda-api
Project-URL: Documentation, https://gitlab.com/yourusername/vanda-api#readme
Project-URL: Repository, https://gitlab.com/yourusername/vanda-api
Project-URL: Issues, https://gitlab.com/yourusername/vanda-api/-/issues
Author-email: Jonathan Aina <Jonathan.Aina@vanda.com>
License: MIT
License-File: LICENSE
Keywords: analytics,api,finance,market-data,retail-trading,vanda
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: black>=23.0.0; extra == 'dev'
Requires-Dist: mypy>=1.5.0; extra == 'dev'
Requires-Dist: pandas-stubs>=2.0.0; extra == 'dev'
Requires-Dist: pre-commit>=3.3.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: respx>=0.20.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=1.5.0; extra == 'pandas'
Description-Content-Type: text/markdown

# Vanda Analytics Data API SDK

Official Python SDK for the Vanda Analytics Data API.

## Installation

```bash
pip install vanda-api
```

For pandas support:

```bash
pip install vanda-api[pandas]
```

## Quick Start

### Synchronous Client

```python
from datetime import date
from vanda import VandaClient

with VandaClient(token="YOUR_TOKEN_HERE") as client:
    data = client.series.get_timeseries(
        symbol="TSLA",
        start_date=date(2025, 12, 1),
        end_date=date(2025, 12, 31),
        fields=["retail_net_turnover", "retail_buy_turnover"],
    )
    print(f"Retrieved {len(data)} records")

# For transformation option
with VandaClient(token="YOUR_TOKEN_HERE") as client:
    data = client.series.get_timeseries(
        symbol="TSLA",
        start_date=date(2025, 12, 1),
        end_date=date(2025, 12, 31),
        fields=["retail_net_turnover", "retail_buy_turnover"],
        transformation="moving_avg",
        window="5",
        is_full_history=True,
    )
    print(f"Retrieved {len(data)} records")

# For transformation and normalisation option
with VandaClient(token="YOUR_TOKEN_HERE") as client:
    data = client.series.get_timeseries(
        symbol="TSLA",
        start_date=date(2025, 12, 1),
        end_date=date(2025, 12, 31),
        fields=["retail_net_turnover", "retail_buy_turnover"],
        transformation="moving_avg",
        window="5",
        normalisation="percentile_rank",
        normalisation_period="1y",
        is_full_history=True,
    )
    print(f"Retrieved {len(data)} records")
```

```python
from vanda import VandaClient

# Pass credentials directly
with VandaClient(email="your_email@example.com", password="your_password") as client:
    data = client.series.get_timeseries(
        symbol="TSLA",
        start_date="2025-12-01",
        end_date="2025-12-31",
        fields=["retail_net_turnover"],
    )

# For transformation option
with VandaClient(email="your_email@example.com", password="your_password") as client:
    data = client.series.get_timeseries(
        symbol="TSLA",
        start_date="2025-12-01",
        end_date="2025-12-31",
        fields=["retail_net_turnover"],
        transformation="moving_avg",
        window="5",
        is_full_history=True,
    )

# For transformation and normalisation option
with VandaClient(email="your_email@example.com", password="your_password") as client:
    data = client.series.get_timeseries(
        symbol="TSLA",
        start_date="2025-12-01",
        end_date="2025-12-31",
        fields=["retail_net_turnover"],
        transformation="moving_avg",
        window="5",
        normalisation="percentile_rank",
        normalisation_period="1y",
        is_full_history=True,
    )

```

### Recommended to use environment variables

```python
export VANDA_LOGIN_EMAIL="your_email@example.com"
export VANDA_PASSWORD="your_password"
```

### Asynchronous Client

```python
import asyncio
from datetime import date
from vanda import AsyncVandaClient

async def main():
    async with AsyncVandaClient(token="YOUR_TOKEN_HERE") as client:
        data = await client.series.get_timeseries(
            symbol="TSLA",
            start_date=date(2025, 12, 1),
            end_date=date(2025, 12, 31),
            fields=["retail_net_turnover"],
        )
        print(f"Retrieved {len(data)} records")

    async with AsyncVandaClient(token="YOUR_TOKEN_HERE") as client:
        data = await client.series.get_timeseries(
            symbol="TSLA",
            start_date=date(2025, 12, 1),
            end_date=date(2025, 12, 31),
            fields=["retail_net_turnover"],
            transformation="moving_avg",
            window="5",
            normalisation="percentile_rank",
            normalisation_period="1y",
            is_full_history=True,
        )
        print(f"Retrieved {len(data)} records")


asyncio.run(main())
```

```python
import asyncio
from vanda import AsyncVandaClient

async def main():
    async with AsyncVandaClient(email="your_email@example.com", password="your_password") as client:
        data = await client.series.get_timeseries(
            symbol="TSLA",
            start_date="2025-12-01",
            end_date="2025-12-31",
            fields=["retail_net_turnover"],
        )

    async with AsyncVandaClient(email="your_email@example.com", password="your_password") as client:
        data = await client.series.get_timeseries(
            symbol="TSLA",
            start_date="2025-12-01",
            end_date="2025-12-31",
            fields=["retail_net_turnover"],
            transformation="moving_avg",
            window="5",
            normalisation="percentile_rank",
            normalisation_period="1y",
            is_full_history=True,
        )

asyncio.run(main())
```

### Recommended to use environment variables

```python
export VANDA_LOGIN_EMAIL="your_email@example.com"
export VANDA_PASSWORD="your_password"
```

## Authentication

### SDK API key

Generate a key in the Vanda frontend at
[www.vanda-analytics.com/tools/api-keys](https://www.vanda-analytics.com/tools/api-keys),
then pass it to the client:

```python
client = VandaClient(sdk_key="your_key_here")
```

or set it in the environment:

```bash
export VANDA_SDK_KEY="your_key_here"
```

A key belongs to your user, so your own entitlement and tier limits apply to
every request made with it. It is sent as a header and needs no token exchange,
so there is nothing to refresh and nothing cached on disk. Keep it secret and
revoke it from the same page if it leaks.

An SDK key authenticates every service the client covers - `series`,
`aggregates`, `positioning` and `flow`. What it does not do is widen your
entitlement: a full-history export, for example, still needs approved Data
Licence access and is otherwise refused with `RateLimitError`.

### Token, or email and password

Set your API token via environment variable:

```bash
export VANDA_API_TOKEN="your_token_here"
```

or

```bash
export VANDA_LOGIN_EMAIL="your_email@example.com"
export VANDA_PASSWORD="your_password"
```

Or pass directly:

```python
client = VandaClient(token="your_token_here")
```

or

```python
client = VandaClient(email="your_email@example.com", password="your_password")
```

If more than one credential is supplied, `sdk_key` wins, then `token`, then
email and password.

## Features

- Sync and async clients with consistent interfaces
- Automatic retry with exponential backoff
- Comprehensive error handling
- Job polling for async operations
- Export utilities (CSV, JSONL)
- Optional pandas support
- Type hints throughout

## API Methods

### Timeseries Data

- `get_timeseries()` - Get timeseries for a single symbol
- `get_timeseries_many()` - Get timeseries for multiple symbols
- `get_leaderboard()` - Get ranked leaderboard data

### Bulk Operations

- `bulk_securities()` - Bulk fetch securities data
- `get_daily_snapshot()` - Get daily snapshot for many securities

### Job Management

- `create_bulk_securities_job()` - Create async job
- `poll_job()` - Poll job until completion
- `get_job_status()` - Get job status
- `export_job_result()` - Export job result to file
- `stream_job_result()` - Stream job result to file

### Export Operations

- `export_timeseries()` - Export timeseries to file

### Metadata

- `list_fields()` - List available fields
- `list_intervals()` - List available intervals
- `list_securities()` - List available securities

### Aggregates Timeseries Data
- `get_timeseries()` - Get timeseries for multiple aggregate id's
- `get_constituents()` - Get constituents using Vanda identifier

### Positioning Data

Accessed via `client.positioning`.

- `get_timeseries()` - Get positioning timeseries for one or more series
- `get_latest()` - Get the latest reading per series, with its crowding signal
- `export_timeseries()` - Export positioning timeseries to file
- `list_catalog()` - Search the catalog and filter by dimension

```python
from vanda import VandaClient

with VandaClient(token="YOUR_TOKEN_HERE") as client:
    catalog = client.positioning.list_catalog(sector="Energy", records_per_page=5)
    series_label = catalog["data"][0]["identity"]["series_label"]

    data = client.positioning.get_timeseries(
        series_ids=series_label,
        start_date="2025-01-01",
        end_date="2025-12-31",
    )
    print(data["count"], data["pagination"])
```

Responses keep their envelope: alongside `data` you get `count`, `metric`,
`revision`, `series_ids`, `version` and `pagination`. Use `pagination` to page -
`records_per_page` defaults to 2000 and is capped by your access tier.

`transformation` and `window` must be passed together, and a transformation has
to resolve to exactly one series.

Positioning normalises by default: omitting `normalisation` /
`normalisation_period` is the same as asking for `zscore` over `All`. Neither
needs its partner, since the service defaults both. Pass `normalisation="none"`
for the raw stored values.

```python
    raw = client.positioning.get_timeseries(series_ids=series_label, normalisation="none")
    ranked = client.positioning.get_timeseries(
        series_ids=series_label,
        normalisation="percentile_rank",
        normalisation_period="1y",
    )
```

Note that `data` always holds the raw stored series - transformed and normalised
values arrive under `transformation["results"]["data"]`. Asking for more than one
series skips the normalisation rather than failing, reporting
`transformation.status` as `"skipped"`. `normalisation_period` is a calendar
lookback (`3m` to `All`), where `window` counts observations.

### Flow Data

Accessed via `client.flow`.

- `get_timeseries()` - Get flow timeseries for one or more series
- `get_latest()` - Get the latest observation per series
- `export_timeseries()` - Export flow timeseries to file
- `list_catalog()` - Search the catalog and filter by dimension
- `get_forecast_runs()` - List the runs, horizons and scenarios a forecast series publishes
- `get_forecast()` - Get one scenario of the forecast cube

```python
from vanda import VandaClient

with VandaClient(token="YOUR_TOKEN_HERE") as client:
    catalog = client.flow.list_catalog(metric="net", records_per_page=5)

    # Catalog entries are keyed by series_label. Read `series` for anything
    # per-series, and address series by `slug` - `series_id` changes when the
    # pipeline reloads.
    slug = catalog["data"][0]["series"][0]["slug"]

    data = client.flow.get_timeseries(
        series_ids=slug,
        metrics=["net", "buy"],
        start_date="2026-01-01",
        end_date="2026-08-03",
    )
    print(data["metrics"], data["unresolved"], data["pagination"])
```

`series_ids`, `metrics` and `frequency` accept either a comma-separated string
or a sequence. Identifiers that matched nothing are reported in `unresolved`
rather than failing the request, so check it before treating a short result as
complete. That holds as long as *something* resolved - a request where no
identifier matches is a 404 and raises `NotFoundError`.

Some flow datasets are published as a cube rather than a flat series, with a
value per combination of dimensions. `get_timeseries()` serves **history only**,
and a forecast series' history is its neutral cell, so that is the one cell
`dimensions` will pin here:

```python
data = client.flow.get_timeseries(
    series_ids=slug, dimensions={"projection": 0, "zscore": "All"}
)
```

Any other cell is refused with a 400 - read the cube with `get_forecast()`
instead. It is sent as JSON because the types decide what matches:
`projection` is stored as a number and `zscore` as a string, so
`{"projection": "0"}` matches nothing at all.

Intraday series (FX options) are capped at 31 days per request. A request with
no dates returns the most recent 31 days rather than the full history, a longer
span is refused, and transformations are not available on them yet.

Flow normalisation is opt-in, where positioning's is on by default, and flow
pairs its params: `normalisation` / `normalisation_period` must be passed
together, as must `transformation` / `window`. A transformation has to resolve to
exactly one series, and flow has no `none` value - simply omit them for raw
values.

#### Forecasts

Some flow datasets publish a forecast **cube**: every combination of a horizon
and a scenario, for each run date. Ask what a series publishes before reading
it - the scenario axis is named per component, `zscore` for `cta`, `rp` and
`vt`, `ret` for `gamma`:

```python
with VandaClient(token="YOUR_TOKEN_HERE") as client:
    axes = client.flow.get_forecast_runs(series_ids="AUFXFUTUAUDMIXINSSPECTA")
    series = axes["data"][0]
    print(series["components"], series["scenario_key"], series["horizons"])

    forecast = client.flow.get_forecast(
        series_ids="AUFXFUTUAUDMIXINSSPECTA",
        scenario="All",
        horizons=[1, 5, 10],
    )
    print(forecast["run_date"], forecast["scenario"])
    for group in forecast["data"]:
        print(group["horizon"], group["component"], group["metrics"])
```

Ask for several branches at once with `scenarios` - a sequence, a
comma-separated string, or `"*"` for every one published:

```python
whole_run = client.flow.get_forecast(
    series_ids="AUFXFUTUAUDMIXINSSPECTA", scenarios="*", horizons=[5], metrics="px"
)
print(whole_run["scenarios"])                      # every branch served, in axis order
print(whole_run["data"][0]["scenario"])            # the branch is now part of the grain
```

`scenario` and `scenarios` are mutually exclusive. With `scenarios` the
top-level `scenario` is `None` and each group carries its own instead. A cta run
is 64 scenarios x 16 horizons, so narrow with `metrics` or `horizons` unless you
really want the lot.

Rows come back as one group per (series, component, horizon), with every metric
nested inside - a forecast is read as a point on a path. Horizon 0 is the run
date itself, so on the z-score axis it exists only for the unconditional
scenario (`"All"`). There is no `forecast_date`: pair `run_date` with `horizon`.
Omitting `horizons` returns every horizon the series publishes, which is the
largest response this endpoint produces - name the ones you need, or page.

Neither method takes a transformation or a normalisation; a projection is not a
series to roll up or re-standardise.

For both services, if the transformation service is unavailable the
untransformed rows are returned with `transformation.status` set to `"Failure"`
rather than raising:

```python
data = client.flow.get_timeseries(
    series_ids=slug, transformation="moving_avg", window="20"
)
if data["transformation"]["status"] == "Failure":
    print("untransformed:", data["transformation"]["detail"])
```

## Examples

See `examples/` directory for complete usage examples.

## Requirements

- Python 3.9+
- httpx

## Development

```bash
git clone https://gitlab.com/yourusername/vanda-api.git
cd vanda-api
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
pre-commit install
pytest
```

# Migration Guides

## Migration Guide (from v1.0.0 to v1.0.1)

Version **1.0.1** introduces a standardized response format across multiple APIs and adds support for transformations and normalisations in timeseries endpoints.

### Breaking Change

The following APIs no longer return a list directly. They now return a dictionary containing the records under a `data` key.

Affected APIs:

```python
client.series.get_timeseries()
client.series.get_timeseries_many()
client.series.get_leaderboard()
client.series.list_securities()
client.series.bulk_securities()
client.series.get_daily_snapshot()

client.aggregates.get_constituents()
```

---

## Response Format Changes

### Previous Response Format (v1.0.0)

```python
response = client.series.get_timeseries(...)

print(type(response))
# list
```

Example response:

```json
[
  {
    "date": "2024-01-01",
    "value": 123
  }
]
```

### New Response Format (v1.0.1)

```python
response = client.series.get_timeseries(...)

print(type(response))
# dict
```

Example response:

```json
{
  "data": [
    {
      "date": "2024-01-01",
      "value": 123
    }
  ]
}
```

The same response structure applies to:

* `get_timeseries()`
* `get_leaderboard()`
* `list_securities()`
* `bulk_securities()`
* `get_daily_snapshot()`
* `get_constituents()`

---

## Accessing Data

### Before

```python
response = client.series.get_timeseries(...)

for row in response:
    print(row)
```

### After

```python
response = client.series.get_timeseries(...)

for row in response["data"]:
    print(row)
```

---

## `get_timeseries_many()` Response Format

`client.series.get_timeseries_many()` returns results grouped by symbol.

### Previous Response Format (v1.0.0)

```json
[
  {
    "symbol": "AAPL",
    "data": [...]
  },
  {
    "symbol": "MSFT",
    "data": [...]
  }
]
```

### New Response Format (v1.0.1)

```json
{
  "data": [
    {
      "symbol": "AAPL",
      "data": [
        {
          "date": "2024-01-01",
          "retail_net_flow": 12345
        }
      ],
      "transformation": {}
    },
    {
      "symbol": "MSFT",
      "data": [
        {
          "date": "2024-01-01",
          "retail_net_flow": 67890
        }
      ],
      "transformation": {}
    }
  ]
}
```

### Accessing Data

```python
response = client.series.get_timeseries_many(...)

for security in response["data"]:
    symbol = security["symbol"]

    for row in security["data"]:
        print(symbol, row)
```

---

## Transformation Metadata

When a transformation and/or normalisation is requested, additional metadata is returned in the `transformation` field.

Example:

```python
response = client.series.get_timeseries(
    symbol="AAPL",
    start_date="2024-01-01",
    end_date="2024-12-31",
    fields=["retail_net_flow"],
    transformation="moving_avg",
    window=20,
)
```

Response:

```json
{
  "data": [
    {
      "date": "2024-01-01",
      "retail_net_flow": 12345
    }
  ],
  "transformation": {
    "results": {
      "data": [
        {
          "date": "2024-01-01",
          "value": 12000
        }
      ]
    }
  }
}
```

---

## New Features

## Additional Parameters

The following optional parameters have been added to:

```python
client.series.get_timeseries()
client.series.get_timeseries_many()
```

### Transformations

```python
transformation="moving_avg"
window=5
```

Supported values:

```python
transformation="rolling_sum"
transformation="moving_avg"
```

### Normalisations

```python
normalisation="percentile_rank"
normalisation_period="1y"
```

Supported values:

```python
normalisation="zscore"
normalisation="percentile_rank"
normalisation="none"          # positioning only - opts out of its default
```

### Supported Normalisation Periods

```python
normalisation_period="3m"
normalisation_period="6m"
normalisation_period="1y"
normalisation_period="2y"
normalisation_period="5y"
normalisation_period="Expanding"
normalisation_period="All"
```

### Full History Support

```python
is_full_history=True
```

---

## Supported Window Values

```python
window=5      # 1 Week
window=20     # 1 Month
window=30     # ~2 Months
window=60     # 3 Months
window=90     # 4 Months
window=120    # 6 Months
window=252    # 1 Year
```

---

## Summary

### Changed

* Standardized API responses to return dictionaries containing a `data` field.
* Added support for transformation metadata in responses.
* `get_timeseries_many()` now returns symbol-grouped results within the top-level `data` field.

### Added

* Transformations (`rolling_sum`, `moving_avg`)
* Normalisations (`zscore`, `percentile_rank`)
* Normalisation periods
* `is_full_history` support


## Migration Guide (From v0.1.2 to v1.0.0)
Old:
client.get_timeseries()
client.get_timeseries_many()
client.get_leaderboard()
client.create_bulk_securities_job()
client.bulk_securities()
client.get_daily_snapshot()
client.get_job()
client.get_job_status()
client.poll_job()
client.wait_for_job()
client.export_job_result()
client.stream_job_result()
client.export_timeseries()
client.list_fields()
client.list_intervals()
client.list_securities()

New:
client.series.get_timeseries()
client.series.get_timeseries_many()
client.series.get_leaderboard()
client.series.create_bulk_securities_job()
client.series.bulk_securities()
client.series.get_daily_snapshot()
client.series.get_job()
client.series.get_job_status()
client.series.poll_job()
client.series.wait_for_job()
client.series.export_job_result()
client.series.stream_job_result()
client.series.export_timeseries()
client.series.list_fields()
client.series.list_intervals()
client.series.list_securities()

## License

MIT License - see LICENSE file for details.

## Support

For issues and questions, please open an issue on GitLab.
