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/ticker.py
ADDED
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
"""Ticker class for stock data - yfinance-like API."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from borsapy._providers.kap import get_kap_provider
|
|
11
|
+
from borsapy._providers.paratic import get_paratic_provider
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FastInfo:
|
|
15
|
+
"""
|
|
16
|
+
Fast access to common ticker information.
|
|
17
|
+
|
|
18
|
+
Similar to yfinance's FastInfo, provides quick access to
|
|
19
|
+
frequently used data through a dict-like interface.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
currency: Trading currency (TRY)
|
|
23
|
+
exchange: Exchange name (BIST)
|
|
24
|
+
timezone: Market timezone
|
|
25
|
+
last_price: Last traded price
|
|
26
|
+
open: Opening price
|
|
27
|
+
day_high: Day's high
|
|
28
|
+
day_low: Day's low
|
|
29
|
+
previous_close: Previous close price
|
|
30
|
+
volume: Trading volume (lot)
|
|
31
|
+
amount: Trading volume (TL)
|
|
32
|
+
market_cap: Market capitalization
|
|
33
|
+
shares: Shares outstanding
|
|
34
|
+
pe_ratio: Price/Earnings ratio (F/K)
|
|
35
|
+
pb_ratio: Price/Book ratio (PD/DD)
|
|
36
|
+
year_high: 52-week high
|
|
37
|
+
year_low: 52-week low
|
|
38
|
+
fifty_day_average: 50-day moving average
|
|
39
|
+
two_hundred_day_average: 200-day moving average
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_KEYS = [
|
|
43
|
+
"currency",
|
|
44
|
+
"exchange",
|
|
45
|
+
"timezone",
|
|
46
|
+
"last_price",
|
|
47
|
+
"open",
|
|
48
|
+
"day_high",
|
|
49
|
+
"day_low",
|
|
50
|
+
"previous_close",
|
|
51
|
+
"volume",
|
|
52
|
+
"amount",
|
|
53
|
+
"market_cap",
|
|
54
|
+
"shares",
|
|
55
|
+
"pe_ratio",
|
|
56
|
+
"pb_ratio",
|
|
57
|
+
"year_high",
|
|
58
|
+
"year_low",
|
|
59
|
+
"fifty_day_average",
|
|
60
|
+
"two_hundred_day_average",
|
|
61
|
+
"free_float",
|
|
62
|
+
"foreign_ratio",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def __init__(self, ticker: "Ticker"):
|
|
66
|
+
self._ticker = ticker
|
|
67
|
+
self._data: dict[str, Any] | None = None
|
|
68
|
+
|
|
69
|
+
def _load(self) -> dict[str, Any]:
|
|
70
|
+
"""Load all fast info data."""
|
|
71
|
+
if self._data is not None:
|
|
72
|
+
return self._data
|
|
73
|
+
|
|
74
|
+
# Get basic quote info
|
|
75
|
+
info = self._ticker.info
|
|
76
|
+
|
|
77
|
+
# Get company metrics from İş Yatırım
|
|
78
|
+
try:
|
|
79
|
+
metrics = self._ticker._get_isyatirim().get_company_metrics(
|
|
80
|
+
self._ticker._symbol
|
|
81
|
+
)
|
|
82
|
+
except Exception:
|
|
83
|
+
metrics = {}
|
|
84
|
+
|
|
85
|
+
# Calculate 52-week high/low and moving averages from history
|
|
86
|
+
year_high = None
|
|
87
|
+
year_low = None
|
|
88
|
+
fifty_day_avg = None
|
|
89
|
+
two_hundred_day_avg = None
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
hist = self._ticker.history(period="1y")
|
|
93
|
+
if not hist.empty:
|
|
94
|
+
year_high = float(hist["High"].max())
|
|
95
|
+
year_low = float(hist["Low"].min())
|
|
96
|
+
if len(hist) >= 50:
|
|
97
|
+
fifty_day_avg = float(hist["Close"].tail(50).mean())
|
|
98
|
+
if len(hist) >= 200:
|
|
99
|
+
two_hundred_day_avg = float(hist["Close"].tail(200).mean())
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
# Calculate shares from market cap and price
|
|
104
|
+
shares = None
|
|
105
|
+
if metrics.get("market_cap") and info.get("last"):
|
|
106
|
+
shares = int(metrics["market_cap"] / info["last"])
|
|
107
|
+
|
|
108
|
+
self._data = {
|
|
109
|
+
"currency": "TRY",
|
|
110
|
+
"exchange": "BIST",
|
|
111
|
+
"timezone": "Europe/Istanbul",
|
|
112
|
+
"last_price": info.get("last"),
|
|
113
|
+
"open": info.get("open"),
|
|
114
|
+
"day_high": info.get("high"),
|
|
115
|
+
"day_low": info.get("low"),
|
|
116
|
+
"previous_close": info.get("close"),
|
|
117
|
+
"volume": info.get("volume"),
|
|
118
|
+
"amount": info.get("amount"),
|
|
119
|
+
"market_cap": metrics.get("market_cap"),
|
|
120
|
+
"shares": shares,
|
|
121
|
+
"pe_ratio": metrics.get("pe_ratio"),
|
|
122
|
+
"pb_ratio": metrics.get("pb_ratio"),
|
|
123
|
+
"year_high": year_high,
|
|
124
|
+
"year_low": year_low,
|
|
125
|
+
"fifty_day_average": round(fifty_day_avg, 2) if fifty_day_avg else None,
|
|
126
|
+
"two_hundred_day_average": (
|
|
127
|
+
round(two_hundred_day_avg, 2) if two_hundred_day_avg else None
|
|
128
|
+
),
|
|
129
|
+
"free_float": metrics.get("free_float"),
|
|
130
|
+
"foreign_ratio": metrics.get("foreign_ratio"),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return self._data
|
|
134
|
+
|
|
135
|
+
def keys(self) -> list[str]:
|
|
136
|
+
"""Return available keys."""
|
|
137
|
+
return self._KEYS.copy()
|
|
138
|
+
|
|
139
|
+
def __getitem__(self, key: str) -> Any:
|
|
140
|
+
if key not in self._KEYS:
|
|
141
|
+
raise KeyError(f"Invalid key '{key}'. Valid keys: {self._KEYS}")
|
|
142
|
+
return self._load().get(key)
|
|
143
|
+
|
|
144
|
+
def __getattr__(self, name: str) -> Any:
|
|
145
|
+
if name.startswith("_"):
|
|
146
|
+
raise AttributeError(name)
|
|
147
|
+
if name not in self._KEYS:
|
|
148
|
+
raise AttributeError(
|
|
149
|
+
f"'{type(self).__name__}' has no attribute '{name}'. "
|
|
150
|
+
f"Valid attributes: {self._KEYS}"
|
|
151
|
+
)
|
|
152
|
+
return self._load().get(name)
|
|
153
|
+
|
|
154
|
+
def __iter__(self):
|
|
155
|
+
return iter(self._load().items())
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
data = self._load()
|
|
159
|
+
items = [f"{k}={v!r}" for k, v in data.items() if v is not None]
|
|
160
|
+
return f"FastInfo({', '.join(items)})"
|
|
161
|
+
|
|
162
|
+
def todict(self) -> dict[str, Any]:
|
|
163
|
+
"""Return all data as a dictionary."""
|
|
164
|
+
return self._load().copy()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class EnrichedInfo:
|
|
168
|
+
"""
|
|
169
|
+
Lazy-loading info dictionary with yfinance-compatible field names.
|
|
170
|
+
|
|
171
|
+
Provides dict-like access to ticker information with three lazy-loaded groups:
|
|
172
|
+
- Basic fields (from Paratic quote): last, open, high, low, close, volume, amount
|
|
173
|
+
- Extended fields (from İş Yatırım + calculations): marketCap, trailingPE, etc.
|
|
174
|
+
- Dividend fields (calculated): dividendYield, exDividendDate
|
|
175
|
+
|
|
176
|
+
yfinance aliases are supported for common field names:
|
|
177
|
+
- regularMarketPrice, currentPrice -> last
|
|
178
|
+
- regularMarketOpen -> open
|
|
179
|
+
- regularMarketDayHigh -> high
|
|
180
|
+
- regularMarketDayLow -> low
|
|
181
|
+
- regularMarketPreviousClose -> close
|
|
182
|
+
- regularMarketVolume -> volume
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
>>> stock = Ticker("THYAO")
|
|
186
|
+
>>> stock.info['last'] # Basic field - fast
|
|
187
|
+
268.5
|
|
188
|
+
>>> stock.info['marketCap'] # Extended field - lazy loaded
|
|
189
|
+
370530000000
|
|
190
|
+
>>> stock.info['regularMarketPrice'] # yfinance alias
|
|
191
|
+
268.5
|
|
192
|
+
>>> stock.info.get('dividendYield') # Safe access
|
|
193
|
+
1.28
|
|
194
|
+
>>> stock.info.todict() # Get all as regular dict
|
|
195
|
+
{...}
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
_YFINANCE_ALIASES: dict[str, str] = {
|
|
199
|
+
"regularMarketPrice": "last",
|
|
200
|
+
"currentPrice": "last",
|
|
201
|
+
"regularMarketOpen": "open",
|
|
202
|
+
"regularMarketDayHigh": "high",
|
|
203
|
+
"regularMarketDayLow": "low",
|
|
204
|
+
"regularMarketPreviousClose": "close",
|
|
205
|
+
"regularMarketVolume": "volume",
|
|
206
|
+
"regularMarketChange": "change",
|
|
207
|
+
"regularMarketChangePercent": "change_percent",
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_BASIC_KEYS = [
|
|
211
|
+
"symbol",
|
|
212
|
+
"last",
|
|
213
|
+
"open",
|
|
214
|
+
"high",
|
|
215
|
+
"low",
|
|
216
|
+
"close",
|
|
217
|
+
"volume", # Lot bazında hacim
|
|
218
|
+
"amount", # TL bazında hacim
|
|
219
|
+
"change",
|
|
220
|
+
"change_percent",
|
|
221
|
+
"update_time",
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
_EXTENDED_KEYS = [
|
|
225
|
+
"currency",
|
|
226
|
+
"exchange",
|
|
227
|
+
"timezone",
|
|
228
|
+
"sector",
|
|
229
|
+
"industry",
|
|
230
|
+
"website",
|
|
231
|
+
"marketCap",
|
|
232
|
+
"sharesOutstanding",
|
|
233
|
+
"trailingPE",
|
|
234
|
+
"priceToBook",
|
|
235
|
+
"enterpriseToEbitda",
|
|
236
|
+
"netDebt",
|
|
237
|
+
"floatShares",
|
|
238
|
+
"foreignRatio",
|
|
239
|
+
"fiftyTwoWeekHigh",
|
|
240
|
+
"fiftyTwoWeekLow",
|
|
241
|
+
"fiftyDayAverage",
|
|
242
|
+
"twoHundredDayAverage",
|
|
243
|
+
"longBusinessSummary",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
_DIVIDEND_KEYS = [
|
|
247
|
+
"dividendYield",
|
|
248
|
+
"exDividendDate",
|
|
249
|
+
"trailingAnnualDividendRate",
|
|
250
|
+
"trailingAnnualDividendYield",
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
def __init__(self, ticker: "Ticker"):
|
|
254
|
+
self._ticker = ticker
|
|
255
|
+
self._basic_data: dict[str, Any] | None = None
|
|
256
|
+
self._extended_data: dict[str, Any] | None = None
|
|
257
|
+
self._dividend_data: dict[str, Any] | None = None
|
|
258
|
+
|
|
259
|
+
def _load_basic(self) -> dict[str, Any]:
|
|
260
|
+
"""Load basic quote data from Paratic."""
|
|
261
|
+
if self._basic_data is None:
|
|
262
|
+
self._basic_data = self._ticker._paratic.get_quote(self._ticker._symbol)
|
|
263
|
+
return self._basic_data
|
|
264
|
+
|
|
265
|
+
def _load_extended(self) -> dict[str, Any]:
|
|
266
|
+
"""Load extended metrics from İş Yatırım + calculations."""
|
|
267
|
+
if self._extended_data is not None:
|
|
268
|
+
return self._extended_data
|
|
269
|
+
|
|
270
|
+
basic = self._load_basic()
|
|
271
|
+
|
|
272
|
+
# Get İş Yatırım metrics
|
|
273
|
+
try:
|
|
274
|
+
metrics = self._ticker._get_isyatirim().get_company_metrics(
|
|
275
|
+
self._ticker._symbol
|
|
276
|
+
)
|
|
277
|
+
except Exception:
|
|
278
|
+
metrics = {}
|
|
279
|
+
|
|
280
|
+
# Calculate 52-week and moving averages
|
|
281
|
+
year_high = year_low = fifty_avg = two_hundred_avg = None
|
|
282
|
+
try:
|
|
283
|
+
hist = self._ticker.history(period="1y")
|
|
284
|
+
if not hist.empty:
|
|
285
|
+
year_high = float(hist["High"].max())
|
|
286
|
+
year_low = float(hist["Low"].min())
|
|
287
|
+
if len(hist) >= 50:
|
|
288
|
+
fifty_avg = round(float(hist["Close"].tail(50).mean()), 2)
|
|
289
|
+
if len(hist) >= 200:
|
|
290
|
+
two_hundred_avg = round(float(hist["Close"].tail(200).mean()), 2)
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
# Calculate shares
|
|
295
|
+
shares = None
|
|
296
|
+
if metrics.get("market_cap") and basic.get("last"):
|
|
297
|
+
shares = int(metrics["market_cap"] / basic["last"])
|
|
298
|
+
|
|
299
|
+
# Get company details from KAP (sector, market, website, businessSummary)
|
|
300
|
+
try:
|
|
301
|
+
kap_details = get_kap_provider().get_company_details(
|
|
302
|
+
self._ticker._symbol
|
|
303
|
+
)
|
|
304
|
+
except Exception:
|
|
305
|
+
kap_details = {}
|
|
306
|
+
|
|
307
|
+
self._extended_data = {
|
|
308
|
+
"currency": "TRY",
|
|
309
|
+
"exchange": "BIST",
|
|
310
|
+
"timezone": "Europe/Istanbul",
|
|
311
|
+
"sector": kap_details.get("sector"),
|
|
312
|
+
"industry": kap_details.get("sector"), # KAP has single level
|
|
313
|
+
"website": kap_details.get("website"),
|
|
314
|
+
"marketCap": metrics.get("market_cap"),
|
|
315
|
+
"sharesOutstanding": shares,
|
|
316
|
+
"trailingPE": metrics.get("pe_ratio"),
|
|
317
|
+
"priceToBook": metrics.get("pb_ratio"),
|
|
318
|
+
"enterpriseToEbitda": metrics.get("ev_ebitda"),
|
|
319
|
+
"netDebt": metrics.get("net_debt"),
|
|
320
|
+
"floatShares": metrics.get("free_float"),
|
|
321
|
+
"foreignRatio": metrics.get("foreign_ratio"),
|
|
322
|
+
"fiftyTwoWeekHigh": year_high,
|
|
323
|
+
"fiftyTwoWeekLow": year_low,
|
|
324
|
+
"fiftyDayAverage": fifty_avg,
|
|
325
|
+
"twoHundredDayAverage": two_hundred_avg,
|
|
326
|
+
"longBusinessSummary": kap_details.get("businessSummary"),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return self._extended_data
|
|
330
|
+
|
|
331
|
+
def _load_dividends(self) -> dict[str, Any]:
|
|
332
|
+
"""Load dividend-related fields."""
|
|
333
|
+
if self._dividend_data is not None:
|
|
334
|
+
return self._dividend_data
|
|
335
|
+
|
|
336
|
+
self._dividend_data = {
|
|
337
|
+
"dividendYield": None,
|
|
338
|
+
"exDividendDate": None,
|
|
339
|
+
"trailingAnnualDividendRate": None,
|
|
340
|
+
"trailingAnnualDividendYield": None,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
divs = self._ticker.dividends
|
|
345
|
+
if divs.empty:
|
|
346
|
+
return self._dividend_data
|
|
347
|
+
|
|
348
|
+
# Last dividend date
|
|
349
|
+
self._dividend_data["exDividendDate"] = divs.index[0]
|
|
350
|
+
|
|
351
|
+
# Trailing annual dividend (sum of last 1 year)
|
|
352
|
+
one_year_ago = datetime.now() - timedelta(days=365)
|
|
353
|
+
annual_divs = divs[divs.index >= one_year_ago]
|
|
354
|
+
annual_total = (
|
|
355
|
+
annual_divs["Amount"].sum() if not annual_divs.empty else 0.0
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
self._dividend_data["trailingAnnualDividendRate"] = round(annual_total, 4)
|
|
359
|
+
|
|
360
|
+
# Yield calculation
|
|
361
|
+
basic = self._load_basic()
|
|
362
|
+
current_price = basic.get("last", 0)
|
|
363
|
+
if current_price and annual_total:
|
|
364
|
+
yield_pct = (annual_total / current_price) * 100
|
|
365
|
+
self._dividend_data["dividendYield"] = round(yield_pct, 2)
|
|
366
|
+
self._dividend_data["trailingAnnualDividendYield"] = round(
|
|
367
|
+
yield_pct / 100, 4
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
return self._dividend_data
|
|
374
|
+
|
|
375
|
+
def _resolve_key(self, key: str) -> str:
|
|
376
|
+
"""Resolve yfinance alias to actual key."""
|
|
377
|
+
return self._YFINANCE_ALIASES.get(key, key)
|
|
378
|
+
|
|
379
|
+
def __getitem__(self, key: str) -> Any:
|
|
380
|
+
resolved_key = self._resolve_key(key)
|
|
381
|
+
|
|
382
|
+
# Try basic first (fastest)
|
|
383
|
+
basic = self._load_basic()
|
|
384
|
+
if resolved_key in basic:
|
|
385
|
+
return basic[resolved_key]
|
|
386
|
+
|
|
387
|
+
# Try extended
|
|
388
|
+
extended = self._load_extended()
|
|
389
|
+
if resolved_key in extended:
|
|
390
|
+
return extended[resolved_key]
|
|
391
|
+
|
|
392
|
+
# Try dividend fields
|
|
393
|
+
dividend = self._load_dividends()
|
|
394
|
+
if resolved_key in dividend:
|
|
395
|
+
return dividend[resolved_key]
|
|
396
|
+
|
|
397
|
+
raise KeyError(f"Key '{key}' not found in info")
|
|
398
|
+
|
|
399
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
400
|
+
"""Get a value with optional default."""
|
|
401
|
+
try:
|
|
402
|
+
return self[key]
|
|
403
|
+
except KeyError:
|
|
404
|
+
return default
|
|
405
|
+
|
|
406
|
+
def keys(self) -> list[str]:
|
|
407
|
+
"""Return all available keys including yfinance aliases."""
|
|
408
|
+
all_keys = (
|
|
409
|
+
self._BASIC_KEYS
|
|
410
|
+
+ self._EXTENDED_KEYS
|
|
411
|
+
+ self._DIVIDEND_KEYS
|
|
412
|
+
+ list(self._YFINANCE_ALIASES.keys())
|
|
413
|
+
)
|
|
414
|
+
return all_keys
|
|
415
|
+
|
|
416
|
+
def items(self) -> Iterator[tuple[str, Any]]:
|
|
417
|
+
"""Return all key-value pairs."""
|
|
418
|
+
result = {}
|
|
419
|
+
result.update(self._load_basic())
|
|
420
|
+
result.update(self._load_extended())
|
|
421
|
+
result.update(self._load_dividends())
|
|
422
|
+
return iter(result.items())
|
|
423
|
+
|
|
424
|
+
def values(self) -> Iterator[Any]:
|
|
425
|
+
"""Return all values."""
|
|
426
|
+
result = {}
|
|
427
|
+
result.update(self._load_basic())
|
|
428
|
+
result.update(self._load_extended())
|
|
429
|
+
result.update(self._load_dividends())
|
|
430
|
+
return iter(result.values())
|
|
431
|
+
|
|
432
|
+
def __iter__(self) -> Iterator[str]:
|
|
433
|
+
"""Iterate over keys."""
|
|
434
|
+
return iter(self.keys())
|
|
435
|
+
|
|
436
|
+
def __contains__(self, key: str) -> bool:
|
|
437
|
+
"""Check if key exists."""
|
|
438
|
+
resolved_key = self._resolve_key(key)
|
|
439
|
+
return (
|
|
440
|
+
resolved_key in self._BASIC_KEYS
|
|
441
|
+
or resolved_key in self._EXTENDED_KEYS
|
|
442
|
+
or resolved_key in self._DIVIDEND_KEYS
|
|
443
|
+
or key in self._YFINANCE_ALIASES
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def __len__(self) -> int:
|
|
447
|
+
"""Return number of fields."""
|
|
448
|
+
return (
|
|
449
|
+
len(self._BASIC_KEYS) + len(self._EXTENDED_KEYS) + len(self._DIVIDEND_KEYS)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def __repr__(self) -> str:
|
|
453
|
+
# Only show basic data to avoid triggering extended loads
|
|
454
|
+
basic = self._load_basic()
|
|
455
|
+
return f"EnrichedInfo({basic})"
|
|
456
|
+
|
|
457
|
+
def todict(self) -> dict[str, Any]:
|
|
458
|
+
"""Return all data as a regular dictionary (triggers all loads)."""
|
|
459
|
+
result = {}
|
|
460
|
+
result.update(self._load_basic())
|
|
461
|
+
result.update(self._load_extended())
|
|
462
|
+
result.update(self._load_dividends())
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class Ticker:
|
|
467
|
+
"""
|
|
468
|
+
A yfinance-like interface for Turkish stock data.
|
|
469
|
+
|
|
470
|
+
Examples:
|
|
471
|
+
>>> import borsapy as bp
|
|
472
|
+
>>> stock = bp.Ticker("THYAO")
|
|
473
|
+
>>> stock.info
|
|
474
|
+
{'symbol': 'THYAO', 'last': 268.5, ...}
|
|
475
|
+
>>> stock.history(period="1mo")
|
|
476
|
+
Open High Low Close Volume
|
|
477
|
+
Date
|
|
478
|
+
2024-12-01 265.00 268.00 264.00 267.50 12345678
|
|
479
|
+
...
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
def __init__(self, symbol: str):
|
|
483
|
+
"""
|
|
484
|
+
Initialize a Ticker object.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS").
|
|
488
|
+
The ".IS" or ".E" suffix is optional and will be removed.
|
|
489
|
+
"""
|
|
490
|
+
self._symbol = symbol.upper().replace(".IS", "").replace(".E", "")
|
|
491
|
+
self._paratic = get_paratic_provider()
|
|
492
|
+
self._isyatirim = None # Lazy load for financial statements
|
|
493
|
+
self._kap = None # Lazy load for KAP disclosures
|
|
494
|
+
self._isin_provider = None # Lazy load for ISIN lookup
|
|
495
|
+
self._hedeffiyat = None # Lazy load for analyst price targets
|
|
496
|
+
|
|
497
|
+
def _get_isyatirim(self):
|
|
498
|
+
"""Lazy load İş Yatırım provider for financial statements."""
|
|
499
|
+
if self._isyatirim is None:
|
|
500
|
+
from borsapy._providers.isyatirim import get_isyatirim_provider
|
|
501
|
+
|
|
502
|
+
self._isyatirim = get_isyatirim_provider()
|
|
503
|
+
return self._isyatirim
|
|
504
|
+
|
|
505
|
+
def _get_kap(self):
|
|
506
|
+
"""Lazy load KAP provider for disclosures and calendar."""
|
|
507
|
+
if self._kap is None:
|
|
508
|
+
from borsapy._providers.kap import get_kap_provider
|
|
509
|
+
|
|
510
|
+
self._kap = get_kap_provider()
|
|
511
|
+
return self._kap
|
|
512
|
+
|
|
513
|
+
def _get_isin_provider(self):
|
|
514
|
+
"""Lazy load ISIN provider."""
|
|
515
|
+
if self._isin_provider is None:
|
|
516
|
+
from borsapy._providers.isin import get_isin_provider
|
|
517
|
+
|
|
518
|
+
self._isin_provider = get_isin_provider()
|
|
519
|
+
return self._isin_provider
|
|
520
|
+
|
|
521
|
+
def _get_hedeffiyat(self):
|
|
522
|
+
"""Lazy load hedeffiyat.com.tr provider for analyst price targets."""
|
|
523
|
+
if self._hedeffiyat is None:
|
|
524
|
+
from borsapy._providers.hedeffiyat import get_hedeffiyat_provider
|
|
525
|
+
|
|
526
|
+
self._hedeffiyat = get_hedeffiyat_provider()
|
|
527
|
+
return self._hedeffiyat
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def symbol(self) -> str:
|
|
531
|
+
"""Return the ticker symbol."""
|
|
532
|
+
return self._symbol
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def fast_info(self) -> FastInfo:
|
|
536
|
+
"""
|
|
537
|
+
Get fast access to common ticker information.
|
|
538
|
+
|
|
539
|
+
Returns a FastInfo object with quick access to frequently used data:
|
|
540
|
+
- currency, exchange, timezone
|
|
541
|
+
- last_price, open, day_high, day_low, previous_close, volume
|
|
542
|
+
- market_cap, shares, pe_ratio, pb_ratio
|
|
543
|
+
- year_high, year_low (52-week)
|
|
544
|
+
- fifty_day_average, two_hundred_day_average
|
|
545
|
+
- free_float, foreign_ratio
|
|
546
|
+
|
|
547
|
+
Examples:
|
|
548
|
+
>>> stock = Ticker("THYAO")
|
|
549
|
+
>>> stock.fast_info.market_cap
|
|
550
|
+
370530000000
|
|
551
|
+
>>> stock.fast_info['pe_ratio']
|
|
552
|
+
2.8
|
|
553
|
+
>>> stock.fast_info.keys()
|
|
554
|
+
['currency', 'exchange', 'timezone', ...]
|
|
555
|
+
"""
|
|
556
|
+
if not hasattr(self, "_fast_info"):
|
|
557
|
+
self._fast_info = FastInfo(self)
|
|
558
|
+
return self._fast_info
|
|
559
|
+
|
|
560
|
+
@property
|
|
561
|
+
def info(self) -> EnrichedInfo:
|
|
562
|
+
"""
|
|
563
|
+
Get comprehensive ticker information with yfinance-compatible fields.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
EnrichedInfo object providing dict-like access to:
|
|
567
|
+
|
|
568
|
+
Basic fields (always loaded, fast):
|
|
569
|
+
- symbol, last, open, high, low, close, volume
|
|
570
|
+
- change, change_percent, update_time
|
|
571
|
+
|
|
572
|
+
yfinance aliases (map to basic fields):
|
|
573
|
+
- regularMarketPrice, currentPrice -> last
|
|
574
|
+
- regularMarketOpen -> open
|
|
575
|
+
- regularMarketDayHigh -> high
|
|
576
|
+
- regularMarketDayLow -> low
|
|
577
|
+
- regularMarketPreviousClose -> close
|
|
578
|
+
- regularMarketVolume -> volume
|
|
579
|
+
|
|
580
|
+
Extended fields (lazy-loaded on access):
|
|
581
|
+
- marketCap, trailingPE, priceToBook, enterpriseToEbitda
|
|
582
|
+
- sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow
|
|
583
|
+
- fiftyDayAverage, twoHundredDayAverage
|
|
584
|
+
- floatShares, foreignRatio, netDebt
|
|
585
|
+
- currency, exchange, timezone
|
|
586
|
+
|
|
587
|
+
Dividend fields (lazy-loaded on access):
|
|
588
|
+
- dividendYield, exDividendDate
|
|
589
|
+
- trailingAnnualDividendRate, trailingAnnualDividendYield
|
|
590
|
+
|
|
591
|
+
Examples:
|
|
592
|
+
>>> stock = Ticker("THYAO")
|
|
593
|
+
>>> stock.info['last'] # Basic field - fast
|
|
594
|
+
268.5
|
|
595
|
+
>>> stock.info['marketCap'] # Extended field - fetches İş Yatırım
|
|
596
|
+
370530000000
|
|
597
|
+
>>> stock.info['trailingPE'] # yfinance compatible name
|
|
598
|
+
2.8
|
|
599
|
+
>>> stock.info.get('dividendYield') # Safe access
|
|
600
|
+
1.28
|
|
601
|
+
>>> stock.info.todict() # Get all as regular dict
|
|
602
|
+
{...}
|
|
603
|
+
"""
|
|
604
|
+
if not hasattr(self, "_enriched_info"):
|
|
605
|
+
self._enriched_info = EnrichedInfo(self)
|
|
606
|
+
return self._enriched_info
|
|
607
|
+
|
|
608
|
+
def history(
|
|
609
|
+
self,
|
|
610
|
+
period: str = "1mo",
|
|
611
|
+
interval: str = "1d",
|
|
612
|
+
start: datetime | str | None = None,
|
|
613
|
+
end: datetime | str | None = None,
|
|
614
|
+
actions: bool = False,
|
|
615
|
+
) -> pd.DataFrame:
|
|
616
|
+
"""
|
|
617
|
+
Get historical OHLCV data.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
period: How much data to fetch. Valid periods:
|
|
621
|
+
1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
|
|
622
|
+
Ignored if start is provided.
|
|
623
|
+
interval: Data granularity. Valid intervals:
|
|
624
|
+
1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo.
|
|
625
|
+
start: Start date (string or datetime).
|
|
626
|
+
end: End date (string or datetime). Defaults to today.
|
|
627
|
+
actions: If True, include Dividends and Stock Splits columns.
|
|
628
|
+
Defaults to False.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
DataFrame with columns: Open, High, Low, Close, Volume.
|
|
632
|
+
If actions=True, also includes Dividends and Stock Splits columns.
|
|
633
|
+
Index is the Date.
|
|
634
|
+
|
|
635
|
+
Examples:
|
|
636
|
+
>>> stock = Ticker("THYAO")
|
|
637
|
+
>>> stock.history(period="1mo") # Last month
|
|
638
|
+
>>> stock.history(period="1y", interval="1wk") # Weekly for 1 year
|
|
639
|
+
>>> stock.history(start="2024-01-01", end="2024-06-30") # Date range
|
|
640
|
+
>>> stock.history(period="1y", actions=True) # With dividends/splits
|
|
641
|
+
"""
|
|
642
|
+
# Parse dates if strings
|
|
643
|
+
start_dt = self._parse_date(start) if start else None
|
|
644
|
+
end_dt = self._parse_date(end) if end else None
|
|
645
|
+
|
|
646
|
+
df = self._paratic.get_history(
|
|
647
|
+
symbol=self._symbol,
|
|
648
|
+
period=period,
|
|
649
|
+
interval=interval,
|
|
650
|
+
start=start_dt,
|
|
651
|
+
end=end_dt,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if actions and not df.empty:
|
|
655
|
+
df = self._add_actions_to_history(df)
|
|
656
|
+
|
|
657
|
+
return df
|
|
658
|
+
|
|
659
|
+
def _add_actions_to_history(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
660
|
+
"""
|
|
661
|
+
Add Dividends and Stock Splits columns to historical data.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
df: Historical OHLCV DataFrame.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
DataFrame with added Dividends and Stock Splits columns.
|
|
668
|
+
"""
|
|
669
|
+
# Initialize columns with zeros
|
|
670
|
+
df = df.copy()
|
|
671
|
+
df["Dividends"] = 0.0
|
|
672
|
+
df["Stock Splits"] = 0.0
|
|
673
|
+
|
|
674
|
+
# Get dividends
|
|
675
|
+
try:
|
|
676
|
+
divs = self.dividends
|
|
677
|
+
if not divs.empty:
|
|
678
|
+
for div_date, row in divs.iterrows():
|
|
679
|
+
# Normalize dates for comparison
|
|
680
|
+
div_date_normalized = pd.Timestamp(div_date).normalize()
|
|
681
|
+
for idx in df.index:
|
|
682
|
+
idx_normalized = pd.Timestamp(idx).normalize()
|
|
683
|
+
if div_date_normalized == idx_normalized:
|
|
684
|
+
df.loc[idx, "Dividends"] = row.get("Amount", 0)
|
|
685
|
+
break
|
|
686
|
+
except Exception:
|
|
687
|
+
pass
|
|
688
|
+
|
|
689
|
+
# Get stock splits (capital increases)
|
|
690
|
+
try:
|
|
691
|
+
splits = self.splits
|
|
692
|
+
if not splits.empty:
|
|
693
|
+
for split_date, row in splits.iterrows():
|
|
694
|
+
split_date_normalized = pd.Timestamp(split_date).normalize()
|
|
695
|
+
# Calculate split ratio
|
|
696
|
+
# BonusFromCapital + BonusFromDividend = total bonus percentage
|
|
697
|
+
bonus_pct = row.get("BonusFromCapital", 0) + row.get(
|
|
698
|
+
"BonusFromDividend", 0
|
|
699
|
+
)
|
|
700
|
+
if bonus_pct > 0:
|
|
701
|
+
# Convert percentage to split ratio (e.g., 20% bonus = 1.2 split)
|
|
702
|
+
split_ratio = 1 + (bonus_pct / 100)
|
|
703
|
+
for idx in df.index:
|
|
704
|
+
idx_normalized = pd.Timestamp(idx).normalize()
|
|
705
|
+
if split_date_normalized == idx_normalized:
|
|
706
|
+
df.loc[idx, "Stock Splits"] = split_ratio
|
|
707
|
+
break
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
710
|
+
|
|
711
|
+
return df
|
|
712
|
+
|
|
713
|
+
@cached_property
|
|
714
|
+
def dividends(self) -> pd.DataFrame:
|
|
715
|
+
"""
|
|
716
|
+
Get dividend history.
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
DataFrame with dividend history:
|
|
720
|
+
- Amount: Dividend per share (TL)
|
|
721
|
+
- GrossRate: Gross dividend rate (%)
|
|
722
|
+
- NetRate: Net dividend rate (%)
|
|
723
|
+
- TotalDividend: Total dividend distributed (TL)
|
|
724
|
+
|
|
725
|
+
Examples:
|
|
726
|
+
>>> stock = Ticker("THYAO")
|
|
727
|
+
>>> stock.dividends
|
|
728
|
+
Amount GrossRate NetRate TotalDividend
|
|
729
|
+
Date
|
|
730
|
+
2025-09-02 3.442 344.20 292.57 4750000000.0
|
|
731
|
+
2025-06-16 3.442 344.20 292.57 4750000000.0
|
|
732
|
+
"""
|
|
733
|
+
return self._get_isyatirim().get_dividends(self._symbol)
|
|
734
|
+
|
|
735
|
+
@cached_property
|
|
736
|
+
def splits(self) -> pd.DataFrame:
|
|
737
|
+
"""
|
|
738
|
+
Get capital increase (split) history.
|
|
739
|
+
|
|
740
|
+
Note: Turkish market uses capital increases instead of traditional splits.
|
|
741
|
+
- RightsIssue: Paid capital increase (bedelli)
|
|
742
|
+
- BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
|
|
743
|
+
- BonusFromDividend: Free shares from dividend (bedelsiz temettüden)
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
DataFrame with capital increase history:
|
|
747
|
+
- Capital: New capital after increase (TL)
|
|
748
|
+
- RightsIssue: Rights issue rate (%)
|
|
749
|
+
- BonusFromCapital: Bonus from capital (%)
|
|
750
|
+
- BonusFromDividend: Bonus from dividend (%)
|
|
751
|
+
|
|
752
|
+
Examples:
|
|
753
|
+
>>> stock = Ticker("THYAO")
|
|
754
|
+
>>> stock.splits
|
|
755
|
+
Capital RightsIssue BonusFromCapital BonusFromDividend
|
|
756
|
+
Date
|
|
757
|
+
2013-06-26 1380000000.0 0.0 15.00 0.0
|
|
758
|
+
2011-07-11 1200000000.0 0.0 0.00 20.0
|
|
759
|
+
"""
|
|
760
|
+
return self._get_isyatirim().get_capital_increases(self._symbol)
|
|
761
|
+
|
|
762
|
+
@cached_property
|
|
763
|
+
def actions(self) -> pd.DataFrame:
|
|
764
|
+
"""
|
|
765
|
+
Get combined dividends and splits history.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
DataFrame with combined dividend and split actions:
|
|
769
|
+
- Dividends: Dividend per share (TL) or 0
|
|
770
|
+
- Splits: Combined split ratio (0 if no split)
|
|
771
|
+
|
|
772
|
+
Examples:
|
|
773
|
+
>>> stock = Ticker("THYAO")
|
|
774
|
+
>>> stock.actions
|
|
775
|
+
Dividends Splits
|
|
776
|
+
Date
|
|
777
|
+
2025-09-02 3.442 0.0
|
|
778
|
+
2013-06-26 0.000 15.0
|
|
779
|
+
"""
|
|
780
|
+
dividends = self.dividends
|
|
781
|
+
splits = self.splits
|
|
782
|
+
|
|
783
|
+
# Merge on index (Date)
|
|
784
|
+
if dividends.empty and splits.empty:
|
|
785
|
+
return pd.DataFrame(columns=["Dividends", "Splits"])
|
|
786
|
+
|
|
787
|
+
# Extract relevant columns
|
|
788
|
+
div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float)
|
|
789
|
+
split_series = (
|
|
790
|
+
splits["BonusFromCapital"] + splits["BonusFromDividend"]
|
|
791
|
+
if not splits.empty
|
|
792
|
+
else pd.Series(dtype=float)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# Combine into single DataFrame
|
|
796
|
+
result = pd.DataFrame({"Dividends": div_series, "Splits": split_series})
|
|
797
|
+
result = result.fillna(0)
|
|
798
|
+
result = result.sort_index(ascending=False)
|
|
799
|
+
|
|
800
|
+
return result
|
|
801
|
+
|
|
802
|
+
@cached_property
|
|
803
|
+
def balance_sheet(self) -> pd.DataFrame:
|
|
804
|
+
"""
|
|
805
|
+
Get annual balance sheet data.
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
DataFrame with balance sheet items as rows and years as columns.
|
|
809
|
+
"""
|
|
810
|
+
return self._get_isyatirim().get_financial_statements(
|
|
811
|
+
symbol=self._symbol,
|
|
812
|
+
statement_type="balance_sheet",
|
|
813
|
+
quarterly=False,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
@cached_property
|
|
817
|
+
def quarterly_balance_sheet(self) -> pd.DataFrame:
|
|
818
|
+
"""
|
|
819
|
+
Get quarterly balance sheet data.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
DataFrame with balance sheet items as rows and quarters as columns.
|
|
823
|
+
"""
|
|
824
|
+
return self._get_isyatirim().get_financial_statements(
|
|
825
|
+
symbol=self._symbol,
|
|
826
|
+
statement_type="balance_sheet",
|
|
827
|
+
quarterly=True,
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
@cached_property
|
|
831
|
+
def income_stmt(self) -> pd.DataFrame:
|
|
832
|
+
"""
|
|
833
|
+
Get annual income statement data.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
DataFrame with income statement items as rows and years as columns.
|
|
837
|
+
"""
|
|
838
|
+
return self._get_isyatirim().get_financial_statements(
|
|
839
|
+
symbol=self._symbol,
|
|
840
|
+
statement_type="income_stmt",
|
|
841
|
+
quarterly=False,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
@cached_property
|
|
845
|
+
def quarterly_income_stmt(self) -> pd.DataFrame:
|
|
846
|
+
"""
|
|
847
|
+
Get quarterly income statement data.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
DataFrame with income statement items as rows and quarters as columns.
|
|
851
|
+
"""
|
|
852
|
+
return self._get_isyatirim().get_financial_statements(
|
|
853
|
+
symbol=self._symbol,
|
|
854
|
+
statement_type="income_stmt",
|
|
855
|
+
quarterly=True,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
@cached_property
|
|
859
|
+
def cashflow(self) -> pd.DataFrame:
|
|
860
|
+
"""
|
|
861
|
+
Get annual cash flow statement data.
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
DataFrame with cash flow items as rows and years as columns.
|
|
865
|
+
"""
|
|
866
|
+
return self._get_isyatirim().get_financial_statements(
|
|
867
|
+
symbol=self._symbol,
|
|
868
|
+
statement_type="cashflow",
|
|
869
|
+
quarterly=False,
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
@cached_property
|
|
873
|
+
def quarterly_cashflow(self) -> pd.DataFrame:
|
|
874
|
+
"""
|
|
875
|
+
Get quarterly cash flow statement data.
|
|
876
|
+
|
|
877
|
+
Returns:
|
|
878
|
+
DataFrame with cash flow items as rows and quarters as columns.
|
|
879
|
+
"""
|
|
880
|
+
return self._get_isyatirim().get_financial_statements(
|
|
881
|
+
symbol=self._symbol,
|
|
882
|
+
statement_type="cashflow",
|
|
883
|
+
quarterly=True,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
def _calculate_ttm(self, quarterly_df: pd.DataFrame) -> pd.DataFrame:
|
|
887
|
+
"""
|
|
888
|
+
Calculate trailing twelve months (TTM) by summing last 4 quarters.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
quarterly_df: DataFrame with quarterly data (columns in YYYYQN format).
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
DataFrame with single TTM column containing summed values.
|
|
895
|
+
"""
|
|
896
|
+
if quarterly_df.empty or len(quarterly_df.columns) < 4:
|
|
897
|
+
return pd.DataFrame(columns=["TTM"])
|
|
898
|
+
|
|
899
|
+
# First 4 columns = last 4 quarters (most recent first)
|
|
900
|
+
last_4_quarters = quarterly_df.iloc[:, :4]
|
|
901
|
+
|
|
902
|
+
# Convert to numeric, coercing errors to NaN
|
|
903
|
+
numeric_df = last_4_quarters.apply(pd.to_numeric, errors="coerce")
|
|
904
|
+
|
|
905
|
+
return numeric_df.sum(axis=1).to_frame(name="TTM")
|
|
906
|
+
|
|
907
|
+
@cached_property
|
|
908
|
+
def ttm_income_stmt(self) -> pd.DataFrame:
|
|
909
|
+
"""
|
|
910
|
+
Get trailing twelve months (TTM) income statement.
|
|
911
|
+
|
|
912
|
+
Calculates TTM by summing the last 4 quarters of income statement data.
|
|
913
|
+
Useful for analyzing recent performance without waiting for annual reports.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
DataFrame with TTM column containing summed values for each line item.
|
|
917
|
+
|
|
918
|
+
Examples:
|
|
919
|
+
>>> stock = Ticker("THYAO")
|
|
920
|
+
>>> stock.ttm_income_stmt
|
|
921
|
+
TTM
|
|
922
|
+
Net Satışlar 4.5e+11
|
|
923
|
+
Brüt Kar 1.2e+11
|
|
924
|
+
Faaliyet Karı 8.0e+10
|
|
925
|
+
Net Kar 3.0e+10
|
|
926
|
+
"""
|
|
927
|
+
return self._calculate_ttm(self.quarterly_income_stmt)
|
|
928
|
+
|
|
929
|
+
@cached_property
|
|
930
|
+
def ttm_cashflow(self) -> pd.DataFrame:
|
|
931
|
+
"""
|
|
932
|
+
Get trailing twelve months (TTM) cash flow statement.
|
|
933
|
+
|
|
934
|
+
Calculates TTM by summing the last 4 quarters of cash flow data.
|
|
935
|
+
|
|
936
|
+
Returns:
|
|
937
|
+
DataFrame with TTM column containing summed values for each line item.
|
|
938
|
+
|
|
939
|
+
Examples:
|
|
940
|
+
>>> stock = Ticker("THYAO")
|
|
941
|
+
>>> stock.ttm_cashflow
|
|
942
|
+
TTM
|
|
943
|
+
İşletme Faaliyetlerinden Nakit 5.0e+10
|
|
944
|
+
Yatırım Faaliyetlerinden Nakit -2.0e+10
|
|
945
|
+
Finansman Faaliyetlerinden Nakit -1.0e+10
|
|
946
|
+
"""
|
|
947
|
+
return self._calculate_ttm(self.quarterly_cashflow)
|
|
948
|
+
|
|
949
|
+
@cached_property
|
|
950
|
+
def major_holders(self) -> pd.DataFrame:
|
|
951
|
+
"""
|
|
952
|
+
Get major shareholders (ortaklık yapısı).
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
DataFrame with shareholder names and percentages:
|
|
956
|
+
- Index: Holder name
|
|
957
|
+
- Percentage: Ownership percentage (%)
|
|
958
|
+
|
|
959
|
+
Examples:
|
|
960
|
+
>>> stock = Ticker("THYAO")
|
|
961
|
+
>>> stock.major_holders
|
|
962
|
+
Percentage
|
|
963
|
+
Holder
|
|
964
|
+
Diğer 50.88
|
|
965
|
+
Türkiye Varlık Fonu 49.12
|
|
966
|
+
"""
|
|
967
|
+
return self._get_isyatirim().get_major_holders(self._symbol)
|
|
968
|
+
|
|
969
|
+
@cached_property
|
|
970
|
+
def recommendations(self) -> dict:
|
|
971
|
+
"""
|
|
972
|
+
Get analyst recommendations and target price.
|
|
973
|
+
|
|
974
|
+
Returns:
|
|
975
|
+
Dictionary with:
|
|
976
|
+
- recommendation: Buy/Hold/Sell (AL/TUT/SAT)
|
|
977
|
+
- target_price: Analyst target price (TL)
|
|
978
|
+
- upside_potential: Expected upside (%)
|
|
979
|
+
|
|
980
|
+
Examples:
|
|
981
|
+
>>> stock = Ticker("THYAO")
|
|
982
|
+
>>> stock.recommendations
|
|
983
|
+
{'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}
|
|
984
|
+
"""
|
|
985
|
+
return self._get_isyatirim().get_recommendations(self._symbol)
|
|
986
|
+
|
|
987
|
+
@cached_property
|
|
988
|
+
def recommendations_summary(self) -> dict[str, int]:
|
|
989
|
+
"""
|
|
990
|
+
Get analyst recommendation summary with buy/hold/sell counts.
|
|
991
|
+
|
|
992
|
+
Aggregates individual analyst recommendations from hedeffiyat.com.tr
|
|
993
|
+
into yfinance-compatible categories.
|
|
994
|
+
|
|
995
|
+
Returns:
|
|
996
|
+
Dictionary with counts:
|
|
997
|
+
- strongBuy: Strong buy recommendations
|
|
998
|
+
- buy: Buy recommendations (includes "Endeks Üstü Getiri")
|
|
999
|
+
- hold: Hold recommendations (includes "Nötr", "Endekse Paralel")
|
|
1000
|
+
- sell: Sell recommendations (includes "Endeks Altı Getiri")
|
|
1001
|
+
- strongSell: Strong sell recommendations
|
|
1002
|
+
|
|
1003
|
+
Examples:
|
|
1004
|
+
>>> stock = Ticker("THYAO")
|
|
1005
|
+
>>> stock.recommendations_summary
|
|
1006
|
+
{'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}
|
|
1007
|
+
"""
|
|
1008
|
+
return self._get_hedeffiyat().get_recommendations_summary(self._symbol)
|
|
1009
|
+
|
|
1010
|
+
@cached_property
|
|
1011
|
+
def news(self) -> pd.DataFrame:
|
|
1012
|
+
"""
|
|
1013
|
+
Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.
|
|
1014
|
+
|
|
1015
|
+
Fetches directly from KAP - the official disclosure platform for
|
|
1016
|
+
publicly traded companies in Turkey.
|
|
1017
|
+
|
|
1018
|
+
Returns:
|
|
1019
|
+
DataFrame with columns:
|
|
1020
|
+
- Date: Disclosure date and time
|
|
1021
|
+
- Title: Disclosure headline
|
|
1022
|
+
- URL: Link to full disclosure on KAP
|
|
1023
|
+
|
|
1024
|
+
Examples:
|
|
1025
|
+
>>> stock = Ticker("THYAO")
|
|
1026
|
+
>>> stock.news
|
|
1027
|
+
Date Title URL
|
|
1028
|
+
0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826
|
|
1029
|
+
1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656
|
|
1030
|
+
"""
|
|
1031
|
+
return self._get_kap().get_disclosures(self._symbol)
|
|
1032
|
+
|
|
1033
|
+
def get_news_content(self, disclosure_id: int | str) -> str | None:
|
|
1034
|
+
"""
|
|
1035
|
+
Get full HTML content of a KAP disclosure by ID.
|
|
1036
|
+
|
|
1037
|
+
Args:
|
|
1038
|
+
disclosure_id: KAP disclosure ID from news DataFrame URL.
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
Raw HTML content or None if failed.
|
|
1042
|
+
|
|
1043
|
+
Examples:
|
|
1044
|
+
>>> stock = Ticker("THYAO")
|
|
1045
|
+
>>> html = stock.get_news_content(1530826)
|
|
1046
|
+
"""
|
|
1047
|
+
return self._get_kap().get_disclosure_content(disclosure_id)
|
|
1048
|
+
|
|
1049
|
+
@cached_property
|
|
1050
|
+
def calendar(self) -> pd.DataFrame:
|
|
1051
|
+
"""
|
|
1052
|
+
Get expected disclosure calendar for the stock from KAP.
|
|
1053
|
+
|
|
1054
|
+
Returns upcoming expected disclosures like financial reports,
|
|
1055
|
+
annual reports, sustainability reports, and corporate governance reports.
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
DataFrame with columns:
|
|
1059
|
+
- StartDate: Expected disclosure window start
|
|
1060
|
+
- EndDate: Expected disclosure window end
|
|
1061
|
+
- Subject: Type of disclosure (e.g., "Finansal Rapor")
|
|
1062
|
+
- Period: Report period (e.g., "Yıllık", "3 Aylık")
|
|
1063
|
+
- Year: Fiscal year
|
|
1064
|
+
|
|
1065
|
+
Examples:
|
|
1066
|
+
>>> stock = Ticker("THYAO")
|
|
1067
|
+
>>> stock.calendar
|
|
1068
|
+
StartDate EndDate Subject Period Year
|
|
1069
|
+
0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025
|
|
1070
|
+
1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025
|
|
1071
|
+
2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026
|
|
1072
|
+
"""
|
|
1073
|
+
return self._get_kap().get_calendar(self._symbol)
|
|
1074
|
+
|
|
1075
|
+
@cached_property
|
|
1076
|
+
def isin(self) -> str | None:
|
|
1077
|
+
"""
|
|
1078
|
+
Get ISIN (International Securities Identification Number) code.
|
|
1079
|
+
|
|
1080
|
+
ISIN is a 12-character alphanumeric code that uniquely identifies
|
|
1081
|
+
a security, standardized by ISO 6166.
|
|
1082
|
+
|
|
1083
|
+
Returns:
|
|
1084
|
+
ISIN code string (e.g., "TRATHYAO91M5") or None if not found.
|
|
1085
|
+
|
|
1086
|
+
Examples:
|
|
1087
|
+
>>> stock = Ticker("THYAO")
|
|
1088
|
+
>>> stock.isin
|
|
1089
|
+
'TRATHYAO91M5'
|
|
1090
|
+
"""
|
|
1091
|
+
return self._get_isin_provider().get_isin(self._symbol)
|
|
1092
|
+
|
|
1093
|
+
@cached_property
|
|
1094
|
+
def analyst_price_targets(self) -> dict[str, float | int | None]:
|
|
1095
|
+
"""
|
|
1096
|
+
Get analyst price target data from hedeffiyat.com.tr.
|
|
1097
|
+
|
|
1098
|
+
Returns aggregated price target information from multiple analysts.
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
Dictionary with:
|
|
1102
|
+
- current: Current stock price
|
|
1103
|
+
- low: Lowest analyst target price
|
|
1104
|
+
- high: Highest analyst target price
|
|
1105
|
+
- mean: Average target price
|
|
1106
|
+
- median: Median target price
|
|
1107
|
+
- numberOfAnalysts: Number of analysts covering the stock
|
|
1108
|
+
|
|
1109
|
+
Examples:
|
|
1110
|
+
>>> stock = Ticker("THYAO")
|
|
1111
|
+
>>> stock.analyst_price_targets
|
|
1112
|
+
{'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49,
|
|
1113
|
+
'median': 465.0, 'numberOfAnalysts': 19}
|
|
1114
|
+
"""
|
|
1115
|
+
return self._get_hedeffiyat().get_price_targets(self._symbol)
|
|
1116
|
+
|
|
1117
|
+
@cached_property
|
|
1118
|
+
def earnings_dates(self) -> pd.DataFrame:
|
|
1119
|
+
"""
|
|
1120
|
+
Get upcoming earnings announcement dates.
|
|
1121
|
+
|
|
1122
|
+
Derived from KAP calendar, showing expected financial report dates.
|
|
1123
|
+
Compatible with yfinance earnings_dates format.
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
DataFrame with index as Earnings Date and columns:
|
|
1127
|
+
- EPS Estimate: Always None (not available for BIST)
|
|
1128
|
+
- Reported EPS: Always None (not available for BIST)
|
|
1129
|
+
- Surprise (%): Always None (not available for BIST)
|
|
1130
|
+
|
|
1131
|
+
Examples:
|
|
1132
|
+
>>> stock = Ticker("THYAO")
|
|
1133
|
+
>>> stock.earnings_dates
|
|
1134
|
+
EPS Estimate Reported EPS Surprise(%)
|
|
1135
|
+
Earnings Date
|
|
1136
|
+
2026-03-11 None None None
|
|
1137
|
+
2026-05-11 None None None
|
|
1138
|
+
"""
|
|
1139
|
+
cal = self.calendar
|
|
1140
|
+
if cal.empty:
|
|
1141
|
+
return pd.DataFrame(
|
|
1142
|
+
columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
# Filter for financial reports only
|
|
1146
|
+
financial_reports = cal[
|
|
1147
|
+
cal["Subject"].str.contains("Finansal Rapor", case=False, na=False)
|
|
1148
|
+
]
|
|
1149
|
+
|
|
1150
|
+
if financial_reports.empty:
|
|
1151
|
+
return pd.DataFrame(
|
|
1152
|
+
columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
# Use EndDate as the earnings date (latest expected date)
|
|
1156
|
+
earnings_dates = []
|
|
1157
|
+
for _, row in financial_reports.iterrows():
|
|
1158
|
+
end_date = row.get("EndDate", "")
|
|
1159
|
+
if end_date:
|
|
1160
|
+
try:
|
|
1161
|
+
# Parse Turkish date format (DD.MM.YYYY)
|
|
1162
|
+
parsed = datetime.strptime(end_date, "%d.%m.%Y")
|
|
1163
|
+
earnings_dates.append(parsed)
|
|
1164
|
+
except ValueError:
|
|
1165
|
+
continue
|
|
1166
|
+
|
|
1167
|
+
if not earnings_dates:
|
|
1168
|
+
return pd.DataFrame(
|
|
1169
|
+
columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
result = pd.DataFrame(
|
|
1173
|
+
{
|
|
1174
|
+
"EPS Estimate": [None] * len(earnings_dates),
|
|
1175
|
+
"Reported EPS": [None] * len(earnings_dates),
|
|
1176
|
+
"Surprise(%)": [None] * len(earnings_dates),
|
|
1177
|
+
},
|
|
1178
|
+
index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"),
|
|
1179
|
+
)
|
|
1180
|
+
result = result.sort_index()
|
|
1181
|
+
return result
|
|
1182
|
+
|
|
1183
|
+
def _parse_date(self, date: str | datetime) -> datetime:
|
|
1184
|
+
"""Parse a date string to datetime."""
|
|
1185
|
+
if isinstance(date, datetime):
|
|
1186
|
+
return date
|
|
1187
|
+
# Try common formats
|
|
1188
|
+
for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
|
|
1189
|
+
try:
|
|
1190
|
+
return datetime.strptime(date, fmt)
|
|
1191
|
+
except ValueError:
|
|
1192
|
+
continue
|
|
1193
|
+
raise ValueError(f"Could not parse date: {date}")
|
|
1194
|
+
|
|
1195
|
+
def __repr__(self) -> str:
|
|
1196
|
+
return f"Ticker('{self._symbol}')"
|