Metadata-Version: 2.4
Name: nordnet-sdk
Version: 0.1.0
Summary: Unofficial Python SDK for Nordnet's public market data API — no credentials needed
Author-email: luimu <54173370+luimu64@users.noreply.github.com>
License: MIT
Keywords: nordnet,stocks,funds,market-data,finance,api
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
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: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# nordnet-sdk

Unofficial Python client for Nordnet's public market data API.

**No account. No API key. No login.** This library talks to the same API
Nordnet's website uses for anonymous visitors, so it works out of the box
and runs anywhere a Python interpreter does.

## Features

- Quote lookups for stocks, ETFs and funds — last, bid/ask, open/close,
  high/low, spread, volumes, turnover
- Free-text search that resolves instrument IDs from names and tickers
- Trade tape — recent executed trades with price, volume and type
- Market overview lists — risers/fallers, most traded, filterable and sortable
- Chart data — price time series in 8 periods, from 1-day intraday
  (`MIN_1`) to all-time monthly bars
- News feeds per instrument, paginated
- Fund profiles — NAV with history, fees, managers, holdings, risk class,
  SFDR article, dividend strategy, minimum investment
- ETF profiles — fees, distribution type, holdings, sector/region
  exposures, sustainability and risk statistics
- Stock profiles — company identity, market cap and key figures with
  fiscal-year history, earnings reports, dividends, news
- FX rates and other indicators — EUR/USD, EUR/SEK and more, in realtime
- Market indices — OMX, S&P, NASDAQ, DAX and more, with sparkline data
- Market hours — trading sessions and status per order book
- Historical returns and key ratios — 1d/1w/1m/1y/3y/5y yields, P/E, P/B,
  dividend yield, number of owners
- Zero dependencies — pure Python standard library, no install-time build
- Typed data models with a `raw` escape hatch for unmapped fields
- Retries with exponential backoff on transient failures
- A clear error hierarchy (`NordnetApiError`, `NordnetSessionError`,
  `NordnetHttpError`)

## Install

```bash
pip install nordnet-sdk
```

or from a checkout:

```bash
pip install .
```

Requires Python 3.9+.

## Quickstart

```python
from nordnet import NordnetClient

client = NordnetClient()

# A stock (Wärtsilä B, Nasdaq Helsinki)
stock = client.instruments.get(16100884)
print(stock.name, stock.symbol, stock.last_price)
# Wärtsilä Corporation WRT1V 29.98

# A fund (Nordnet One Rohkea EUR)
fund = client.funds.get(17794251)
print(fund.name, fund.last_price)
print(fund.fund_info.fund_yearly_fee, fund.fund_info.fund_sfdr_article)
# Nordnet One Rohkea EUR 17.15
# 0.5 Artikla 8
```

Find instruments by name, chart them, browse indices and ETF profiles:

```python
# Search resolves instrument IDs from free text
groups = client.search.search("wartsila")
hit = next(g for g in groups if g.display_group_type == "EQUITY").results[0]
print(hit.instrument_id, hit.display_name, hit.last_price.price)
# 16100884 Wärtsilä Corporation 29.96

# Chart data, resolving the order book id automatically
series = client.charts.history(hit.instrument_id, period="YEAR_1")
print(series.period, series.resolution, len(series.price_points))
# YEAR_1 DAY 246

# Market indices
for quote in client.markets.indicators("FI_INDEX_GLOBAL")[:3]:
    print(quote.symbol, quote.price.last, quote.price.development)

# ETF profile
etf = client.etfs.profile(18166615)  # SPDR S&P 500 UCITS ETF
print(etf.fees.total_fee, etf.distribution_type, etf.holdings[0].name)
# 0.03 ACCUMULATED NVIDIA

# FX rate
fx = client.markets.indicator("eurusd")
print(fx.name, fx.price.last, fx.price.development)
# EUR/USD 1.1509 0.02

# Stock profile
profile = client.stocks.profile(16100884)  # Wärtsilä B
print(profile.company.company_name, profile.next_earnings_report.date)
print(profile.current_dividend.x_date, profile.current_dividend.amount_value)
# Wartsila Oyj Abp 2026-10-27
# 2026-09-15 0.27
```

## API

| Method | Returns |
| --- | --- |
| `client.instruments.get(id)` | full snapshot for one instrument (stocks, ETFs, funds) |
| `client.instruments.quote(id)` | just the latest price block |
| `client.instruments.query(**params)` | raw results for arbitrary filters |
| `client.instruments.trades(id, count=100)` | recent executed trades (the tape) |
| `client.instruments.stocklist(...)` | market overview list (risers/fallers) |
| `client.funds.get(id)` | fund profile incl. NAV and fund metadata |
| `client.funds.nav(id)` | just the latest NAV |
| `client.funds.query(**params)` | raw results for arbitrary filters |
| `client.funds.profile(id)` | full screening profile (NAV history, holdings, managers) |
| `client.funds.profile_by_slug(slug)` | screening profile for a display slug |
| `client.search.search(query)` | free-text search results grouped by asset class |
| `client.news.for_instrument(id, limit, offset)` | paginated news feed for an instrument |
| `client.news.by_identifier(uuid, limit, offset)` | news feed for a raw NNX identifier |
| `client.charts.history(id, period)` | price time series for an instrument |
| `client.charts.history_by_identifier(uuid, period)` | time series for a raw order book id |
| `client.markets.indicators(list_id)` | index quotes for a market list |
| `client.markets.indicator(slug)` | single indicator / FX quote (e.g. `eurusd`) |
| `client.markets.market_hours(order_book_id)` | market hours; `None` when the API has none |
| `client.markets.market_hours_for(id)` | market hours for an instrument |
| `client.etfs.profile(id)` | ETF profile for an instrument |
| `client.etfs.profile_by_slug(slug)` | ETF profile for a display slug |
| `client.stocks.profile(id)` | stock profile for an instrument |
| `client.stocks.profile_by_slug(slug)` | stock profile for a display slug |

Chart periods (`CHART_PERIODS`): `DAY_1`, `WEEK_1`, `MONTH_1`, `MONTH_3`,
`MONTH_6`, `YTD`, `YEAR_1`, `ALL`.

### Data model

- `InstrumentSnapshot` — identity (name, symbol, ISIN, currency, type),
  quote (`price`), trading status, exchanges, historical returns, key
  ratios, NNX metadata
- `PriceInfo` / `PriceLevel` — the `price_info` block with typed
  price levels (`price` + `decimals`)
- `FundSnapshot` — an `InstrumentSnapshot` plus `fund_info` (`FundInfo`)
  and `annual_growth` (`AnnualGrowth`)

Every model also carries the original payload in `raw`.

### Errors

All exceptions derive from `NordnetError`:

- `NordnetApiError` — the API returned an error envelope (`{"code": ...}`)
- `NordnetSessionError` — the anonymous session was rejected
  (`NEXT_INVALID_SESSION`); usually means the unlock headers were overridden
- `NordnetHttpError` — an unexpected HTTP status

### Configuration

```python
client = NordnetClient(
    base_url="https://www.nordnet.fi",   # .se / .dk / .no also work
    market_data_base_url="https://api.prod.nntech.io",
    locale="fi-FI",                      # sent as x-locale on the market-data host
    timeout=10.0,
    max_retries=3,
    backoff_base=0.5,
    headers={"user-agent": "my-app/1.0"},  # merged over the defaults
)
```

Two hosts are used. The main site host (`www.nordnet.<tld>/api/2`) needs the
anonymous unlock headers (`client-id: NEXT`, `ntag: NO_NTAG_RECEIVED_YET`),
which are added automatically; `referer` and `x-nn-href` follow `base_url`.
The market-data host (`api.prod.nntech.io`) serves charts, indices and
screening, and needs `x-locale` instead — also sent automatically.

## About instrument IDs

Nordnet identifies instruments by internal numeric IDs (e.g. `16100884` =
Wärtsilä B, `17794251` = Nordnet One Rohkea EUR). You can discover them with
`client.search.search(...)`, or read them from the instrument pages on
nordnet.fi.

## Supported endpoints

| Endpoint | Host | Use |
| --- | --- | --- |
| `GET /api/2/instrument_search/query/instrument` | main site | instrument snapshots (stocks, ETFs, funds) |
| `GET /api/2/instrument_search/query/fundlist` | main site | fund profiles with fund metadata |
| `GET /api/2/instrument_search/query/stocklist` | main site | market overview lists (sortable/filterable) |
| `GET /api/2/main_search` | main site | free-text search |
| `GET /api/2/tradables/trades/{market}:{identifier}` | main site | trade tape |
| `GET /news/v1/news/preview/instrument/{nnx_id}` | market data | news feed |
| `GET /market-data/v3/price-time-series/period/{p}/identifier/{uuid}` | market data | chart data |
| `GET /market-data/v1/market-hours/{order_book_id}` | market data | market hours (204 → none) |
| `GET /instrument-screening/v1/indicator/web/list/{id}` | market data | index / FX quotes |
| `GET /instrument-screening/v1/indicator/web/{slug}` | market data | single indicator / FX quote |
| `GET /instrument-screening/v2/exchange-traded-funds/web/{slug}` | market data | ETF profiles |
| `GET /instrument-screening/v2/mutual-funds/web/{slug}` | market data | fund profiles |
| `GET /instrument-screening/v3/stocks/web/{slug}` | market data | stock profiles |

## Public data boundary

Everything in this library is served to anonymous visitors — that's the
point. One notable thing is **not** public: the full order book depth (the
multi-level bid/ask ladder, "Tarjoustasot"). Without a login you only get
the top of book — best bid/ask prices with their resting volumes
(`quote.bid`, `quote.ask`, `quote.bid_volume`, `quote.ask_volume`).
Everything else this SDK exposes was verified live with no session.

## Development

```bash
pip install -e ".[dev]"
pytest
```

The test suite runs against a local fixture server — no network access
required.

## Legal

Unofficial, not affiliated with or endorsed by Nordnet. This library only
reads public data; please be polite to the API (the client retries
transient failures with backoff, and you should not hammer it). Use at your
own risk; market data may be delayed.
