fundly 0.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 (56) hide show
  1. fundly-0.1.0/.gitignore +28 -0
  2. fundly-0.1.0/LICENSE +75 -0
  3. fundly-0.1.0/PKG-INFO +325 -0
  4. fundly-0.1.0/README.md +296 -0
  5. fundly-0.1.0/fundly/__init__.py +70 -0
  6. fundly-0.1.0/fundly/_version.py +1 -0
  7. fundly-0.1.0/fundly/cache.py +227 -0
  8. fundly-0.1.0/fundly/client.py +648 -0
  9. fundly-0.1.0/fundly/currency.py +25 -0
  10. fundly-0.1.0/fundly/errors.py +22 -0
  11. fundly-0.1.0/fundly/fx.py +121 -0
  12. fundly-0.1.0/fundly/ids.py +84 -0
  13. fundly-0.1.0/fundly/models.py +118 -0
  14. fundly-0.1.0/fundly/quality.py +55 -0
  15. fundly-0.1.0/fundly/search.py +73 -0
  16. fundly-0.1.0/fundly/sources/__init__.py +0 -0
  17. fundly-0.1.0/fundly/sources/argentina.py +233 -0
  18. fundly-0.1.0/fundly/sources/base.py +237 -0
  19. fundly-0.1.0/fundly/sources/brazil.py +203 -0
  20. fundly-0.1.0/fundly/sources/chile.py +114 -0
  21. fundly-0.1.0/fundly/sources/china.py +96 -0
  22. fundly-0.1.0/fundly/sources/colombia.py +106 -0
  23. fundly-0.1.0/fundly/sources/comparadorfondos.py +62 -0
  24. fundly-0.1.0/fundly/sources/finect.py +159 -0
  25. fundly-0.1.0/fundly/sources/ft.py +84 -0
  26. fundly-0.1.0/fundly/sources/id_managers.py +171 -0
  27. fundly-0.1.0/fundly/sources/india.py +192 -0
  28. fundly-0.1.0/fundly/sources/indonesia.py +176 -0
  29. fundly-0.1.0/fundly/sources/israel.py +110 -0
  30. fundly-0.1.0/fundly/sources/morningstar.py +102 -0
  31. fundly-0.1.0/fundly/sources/poland.py +61 -0
  32. fundly-0.1.0/fundly/sources/quefondos.py +34 -0
  33. fundly-0.1.0/fundly/sources/vietnam.py +103 -0
  34. fundly-0.1.0/fundly/sources/yahoo.py +80 -0
  35. fundly-0.1.0/pyproject.toml +67 -0
  36. fundly-0.1.0/tests/conftest.py +6 -0
  37. fundly-0.1.0/tests/test_client.py +239 -0
  38. fundly-0.1.0/tests/test_client_edges.py +285 -0
  39. fundly-0.1.0/tests/test_fx.py +176 -0
  40. fundly-0.1.0/tests/test_ids.py +94 -0
  41. fundly-0.1.0/tests/test_live.py +100 -0
  42. fundly-0.1.0/tests/test_live_asia.py +56 -0
  43. fundly-0.1.0/tests/test_live_latam.py +56 -0
  44. fundly-0.1.0/tests/test_live_misc.py +57 -0
  45. fundly-0.1.0/tests/test_live_sea.py +86 -0
  46. fundly-0.1.0/tests/test_multiprocess.py +61 -0
  47. fundly-0.1.0/tests/test_properties.py +122 -0
  48. fundly-0.1.0/tests/test_quality.py +209 -0
  49. fundly-0.1.0/tests/test_resilience.py +227 -0
  50. fundly-0.1.0/tests/test_search.py +168 -0
  51. fundly-0.1.0/tests/test_sources.py +352 -0
  52. fundly-0.1.0/tests/test_src_asia.py +170 -0
  53. fundly-0.1.0/tests/test_src_id_managers.py +130 -0
  54. fundly-0.1.0/tests/test_src_latam.py +257 -0
  55. fundly-0.1.0/tests/test_src_misc.py +313 -0
  56. fundly-0.1.0/tests/test_src_sea.py +208 -0
@@ -0,0 +1,28 @@
1
+ # environments (.venv is a link to .venv.nosync so iCloud doesn't sync it)
2
+ .venv
3
+ .venv/
4
+ .venv.nosync/
5
+ __pycache__/
6
+ *.pyc
7
+
8
+ # tool caches and build output
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .hypothesis/
13
+ .coverage
14
+ htmlcov/
15
+ dist/
16
+ build/
17
+
18
+ # local fundly caches
19
+ *.db
20
+ *.db-*
21
+
22
+ # duplicates iCloud creates on sync conflicts ("uv 2.lock")
23
+ * 2
24
+ * 2.*
25
+ .DS_Store
26
+
27
+ # git metadata lives here so iCloud does not sync it (.git is a pointer file)
28
+ .git.nosync/
fundly-0.1.0/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Required Notice: Copyright (c) 2026 antooro
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
fundly-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.5
2
+ Name: fundly
3
+ Version: 0.1.0
4
+ Summary: Prices, history and search for mutual funds, ETFs and pension plans worldwide, by ISIN, ticker or local code
5
+ Project-URL: Homepage, https://github.com/antooro/fundly
6
+ Project-URL: Source, https://github.com/antooro/fundly
7
+ Project-URL: Issues, https://github.com/antooro/fundly/issues
8
+ Project-URL: Changelog, https://github.com/antooro/fundly/blob/main/CHANGELOG.md
9
+ License-Expression: PolyForm-Noncommercial-1.0.0
10
+ License-File: LICENSE
11
+ Keywords: etf,finance,fondos de inversión,funds,isin,morningstar,mutual funds,nav,pension,planes de pensiones
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Office/Business :: Financial :: Investment
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: tzdata; sys_platform == 'win32'
26
+ Provides-Extra: pandas
27
+ Requires-Dist: pandas>=1.5; extra == 'pandas'
28
+ Description-Content-Type: text/markdown
29
+
30
+ <p align="center">
31
+ <img src="https://raw.githubusercontent.com/antooro/fundly/main/docs/banner.svg" alt="fundly: prices for funds, ETFs and pension plans, worldwide" width="100%">
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://github.com/antooro/fundly/actions/workflows/tests.yml"><img alt="Tests" src="https://github.com/antooro/fundly/actions/workflows/tests.yml/badge.svg"></a>
36
+ <img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white">
37
+ <img alt="License: PolyForm Noncommercial 1.0.0" src="https://img.shields.io/badge/license-PolyForm%20Noncommercial-3ddc97">
38
+ <img alt="Status: alpha" src="https://img.shields.io/badge/status-alpha-f5b84b">
39
+ <img alt="Sources: 17" src="https://img.shields.io/badge/sources-17-7aa2ff">
40
+ <img alt="Dependencies: httpx" src="https://img.shields.io/badge/dependencies-httpx-7aa2ff">
41
+ </p>
42
+
43
+ <p align="center">
44
+ <b>One Python call for the price of any mutual fund, ETF or pension plan in the world.<br>
45
+ Look it up by ISIN, ticker or its local code; no API keys, no accounts.</b>
46
+ </p>
47
+
48
+ <p align="center">
49
+ Over <b>350,000 funds</b> and <b>38,000 ETF listings</b> worldwide, plus <b>45,000+ funds and pension plans</b> from national sources.
50
+ </p>
51
+
52
+ ---
53
+
54
+ ```python
55
+ import fundly
56
+
57
+ fundly.quote("VTSAX").price # 182.82 US mutual fund, by ticker
58
+ fundly.quote("ES0112611001").price # 367.82113 Spanish fund, by ISIN
59
+ fundly.quote("cl-afp:HABITAT-A").price # 100365.02 Chilean pension fund, local code
60
+ fundly.history("INF179K01XQ0").to_pandas() # Indian fund, daily history as a pandas Series
61
+ ```
62
+
63
+ ## Why fundly
64
+
65
+ Ticker-based tools leave most of the world's savings out: European share classes, Indian schemes, Chilean AFPs, Spanish pension plans. fundly starts from the identifiers people actually have, asks several sources, and tells you exactly what kind of price you got.
66
+
67
+ | | **fundly** | yfinance | mstarpy |
68
+ |---|:---:|:---:|:---:|
69
+ | Look up by **ISIN** | ✅ | via search | via search |
70
+ | **Local codes** (AMFI, CAFCI, AFP, DGS…) | ✅ | ❌ | ❌ |
71
+ | **Pension plans** in 7 countries | ✅ | ❌ | ❌ |
72
+ | Says whether a price is **NAV, exchange close or index** | ✅ | ❌ | ❌ |
73
+ | **Several sources** with automatic fallback | ✅ | ❌ | ❌ |
74
+ | **Cross-check** sources side by side | ✅ | ❌ | ❌ |
75
+ | Needs a browser / Selenium | no | no | yes (v10+) |
76
+
77
+ ## Install
78
+
79
+ ```bash
80
+ pip install fundly # pip install "fundly[pandas]" for .to_pandas()
81
+ ```
82
+
83
+ ## A quick tour
84
+
85
+ **Search by name, in any language.** Every word counts, in any order, accents optional:
86
+
87
+ ```python
88
+ for f in fundly.search("emaxis slim", limit=3):
89
+ print(f.id, f.name)
90
+ # JP90C000PZY9 MUAM eMAXIS Slim All US Equity
91
+ # JP90C000G7W3 MUAM eMAXIS Slim All World Eq 3 Reg Eql
92
+ # JP90C000H1T1 MUAM eMAXIS Slim All Wld Eq All Country
93
+
94
+ [f.id for f in fundly.search("superfondo acciones", limit=3)]
95
+ # ['ar-cafci:148', 'ar-cafci:304', 'ar-cafci:1373']
96
+ ```
97
+
98
+ **Any identifier lands on the same fund.** Tickers and local codes are translated to the ISIN when one exists, so caching and cross-checking work across them:
99
+
100
+ ```python
101
+ fundly.quote("in-amfi:118989").id # 'INF179K01XQ0' (AMFI scheme code → ISIN)
102
+ fundly.quote("VOO").id, fundly.instrument("VOO").type # ('US9229083632', 'etf')
103
+ ```
104
+
105
+ **Daily history**, straight into pandas:
106
+
107
+ ```python
108
+ fundly.history("N5508", start="2026-10-01").to_pandas().tail(3) # Spanish pension plan
109
+ # date
110
+ # 2026-10-21 134.58548
111
+ # 2026-10-22 133.59280
112
+ # 2026-10-23 133.50458
113
+ ```
114
+
115
+ **Cross-check every source.** Independent feeds agreeing to the last decimal is how you know a price is right:
116
+
117
+ ```python
118
+ c = fundly.compare("ES0173311103") # Renta 4 Numantia Patrimonio Global
119
+ for q in c.quotes:
120
+ print(f"{q.source:<12} {q.price:>9} {q.currency} {q.date}")
121
+ # morningstar 29.3605 EUR 2026-10-23
122
+ # finect 29.00609 EUR 2026-10-21
123
+ # ft 29.3605 EUR 2026-10-23
124
+ # quefondos 29.41016 EUR 2026-10-22
125
+ # yahoo 29.4102 EUR 2026-10-22
126
+
127
+ c.nav_spread_pct # 0.0 (spread between sources that share the latest date)
128
+ ```
129
+
130
+ **Know what kind of price you got.** Every quote says whether it is an official NAV, an exchange close, or an index rebuilt from published returns:
131
+
132
+ ```python
133
+ fundly.quote("IE00B4L5Y983").price_type # 'nav' iShares Core MSCI World: issuer NAV
134
+ fundly.quote("IE00B4L5Y983", source="morningstar").price_type # 'market' exchange close
135
+ fundly.quote("il-gemel:880").price_type # 'index' Israeli fund: monthly returns only
136
+ ```
137
+
138
+ **Spot bad data instead of trusting it blindly.** A price older than expected is flagged `stale`; one far from what other sources said in the last month is checked against the next source and replaced or flagged `suspicious`. Histories note isolated spikes and ×100 jumps:
139
+
140
+ ```python
141
+ q = fundly.quote("ar-cafci:3906") # Argentine agro fund that stopped reporting
142
+ q.stale, q.age_days, q.warnings
143
+ # (True, 90, ('stale: last price is 90 days old (2026-07-31)',))
144
+
145
+ q.to_dict() # every result is JSON-ready
146
+ # {'id': 'ar-cafci:3906', 'name': 'ADBlick Granos Plus FCIC', 'price': 0.488829, 'date': '2026-07-31', ...}
147
+ ```
148
+
149
+ **Value everything in one currency.** Prices come in each fund's own currency; ask for another and fundly converts at the ECB reference rate of the price's date, keeping the original figure:
150
+
151
+ ```python
152
+ q = fundly.quote("INF179K01XQ0", currency="EUR") # Indian fund, in euros
153
+ round(q.price, 4), q.original_price, q.original_currency, round(q.fx_rate, 6), q.fx_source
154
+ # (2.0377, 216.123, 'INR', 0.009428, 'ecb')
155
+
156
+ fundly.history("VTSAX", start="2025-01-01", currency="EUR") # each day at that day's rate
157
+ ```
158
+
159
+ ## Coverage
160
+
161
+ Real quotes taken with fundly on 27 October 2026:
162
+
163
+ | Market | Example id | What it is | Latest | Source |
164
+ |---|---|---|---|---|
165
+ | 🌍 Worldwide funds | `ZAE000025029` | Allan Gray Balanced A | 201.67 ZAR | Morningstar |
166
+ | 🌍 Worldwide ETFs | `VOO` → `US9229083632` | Vanguard S&P 500 ETF | 703.41 USD (market) | Morningstar |
167
+ | 🇺🇸 US mutual funds | `VTSAX` | Vanguard Total Stock Market Admiral | 182.82 USD | Morningstar |
168
+ | 🇪🇸 Spanish funds | `ES0112611001` | Azvalor Internacional FI | 367.82 EUR | Morningstar, Finect, FT, Quefondos |
169
+ | 🇪🇸 Spanish pension plans | `N5508` | Santander Autónomos RVM PPES | 133.50 EUR | Finect, comparadorfondos |
170
+ | 🇬🇧 UK pension & life funds | `ft:0P00009VB8` | Aviva Pension Invesco Emerging Markets | 2.53 GBP | FT |
171
+ | 🇵🇱 Poland (funds, PPK, OFE) | `pl-analizy:ARK52` | Erste PPK 2050 | 2,439.80 PLN | analizy.pl |
172
+ | 🇮🇳 India mutual funds | `in-amfi:118989` | HDFC Mid Cap Direct Growth | 216.12 INR | AMFI (official) |
173
+ | 🇮🇳 India NPS pensions | `in-nps:SM001001` | SBI Pension Fund, Central Govt | 49.36 INR | npsnav.in |
174
+ | 🇨🇳 China | `cn:110022` | 易方达消费行业股票 | 2.79 CNY | Eastmoney |
175
+ | 🇯🇵 Japan | `JP90C000H1T1` | eMAXIS Slim All Country | 37,441 JPY | Morningstar |
176
+ | 🇻🇳 Vietnam | `vn-fmarket:VFF` | VinaCapital Enhanced Bond | 27,154.70 VND | fmarket |
177
+ | 🇮🇩 Indonesia | `IDN000007002` | Simpan Balanced | 944.27 IDR | Morningstar, Pasardana, fund managers |
178
+ | 🇨🇱 Chile pension funds | `cl-afp:HABITAT-A` | AFP Habitat, Fund A | 100,365.02 CLP | Superintendencia de Pensiones |
179
+ | 🇨🇴 Colombia pensions & funds | `co-pension:10-6000` | Colfondos, higher-risk fund | 97,116.41 COP | datos.gov.co |
180
+ | 🇧🇷 Brazil funds | `br-cvm:00017024000153` | Renda fixa class | 45.26 BRL | CVM (official open data) |
181
+ | 🇦🇷 Argentina funds | `ar-cafci:148` | Superfondo Acciones, Clase A | 486.53 ARS | CAFCI, argentinadatos |
182
+ | 🇮🇱 Israel provident & pension | `il-gemel:880` | Meitav Hishtalmut General | index | Gemel-net (gov) |
183
+
184
+ **Tested at random:** in 326 funds, ETFs and pension plans drawn at random from 15 universes (worldwide Morningstar pages, AMFI, CAFCI, CVM, Chilean AFPs, Spanish plans…), **96% returned a price** and none raised an unexpected error. The misses were mostly funds that have since closed but are still listed.
185
+
186
+ Morningstar alone reaches funds and ETFs in most countries (US, Canada, Europe, Japan, Korea, Taiwan, Hong Kong, Singapore, Australia, South Africa, the Gulf…). The national sources add official data and products Morningstar doesn't carry. Not covered yet: Turkey, Peru, Mexico, Hong Kong MPF and Australian superannuation. Their public data sits behind bot protection or isn't published.
187
+
188
+ **History depth.** Argentine funds have daily history from January 2020 for every class, from the argentinadatos.com community mirror of the regulator's (CNV) daily sheets. Indonesian funds get full history since launch from Morningstar (about a third of them), or from the manager's own site for Batavia, Trimegah, Avrist and Syailendra (about 140 more funds, some back to 1996). Others have Pasardana's last 12 months: older data is a paid product everywhere we looked. With the cache on, fundly keeps every Indonesian and Argentine price it downloads, so those histories grow on your machine instead of sliding along.
189
+
190
+ ## Identifiers
191
+
192
+ | You type | Means | Example |
193
+ |---|---|---|
194
+ | ISIN | any fund or ETF | `IE00B4L5Y983` |
195
+ | ticker | listed ETF or US/CA mutual fund | `VTSAX`, `ticker:VOO` |
196
+ | `N####` · `S####` | Spanish pension plan · employment sub-plan (DGS code) | `N5508`, `S0156` |
197
+ | `ms:` | Morningstar id, for funds without ISIN (Korea, China…) | `ms:F00001H9R0` |
198
+ | `ft:` | FT symbol, for UK pension/life funds | `ft:0P00009VB8` |
199
+ | `in-amfi:` · `in-nps:` | India scheme code · NPS scheme | `in-amfi:118989` |
200
+ | `cn:` | Chinese fund code | `cn:110022` |
201
+ | `cl-afp:` | Chilean AFP + fund letter | `cl-afp:HABITAT-A` |
202
+ | `co-pension:` · `co-fic:` | Colombia entity–fund · collective fund | `co-pension:10-6000` |
203
+ | `br-cvm:` | Brazilian CNPJ (digits) | `br-cvm:00017024000153` |
204
+ | `ar-cafci:` | CAFCI class code | `ar-cafci:148` |
205
+ | `pl-analizy:` | analizy.pl code | `pl-analizy:PZU50` |
206
+ | `il-gemel:` · `il-pensia:` | Israeli fund id | `il-gemel:880` |
207
+ | `vn-fmarket:` · `id-pasardana:` | Vietnam fund code · Pasardana id | `vn-fmarket:VFF` |
208
+
209
+ Results always carry a canonical `id`: the ISIN when there is one, otherwise `scheme:code`.
210
+
211
+ ## How it works
212
+
213
+ ```mermaid
214
+ flowchart LR
215
+ Q["fundly.quote(id)"] --> I["Translate the id<br/>(ticker → ISIN…)"]
216
+ I --> C{"Fresh in the<br/>local cache?"}
217
+ C -- yes --> R(["Quote"])
218
+ C -- no --> T["Work out the type<br/>fund · ETF · plan"]
219
+ T --> S1["1st source that<br/>knows this id"]
220
+ S1 -- answers --> R
221
+ S1 -- "fails or 429" --> S2["next source"]
222
+ S2 -- answers --> R
223
+ ```
224
+
225
+ - **Each source only gets the ids it understands.** National sources answer for their own codes; Morningstar, FT and Yahoo for ISINs and tickers.
226
+ - **Priority depends on the product.** ETFs go to the official NAV first, Indian funds to AMFI first, and pension plans to the portals that carry them.
227
+ - **If a source fails, the next one answers.** Rate-limited sources are paused, not hammered.
228
+ - **Search only asks who can answer.** An ISIN or a local code is looked up directly. Text goes to the worldwide sources, plus national ones whose words appear in it ("afp" → Chile, "hdfc" → India…); Chinese, Hebrew and Vietnamese text go straight to the sources that publish names in that script. Other national catalogues are only tried when nothing matches every word. Results are ranked by how well the name matches, then by fund size.
229
+ - **Bad data is flagged, not hidden.** A price far from the last month's prices from other sources (a ×100 unit error, a wrong share class) makes fundly ask the next source; whichever two agree wins, and the quote says what was replaced. Old prices are marked `stale` (10 days; 45 for monthly series).
230
+ - **Local cache** in your platform's cache folder (`~/.cache/fundly` on Linux, `~/Library/Caches/fundly` on macOS, `%LOCALAPPDATA%\fundly\Cache` on Windows; or `$FUNDLY_CACHE_DIR`): quotes are reused for 6 h, history and searches for 24 h. The catalogue starts empty and only grows with what you look up. Bulk files (Brazil's monthly CVM archives, Argentine history files) are kept there too and revalidated with the server, so a later session downloads only what changed.
231
+
232
+ ## API
233
+
234
+ | Call | Returns |
235
+ |---|---|
236
+ | `fundly.search(q, type=None, category=None, live=True, limit=20)` | `list[Instrument]` |
237
+ | `fundly.instrument(id)` | `Instrument` (name, type, currency, domicile, provider, category, TER, size, other ids) |
238
+ | `fundly.quote(id, source=None, currency=None)` | `Quote` (price, date, currency, `price_type`, source, `warnings`, `stale`, `suspicious`, `age_days`; plus the original price and FX rate if converted) |
239
+ | `fundly.history(id, start=None, end=None, source=None, currency=None)` | `History`, with `.points`, `.warnings` and `.to_pandas()` |
240
+ | `fundly.compare(id)` | `Comparison`, with every source's quote (the odd one out flagged `suspicious`) and `nav_spread_pct` |
241
+
242
+ Every result has `.to_dict()`, with dates as ISO strings, ready for `json.dumps`.
243
+
244
+ For custom settings, create your own client:
245
+
246
+ ```python
247
+ from fundly import Client
248
+
249
+ with Client(cache="~/data/fundly", min_interval=1.0, max_wait=60) as fl:
250
+ fl.history("IE00B4L5Y983", start="2015-01-01")
251
+ ```
252
+
253
+ - `min_interval`: seconds between two requests to the same site (default 0.5).
254
+ - `max_wait`: total seconds one call may take across every source it tries (default 120; `None` for no limit, e.g. for many years of Brazilian history, whose first download takes a while; if a call times out, the download carries on in the background and the next call finds it on disk). A hanging site can't hold your program beyond it.
255
+ - `exclude`: sources to leave out.
256
+
257
+ Errors: `InvalidId`, `NotFound`, `UnknownSource`, `FXUnavailable` and `FundlyTimeout`, all subclasses of `FundlyError`. Source fallbacks are logged on the `fundly` logger.
258
+
259
+ ### Currencies
260
+
261
+ - Prices are always in the share class's **own currency**, never silently converted. Prices quoted in pence, cents or agorot (GBp/GBX, ZAc, ILA) are turned into pounds, rand and shekels.
262
+ - With `currency="EUR"` (or any ISO code) the price is converted through the euro at the **rate in force on the price's date**; weekends and holidays use the last published rate. History converts each day with its own rate.
263
+ - Rates are **ECB euro reference rates** (29 currencies, source: ECB). Currencies the ECB doesn't publish (ARS, CLP, COP, VND, TWD…) come from Yahoo Finance. Converted quotes keep `original_price`, `original_currency`, `fx_rate`, `fx_date` and `fx_source`, so a converted price is never mistaken for a native one.
264
+
265
+ ## Sources
266
+
267
+ | Source | Covers | Data |
268
+ |---|---|---|
269
+ | Morningstar | Funds & ETFs worldwide, search | NAV / exchange close |
270
+ | Finect | Spanish & EU funds, ETF NAVs, Spanish plans | NAV |
271
+ | Financial Times | Funds & ETFs, UK pension funds | NAV / exchange close |
272
+ | Quefondos | Spanish funds & plans | latest NAV |
273
+ | comparadorfondos | Spanish pension plans | NAV |
274
+ | Yahoo Finance | Funds & ETFs (fallback) | NAV / exchange close |
275
+ | AMFI (+ mfapi.in history) | India mutual funds | NAV, official |
276
+ | npsnav.in | India NPS pensions | NAV |
277
+ | Eastmoney | China funds | NAV |
278
+ | Superintendencia de Pensiones | Chile AFP funds A–E | NAV, official |
279
+ | datos.gov.co | Colombia pensions & collective funds | NAV, official |
280
+ | CVM open data | Brazil funds | NAV, official (ODbL) |
281
+ | CAFCI (+ argentinadatos.com history) | Argentina funds | NAV, official |
282
+ | analizy.pl | Poland funds, PPK, OFE | NAV |
283
+ | Gemel-net / Pensia-net | Israel provident & pension funds | index from monthly returns, official |
284
+ | fmarket | Vietnam funds | NAV |
285
+ | Pasardana | Indonesia funds | NAV, last 12 months of history |
286
+ | Batavia, Trimegah, Avrist, Syailendra | Their Indonesian funds | full NAV history, from each manager's site |
287
+
288
+ ## Responsible use
289
+
290
+ fundly is an independent, source-available project, **not affiliated with any of the sites above**. All data belongs to its owners.
291
+
292
+ - **No data ships with fundly.** Every request goes from your machine to the public website, as if you visited it, and you are responsible for following each site's terms of use.
293
+ - **Personal, non-commercial use.** Most of these sites allow personal use and **forbid redistributing or reselling their data**. Don't use fundly to build a data feed, a paid API or any service that republishes it.
294
+ - **robots.txt.** Morningstar, Finect, Yahoo, npsnav.in, analizy.pl and Gemel-net ask automated clients to stay away in their robots.txt. Like yfinance, fundly uses them by default. If you prefer to respect robots.txt strictly, leave them out:
295
+ ```python
296
+ Client(exclude=["morningstar", "finect", "yahoo", "npsnav", "analizy", "gemelnet"])
297
+ ```
298
+ - **Gentle by default.** At most one request every 0.5 s per site, cached results, and a pause whenever a site answers *429 Too Many Requests*.
299
+ - **Honest.** fundly identifies itself in its `User-Agent` and never bypasses anti-bot protection. Sites that block automated clients are not supported.
300
+ - **No advice.** Prices can be late, missing or wrong. Nothing here is investment advice, and the software comes with no warranty.
301
+
302
+ If you run one of these sites and want it out of fundly, open an issue.
303
+
304
+ ## License
305
+
306
+ fundly is released under the [PolyForm Noncommercial License 1.0.0](LICENSE).
307
+
308
+ - ✅ **Free** for personal use, research, education, hobby projects, and non-profit, educational and government organisations.
309
+ - ❌ **No commercial use.** You can't sell it, include it in a paid product or service, or use it inside a business.
310
+
311
+ Because it forbids commercial use, fundly is *source-available*, not "open source" in the OSI sense.
312
+
313
+ ## Contributing
314
+
315
+ ```bash
316
+ uv sync
317
+ uv run pytest # 527 offline tests: parsers, client, currencies, broken sites, properties, concurrency
318
+ uv run pytest -m live # 75 tests against the real sites
319
+ ```
320
+
321
+ Adding a country is one class with `get_quote`, `get_history` and, optionally, `search`, plus an id scheme registered in `fundly.ids`. See `fundly/sources/`.
322
+
323
+ ---
324
+
325
+ <sub>🌍 <b>En español:</b> fundly consulta el valor liquidativo de fondos de inversión, ETFs y planes de pensiones de todo el mundo por ISIN, ticker o código local, cruzando varias fuentes. Licencia PolyForm Noncommercial: uso personal y no comercial; no redistribuye datos.</sub>