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/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}')"