vanda-api 1.0.2__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. vanda_api-1.1.0/CHANGELOG.md +118 -0
  2. {vanda_api-1.0.2 → vanda_api-1.1.0}/PKG-INFO +114 -8
  3. {vanda_api-1.0.2 → vanda_api-1.1.0}/README.md +113 -7
  4. {vanda_api-1.0.2 → vanda_api-1.1.0}/pyproject.toml +1 -1
  5. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/__init__.py +1 -1
  6. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/_base/async_base_client.py +19 -6
  7. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/_base/base_client.py +4 -1
  8. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/async_auth.py +6 -0
  9. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/async_client.py +6 -0
  10. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/auth.py +32 -4
  11. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/client.py +9 -0
  12. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/models.py +10 -0
  13. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/retry.py +27 -2
  14. vanda_api-1.1.0/src/vanda/services/async_flow.py +385 -0
  15. vanda_api-1.1.0/src/vanda/services/async_positioning.py +253 -0
  16. vanda_api-1.1.0/src/vanda/services/flow.py +383 -0
  17. vanda_api-1.1.0/src/vanda/services/positioning.py +253 -0
  18. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/utils/normalize.py +47 -2
  19. vanda_api-1.1.0/src/vanda/utils/params.py +18 -0
  20. vanda_api-1.1.0/tests/test_async_flow_client.py +425 -0
  21. vanda_api-1.1.0/tests/test_async_positioning_client.py +269 -0
  22. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_auth.py +56 -0
  23. vanda_api-1.1.0/tests/test_flow_client.py +423 -0
  24. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_normalize.py +25 -0
  25. vanda_api-1.1.0/tests/test_positioning_client.py +257 -0
  26. vanda_api-1.1.0/tests/test_retry.py +63 -0
  27. vanda_api-1.0.2/CHANGELOG.md +0 -85
  28. vanda_api-1.0.2/tests/test_retry.py +0 -24
  29. {vanda_api-1.0.2 → vanda_api-1.1.0}/.gitignore +0 -0
  30. {vanda_api-1.0.2 → vanda_api-1.1.0}/LICENSE +0 -0
  31. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/_base/__init__.py +0 -0
  32. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/errors.py +0 -0
  33. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/services/__init__.py +0 -0
  34. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/services/aggregates.py +0 -0
  35. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/services/async_aggregates.py +0 -0
  36. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/services/async_series.py +0 -0
  37. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/services/series.py +0 -0
  38. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/utils/__init__.py +0 -0
  39. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/utils/dates.py +0 -0
  40. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/utils/deprecation.py +0 -0
  41. {vanda_api-1.0.2 → vanda_api-1.1.0}/src/vanda/utils/io.py +0 -0
  42. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/__init__.py +0 -0
  43. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_aggregates_client.py +0 -0
  44. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_async_aggregates_client.py +0 -0
  45. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_async_series_client.py +0 -0
  46. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_dates.py +0 -0
  47. {vanda_api-1.0.2 → vanda_api-1.1.0}/tests/test_series_client.py +0 -0
@@ -0,0 +1,118 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] - 2026-09-23
9
+
10
+ ### Added
11
+ - `client.positioning` / `AsyncVandaClient.positioning` - Positioning API support
12
+ - `client.flow` / `AsyncVandaClient.flow` - Flow API support
13
+ - Both expose `get_timeseries()`, `get_latest()`, `export_timeseries()` and `list_catalog()`; flow adds the two forecast methods below
14
+ - `series_ids`, `metrics` and `frequency` accept a sequence as well as a comma-separated string
15
+ - `normalize_result()` accepts `preserve` so responses can keep envelope keys such as `pagination` and `count`
16
+ - Flow's `unresolved` list is preserved, so identifiers that matched nothing are visible to the caller
17
+ - `normalisation` / `normalisation_period` on `positioning.get_timeseries()`, including the positioning-only `"none"` that opts out of the service's default
18
+ - `flow.get_forecast_runs()` - the run dates, horizons, components and scenario tokens a forecast series publishes
19
+ - `flow.get_forecast()` - one scenario of the forecast cube, grouped one object per (series, component, horizon)
20
+ - `dimensions` on `flow.get_timeseries()`, to pin one cell of a dimensioned dataset; accepts a dict and sends it as JSON
21
+ - `sdk_key=` on `VandaClient` / `AsyncVandaClient`, and the `VANDA_SDK_KEY` environment variable - the user API key generated at https://www.vanda-analytics.com/tools/api-keys, sent as the `x-api-key` header
22
+ - Unit tests and examples for both services
23
+
24
+ ### Fixed
25
+ - A failed transformation no longer discards the response. positioning and flow serve the untransformed rows under the upstream's status code; `get_timeseries()` now returns those rows with `transformation.status` reporting `"Failure"` instead of raising `ServerError`
26
+ - Such responses are no longer retried - they are deterministic, and retrying only added latency
27
+
28
+ ### Notes
29
+ - `transformation` requires `window`, and vice versa
30
+ - Positioning normalises by default (`zscore` over `All`), so omitting both params is not the same as raw data; pass `normalisation="none"` for that. Flow's normalisation is opt-in and pairs its two params
31
+ - `data` always holds the raw stored series; transformed and normalised values arrive under `transformation`
32
+ - `flow.get_forecast()` takes neither a transformation nor a normalisation. Horizon 0 is the run date, so on the z-score axis it exists only for the `"All"` scenario, and omitting `horizons` returns every one the series publishes
33
+ - The forecast scenario axis is named per component - `zscore` for `cta`, `rp` and `vt`, `ret` for `gamma` - so read `scenario_key` from `get_forecast_runs()` rather than assuming
34
+ - `dimensions` is matched by JSON containment, so the value types are load-bearing: `{"projection": "10"}` matches nothing where `{"projection": 10}` matches
35
+ - Flow's `unresolved` list only appears when at least one identifier resolved; a request where none do raises `NotFoundError`
36
+ - An SDK key resolves to its owner, so that user's entitlement and tier limits apply; it needs no token exchange and nothing is cached on disk. It authenticates all four services, but does not widen entitlement - a full-history export without Data Licence access still raises `RateLimitError`
37
+ - When several credentials are supplied, `sdk_key` wins, then `token`, then email/password
38
+ - Intraday flow series are capped at 31 days per request, an undated request returns the latest 31 days, and transformations on them are refused by the service
39
+
40
+
41
+ ## [1.0.1] - 2026-06-03
42
+
43
+ ### Changed
44
+ - `get_timeseries()` now returns `dict[str, Any]`
45
+ - `get_timeseries_many()` now returns `dict[str, Any]`
46
+ - `get_leaderboard()` now returns `dict[str, Any]`
47
+ - `list_securities()` now returns `dict[str, Any]`
48
+ - `get_constituents()` now returns `dict[str, Any]`
49
+ - `bulk_securities()` now returns `Union[str, dict[str, Any]]`
50
+ - `get_daily_snapshot()` now returns `Union[str, dict[str, Any]]`
51
+
52
+ ### Added
53
+ - Support for transformations (`rolling_sum`, `moving_avg`)
54
+ - Support for normalisation (`zscore`, `percentile_rank`)
55
+ - Support for configurable rolling windows
56
+ - Support for configurable normalisation periods
57
+ - Support for `is_full_history`
58
+
59
+
60
+ ## [1.0.0] - 2026-03-26
61
+
62
+ ### Added
63
+
64
+ - Moved API methods under namespaces:
65
+ - client.get_timeseries() to client.series.get_timeseries()
66
+ - client.get_timeseries_many to client.series.get_timeseries_many()
67
+ - client.get_leaderboard to client.series.get_leaderboard()
68
+ - client.create_bulk_securities_job to client.series.create_bulk_securities_job()
69
+ - client.bulk_securities to client.series.bulk_securities()
70
+ - client.get_daily_snapshot to client.series.get_daily_snapshot()
71
+ - client.get_job to client.series.get_job()
72
+ - client.get_job_status to client.series.get_job_status()
73
+ - client.poll_job to client.series.poll_job()
74
+ - client.wait_for_job to client.series.wait_for_job()
75
+ - client.export_job_result to client.series.export_job_result()
76
+ - client.stream_job_result to client.series.stream_job_result()
77
+ - client.export_timeseries to client.series.export_timeseries()
78
+ - client.list_fields to client.series.list_fields()
79
+ - client.list_intervals to client.series.list_intervals()
80
+ - client.list_securities to client.series.list_securities()
81
+ - Old methods still work but are deprecated.
82
+ - Added new endpoints for aggregates timeseries and constituents
83
+ - Refactored code to make use of common functions in series and aggregates endpoints
84
+ - Updated unit test cases with new changes for calling the endpoints
85
+ - Updated examples file to use series and aggregates endpoints
86
+
87
+
88
+ ## [0.1.1] - 2026-02-20
89
+
90
+ ### Added
91
+
92
+ - Email/password authentication support with automatic token caching
93
+ - Token cache with expiration management in `~/.vanda/token_cache.json`
94
+ - Entitlement validation on authentication
95
+ - Support for `VANDA_LOGIN_EMAIL` and `VANDA_PASSWORD` environment variables
96
+ - Async authentication methods in `AsyncAuth` class
97
+
98
+ ### Fixed
99
+
100
+ - Fixed `asset_class` parameter format in bulk operations (now sent as list)
101
+ - Fixed async authentication to include entitlement check
102
+
103
+ ## [0.1.0] - 2024-02-06
104
+
105
+ ### Added
106
+
107
+ - Initial release
108
+ - Synchronous VandaClient
109
+ - Asynchronous AsyncVandaClient
110
+ - All 12 API endpoints implemented
111
+ - Retry logic with exponential backoff
112
+ - Comprehensive error handling
113
+ - Job polling utilities
114
+ - CSV/JSONL export support
115
+ - Optional pandas support
116
+ - Type hints throughout
117
+ - Full test coverage
118
+ - CI/CD with GitHub Actions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vanda-api
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: Official Python SDK for Vanda Analytics Data API
5
5
  Project-URL: Homepage, https://gitlab.com/yourusername/vanda-api
6
6
  Project-URL: Documentation, https://gitlab.com/yourusername/vanda-api#readme
@@ -218,6 +218,34 @@ export VANDA_PASSWORD="your_password"
218
218
 
219
219
  ## Authentication
220
220
 
221
+ ### SDK API key
222
+
223
+ Generate a key in the Vanda frontend at
224
+ [www.vanda-analytics.com/tools/api-keys](https://www.vanda-analytics.com/tools/api-keys),
225
+ then pass it to the client:
226
+
227
+ ```python
228
+ client = VandaClient(sdk_key="your_key_here")
229
+ ```
230
+
231
+ or set it in the environment:
232
+
233
+ ```bash
234
+ export VANDA_SDK_KEY="your_key_here"
235
+ ```
236
+
237
+ A key belongs to your user, so your own entitlement and tier limits apply to
238
+ every request made with it. It is sent as a header and needs no token exchange,
239
+ so there is nothing to refresh and nothing cached on disk. Keep it secret and
240
+ revoke it from the same page if it leaks.
241
+
242
+ An SDK key authenticates every service the client covers - `series`,
243
+ `aggregates`, `positioning` and `flow`. What it does not do is widen your
244
+ entitlement: a full-history export, for example, still needs approved Data
245
+ Licence access and is otherwise refused with `RateLimitError`.
246
+
247
+ ### Token, or email and password
248
+
221
249
  Set your API token via environment variable:
222
250
 
223
251
  ```bash
@@ -243,6 +271,9 @@ or
243
271
  client = VandaClient(email="your_email@example.com", password="your_password")
244
272
  ```
245
273
 
274
+ If more than one credential is supplied, `sdk_key` wins, then `token`, then
275
+ email and password.
276
+
246
277
  ## Features
247
278
 
248
279
  - Sync and async clients with consistent interfaces
@@ -316,9 +347,28 @@ Responses keep their envelope: alongside `data` you get `count`, `metric`,
316
347
  `revision`, `series_ids`, `version` and `pagination`. Use `pagination` to page -
317
348
  `records_per_page` defaults to 2000 and is capped by your access tier.
318
349
 
319
- Unlike aggregates, positioning does not accept `normalisation` /
320
- `normalisation_period`; its values are already z-scores. `transformation` and
321
- `window` must be passed together.
350
+ `transformation` and `window` must be passed together, and a transformation has
351
+ to resolve to exactly one series.
352
+
353
+ Positioning normalises by default: omitting `normalisation` /
354
+ `normalisation_period` is the same as asking for `zscore` over `All`. Neither
355
+ needs its partner, since the service defaults both. Pass `normalisation="none"`
356
+ for the raw stored values.
357
+
358
+ ```python
359
+ raw = client.positioning.get_timeseries(series_ids=series_label, normalisation="none")
360
+ ranked = client.positioning.get_timeseries(
361
+ series_ids=series_label,
362
+ normalisation="percentile_rank",
363
+ normalisation_period="1y",
364
+ )
365
+ ```
366
+
367
+ Note that `data` always holds the raw stored series - transformed and normalised
368
+ values arrive under `transformation["results"]["data"]`. Asking for more than one
369
+ series skips the normalisation rather than failing, reporting
370
+ `transformation.status` as `"skipped"`. `normalisation_period` is a calendar
371
+ lookback (`3m` to `All`), where `window` counts observations.
322
372
 
323
373
  ### Flow Data
324
374
 
@@ -328,6 +378,8 @@ Accessed via `client.flow`.
328
378
  - `get_latest()` - Get the latest observation per series
329
379
  - `export_timeseries()` - Export flow timeseries to file
330
380
  - `list_catalog()` - Search the catalog and filter by dimension
381
+ - `get_forecast_runs()` - List the runs, horizons and scenarios a forecast series publishes
382
+ - `get_forecast()` - Get one scenario of the forecast cube
331
383
 
332
384
  ```python
333
385
  from vanda import VandaClient
@@ -352,11 +404,64 @@ with VandaClient(token="YOUR_TOKEN_HERE") as client:
352
404
  `series_ids`, `metrics` and `frequency` accept either a comma-separated string
353
405
  or a sequence. Identifiers that matched nothing are reported in `unresolved`
354
406
  rather than failing the request, so check it before treating a short result as
355
- complete.
407
+ complete. That holds as long as *something* resolved - a request where no
408
+ identifier matches is a 404 and raises `NotFoundError`.
409
+
410
+ Some flow datasets are published as a cube rather than a flat series, with a
411
+ value per combination of dimensions. Pass `dimensions` to pin one cell:
412
+
413
+ ```python
414
+ data = client.flow.get_timeseries(
415
+ series_ids=slug, dimensions={"projection": 10, "zscore": "All"}
416
+ )
417
+ ```
418
+
419
+ It is sent as JSON because the types decide what matches - `projection` is
420
+ stored as a number and `zscore` as a string, so `{"projection": "10"}` matches
421
+ nothing at all.
422
+
423
+ Intraday series (FX options) are capped at 31 days per request. A request with
424
+ no dates returns the most recent 31 days rather than the full history, a longer
425
+ span is refused, and transformations are not available on them yet.
426
+
427
+ Flow normalisation is opt-in, where positioning's is on by default, and flow
428
+ pairs its params: `normalisation` / `normalisation_period` must be passed
429
+ together, as must `transformation` / `window`. A transformation has to resolve to
430
+ exactly one series, and flow has no `none` value - simply omit them for raw
431
+ values.
432
+
433
+ #### Forecasts
434
+
435
+ Some flow datasets publish a forecast **cube**: every combination of a horizon
436
+ and a scenario, for each run date. Ask what a series publishes before reading
437
+ it - the scenario axis is named per component, `zscore` for `cta`, `rp` and
438
+ `vt`, `ret` for `gamma`:
439
+
440
+ ```python
441
+ with VandaClient(token="YOUR_TOKEN_HERE") as client:
442
+ axes = client.flow.get_forecast_runs(series_ids="AUFXFUTUAUDMIXINSSPECTA")
443
+ series = axes["data"][0]
444
+ print(series["components"], series["scenario_key"], series["horizons"])
445
+
446
+ forecast = client.flow.get_forecast(
447
+ series_ids="AUFXFUTUAUDMIXINSSPECTA",
448
+ scenario="All",
449
+ horizons=[1, 5, 10],
450
+ )
451
+ print(forecast["run_date"], forecast["scenario"])
452
+ for group in forecast["data"]:
453
+ print(group["horizon"], group["component"], group["metrics"])
454
+ ```
455
+
456
+ Rows come back as one group per (series, component, horizon), with every metric
457
+ nested inside - a forecast is read as a point on a path. Horizon 0 is the run
458
+ date itself, so on the z-score axis it exists only for the unconditional
459
+ scenario (`"All"`). There is no `forecast_date`: pair `run_date` with `horizon`.
460
+ Omitting `horizons` returns every horizon the series publishes, which is the
461
+ largest response this endpoint produces - name the ones you need, or page.
356
462
 
357
- Unlike positioning, flow accepts `normalisation` / `normalisation_period` - each
358
- must be passed with its partner, as must `transformation` / `window`. A
359
- transformation has to resolve to exactly one series.
463
+ Neither method takes a transformation or a normalisation; a projection is not a
464
+ series to roll up or re-standardise.
360
465
 
361
466
  For both services, if the transformation service is unavailable the
362
467
  untransformed rows are returned with `transformation.status` set to `"Failure"`
@@ -634,6 +739,7 @@ Supported values:
634
739
  ```python
635
740
  normalisation="zscore"
636
741
  normalisation="percentile_rank"
742
+ normalisation="none" # positioning only - opts out of its default
637
743
  ```
638
744
 
639
745
  ### Supported Normalisation Periods
@@ -180,6 +180,34 @@ export VANDA_PASSWORD="your_password"
180
180
 
181
181
  ## Authentication
182
182
 
183
+ ### SDK API key
184
+
185
+ Generate a key in the Vanda frontend at
186
+ [www.vanda-analytics.com/tools/api-keys](https://www.vanda-analytics.com/tools/api-keys),
187
+ then pass it to the client:
188
+
189
+ ```python
190
+ client = VandaClient(sdk_key="your_key_here")
191
+ ```
192
+
193
+ or set it in the environment:
194
+
195
+ ```bash
196
+ export VANDA_SDK_KEY="your_key_here"
197
+ ```
198
+
199
+ A key belongs to your user, so your own entitlement and tier limits apply to
200
+ every request made with it. It is sent as a header and needs no token exchange,
201
+ so there is nothing to refresh and nothing cached on disk. Keep it secret and
202
+ revoke it from the same page if it leaks.
203
+
204
+ An SDK key authenticates every service the client covers - `series`,
205
+ `aggregates`, `positioning` and `flow`. What it does not do is widen your
206
+ entitlement: a full-history export, for example, still needs approved Data
207
+ Licence access and is otherwise refused with `RateLimitError`.
208
+
209
+ ### Token, or email and password
210
+
183
211
  Set your API token via environment variable:
184
212
 
185
213
  ```bash
@@ -205,6 +233,9 @@ or
205
233
  client = VandaClient(email="your_email@example.com", password="your_password")
206
234
  ```
207
235
 
236
+ If more than one credential is supplied, `sdk_key` wins, then `token`, then
237
+ email and password.
238
+
208
239
  ## Features
209
240
 
210
241
  - Sync and async clients with consistent interfaces
@@ -278,9 +309,28 @@ Responses keep their envelope: alongside `data` you get `count`, `metric`,
278
309
  `revision`, `series_ids`, `version` and `pagination`. Use `pagination` to page -
279
310
  `records_per_page` defaults to 2000 and is capped by your access tier.
280
311
 
281
- Unlike aggregates, positioning does not accept `normalisation` /
282
- `normalisation_period`; its values are already z-scores. `transformation` and
283
- `window` must be passed together.
312
+ `transformation` and `window` must be passed together, and a transformation has
313
+ to resolve to exactly one series.
314
+
315
+ Positioning normalises by default: omitting `normalisation` /
316
+ `normalisation_period` is the same as asking for `zscore` over `All`. Neither
317
+ needs its partner, since the service defaults both. Pass `normalisation="none"`
318
+ for the raw stored values.
319
+
320
+ ```python
321
+ raw = client.positioning.get_timeseries(series_ids=series_label, normalisation="none")
322
+ ranked = client.positioning.get_timeseries(
323
+ series_ids=series_label,
324
+ normalisation="percentile_rank",
325
+ normalisation_period="1y",
326
+ )
327
+ ```
328
+
329
+ Note that `data` always holds the raw stored series - transformed and normalised
330
+ values arrive under `transformation["results"]["data"]`. Asking for more than one
331
+ series skips the normalisation rather than failing, reporting
332
+ `transformation.status` as `"skipped"`. `normalisation_period` is a calendar
333
+ lookback (`3m` to `All`), where `window` counts observations.
284
334
 
285
335
  ### Flow Data
286
336
 
@@ -290,6 +340,8 @@ Accessed via `client.flow`.
290
340
  - `get_latest()` - Get the latest observation per series
291
341
  - `export_timeseries()` - Export flow timeseries to file
292
342
  - `list_catalog()` - Search the catalog and filter by dimension
343
+ - `get_forecast_runs()` - List the runs, horizons and scenarios a forecast series publishes
344
+ - `get_forecast()` - Get one scenario of the forecast cube
293
345
 
294
346
  ```python
295
347
  from vanda import VandaClient
@@ -314,11 +366,64 @@ with VandaClient(token="YOUR_TOKEN_HERE") as client:
314
366
  `series_ids`, `metrics` and `frequency` accept either a comma-separated string
315
367
  or a sequence. Identifiers that matched nothing are reported in `unresolved`
316
368
  rather than failing the request, so check it before treating a short result as
317
- complete.
369
+ complete. That holds as long as *something* resolved - a request where no
370
+ identifier matches is a 404 and raises `NotFoundError`.
371
+
372
+ Some flow datasets are published as a cube rather than a flat series, with a
373
+ value per combination of dimensions. Pass `dimensions` to pin one cell:
374
+
375
+ ```python
376
+ data = client.flow.get_timeseries(
377
+ series_ids=slug, dimensions={"projection": 10, "zscore": "All"}
378
+ )
379
+ ```
380
+
381
+ It is sent as JSON because the types decide what matches - `projection` is
382
+ stored as a number and `zscore` as a string, so `{"projection": "10"}` matches
383
+ nothing at all.
384
+
385
+ Intraday series (FX options) are capped at 31 days per request. A request with
386
+ no dates returns the most recent 31 days rather than the full history, a longer
387
+ span is refused, and transformations are not available on them yet.
388
+
389
+ Flow normalisation is opt-in, where positioning's is on by default, and flow
390
+ pairs its params: `normalisation` / `normalisation_period` must be passed
391
+ together, as must `transformation` / `window`. A transformation has to resolve to
392
+ exactly one series, and flow has no `none` value - simply omit them for raw
393
+ values.
394
+
395
+ #### Forecasts
396
+
397
+ Some flow datasets publish a forecast **cube**: every combination of a horizon
398
+ and a scenario, for each run date. Ask what a series publishes before reading
399
+ it - the scenario axis is named per component, `zscore` for `cta`, `rp` and
400
+ `vt`, `ret` for `gamma`:
401
+
402
+ ```python
403
+ with VandaClient(token="YOUR_TOKEN_HERE") as client:
404
+ axes = client.flow.get_forecast_runs(series_ids="AUFXFUTUAUDMIXINSSPECTA")
405
+ series = axes["data"][0]
406
+ print(series["components"], series["scenario_key"], series["horizons"])
407
+
408
+ forecast = client.flow.get_forecast(
409
+ series_ids="AUFXFUTUAUDMIXINSSPECTA",
410
+ scenario="All",
411
+ horizons=[1, 5, 10],
412
+ )
413
+ print(forecast["run_date"], forecast["scenario"])
414
+ for group in forecast["data"]:
415
+ print(group["horizon"], group["component"], group["metrics"])
416
+ ```
417
+
418
+ Rows come back as one group per (series, component, horizon), with every metric
419
+ nested inside - a forecast is read as a point on a path. Horizon 0 is the run
420
+ date itself, so on the z-score axis it exists only for the unconditional
421
+ scenario (`"All"`). There is no `forecast_date`: pair `run_date` with `horizon`.
422
+ Omitting `horizons` returns every horizon the series publishes, which is the
423
+ largest response this endpoint produces - name the ones you need, or page.
318
424
 
319
- Unlike positioning, flow accepts `normalisation` / `normalisation_period` - each
320
- must be passed with its partner, as must `transformation` / `window`. A
321
- transformation has to resolve to exactly one series.
425
+ Neither method takes a transformation or a normalisation; a projection is not a
426
+ series to roll up or re-standardise.
322
427
 
323
428
  For both services, if the transformation service is unavailable the
324
429
  untransformed rows are returned with `transformation.status` set to `"Failure"`
@@ -596,6 +701,7 @@ Supported values:
596
701
  ```python
597
702
  normalisation="zscore"
598
703
  normalisation="percentile_rank"
704
+ normalisation="none" # positioning only - opts out of its default
599
705
  ```
600
706
 
601
707
  ### Supported Normalisation Periods
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vanda-api"
7
- version = "1.0.2"
7
+ version = "1.1.0"
8
8
  description = "Official Python SDK for Vanda Analytics Data API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -10,7 +10,7 @@ from vanda.errors import (
10
10
  VandaError,
11
11
  )
12
12
 
13
- __version__ = "0.1.0"
13
+ __version__ = "1.1.0"
14
14
 
15
15
  __all__ = [
16
16
  "VandaClient",
@@ -28,6 +28,7 @@ class AsyncBaseClient:
28
28
  timeout: float = 600.0,
29
29
  max_retries: int = 3,
30
30
  cache_file: Optional[str] = None,
31
+ sdk_key: Optional[str] = None,
31
32
  ):
32
33
  """
33
34
  Initialize async base client.
@@ -40,13 +41,19 @@ class AsyncBaseClient:
40
41
  timeout: Request timeout in seconds.
41
42
  max_retries: Maximum retry attempts.
42
43
  cache_file: Path to token cache file.
44
+ sdk_key: SDK API key. If None, reads from VANDA_SDK_KEY environment variable.
43
45
 
44
46
  Raises:
45
- AuthError: If token is not provided.
47
+ AuthError: If no credential is provided.
46
48
  """
47
49
 
48
50
  self.auth = AsyncAuth(
49
- token=token, email=email, password=password, base_url=base_url, cache_file=cache_file
51
+ token=token,
52
+ email=email,
53
+ password=password,
54
+ base_url=base_url,
55
+ cache_file=cache_file,
56
+ sdk_key=sdk_key,
50
57
  )
51
58
  self.base_url = base_url.rstrip("/")
52
59
  self.timeout = timeout
@@ -85,10 +92,16 @@ class AsyncBaseClient:
85
92
 
86
93
  return self._client
87
94
  async def _refresh_client_headers(self) -> None:
88
- """Refresh client headers with updated token."""
89
- if self._client:
90
- token = await self.auth.get_token_async()
91
- self._client.headers.update({"Authorization": f"Bearer {token}"})
95
+ """Refresh client headers with updated credentials."""
96
+ if not self._client:
97
+ return
98
+
99
+ if self.auth._auth_mode == "sdk_key":
100
+ self._client.headers.update(self.auth.get_headers())
101
+ return
102
+
103
+ token = await self.auth.get_token_async()
104
+ self._client.headers.update({"Authorization": f"Bearer {token}"})
92
105
 
93
106
  async def _request(
94
107
  self,
@@ -29,6 +29,7 @@ class BaseClient:
29
29
  timeout: float,
30
30
  max_retries: int,
31
31
  cache_file: Optional[str],
32
+ sdk_key: Optional[str] = None,
32
33
  ):
33
34
  """
34
35
  Initialize Base client.
@@ -41,9 +42,10 @@ class BaseClient:
41
42
  timeout: Request timeout in seconds.
42
43
  max_retries: Maximum retry attempts.
43
44
  cache_file: Path to token cache file.
45
+ sdk_key: SDK API key. If None, reads from VANDA_SDK_KEY environment variable.
44
46
 
45
47
  Raises:
46
- AuthError: If token is not provided.
48
+ AuthError: If no credential is provided.
47
49
  """
48
50
 
49
51
  self.auth = Auth(
@@ -52,6 +54,7 @@ class BaseClient:
52
54
  password=password,
53
55
  base_url=base_url,
54
56
  cache_file=cache_file,
57
+ sdk_key=sdk_key,
55
58
  )
56
59
 
57
60
  self.base_url = base_url.rstrip("/")
@@ -80,7 +80,13 @@ class AsyncAuth(Auth):
80
80
 
81
81
  Returns:
82
82
  Valid access token.
83
+
84
+ Raises:
85
+ AuthError: In SDK key mode, where there is no bearer token.
83
86
  """
87
+ if self._auth_mode == "sdk_key":
88
+ raise AuthError("No bearer token in SDK key mode; the key is sent as a header.")
89
+
84
90
  if self._auth_mode == "basic":
85
91
  cached_token = self._token_cache.load()
86
92
  if cached_token:
@@ -2,6 +2,8 @@ from typing import Optional
2
2
 
3
3
  from vanda._base.async_base_client import AsyncBaseClient
4
4
  from vanda.services.async_aggregates import AsyncAggregatesAPI
5
+ from vanda.services.async_flow import AsyncFlowAPI
6
+ from vanda.services.async_positioning import AsyncPositioningAPI
5
7
  from vanda.services.async_series import AsyncSeriesAPI
6
8
  from vanda.utils.deprecation import deprecated
7
9
 
@@ -17,6 +19,7 @@ class AsyncVandaClient(AsyncBaseClient):
17
19
  timeout: float = 600.0,
18
20
  max_retries: int = 3,
19
21
  cache_file: Optional[str] = None,
22
+ sdk_key: Optional[str] = None,
20
23
  ):
21
24
 
22
25
  super().__init__(
@@ -27,10 +30,13 @@ class AsyncVandaClient(AsyncBaseClient):
27
30
  timeout,
28
31
  max_retries,
29
32
  cache_file,
33
+ sdk_key=sdk_key,
30
34
  )
31
35
 
32
36
  self.series = AsyncSeriesAPI(self)
33
37
  self.aggregates = AsyncAggregatesAPI(self)
38
+ self.positioning = AsyncPositioningAPI(self)
39
+ self.flow = AsyncFlowAPI(self)
34
40
 
35
41
  async def get_timeseries(self, *args, **kwargs):
36
42
  deprecated("client.get_timeseries", "client.series.get_timeseries")