Metadata-Version: 2.4
Name: tefas-client
Version: 2.0.0
Summary: Fetch fund data from https://www.tefas.gov.tr
Project-URL: Homepage, https://github.com/semudu/tefas-client
Project-URL: Repository, https://github.com/semudu/tefas-client
Project-URL: Bug Tracker, https://github.com/semudu/tefas-client/issues
Project-URL: Changelog, https://github.com/semudu/tefas-client/releases
Author-email: Serhat Mutlu Durmaz <serhat.md@gmail.com>
License: MIT License
        
        Copyright (c) 2020 Burak Yilmaz
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: TEFAS,client,finance,fon,fund,turkey
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# tefas-client

[![PyPI - Version](https://img.shields.io/pypi/v/tefas-client.svg)](https://pypi.org/project/tefas-client/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tefas-client.svg)](https://pypi.org/project/tefas-client/)
[![PyPI - License](https://img.shields.io/pypi/l/tefas-client.svg)](LICENSE)
[![CI](https://github.com/semudu/tefas-client/actions/workflows/ci.yml/badge.svg)](https://github.com/semudu/tefas-client/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://github.com/semudu/tefas-client#readme)

**tefas-client** is a type-safe Python client for real-time fund data from [TEFAS](https://www.tefas.gov.tr) (Türkiye Elektronik Fon Alım Satım Platformu).

Fetch fund prices, allocations, and metrics with a simple, synchronous API. Perfect for building financial dashboards, analysis tools, and investment tracking apps.

> **v2 API** — Production-ready, fully tested, zero breaking changes.

## Features

- **📊 Real-time data**: Fund prices (NAV), market cap, investor counts
- **🔍 Portfolio breakdown**: Asset allocation across thousands of securities
- **🗂️ Fund discovery**: List umbrella fund types and founder institutions
- **📋 Fund snapshots**: Instant overview (price, daily return, category rank, market share)
- **🌍 Language support**: Turkish (`TR`) and English (`EN`) responses
- **🔎 Advanced filters**: Filter by umbrella type code or founder institution
- **⏱️ Smart chunking**: Automatic handling of TEFAS's 28-day query limits
- **🛡️ Type-safe**: Full Pydantic validation, mypy compatible
- **⚡ Resilient**: Exponential backoff, automatic weekend date handling
- **🐍 Modern Python**: 3.10+ with context manager support

## Installation

```bash
pip install tefas-client
```

### Other package managers

```bash
# uv
uv pip install tefas-client

# Poetry
poetry add tefas-client

# Conda
conda install -c conda-forge tefas-client
```

## Quick start

```python
from datetime import date
from tefas_client import Tefas

# Basic usage – single fund
with Tefas() as tefas:
    funds = tefas.fetch("AAK", start_date=date(2024, 2, 1), end_date=date(2024, 2, 1))
    fund = funds["AAK"]
    print(f"{fund.title}: {fund.latest().price} TRY")
    # Output: Ak Portfoy Amerikan Dolar Yabanci BYF: 23.456789 TRY
```

## Usage Examples

### Basic: Fetch latest price

```python
from datetime import date, timedelta
from tefas_client import Tefas

with Tefas(timeout=15.0) as tefas:
    today = date.today()
    funds = tefas.fetch("AAK", start_date=today, end_date=today)
    latest = funds["AAK"].latest()
    print(f"Price: {latest.price}")
    print(f"Market Cap: {latest.market_cap} TRY")
    print(f"Investors: {latest.number_of_investors}")
```

### Advanced: Date range with allocation

```python
from datetime import date
from tefas_client import Tefas

with Tefas() as tefas:
    # Fetch 3-month history with portfolio breakdown
    funds = tefas.fetch(
        "AAK",
        start_date=date(2024, 1, 1),
        end_date=date(2024, 3, 31),
        include_allocation=True,
    )
    
    for history in funds["AAK"].history:
        print(f"{history.date}: {history.price} TRY")
        
        if history.allocation:
            print("  Top holdings:")
            for code in list(history.allocation.assets.keys())[:3]:
                pct = history.allocation.assets[code]
                name = history.allocation.asset_names[code]
                print(f"    {name}: {pct:.2f}%")
```

### Batch: Multiple funds at once

```python
from datetime import date
from tefas_client import Tefas

fund_codes = ["AAK", "AAFTIYTF", "AKBLV"]

with Tefas() as tefas:
    # Fetch all funds at once (empty code = all available funds)
    all_funds = tefas.fetch(start_date=date(2024, 2, 1), end_date=date(2024, 2, 28))
    
    # Filter to codes of interest
    for code in fund_codes:
        if code in all_funds:
            fund = all_funds[code]
            price = fund.latest().price
            print(f"{code}: {price}")
```

### Fund overview: instant snapshot

```python
from tefas_client import Tefas

with Tefas() as tefas:
    ov = tefas.fetch_overview("IPB")
    print(f"{ov.title}")
    print(f"  Price     : {ov.price} TRY")
    print(f"  Daily ret : {ov.daily_return}%")
    print(f"  Rank      : {ov.category_rank}/{ov.category_fund_count} in {ov.category}")
    print(f"  Investors : {ov.number_of_investors:,}")
    print(f"  Mkt share : {ov.market_share}%")
```

### Discovery: list fund types and founders

```python
from tefas_client import Tefas

with Tefas() as tefas:
    # Umbrella fund type codes — use as umbrella_type= in fetch()
    types = tefas.fetch_fund_types()
    for t in types:
        print(t.code, t.name)

    # Founder codes — use as founder_code= in fetch()
    founders = tefas.fetch_founders()
    for f in founders:
        print(f.code, f.name)
```

### Filtered fetch: umbrella type + founder

```python
from datetime import date
from tefas_client import Tefas

with Tefas() as tefas:
    # Only equity umbrella funds (code 104) by a specific founder
    funds = tefas.fetch(
        fund_type="YAT",
        umbrella_type=104,
        founder_code="IPO",
        start_date=date(2024, 2, 1),
        end_date=date(2024, 2, 29),
    )
    for code, fund in funds.items():
        print(code, fund.latest().price)
```

### English language responses

```python
from tefas_client import Tefas

with Tefas(lang="EN") as tefas:
    ov = tefas.fetch_overview("IPB")
    print(ov.category)   # "Money Market Fund" instead of "Para Piyasası Fonu"

    types = tefas.fetch_fund_types()
    print(types[0].name) # English umbrella type names
```

### Integration: Export to Pandas

```python
import pandas as pd
from datetime import date
from tefas_client import Tefas

with Tefas() as tefas:
    funds = tefas.fetch("AAK", start_date=date(2024, 1, 1), end_date=date(2024, 3, 31))
    
    # Convert to DataFrame
    df = pd.DataFrame([
        {
            "date": h.date,
            "price": h.price,
            "market_cap": h.market_cap,
            "investors": h.number_of_investors,
        }
        for h in funds["AAK"].history
    ])
    
    print(df.describe())
```

### Error handling

```python
from tefas_client import Tefas, RateLimitError, EmptyResponseError

try:
    with Tefas() as tefas:
        funds = tefas.fetch("INVALID")
except EmptyResponseError:
    print("No data for this date range")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds")
```

## API reference

### `Tefas(timeout: float = 30.0, lang: str = "TR")`

Context manager for managing HTTP connections and sessions.

```python
with Tefas(timeout=15.0, lang="EN") as tefas:
    funds = tefas.fetch("AAK", start_date=date.today(), end_date=date.today())
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `timeout` | `float` | `30.0` | Per-request HTTP timeout in seconds |
| `lang` | `str` | `"TR"` | Response language: `"TR"` (Turkish) or `"EN"` (English) |

### `Tefas.fetch(...) -> dict[str, Fund]`

Fetch fund price history for a given date range. Large ranges are automatically split into ≤28-day chunks.

```python
with Tefas() as tefas:
    # Single fund — type auto-detected (YAT → EMK → BYF)
    funds = tefas.fetch("AAK", start_date=date(2024, 1, 1), end_date=date(2024, 3, 31))

    # All investment funds
    all_funds = tefas.fetch(start_date=date.today(), end_date=date.today())

    # Pension (BES) funds filtered by founder
    funds = tefas.fetch(fund_type="EMK", founder_code="IPO", start_date=date(2024, 2, 1))

    # Filter by umbrella type (e.g. equity funds = 104)
    funds = tefas.fetch(fund_type="YAT", umbrella_type=104, start_date=date(2024, 2, 1))
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `fund_code` | `str \| list[str]` | `""` | TEFAS code(s), e.g. `"AAK"` or `["AAK", "TLY"]`. Empty = all funds |
| `start_date` | `date \| None` | `end_date` | Inclusive range start. Defaults to `end_date` |
| `end_date` | `date \| None` | today | Inclusive range end. Auto-adjusted to nearest Friday if weekend |
| `include_allocation` | `bool` | `False` | Include portfolio allocation breakdown |
| `fund_type` | `"YAT" \| "EMK" \| "BYF" \| None` | `None` | Fund type. When `None` and a specific code is given, auto-detected via YAT → EMK → BYF |
| `umbrella_type` | `int \| None` | `None` | Umbrella fund type code (e.g. `104`). Use `fetch_fund_types()` to list valid codes |
| `founder_code` | `str \| None` | `None` | Founder institution code (e.g. `"IPO"`). Use `fetch_founders()` to list valid codes |

**Returns:** `dict[str, Fund]` — mapping of fund code → fund data

---

### `Tefas.fetch_overview(fund_code: str) -> FundOverview`

Fetch an instant snapshot of a single fund — current price, daily return, category ranking, and market share. Not a time-series; reflects the state at call time.

```python
from tefas_client import Tefas, FundOverview

with Tefas() as tefas:
    ov = tefas.fetch_overview("IPB")
    print(f"{ov.title}: {ov.price} TRY, rank {ov.category_rank}/{ov.category_fund_count}")
```

| Field | Type | Description |
|-------|------|-------------|
| `code` | `str` | TEFAS fund code |
| `title` | `str` | Full fund name |
| `price` | `float \| None` | Latest NAV in TRY |
| `daily_return` | `float \| None` | Daily return percentage |
| `shares` | `float \| None` | Total circulating shares |
| `market_cap` | `float \| None` | Portfolio size in TRY |
| `category` | `str \| None` | Fund category name |
| `category_rank` | `int \| None` | Rank within category |
| `category_fund_count` | `int \| None` | Total funds in category |
| `number_of_investors` | `int \| None` | Active investor count |
| `market_share` | `float \| None` | Market share percentage |

---

### `Tefas.fetch_fund_types(fund_type: "YAT" | "EMK" = "YAT") -> list[UmbrellaFundType]`

List umbrella fund type codes and names. Pass the returned `code` values as `umbrella_type` in `fetch()`.

```python
from tefas_client import Tefas

with Tefas() as tefas:
    types = tefas.fetch_fund_types()
    for t in types:
        print(t.code, t.name)  # e.g. 104 "Hisse Senedi Şemsiye Fonu"
```

| Field | Type | Description |
|-------|------|-------------|
| `code` | `int` | Numeric umbrella type code |
| `name` | `str` | Umbrella type name |

---

### `Tefas.fetch_founders(fund_type: "YAT" | "EMK" = "YAT") -> list[Founder]`

List founder institution codes and names. Pass the returned `code` values as `founder_code` in `fetch()`.

```python
from tefas_client import Tefas

with Tefas() as tefas:
    founders = tefas.fetch_founders()
    for f in founders:
        print(f.code, f.name)  # e.g. "IPO" "İŞ PORTFÖY YÖNETİMİ A.Ş."
```

| Field | Type | Description |
|-------|------|-------------|
| `code` | `str` | Founder institution code |
| `name` | `str` | Founder institution name |
| `fund_type` | `str \| None` | Fund type indicator |

**Returns:** `dict[str, Fund]` — mapping of fund code → fund data

### `Fund`

Represents a single fund.

```python
fund: Fund
fund.code          # "AAK"
fund.title         # "Ak Portfoy Amerikan Dolar Yabanci BYF"
fund.latest()      # History (most recent entry)
fund.history       # list[History] (all historical entries, oldest first)
```

| Attribute | Type | Description |
|-----------|------|-------------|
| `code` | `str` | TEFAS fund code (e.g. "AAK") |
| `title` | `str` | Full fund name in Turkish |
| `history` | `list[History]` | Chronological price/metric history, oldest first |

### `History`

Single fund snapshot for a trading date.

```python
h: History
h.price                    # Fund unit price (NAV) in TRY
h.market_cap              # Portfolio value in TRY
h.number_of_investors     # Active investor count
h.allocation              # Allocation data (if requested)
```

| Field | Type | Description |
|-------|------|-------------|
| `date` | `date` | Trading date (never a weekend/holiday) |
| `price` | `float \| None` | Fund unit price (NAV) in TRY |
| `market_cap` | `float \| None` | Portfolio size (TRY) |
| `number_of_shares` | `float \| None` | Total circulating shares |
| `number_of_investors` | `int \| None` | Active investor count |
| `exchange_bulletin_price` | `float \| None` | Exchange bulletin price (BYF only) |
| `allocation` | `Allocation \| None` | Portfolio breakdown (if `include_allocation=True`) |

### `Allocation`

Portfolio composition snapshot.

```python
a: Allocation
a.assets           # {"US0378331005": 45.5, "IE00B4L5Y983": 30.2, ...}
a.asset_names      # {"US0378331005": "APPLE INC.", ...}
```

| Field | Type | Description |
|-------|------|-------------|
| `date` | `date` | Allocation date |
| `assets` | `dict[str, float]` | `{ISIN: percentage_allocation}` |
| `asset_names` | `dict[str, str]` | `{ISIN: security_name}` |

## Exceptions

All exceptions inherit from `TefasError` and can be caught with:

```python
from tefas_client import Tefas, TefasError

try:
    with Tefas() as tefas:
        funds = tefas.fetch("AAK", start_date=date(2024, 1, 1))
except TefasError as e:
    print(f"TEFAS error: {e}")
```

| Exception | When | Example |
|-----------|------|---------|
| `TefasError` | Base class for all library errors | `except TefasError: pass` |
| `RateLimitError` | HTTP 429 after retries (has `retry_after` attribute) | `except RateLimitError as e: time.sleep(e.retry_after)` |
| `EmptyResponseError` | API returned 200 but no rows for query | `except EmptyResponseError: print("No data")` |

## Known Constraints

| Constraint | Impact | Workaround |
|-----------|--------|-----------|
| **Rate limit** | ~6 req/min, retries with backoff, raises after 3 failures | Space requests ≥10s apart, use batch queries |
| **28-day window** | Max query is ~28 calendar days | Automatic; `fetch()` chunks larger ranges |
| **No weekend data** | TEFAS closed weekends/holidays | Dates auto-adjust to nearest Friday |
| **WAF/IP blocks** | Datacenter IPs may get 403/503 | Use residential IP or VPN |
| **Synchronous only** | No built-in async/await | Use `asyncio.to_thread()` if needed (advanced) |

## Troubleshooting

### "403 Forbidden" or "503 Service Unavailable"

**Cause:** TEFAS WAF blocks datacenter IP ranges (AWS, Azure, etc.)

**Solution:**
- Use a residential VPN or local ISP connection
- For production: Use a proxy service with residential IPs
- The library auto-retries; wait a few seconds before trying again

### "RateLimitError: Retry after X seconds"

**Cause:** Too many requests to TEFAS API (>6/min)

**Solution:**
```python
import time
from tefas_client import Tefas, RateLimitError

with Tefas() as tefas:
    for fund_code in ["AAK", "AAFTIYTF", "AKBLV"]:
        try:
            funds = tefas.fetch(fund_code, start_date=date.today(), end_date=date.today())
        except RateLimitError as e:
            print(f"Rate limited. Waiting {e.retry_after}s...")
            time.sleep(e.retry_after + 1)  # +1 for safety margin
            # Retry logic here
```

### "EmptyResponseError"

**Cause:** No trading data for that date (e.g., weekend, holiday, fund didn't exist)

**Solution:**
```python
from tefas_client import Tefas, EmptyResponseError

try:
    with Tefas() as tefas:
        # Try a date range instead of single day
        funds = tefas.fetch("AAK", start_date=date(2024, 1, 1), end_date=date(2024, 1, 10))
except EmptyResponseError:
    print("No data available; fund may not have existed or dates were all holidays")
```

### Slow performance

**Tip 1:** Use context manager (reuses connection across multiple `fetch()` calls)
```python
# ✅ Good: single connection for 3 calls
with Tefas() as tefas:
    funds1 = tefas.fetch("AAK", ...)
    funds2 = tefas.fetch("AAFTIYTF", ...)
    funds3 = tefas.fetch("AKBLV", ...)
```

**Tip 2:** Fetch all funds in one call instead of per-code
```python
# ✅ Good: 1 request
with Tefas() as tefas:
    all_funds = tefas.fetch(start_date=date.today(), end_date=date.today())  
    tefas_funds = {k: v for k, v in all_funds.items() if k.startswith("AAFTI")}

# ❌ Avoid: 3 requests
with Tefas() as tefas:
    funds1 = tefas.fetch("AAK", ...)
    funds2 = tefas.fetch("AAFTIYTF", ...)
    funds3 = tefas.fetch("AKBLV", ...)
```

**Tip 3:** Use shorter date ranges
```python
# ✅ Good: smaller response, faster
with Tefas() as tefas:
    funds = tefas.fetch("AAK", start_date=date(2024, 1, 1), end_date=date(2024, 1, 31))

# ❌ Slow: year of data
with Tefas() as tefas:
    funds = tefas.fetch("AAK", start_date=date(2023, 1, 1), end_date=date(2024, 12, 31))
```

## Contributing

We welcome contributions! To get started:

```bash
# Clone and install
git clone https://github.com/semudu/tefas-client
cd tefas-client
make install

# Run tests
make test

# Lint and format
make lint
make format
```

**Development workflow:**
- Fork the repository on GitHub
- Create a feature branch: `git checkout -b feature/my-feature`
- Make your changes and ensure tests pass (`make test`)
- Commit: `git commit -am "Add my feature"`
- Push and open a pull request

**Code style:**
- Python 3.10+ with type hints
- Ruff for formatting and linting
- Mypy for static type checking
- Pytest for testing

See [CONTRIBUTING.md](CONTRIBUTING.md) (if available) or open an issue to discuss ideas.

## License

[MIT](LICENSE) — Free for personal and commercial use

---

<div align="center">

**Questions?** [Open an issue](https://github.com/semudu/tefas-client/issues)  
**Found a bug?** [Report it](https://github.com/semudu/tefas-client/issues/new)  
**Have a feature idea?** [Suggest it](https://github.com/semudu/tefas-client/discussions)

</div>
