borsapy 0.4.0__py3-none-any.whl
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.
- borsapy/__init__.py +134 -0
- borsapy/_models/__init__.py +1 -0
- borsapy/_providers/__init__.py +5 -0
- borsapy/_providers/base.py +94 -0
- borsapy/_providers/bist_index.py +150 -0
- borsapy/_providers/btcturk.py +230 -0
- borsapy/_providers/canlidoviz.py +773 -0
- borsapy/_providers/dovizcom.py +869 -0
- borsapy/_providers/dovizcom_calendar.py +276 -0
- borsapy/_providers/dovizcom_tahvil.py +172 -0
- borsapy/_providers/hedeffiyat.py +376 -0
- borsapy/_providers/isin.py +247 -0
- borsapy/_providers/isyatirim.py +943 -0
- borsapy/_providers/isyatirim_screener.py +468 -0
- borsapy/_providers/kap.py +534 -0
- borsapy/_providers/paratic.py +278 -0
- borsapy/_providers/tcmb.py +317 -0
- borsapy/_providers/tefas.py +802 -0
- borsapy/_providers/viop.py +204 -0
- borsapy/bond.py +162 -0
- borsapy/cache.py +86 -0
- borsapy/calendar.py +272 -0
- borsapy/crypto.py +153 -0
- borsapy/exceptions.py +64 -0
- borsapy/fund.py +471 -0
- borsapy/fx.py +388 -0
- borsapy/index.py +285 -0
- borsapy/inflation.py +166 -0
- borsapy/market.py +53 -0
- borsapy/multi.py +227 -0
- borsapy/screener.py +365 -0
- borsapy/ticker.py +1196 -0
- borsapy/viop.py +162 -0
- borsapy-0.4.0.dist-info/METADATA +969 -0
- borsapy-0.4.0.dist-info/RECORD +37 -0
- borsapy-0.4.0.dist-info/WHEEL +4 -0
- borsapy-0.4.0.dist-info/licenses/LICENSE +190 -0
borsapy/fund.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Fund class for mutual fund data - yfinance-like API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from borsapy._providers.tefas import get_tefas_provider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Fund:
|
|
13
|
+
"""
|
|
14
|
+
A yfinance-like interface for mutual fund data from TEFAS.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
>>> import borsapy as bp
|
|
18
|
+
>>> fund = bp.Fund("AAK")
|
|
19
|
+
>>> fund.info
|
|
20
|
+
{'fund_code': 'AAK', 'name': 'Ak Portföy...', 'price': 1.234, ...}
|
|
21
|
+
>>> fund.history(period="1mo")
|
|
22
|
+
Price FundSize Investors
|
|
23
|
+
Date
|
|
24
|
+
2024-12-01 1.200 150000000.0 5000
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
>>> fund = bp.Fund("TTE")
|
|
28
|
+
>>> fund.info['return_1y']
|
|
29
|
+
45.67
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, fund_code: str):
|
|
33
|
+
"""
|
|
34
|
+
Initialize a Fund object.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF")
|
|
38
|
+
"""
|
|
39
|
+
self._fund_code = fund_code.upper()
|
|
40
|
+
self._provider = get_tefas_provider()
|
|
41
|
+
self._info_cache: dict[str, Any] | None = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def fund_code(self) -> str:
|
|
45
|
+
"""Return the fund code."""
|
|
46
|
+
return self._fund_code
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def symbol(self) -> str:
|
|
50
|
+
"""Return the fund code (alias)."""
|
|
51
|
+
return self._fund_code
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def info(self) -> dict[str, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Get detailed fund information.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary with fund details:
|
|
60
|
+
- fund_code: TEFAS fund code
|
|
61
|
+
- name: Fund full name
|
|
62
|
+
- date: Last update date
|
|
63
|
+
- price: Current unit price
|
|
64
|
+
- fund_size: Total fund size (TRY)
|
|
65
|
+
- investor_count: Number of investors
|
|
66
|
+
- founder: Fund founder company
|
|
67
|
+
- manager: Fund manager company
|
|
68
|
+
- fund_type: Fund type
|
|
69
|
+
- category: Fund category
|
|
70
|
+
- risk_value: Risk rating (1-7)
|
|
71
|
+
- return_1m, return_3m, return_6m: Period returns
|
|
72
|
+
- return_ytd: Year-to-date return
|
|
73
|
+
- return_1y, return_3y, return_5y: Annual returns
|
|
74
|
+
- daily_return: Daily return
|
|
75
|
+
"""
|
|
76
|
+
if self._info_cache is None:
|
|
77
|
+
self._info_cache = self._provider.get_fund_detail(self._fund_code)
|
|
78
|
+
return self._info_cache
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def detail(self) -> dict[str, Any]:
|
|
82
|
+
"""Alias for info property."""
|
|
83
|
+
return self.info
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def performance(self) -> dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Get fund performance metrics only.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary with performance data:
|
|
92
|
+
- daily_return: Daily return
|
|
93
|
+
- return_1m, return_3m, return_6m: Period returns
|
|
94
|
+
- return_ytd: Year-to-date return
|
|
95
|
+
- return_1y, return_3y, return_5y: Annual returns
|
|
96
|
+
"""
|
|
97
|
+
info = self.info
|
|
98
|
+
return {
|
|
99
|
+
"daily_return": info.get("daily_return"),
|
|
100
|
+
"return_1m": info.get("return_1m"),
|
|
101
|
+
"return_3m": info.get("return_3m"),
|
|
102
|
+
"return_6m": info.get("return_6m"),
|
|
103
|
+
"return_ytd": info.get("return_ytd"),
|
|
104
|
+
"return_1y": info.get("return_1y"),
|
|
105
|
+
"return_3y": info.get("return_3y"),
|
|
106
|
+
"return_5y": info.get("return_5y"),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def allocation(self) -> pd.DataFrame:
|
|
111
|
+
"""
|
|
112
|
+
Get current portfolio allocation (asset breakdown) for last 7 days.
|
|
113
|
+
|
|
114
|
+
For longer periods, use allocation_history() method.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
DataFrame with columns: Date, asset_type, asset_name, weight.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
>>> fund = Fund("AAK")
|
|
121
|
+
>>> fund.allocation
|
|
122
|
+
Date asset_type asset_name weight
|
|
123
|
+
0 2024-12-20 HS Hisse Senedi 45.32
|
|
124
|
+
1 2024-12-20 DB Devlet Bonusu 30.15
|
|
125
|
+
...
|
|
126
|
+
"""
|
|
127
|
+
return self._provider.get_allocation(self._fund_code)
|
|
128
|
+
|
|
129
|
+
def allocation_history(
|
|
130
|
+
self,
|
|
131
|
+
period: str = "1mo",
|
|
132
|
+
start: datetime | str | None = None,
|
|
133
|
+
end: datetime | str | None = None,
|
|
134
|
+
) -> pd.DataFrame:
|
|
135
|
+
"""
|
|
136
|
+
Get historical portfolio allocation (asset breakdown).
|
|
137
|
+
|
|
138
|
+
Note: TEFAS API supports maximum ~100 days (3 months) of data.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
period: How much data to fetch. Valid periods:
|
|
142
|
+
1d, 5d, 1mo, 3mo (max ~100 days).
|
|
143
|
+
Ignored if start is provided.
|
|
144
|
+
start: Start date (string or datetime).
|
|
145
|
+
end: End date (string or datetime). Defaults to today.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
DataFrame with columns: Date, asset_type, asset_name, weight.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
>>> fund = Fund("AAK")
|
|
152
|
+
>>> fund.allocation_history(period="1mo") # Last month
|
|
153
|
+
>>> fund.allocation_history(period="3mo") # Last 3 months (max)
|
|
154
|
+
>>> fund.allocation_history(start="2024-10-01", end="2024-12-31")
|
|
155
|
+
"""
|
|
156
|
+
start_dt = self._parse_date(start) if start else None
|
|
157
|
+
end_dt = self._parse_date(end) if end else None
|
|
158
|
+
|
|
159
|
+
# If no start date, calculate from period
|
|
160
|
+
if start_dt is None:
|
|
161
|
+
from datetime import timedelta
|
|
162
|
+
end_dt = end_dt or datetime.now()
|
|
163
|
+
days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30)
|
|
164
|
+
# Cap at 100 days (API limit)
|
|
165
|
+
days = min(days, 100)
|
|
166
|
+
start_dt = end_dt - timedelta(days=days)
|
|
167
|
+
|
|
168
|
+
return self._provider.get_allocation(
|
|
169
|
+
fund_code=self._fund_code,
|
|
170
|
+
start=start_dt,
|
|
171
|
+
end=end_dt,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def history(
|
|
175
|
+
self,
|
|
176
|
+
period: str = "1mo",
|
|
177
|
+
start: datetime | str | None = None,
|
|
178
|
+
end: datetime | str | None = None,
|
|
179
|
+
) -> pd.DataFrame:
|
|
180
|
+
"""
|
|
181
|
+
Get historical price data.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
period: How much data to fetch. Valid periods:
|
|
185
|
+
1d, 5d, 1mo, 3mo, 6mo, 1y.
|
|
186
|
+
Ignored if start is provided.
|
|
187
|
+
start: Start date (string or datetime).
|
|
188
|
+
end: End date (string or datetime). Defaults to now.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
DataFrame with columns: Price, FundSize, Investors.
|
|
192
|
+
Index is the Date.
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
>>> fund = Fund("AAK")
|
|
196
|
+
>>> fund.history(period="1mo") # Last month
|
|
197
|
+
>>> fund.history(period="1y") # Last year
|
|
198
|
+
>>> fund.history(start="2024-01-01", end="2024-06-30") # Date range
|
|
199
|
+
"""
|
|
200
|
+
start_dt = self._parse_date(start) if start else None
|
|
201
|
+
end_dt = self._parse_date(end) if end else None
|
|
202
|
+
|
|
203
|
+
return self._provider.get_history(
|
|
204
|
+
fund_code=self._fund_code,
|
|
205
|
+
period=period,
|
|
206
|
+
start=start_dt,
|
|
207
|
+
end=end_dt,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _parse_date(self, date: str | datetime) -> datetime:
|
|
211
|
+
"""Parse a date string to datetime."""
|
|
212
|
+
if isinstance(date, datetime):
|
|
213
|
+
return date
|
|
214
|
+
for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
|
|
215
|
+
try:
|
|
216
|
+
return datetime.strptime(date, fmt)
|
|
217
|
+
except ValueError:
|
|
218
|
+
continue
|
|
219
|
+
raise ValueError(f"Could not parse date: {date}")
|
|
220
|
+
|
|
221
|
+
def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float:
|
|
222
|
+
"""
|
|
223
|
+
Calculate the Sharpe ratio for the fund.
|
|
224
|
+
|
|
225
|
+
Sharpe Ratio = (Rp - Rf) / σp
|
|
226
|
+
Where:
|
|
227
|
+
- Rp = Annualized return of the fund
|
|
228
|
+
- Rf = Risk-free rate (default: 10Y government bond yield)
|
|
229
|
+
- σp = Annualized standard deviation of returns
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
|
|
233
|
+
risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
|
|
234
|
+
If None, uses current 10Y bond yield from bp.risk_free_rate().
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent).
|
|
238
|
+
|
|
239
|
+
Examples:
|
|
240
|
+
>>> fund = bp.Fund("YAY")
|
|
241
|
+
>>> fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate
|
|
242
|
+
0.85
|
|
243
|
+
|
|
244
|
+
>>> fund.sharpe_ratio(period="3y") # 3-year Sharpe
|
|
245
|
+
1.23
|
|
246
|
+
|
|
247
|
+
>>> fund.sharpe_ratio(risk_free_rate=0.25) # Custom risk-free rate
|
|
248
|
+
0.92
|
|
249
|
+
"""
|
|
250
|
+
metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate)
|
|
251
|
+
return metrics.get("sharpe_ratio", np.nan)
|
|
252
|
+
|
|
253
|
+
def risk_metrics(
|
|
254
|
+
self,
|
|
255
|
+
period: str = "1y",
|
|
256
|
+
risk_free_rate: float | None = None,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
Calculate comprehensive risk metrics for the fund.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
|
|
263
|
+
risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
|
|
264
|
+
If None, uses current 10Y bond yield.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary with risk metrics:
|
|
268
|
+
- annualized_return: Annualized return (%)
|
|
269
|
+
- annualized_volatility: Annualized standard deviation (%)
|
|
270
|
+
- sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp
|
|
271
|
+
- sortino_ratio: Downside risk-adjusted return
|
|
272
|
+
- max_drawdown: Maximum peak-to-trough decline (%)
|
|
273
|
+
- risk_free_rate: Risk-free rate used (%)
|
|
274
|
+
- trading_days: Number of trading days in the period
|
|
275
|
+
|
|
276
|
+
Examples:
|
|
277
|
+
>>> fund = bp.Fund("YAY")
|
|
278
|
+
>>> metrics = fund.risk_metrics()
|
|
279
|
+
>>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}")
|
|
280
|
+
>>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%")
|
|
281
|
+
"""
|
|
282
|
+
# Get historical data
|
|
283
|
+
df = self.history(period=period)
|
|
284
|
+
|
|
285
|
+
if df.empty or len(df) < 20:
|
|
286
|
+
return {
|
|
287
|
+
"annualized_return": np.nan,
|
|
288
|
+
"annualized_volatility": np.nan,
|
|
289
|
+
"sharpe_ratio": np.nan,
|
|
290
|
+
"sortino_ratio": np.nan,
|
|
291
|
+
"max_drawdown": np.nan,
|
|
292
|
+
"risk_free_rate": np.nan,
|
|
293
|
+
"trading_days": 0,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Calculate daily returns
|
|
297
|
+
prices = df["Price"]
|
|
298
|
+
daily_returns = prices.pct_change().dropna()
|
|
299
|
+
trading_days = len(daily_returns)
|
|
300
|
+
|
|
301
|
+
# Annualization factor (trading days per year)
|
|
302
|
+
annualization_factor = 252
|
|
303
|
+
|
|
304
|
+
# Annualized return
|
|
305
|
+
total_return = (prices.iloc[-1] / prices.iloc[0]) - 1
|
|
306
|
+
years = trading_days / annualization_factor
|
|
307
|
+
annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100
|
|
308
|
+
|
|
309
|
+
# Annualized volatility
|
|
310
|
+
daily_volatility = daily_returns.std()
|
|
311
|
+
annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100
|
|
312
|
+
|
|
313
|
+
# Get risk-free rate
|
|
314
|
+
if risk_free_rate is None:
|
|
315
|
+
try:
|
|
316
|
+
from borsapy.bond import risk_free_rate as get_rf_rate
|
|
317
|
+
rf = get_rf_rate() * 100 # Returns decimal like 0.28, convert to %
|
|
318
|
+
except Exception:
|
|
319
|
+
rf = 30.0 # Fallback: approximate Turkish 10Y yield
|
|
320
|
+
else:
|
|
321
|
+
rf = risk_free_rate * 100 # Convert decimal to percentage
|
|
322
|
+
|
|
323
|
+
# Sharpe Ratio
|
|
324
|
+
if annualized_volatility > 0:
|
|
325
|
+
sharpe = (annualized_return - rf) / annualized_volatility
|
|
326
|
+
else:
|
|
327
|
+
sharpe = np.nan
|
|
328
|
+
|
|
329
|
+
# Sortino Ratio (uses downside deviation)
|
|
330
|
+
negative_returns = daily_returns[daily_returns < 0]
|
|
331
|
+
if len(negative_returns) > 0:
|
|
332
|
+
downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100
|
|
333
|
+
if downside_deviation > 0:
|
|
334
|
+
sortino = (annualized_return - rf) / downside_deviation
|
|
335
|
+
else:
|
|
336
|
+
sortino = np.nan
|
|
337
|
+
else:
|
|
338
|
+
sortino = np.inf # No negative returns
|
|
339
|
+
|
|
340
|
+
# Maximum Drawdown
|
|
341
|
+
cumulative = (1 + daily_returns).cumprod()
|
|
342
|
+
running_max = cumulative.cummax()
|
|
343
|
+
drawdowns = (cumulative - running_max) / running_max
|
|
344
|
+
max_drawdown = drawdowns.min() * 100 # Negative percentage
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
"annualized_return": round(annualized_return, 2),
|
|
348
|
+
"annualized_volatility": round(annualized_volatility, 2),
|
|
349
|
+
"sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
|
|
350
|
+
"sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
|
|
351
|
+
"max_drawdown": round(max_drawdown, 2),
|
|
352
|
+
"risk_free_rate": round(rf, 2),
|
|
353
|
+
"trading_days": trading_days,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
def __repr__(self) -> str:
|
|
357
|
+
return f"Fund('{self._fund_code}')"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def search_funds(query: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
361
|
+
"""
|
|
362
|
+
Search for funds by name or code.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
query: Search query (fund code or name)
|
|
366
|
+
limit: Maximum number of results
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
List of matching funds with fund_code, name, fund_type, return_1y.
|
|
370
|
+
|
|
371
|
+
Examples:
|
|
372
|
+
>>> import borsapy as bp
|
|
373
|
+
>>> bp.search_funds("ak portföy")
|
|
374
|
+
[{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...]
|
|
375
|
+
>>> bp.search_funds("TTE")
|
|
376
|
+
[{'fund_code': 'TTE', 'name': 'Türkiye...', ...}]
|
|
377
|
+
"""
|
|
378
|
+
provider = get_tefas_provider()
|
|
379
|
+
return provider.search(query, limit)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def screen_funds(
|
|
383
|
+
fund_type: str = "YAT",
|
|
384
|
+
founder: str | None = None,
|
|
385
|
+
min_return_1m: float | None = None,
|
|
386
|
+
min_return_3m: float | None = None,
|
|
387
|
+
min_return_6m: float | None = None,
|
|
388
|
+
min_return_ytd: float | None = None,
|
|
389
|
+
min_return_1y: float | None = None,
|
|
390
|
+
min_return_3y: float | None = None,
|
|
391
|
+
limit: int = 50,
|
|
392
|
+
) -> pd.DataFrame:
|
|
393
|
+
"""
|
|
394
|
+
Screen funds based on fund type and return criteria.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
fund_type: Fund type filter:
|
|
398
|
+
- "YAT": Investment Funds (Yatırım Fonları) - default
|
|
399
|
+
- "EMK": Pension Funds (Emeklilik Fonları)
|
|
400
|
+
founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP")
|
|
401
|
+
min_return_1m: Minimum 1-month return (%)
|
|
402
|
+
min_return_3m: Minimum 3-month return (%)
|
|
403
|
+
min_return_6m: Minimum 6-month return (%)
|
|
404
|
+
min_return_ytd: Minimum year-to-date return (%)
|
|
405
|
+
min_return_1y: Minimum 1-year return (%)
|
|
406
|
+
min_return_3y: Minimum 3-year return (%)
|
|
407
|
+
limit: Maximum number of results (default: 50)
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
DataFrame with funds matching the criteria, sorted by 1-year return.
|
|
411
|
+
|
|
412
|
+
Examples:
|
|
413
|
+
>>> import borsapy as bp
|
|
414
|
+
>>> bp.screen_funds(fund_type="EMK") # All pension funds
|
|
415
|
+
fund_code name return_1y ...
|
|
416
|
+
|
|
417
|
+
>>> bp.screen_funds(min_return_1y=50) # Funds with >50% 1Y return
|
|
418
|
+
fund_code name return_1y ...
|
|
419
|
+
|
|
420
|
+
>>> bp.screen_funds(fund_type="EMK", min_return_ytd=20)
|
|
421
|
+
fund_code name return_ytd ...
|
|
422
|
+
"""
|
|
423
|
+
provider = get_tefas_provider()
|
|
424
|
+
results = provider.screen_funds(
|
|
425
|
+
fund_type=fund_type,
|
|
426
|
+
founder=founder,
|
|
427
|
+
min_return_1m=min_return_1m,
|
|
428
|
+
min_return_3m=min_return_3m,
|
|
429
|
+
min_return_6m=min_return_6m,
|
|
430
|
+
min_return_ytd=min_return_ytd,
|
|
431
|
+
min_return_1y=min_return_1y,
|
|
432
|
+
min_return_3y=min_return_3y,
|
|
433
|
+
limit=limit,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if not results:
|
|
437
|
+
return pd.DataFrame(columns=["fund_code", "name", "fund_type", "return_1y"])
|
|
438
|
+
|
|
439
|
+
return pd.DataFrame(results)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def compare_funds(fund_codes: list[str]) -> dict[str, Any]:
|
|
443
|
+
"""
|
|
444
|
+
Compare multiple funds side by side.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
fund_codes: List of TEFAS fund codes to compare (max 10)
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Dictionary with:
|
|
451
|
+
- funds: List of fund details with performance metrics
|
|
452
|
+
- rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc)
|
|
453
|
+
- summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size)
|
|
454
|
+
|
|
455
|
+
Examples:
|
|
456
|
+
>>> import borsapy as bp
|
|
457
|
+
>>> result = bp.compare_funds(["AAK", "TTE", "YAF"])
|
|
458
|
+
>>> result['rankings']['by_return_1y']
|
|
459
|
+
['TTE', 'YAF', 'AAK']
|
|
460
|
+
|
|
461
|
+
>>> result['summary']
|
|
462
|
+
{'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...}
|
|
463
|
+
|
|
464
|
+
>>> for fund in result['funds']:
|
|
465
|
+
... print(f"{fund['fund_code']}: {fund['return_1y']}%")
|
|
466
|
+
AAK: 32.5%
|
|
467
|
+
TTE: 72.1%
|
|
468
|
+
YAF: 31.0%
|
|
469
|
+
"""
|
|
470
|
+
provider = get_tefas_provider()
|
|
471
|
+
return provider.compare_funds(fund_codes)
|