Skip to content

Add fund-selection tooling for monthly Nordnet CSV exports - #168

Closed
ingehap wants to merge 2 commits into
mainfrom
claude/fond-selection-csv-ssav39
Closed

ingehap wants to merge 2 commits into
mainfrom
claude/fond-selection-csv-ssav39

Conversation

@ingehap

@ingehap ingehap commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

Turns a series of downloaded Nordnet fund lists into a basis for choosing funds. Pure stdlib Python 3.10+, self-contained under fond/, outside the petrolib package and outside pyproject's testpaths — no existing library code, test or CI job is modified. The only change outside fond/ is one added CI job (see Testing).

The problem

Ranking a single export by trailing return measures which market did well, not which fund is good. In the attached 18.8.2026 export, currency-hedged Asia and Norway funds top the 1-year list — that is the krone and one good year on Oslo Børs, not fund quality.

Morningstar's category column can't fix this on its own. It files these five together under Europe Equity Large Cap:

Fund 1 year
Nordnet Norge Indeks 29.6 %
Nordnet Sverige Index 13.7 %
DNB Norden Indeks A 11.8 %
Nordnet Europa Indeks 6.7 %
Nordnet Danmark Indeks B 4.8 %

A 25-percentage-point spread that is pure geography.

Approach

Funds are grouped by an exposure key derived from the fund name — market, sector, size segment, currency hedging, factor/ESG tilt — and ranked only within a group.

Name matching deliberately omits \b anchors for most tokens: Norwegian fund names are compounds (KLP AksjeNorge Indeks, AksjeUSA, AksjeVerden) where a word boundary never matches, and Amundi contracts to EurSRIClmtPrsAlgd. The name takes priority over the category, which is only a fallback. Sector comes from the category rather than the name, since name matching turns Handelsbanken and SpareBank 1 into financials funds.

Hedged and unhedged variants are separate groups. Same index, same manager:

Fund 1 year 3 yr ann.
KLP AksjeUSA Indeks Valutasikret N 20.3 % 21.0 %
KLP AksjeUSA Indeks N 11.7 % 17.7 %

Those 8.6 points are the exchange rate.

Scoring

Z-scores within the group: cost 0.45, return vs. the group median 0.35, consistency across 3m/1y/3y/5y 0.20. Cost weighs heaviest because it is the one field known in advance and it applies every year regardless of what the market does.

Morningstar stars (missing for 22 of 84 funds), the risk number (4 for 74 of 84) and owner counts stay in the panel and the --csv output but out of the score.

What the monthly cadence adds

Siste is the unit price, and NAV for Norwegian and Nordic funds is total return. Stacking exports gives a NAV series per ISIN, and with it volatility, max drawdown, and pairwise correlation — the last identifying funds that are duplicates of each other. These switch on at four snapshots; below that the report says so and falls back to the export's own return columns.

--endringer also surfaces price changes and funds that drop off the list, neither of which anyone notifies you about.

Handling classification error

Name-based classification is a heuristic, so funds deviating more than 3pp from their group are flagged and sorted last rather than allowed to top the list. On the sample data this catches Nordnet Europa Indeks (IE00BMTD2N07) at −3.0pp — an Irish-domiciled fund tracking a different Europe index than its peers.

overrides.csv pins exposure per ISIN and ships with one real case: Alfred Berg Indeks R tracks Oslo Børs, but its name says nothing and Morningstar files it under Europe. Without the override it appeared to beat the Europe group by six percentage points.

Parsing

Nordnet's actual format: UTF-16LE with BOM, tab-separated, decimal comma, thousands separated by NBSP, and U+2212 MINUS SIGN rather than ASCII hyphen. The snapshot date comes from the most common NAV date in the file rather than the filename, so the few funds priced a day later don't move it, and ingest.py is idempotent on re-run.

Testing

27 tests in fond/test_fond.py (stdlib unittest, also runnable under pytest), covering number/date parsing, the compound-name and contracted-name cases, sector-from-category, hedged/unhedged separation, overrides, and — against a synthetic six-month panel — NAV metrics, max drawdown, duplicate detection and the ranking itself.

$ python3 fond/test_fond.py
Ran 27 tests in 0.058s
OK

They live under fond/ rather than tests/ so pyproject's testpaths = ["tests"] keeps them out of the petrolib suite. That also meant nothing in CI ran them — the lint job checks petrolib tests tools and the pytest lane honours testpaths — so the second commit adds a separate fond-tests job on 3.10, the oldest version fond/ claims to support. It shares no setup with the existing jobs and does not alter them.

Note

fond/ is unrelated to petrophysics. It is isolated in its own directory with no imports either way, but say the word if it belongs in a separate repo instead.

claude added 2 commits August 19, 2026 20:17
Turns a series of downloaded Nordnet fund lists into a basis for
choosing funds. Pure stdlib Python, self-contained under fond/ and
outside the petrolib package and test path.

The core problem the tooling addresses: ranking a single export by
trailing return measures which market did well, not which fund is
good. Morningstar's category column is too coarse to fix this on its
own -- it files Norwegian, Swedish, Danish, Nordic and broad European
index funds together under "Europe Equity Large Cap", a 25-percentage-
point spread that is pure geography.

So funds are grouped by an exposure key derived from the fund name
(market, sector, size segment, currency hedging, factor/ESG tilt) and
ranked only within a group. Name matching deliberately omits word
boundaries for most tokens: Norwegian fund names are compounds
("KLP AksjeNorge Indeks", "AksjeUSA", "AksjeVerden") where a \b anchor
never matches, and Amundi contracts to "EurSRIClmtPrsAlgd". The name
takes priority over the category, which is only a fallback.

Scoring within a group weights cost highest (0.45) -- it is the one
field known in advance -- then return relative to the group median,
then consistency across periods. Morningstar stars, the risk number
(4 for 74 of 84 funds) and owner counts are carried in the panel but
kept out of the score.

Stacking exports also yields a NAV series per ISIN, which gives
volatility, max drawdown and pairwise correlation -- the last of these
identifies funds that are duplicates of each other. These switch on at
four snapshots; below that the report says so and falls back to the
export's own return columns.

Classification by name is a heuristic, so funds deviating more than 3pp
from their group are flagged and sorted last rather than topping the
list, and overrides.csv pins exposure per ISIN. It ships with one real
case: Alfred Berg Indeks R tracks Oslo Børs, but its name says nothing
and Morningstar files it under Europe.

Parsing handles Nordnet's actual format: UTF-16LE, tab-separated,
decimal comma, and U+2212 MINUS SIGN rather than ASCII hyphen.

27 tests under fond/test_fond.py (stdlib unittest, also runnable under
pytest); pyproject's testpaths keeps them out of the petrolib suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8cF2pbLZuncatMezhAV63
fond/test_fond.py is stdlib-only and lives outside pyproject's
testpaths, so neither the lint job (which checks petrolib, tests and
tools) nor the petrolib pytest lane ever executed it. Own job, own
runner, pinned to 3.10 -- the oldest version fond/ claims to support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8cF2pbLZuncatMezhAV63

ingehap commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Closing — this moved to its own repository at ingehap/fond_analyse, which is where the note at the bottom of this description suggested it might belong.

The same files now sit at the repo root rather than under fond/, so the commands are python3 ingest.py and python3 analyze.py. Nothing was dropped in the move, and two things were added that this PR did not have:

  • ruff runs over the whole tree there. In this repo the lint job only covered petrolib tests tools, so fond/ was never linted. Running it revealed 24 findings — an unused dataclasses.asdict import, zip() without an explicit strict=, and a batch of over-length lines — all fixed before the first commit landed.
  • CI runs the tests on 3.10, 3.12 and 3.13 rather than 3.10 alone.

All four jobs are green on the initial commit.

The branch claude/fond-selection-csv-ssav39 stays on this repo, so this history remains reachable; nothing here needs to be merged.


Generated by Claude Code

@ingehap ingehap closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants